107.2 Automate system administration tasks by scheduling jobs¶
Weight: 4
Candidates should be able to use cron and systemd timers to run jobs at regular intervals and to use at to run jobs at a specific time.
Objectives
- Manage cron and at jobs.
- Configure user access to cron and at services.
- Understand systemd timer units.
Terms
/etc/cron.{d,daily,hourly,monthly,weekly}/, /etc/at.deny, /etc/at.allow, /etc/crontab, /etc/cron.allow, /etc/cron.deny, /var/spool/cron/, crontab, at, atq, atrm, systemctl, systemd-run
Three tools for scheduling¶
An administrator automates the jobs that repeat: backups, updates, cleanups. Linux gives you three tools:
| Tool | Use it for |
|---|---|
| cron | jobs that repeat on a schedule, like every night at 03:00 |
| at | a job that runs once, at a future time |
| systemd timers | the systemd way to do both |
cron¶
cron is a daemon that runs all the time and wakes up every minute to check its tables, called crontabs, for jobs to run. A job runs only if the machine is on at that moment, so cron suits servers that never switch off. (For machines that are often off, there is anacron, which runs missed jobs at the next boot. It is outside the LPIC-1 scope.)
There are two kinds of crontab:
| Kind | Where | Edited with | Fields |
|---|---|---|---|
| user crontab | one per user, under /var/spool/cron/ |
crontab -e |
5 time fields + command |
| system crontab | /etc/crontab and the files in /etc/cron.d/ |
any editor, as root | 5 time fields + user + command |
The crontab format¶
Each line of a user crontab has six fields, separated by spaces. The first five say when, and the rest of the line is the command:
.---------------- minute (0 - 59)
| .------------- hour (0 - 23)
| | .---------- day of month (1 - 31)
| | | .------- month (1 - 12) OR jan,feb,mar,apr ...
| | | | .---- day of week (0 - 7, Sunday is 0 or 7) OR sun,mon,tue,wed,thu,fri,sat
| | | | |
* * * * * command to be executed
Month and weekday also accept the first three letters of the name. Each time field can hold more than one value:
| Symbol | Means | Example | Matches |
|---|---|---|---|
* |
any value | * in the hour field |
every hour |
, |
a list | 8,18 |
8 and 18 |
- |
a range | 1-5 in the weekday field |
Monday to Friday |
/ |
a step | */5 in the minute field |
every 5 minutes |
Read these until each one is obvious:
0 10 * * * /home/frank/foo.sh # every day at 10:00
5 0 * * * $HOME/bin/daily.job # every day at 00:05
15 14 1 * * $HOME/bin/monthly # 14:15 on the 1st of every month
0 22 * * 1-5 $HOME/bin/reminder.sh # 22:00, Monday to Friday
23 0-23/2 * * * $HOME/bin/check.sh # 00:23, 02:23, 04:23 ... every day
5 4 * * sun $HOME/bin/weekly.sh # 04:05 every Sunday
*/5 * * * * $HOME/bin/poll.sh # every 5 minutes
42 8,18 * * 1-5 $HOME/bin/report.sh # 08:42 and 18:42 on weekdays
0,15,30,45 08 * * 2 /home/frank/bar.sh # Tuesdays at 08:00, 08:15, 08:30 and 08:45
30 20 1-15 1,6 1-5 /home/frank/foobar.sh # 20:30, weekdays, days 1-15 of January and June
Two traps to watch for:
- A
*in the minute field means every minute.* 3 * * *runs 60 times between 03:00 and 03:59, not once. 42 8 1 1 0runs only when the 1st of January is also a Sunday.
Special shortcuts¶
In place of the five time fields you can write one shortcut:
| Shortcut | Runs |
|---|---|
@reboot |
once, after each boot |
@hourly |
at the start of every hour |
@daily (or @midnight) |
every day at midnight |
@weekly |
every week, at midnight on Sunday |
@monthly |
at midnight on the 1st of every month |
@yearly (or @annually) |
at midnight on the 1st of January |
Variables in a crontab¶
A crontab can set variables before the jobs. The common ones:
| Variable | Sets | Default |
|---|---|---|
SHELL |
the shell that runs the commands | /bin/sh |
PATH |
where to look for commands | set by the system |
HOME |
the directory where commands run | the user's home |
MAILTO |
who gets the job's output by mail. Several addresses can be comma separated. Empty means send no mail | the crontab's owner |
Whatever a job prints is mailed to its owner (or to MAILTO). A common habit is to send normal output to /dev/null and leave errors alone, so you get a mail only when something goes wrong:
Or keep both in log files to read later:
User crontabs: the crontab command¶
Each user has their own crontab, named after the account. The file lives under /var/spool/cron/ (for example /var/spool/cron/crontabs/ or /var/spool/cron/tabs/, depending on the distribution). Do not edit it directly. Use crontab, which checks your changes and loads them:
| Command | Does |
|---|---|
crontab -e |
edit your crontab (it is created if needed) |
crontab -l |
list your crontab |
crontab -r |
remove your crontab |
crontab -u frank -e |
edit frank's crontab (root only) |
crontab -e opens the editor named in the VISUAL or EDITOR variable. Some distributions ask you to choose one the first time:
$ crontab -e
no crontab for frank - using an empty one
Select an editor. To change later, run 'select-editor'.
1. /bin/ed
2. /bin/nano <---- easiest
3. /usr/bin/emacs24
4. /usr/bin/vim.tiny
Choose 1-4 [2]:
Add one line, save, and the job is scheduled. To run ~/foo.sh every day at 10:00:
The stored file starts with a warning, which is why you always go through crontab -e:
# cat /var/spool/cron/tabs/frank
# DO NOT EDIT THIS FILE - edit the master and reinstall.
# (/tmp/crontab.khObLu installed on Thu Oct 29 22:04:43 2023)
(...)
* * * * * date >> /tmp/date.cron.txt
Be careful with crontab -r: it deletes the whole crontab without asking.
System crontabs¶
/etc/crontab and every file in /etc/cron.d/ are system crontabs. Only root can change them, and you edit them with a normal editor, not with crontab. They have one extra field, the user that runs the job, between the time and the command:
A typical /etc/crontab sets its variables, then runs the jobs of the hourly, daily, weekly and monthly directories:
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Example of job definition:
# .---------------- minute (0 - 59)
# | .------------- hour (0 - 23)
# | | .---------- day of month (1 - 31)
# | | | .------- month (1 - 12) OR jan,feb,mar,apr ...
# | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat
# | | | | |
# * * * * * user-name command to be executed
Another /etc/crontab, with the line that runs the hourly directory:
# cat /etc/crontab
SHELL=/bin/sh
PATH=/usr/bin:/usr/sbin:/sbin:/bin
MAILTO=root
17 * * * * root cd / && run-parts --report /etc/cron.hourly
The first lines set the shell, the search path, and who receives the output e-mail. run-parts runs every script in the given directory.
Programs drop their own crontabs into /etc/cron.d/, in the same 7-field format. For example the one installed by the monitoring tool munin:
$ cat /etc/cron.d/munin
#
# cron-jobs for munin
#
MAILTO=root
*/5 * * * * munin if [ -x /usr/bin/munin-cron ]; then /usr/bin/munin-cron; fi
14 10 * * * munin if [ -x /usr/share/munin/munin-limits ]; then /usr/share/munin/munin-limits --force --contact nagios --contact old-nagios; fi
The cron.hourly, daily, weekly and monthly directories¶
The easiest way to schedule a script: drop it into one of these directories and it runs at that interval. No time fields needed:
| Directory | Scripts run |
|---|---|
/etc/cron.hourly/ |
every hour |
/etc/cron.daily/ |
every day |
/etc/cron.weekly/ |
every week |
/etc/cron.monthly/ |
every month |
To schedule a daily job, you just copy your executable script into /etc/cron.daily/. This keeps routine cleanup and backup scripts tidy.
$ sudo tree /etc/cron*
/etc/cron.d
├── munin
├── php5
└── sendmail
/etc/cron.daily
├── apt
├── dpkg
├── logrotate
├── man-db
├── mlocate
└── passwd
/etc/cron.hourly
/etc/cron.monthly
/etc/cron.weekly
├── fstrim
└── man-db
These are ordinary scripts. For example, logrotate (108.2) runs daily because its script sits in /etc/cron.daily/. Some distributions use /etc/cron.d/daily and similar instead, so check which directories your system has.
Who may use cron¶
Two files control which users may use crontab. Each holds one user name per line:
| Situation | Who may use cron |
|---|---|
/etc/cron.allow exists |
only the users listed in it |
no cron.allow, but /etc/cron.deny exists |
everyone except the users listed in it. An empty cron.deny allows everyone |
| neither file exists | depends on the distribution |
cron.allow wins: if it exists, cron.deny is ignored. These rules apply to normal users; root can always use cron.
at: run a job once¶
at schedules a job to run one time. Type at and a time, then the commands at the at> prompt, and finish with Ctrl+D:
$ at now +5 minutes
warning: commands will be executed using /bin/sh
at> date
at> <EOT>
job 12 at Sat Sep 14 09:15:00 2019
Like cron, at mails you the job's output. The atd daemon must be running for at to work.
Time formats¶
| You type | Runs |
|---|---|
at 17:30 |
at 17:30. If that time has passed today, tomorrow |
at 09:30 AM |
12-hour format with AM or PM |
at now +5 minutes |
now + a number of minutes, hours, days or weeks |
at noon, at midnight |
at 12:00 or 00:00 |
at teatime |
at 16:00 |
at 17:30 tomorrow |
today or tomorrow after a time |
at 07:15 AM Jan 01 |
on a date. Also MMDDYY, MM/DD/YY, DD.MM.YY and YYYY-MM-DD |
Listing and removing jobs¶
| Command | Same as | Does |
|---|---|---|
atq |
at -l |
lists your pending jobs: ID, date, time, queue, user. Root sees all users' jobs |
atrm 14 |
at -d 14 |
deletes job 14. Several IDs can be given |
at -c 14 |
prints the commands of job 14 |
$ at 09:30 AM
warning: commands will be executed using /bin/sh
at> ./foo.sh
at> <EOT>
job 13 at Sat Sep 14 09:30:00 2019
$ at now +2 hours
warning: commands will be executed using /bin/sh
at> ./bar.sh
at> <EOT>
job 14 at Sat Sep 14 11:10:00 2019
$ atq
14 Sat Sep 14 11:10:00 2019 a frank
13 Sat Sep 14 09:30:00 2019 a frank
12 Sat Sep 14 09:15:00 2019 a frank
$ atrm 14
$ atq
13 Sat Sep 14 09:30:00 2019 a frank
12 Sat Sep 14 09:15:00 2019 a frank
The a is the queue. More at options:
| Option | Does |
|---|---|
-f file |
read the commands from a file instead of typing them |
-m |
send a mail when the job ends, even with no output |
-q letter |
choose a queue, a to z or A to Z (default a for at, b for batch). Later letters run with a higher nice value |
-v |
show the time the job will run |
batch is like at, but runs the job only when the system load is low enough.
Who may use at¶
/etc/at.allow and /etc/at.deny work exactly like the cron files:
| Situation | Who may use at |
|---|---|
/etc/at.allow exists |
only the users listed |
no at.allow, but /etc/at.deny exists |
everyone except the users listed. Empty means everyone |
| neither exists | depends on the distribution |
systemd timers¶
On systemd systems, timers are an alternative to cron. A timer is a unit file ending in .timer. When it fires, it starts another unit, by default the service with the same name: foobar.timer starts foobar.service.
/etc/systemd/system/foobar.timer --- when it fires, starts ---> /etc/systemd/system/foobar.service
[Timer] OnCalendar=... [Service] ExecStart=the job
The schedule goes in the [Timer] section. This runs foobar.service at 05:30 on the first Monday of every month:
[Unit]
Description=Run the foobar service
[Timer]
OnCalendar=Mon *-*-1..7 05:30:00
Persistent=true
[Install]
WantedBy=timers.target
Then enable and start the timer, as root:
# systemctl enable foobar.timer
# systemctl start foobar.timer
# systemctl enable --now myservice.timer # both in one step
After changing a timer file, run systemctl daemon-reload so systemd reads it again.
Real-time timers: OnCalendar¶
OnCalendar= works like a cron time, with this format:
DayOfWeekis optional, written asMonorMonday.*,/and,mean the same as in cron...gives a range:1..7is days 1 to 7. SoMon *-*-1..7means "a Monday in the first 7 days of the month", which is the first Monday.
Instead of the full form you can use a shortcut:
| Shortcut | Runs |
|---|---|
hourly |
at the start of every hour |
daily |
every day at midnight |
weekly |
at midnight on Monday (cron's @weekly is Sunday) |
monthly |
at midnight on the 1st of the month |
yearly |
at midnight on the 1st of January |
A real timer from a Fedora system, updating the locate database every day:
# cat /etc/systemd/system/timers.target.wants/plocate-updatedb.timer
[Unit]
Description=Update the plocate database daily
[Timer]
OnCalendar=daily
RandomizedDelaySec=12h
AccuracySec=20min
Persistent=true
[Install]
WantedBy=timers.target
Monotonic timers¶
Monotonic timers fire a set time after some event, not at a clock time:
| Option | Counts from |
|---|---|
OnBootSec= |
the system boot |
OnActiveSec= |
the moment the timer was activated |
OnStartupSec= |
the start of the service manager (useful for user services, started at login) |
OnUnitActiveSec= |
the last time the service was activated |
OnUnitInactiveSec= |
the last time the service was deactivated |
Times can use units like seconds, minutes, hours, days, weeks:
This runs the service again 5 minutes and 20 seconds after each time it was last started.
Checking timers¶
systemctl list-timers lists the active timers, sorted by when they fire next. Add --all to include inactive ones. Timers log to the journal, so journalctl shows what they did. As a normal user working with your own timers, add --user to systemctl and journalctl.
systemd-run: one-time jobs¶
systemd-run creates a temporary timer for a command, without writing any unit files. It is the systemd alternative to at:
# systemd-run --on-calendar='2019-10-06 11:30' date
# systemd-run --on-active="2m" ./foo.sh
# systemd-run --on-active="2hours" /usr/local/bin/helloworld.sh
The first runs date at 11:30 on 2019-10-06. The second runs ./foo.sh two minutes from now, and the third runs the script once, two hours from now. Like the other systemd tools, add --user to run it as your own user, and read its output with journalctl.
More examples¶
The same commands once more, with other names and values, as a quick reference:
5 0 * * * $HOME/bin/daily.job # 00:05 every day
15 14 1 * * $HOME/bin/monthly # 14:15 on the 1st of each month
0 22 * * 1-5 echo "it is 10pm" # 22:00 on weekdays (Mon-Fri)
23 0-23/2 * * * echo "23 min past every 2nd hour"
*/5 * * * * echo "every 5 minutes"
42 8,18 * * 1-5 echo "08:42 and 18:42 on weekdays"
@reboot echo "runs once after each reboot"
$ crontab -l # list my cron jobs
$ crontab -e # edit them (validated, then loaded)
$ crontab -r # remove my crontab
$ at now + 1 min
warning: commands will be executed using /bin/sh
at> touch /tmp/at
at> <EOT>
job 3 at Thu Oct 29 22:12:00 2015
# /etc/systemd/system/myservice.timer
[Unit]
Description=Run my service
[Timer]
OnCalendar=Sat *-*-1..7 02:42:00 # 02:42 on the first Saturday each month
Persistent=true
[Install]
WantedBy=timers.target
# systemctl daemon-reload
# systemctl enable --now myservice.timer
# systemctl list-timers # add --all to include inactive ones
Summary¶
I pick the tool by the need: cron for repeating jobs, at for one-time jobs, and systemd timers for either kind. cron wakes every minute and reads the crontabs. A user crontab line has five time fields, minute, hour, day of month, month and day of week (where 0 and 7 are Sunday), followed by the command; * means any, , lists, - gives a range and / a step, and shortcuts like @reboot, @daily and @weekly replace the five fields. I manage my own crontab only through crontab -e, -l and -r, root edits other users' with -u, and the files themselves sit under /var/spool/cron/. System crontabs, /etc/crontab and /etc/cron.d/*, are edited directly and have an extra user field, and dropping a script into /etc/cron.hourly, .daily, .weekly or .monthly schedules it at that interval with no crontab line. A job's output is mailed to its owner or to MAILTO, so I redirect what I do not need.
at runs a job once: at now +5 minutes, at 17:30 tomorrow or at teatime, typing the commands and finishing with Ctrl+D. atq lists pending jobs, atrm deletes them, and the atd daemon must be running. Access to both services follows the same rule: an .allow file is a whitelist, so if it exists only the users in it may schedule jobs, otherwise the .deny file is a blacklist and everyone except those in it may, using /etc/cron.allow, /etc/cron.deny, /etc/at.allow and /etc/at.deny.
A systemd timer is a .timer unit that starts the service with the same name. OnCalendar= sets real times in the form DayOfWeek Year-Month-Day Hour:Minute:Second, with .. for ranges and shortcuts like daily and weekly, while monotonic options like OnBootSec= and OnUnitActiveSec= count time from an event. I enable and start the timer with systemctl, run systemctl daemon-reload after edits, check timers with systemctl list-timers and read their logs with journalctl. For a quick one-time job, systemd-run --on-active= or --on-calendar= builds a temporary timer for me.