This post appeared originally in our sysadvent series and has been moved here following the discontinuation of the sysadvent microsite
You might, like me, once have tried to get something to run on the first Monday of the month, or maybe the last Friday of the month, or something else that’s a combination of a weekday (Mon–Sun) and a «week» (i.e. a date range, like 01–07). The naive approach is to use cron as you would expect it to work:
0 0 1-7 * 1 /usr/bin/foo
The surprise comes when this makes foo
run every day of the first week every
month, plus every Monday. Because while cron takes the intersection of
restrictions at all other times, it takes the union in this one special case.
It’s documented in crontab(5)
, at the very end of the DESCRIPTION
section:
Note: The day of a command’s execution can be specified in the following two fields — ‘day of month’, and ‘day of week’. If both fields are restricted (i.e., do not contain the “*” character), the command will be run when either field matches the current time. For example, “
30 4 1,15 * 5
” would cause a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday.
Can this be fixed so it works like everything else? My instincts tell me it’s as entrenched as an old PHP API, so probably not. In any case, you want to get your cronjob running, not spend your time patching cron. You could setup a cronjob for Mondays and use a little script which checks the date. Or you could use systemd timers.
How to use timers
Say you write a little mondays.service
:¹
[Unit]
Description=Is it the first monday of the month
[Service]
Type=oneshot
ExecStart=/usr/bin/foo
and then pair it up with mondays.timer
. systemd will automagically pair up
$foo.{timer,service}
, but you can also specify what the timer activates with
Unit=
.
[Unit]
Description=Is it the first monday of the month
[Timer]
OnCalendar=Mon *-*-1..7 00:00:00
[Install]
WantedBy=timers.target
Now all you have to do is systemctl enable --now mondays.timer
and wait until
the first Monday of the month! The time format of systemd timers is different
from crontab format, and specified in systemd.time(7)
. The options for timers
are specified in systemd.timer(5)
. The power of systemd timers is greater than
this simple cron-like use.
A small table of differences
In this case I’ve used a calendar specification in
pseudo-ISO8601 format (no T), with *
wildcards for
year and month, and a range for days. You can also use relative specifications,
e.g. if you want something to run every hour, just write «hourly». The more
magical examples in systemd.time(7)
don’t work with calendar specifications,
but they do work as sleep times after e.g. unit activation, boot. To rework your
existing cronjobs into systemd format:
cron |
systemd.time |
|
---|---|---|
Free | * |
* |
Group | 1,3,5 | 1,3,5 |
Range | 1-3 | 1..3 |
Step size | */3 | */3 |
Argument ordering | Space-separated reverse of ISO8601 with weekday tacked on at the end | ISO8601 (ish) |
As you can see, the main difference is that the order is reversed, and systemd timers can handle more fine-grained time. This goes beyond the seconds in the date format: you can try to make something run every 0.1 seconds and see systemd complain about start requests repeated too quickly.
The other change is that the dash has been replaced with two dots as the range specifier. This lets you use dashes for the ISO8601 date format, and double dots seems to be a popular range specifier (e.g. it’s used in git, ★modern★ languages like haskell, perl6, ruby, rust, …).
How can I keep track of these timers?
systemctl list-timers
will print active timers, the next time they’ll trigger,
how long until that time, when they last ran, etc. As an example, I’ve got
something running that’ll trigger on the first Monday of every month, and
test.timer
which runs every three minutes after it triggered test.service
:²
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2016-11-24 15:11:38 CET 2min 52s left Thu 2016-11-24 15:08:38 CET 7s ago test.timer test.service
Mon 2016-12-05 07:00:00 CET 1 weeks 3 days left n/a n/a mondays.timer mondays.service
I’m sold!
Great! If you want to take the full plunge, there are some parsers floating around on the internet that can generate systemd units from crontabs. Otherwise, you can just keep your cron service and use systemd timers when they’re a better fit, or when you want a new job/timer. The added power does come with some added complexity, like if you like getting cronmails.
Finally, if you want to use these for your personal account rather than the
system, everything works with the --user
flag.
¹ How to write services is beyond the scope of this post, but if you’re stuck,
try systemctl edit --full --force mondays.service
and likewise for the timer.
Editing with FULL FORCE isn’t as cool as it sounds: --full
specifies you
want to edit the whole service file; --force
creates one if necessary.
² Rather than OnCalendar=
, this has OnUnitActiveSec=3m
Thoughts on the CrowdStrike Outage
Unless you’ve been living under a rock, you probably know that last Friday a global crash of computer systems caused by ‘CrowdStrike’ led to widespread chaos and mayhem: flights were cancelled, shops closed their doors, even some hospitals and pharmacies were affected. When things like this happen, I first have a smug feeling “this would never happen at our place”, then I start thinking. Could it?
Broken Software Updates
Our department do take responsibility for keeping quite a lot ... [continue reading]