Der Cron-Dienst war eigentlich immer das Mittel der Wahl, um wiederholt Aufgaben auszuführen. Mit Systemd ist die Funktion Timer hinzugekommen, über die man ebenfalls zeitgesteuert Programme ausführen kann. Dabei bekommt man dazu all die Möglichkeiten von Systemd, sprich die Werkzeuge zur Steuerung und die Möglichkeiten zur Rechtebegrenzung.
In der Regel handelt es sich bei den Aufgaben um mehr als einen einzigen Befehl,
aber dennoch wäre es praktisch, alles kompakt beisammen zu haben. Viele dieser
Skripte habe ich sonst unter /etc/cron.daily|
weekly|
monthly abgelegt, aber mit
den Timer-Units lässt sich der Startzeitpunkt wesentlich besser steuern.
Ein Timer ist ein Unit-Typ, der eine Service-Unit starten kann. Die Einträge überlappen sich nicht und könnten meiner Ansicht nach auch in eine Unit (die Systemd dann vielleicht im Hintergrund aufteilt), aber leider habe ich dafür noch keine Lösung gefunden. Deshalb muss man zwei Units erstellen, wobei es sinnvoll ist, beiden den gleichen Namen zu geben.
Shellskripte direkt in Service-Units
Da diese kleinen Aufgaben nach der Ausführung beendet sind (und nicht wie
Dienste dauerhaft laufen), muss man Type=oneshot
definieren. Nützlich ist es
auch mit SyslogIdentifier=…
einen Namen zu vergeben, damit im Log nicht nur
sh
als Prozess steht.
Für Einträge in /etc/crontab war es nur sinnvoll, externe Skripte zu hinterlegen, denn mehrere Anweisungen in einer Zeile waren unlesbar. Mit den Cron-Skripten in /etc/cron.* hatte man den Vorteil, dass man dort mehrere Anweisungen in lesbarer Form hinterlegen konnte. Aber sobald man su aufgerufen und den Benutzer gewechselt hat, wurde auch alles wieder zu einem großen Zeichenfolge.
Für Systemd gibt es so eine Zwischenlösung: Man kann als Prozess die Shell, Perl, Python oder ein anderes Programm starten, dass über die Standardeingabe die Anweisungen entgegen nimmt und über StandardInputText kann man das Programm übergeben. Somit sind die Anweisungen auch nicht über die Prozessliste ps zu sehen.
Den Wert von StandardInputText kann man über mehrere Zeilen verteilen, wofür
man am Ende jeder Zeile einen Backslash \
setzen muss. Da auch die gängigen
Escape-Sequenzen unterstützt werden, kann man somit jede Zeile mit \n\
abschließen, da jeder Zeilenumbruch für die Shell ein Befehlsende darstellt.
Alternativ könnte man auch (wie in Makefile üblich) ein Semikolon als Trenner
der Befehle nutzen. Da ebenfalls die Sequenzen mit Prozentzeichen ersetzt
werden, müssen diese entsprechend durch Verdoppelung %%
maskiert werden.
Der Gesamtwert von StandardInputText lässt sich auch durch wiederholte
Einträge zusammensetzen, wobei am Ende jedes Eintrags ein Zeilenumbruch
eingefügt wird. Diese Form gefällt mir aber aufgrund der tieferen Einrückung und
dem immer vorangehenden StandardInputText=
nicht so gut, weshalb ich die
Variante mit \n\
nutze.
Entsprechende Rechteeinschränkungen kann man dann mit den üblichen
Systemd-Parametern vornehmen, womit sich eine Unit wie
folgende ergibt (erstellen mit systemctl edit --full --force NAME.service
):
[Unit]
Description=Download weather forcast image from meteoblue
[Service]
Type=oneshot
SyslogIdentifier=meteogram
User=joerg
Group=joerg
ExecStart=/bin/sh
StandardInput=data
StandardInputText=\
dir=~/meteogram \n\
fname=meteogram-Jena-$(date +%%Y-%%m-%%d\\ %%H:%%M).png \n\
wget --no-verbose -O "$dir/$fname" \
'https://my.meteoblue.com/visimage/meteogram_web_hd?…' \n\
if cmp --quiet "$dir/$fname" "$dir/latest"; then \n\
rm "$dir/$fname" \n\
echo "Removed duplicate" \n\
else \n\
ln -sf "$fname" "dir"/latest \n\
fi
LockPersonality=true
NoNewPrivileges=true
CapabilityBoundingSet=
PrivateDevices=true
PrivateTmp=true
ProtectClock=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
RemoveIPC=true
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
Da dies eine ganz normale Service-Unit ist, kann man sie auch mit systemctl
start NAME
ausführen und sich im Log die entsprechenden Einträge mit
journalctl -b -u NAME
ansehen.
Timer-Unit
Um regelmäßig diese Service-Unit zu starten, muss man eine
Timer-Unit
definieren. Im Abschnitt [Timer]
kann man über OnCalendar
mit einer recht
komplexen Syntax die
Zeitpunkte
definieren. Mit *:20,50
wird zum Beispiel 20 und 50 Minuten nach jeder vollen
Stunde der Timer ausgelöst. Zur Prüfung der Spezifikation kann man
systemd-analyze calendar '…'
verwenden.
Weil ich auf ein externes System zugreife und dessen Last etwas streuen will,
habe ich mit RandomizedDelaySec=10m
eine Schwankungsbreite von 10 Minuten für
die Ausführung angegeben. Hier in diesem Fall mag es keine praktische Bedeutung
haben, aber bei lokalen Aufgaben oder internen Diensten, ist es hilfreich, wenn
nicht exakt 4 Uhr alle Systeme auf einen Dienst einhämmern und die restlichen
24 Stunden passiert nichts.
Sollte die Service-Unit einen abweichenden Namen von der Timer-Unit haben, muss
man diese mit Unit=
angeben. Für den Timer ist es – entgegen dem Service –
wichtig, mit WantedBy
anzugeben, dass er beim Start des Systems automatisch
aktiviert wird. Wenn man dies nicht tut, muss man ihn immer wieder von Hand mit
systemctl start NAME.timer
aktivieren.
[Unit]
Description=Trigger download of meteogram image
[Timer]
# Unit=…
OnCalendar=*:20,50
RandomizedDelaySec=10m
[Install]
WantedBy=timers.target
Nachdem die Unit mit systemctl edit --full --force NAME.timer
erstellt und mit
systemctl enable --now NAME.timer
registriert und gestartet wurde, kann mit
systemctl list-timers
der Zustand der Timer erfragt werden:
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2021-05-11 08:55:23 CEST 27min left Tue 2021-05-11 05:14:12 CEST 3h 14min ago meteogram.timer meteogram.service
1 timers listed.
Pass --all to see loaded but inactive timers, too.
Logüberwachung mit einem Timer
Auf die gleiche Weise kann man auch einen Timer erstellen, der regelmäßig im
Journal nachsieht, ob Fehlermeldungen aufgetreten sind und diese dann per E-Mail
meldet. Hierfür ist die Option
--cursor-file
für journalctl hilfreich, da man mit ihr die neusten Meldungen im Journal
abfragen kann:
# /etc/systemd/system/journal-report.service
[Unit]
Description=Report about error messages in journal to root
[Service]
Type=oneshot
SyslogIdentifier=err-report
ExecStart=/bin/sh
StandardInput=data
StandardInputText=\
out=$(journalctl --cursor-file /var/local/error.cur --since=-2\\ days --no-hostname --no-tail -p err) \n\
if ! echo "$out" | grep -q -e '-- No entries --' \n\
then \
echo "$out" |mail -s "[`hostname`] error log" root || exit \n\
echo "Found $(echo "$out" | wc -l) error messages. Sent e-mail." \n\
fi
# Common security settings https://jo-so.de/2021-05/H%C3%A4rtung-Systemd.html
CapabilityBoundingSet=
LockPersonality=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProtectClock=true
ProtectControlGroups=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectSystem=strict
# Make sure that the process can only see PIDs and process details of itself,
# and the second option disables seeing details of things like system load and
# I/O etc
ProtectProc=invisible
ProcSubset=pid
RemoveIPC=true
RestrictAddressFamilies=AF_UNIX
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @obsolete
# end of common security settings
# Allow suid-exec exim
NoNewPrivileges=false
SecureBits=no-setuid-fixup
# required for Exim's mail delivery
CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_SETGID CAP_SETUID
# write access to /var required by Exim
ProtectSystem=full
# required by Exim
SystemCallFilter=@chown @setuid
# /etc/systemd/system/journal-report.timer
[Unit]
Description=Trigger report about errors in journal
[Timer]
OnCalendar=*:10/10
[Install]
WantedBy=timers.target
Zum Test kann man die Service-Unit einfach starten und ggf. den Cursor /var/local/error.cur entfernen, um wieder von vorn zu beginnen. Der Timer läuft im 10-Minuten-Takt, aber kann auch seltener gestartet werden.
Problematisch ist der Aufruf von mail, da dieser je nach Mail-Dienst und
Konfiguration noch mehr Rechte benötigt, um die E‑Mail auszuliefern. In diesem
Fall muss CapabilityBoundingSet
oder SystemCallFilter
erweitert werden.
Statt der Filteroption -p err
kann man auch die Warnungen mit -p warning
einbeziehen oder mit -t audit
sich regelmäßig über die Verstöße gegen
Sicherheitsrichtlinien (AppArmor, Seccomp) informieren lassen.