Automatisation de tâches avec les timers systemd
systemd-crontabd
Encore un billet d'auto-documentation
, voyons voir comment utiliser les timers de systemd
en lieu et place des vénérables cron : ils offrent en effet quelques sympathiques avantages que la crontab
ne possède pas.
La base
Sous systemd
, un timer est simplement une unité tout comme les services, target, slice... et sert... de minuteur. Il est nécessairement lié à un service et va le lancer selon des conditions temporelles, typiquement un chronomètre ou un calendrier. Donc, en gros oui : systemd
réinvente la crontab
. Voyons voir un cas simple de timer, un script de surveillance. Une de mes machines à la facheuse tendance de perdre épisodiquement sa connectivité IPv6. J'ai donc bricolé un sript qui fait 2-3 tests et relance le nécessaire au besoin. Pour intégrer cette surveillance à systemd
on va créer 2 fichiers : un .timer
et un .service
. Le service est tout ce qu'il y a de plus classique :
[Unit]
Description=Surveillance IPv6
[Service]
User=root
ExecStart=/usr/local/bin/test-ip6
[Install]
WantedBy=basic.target
Et on crée un fichier .timer
avec le même nom (si on crée test.service
, il faut un test.timer
) :
[Unit]
Description=Run test-ip6 every 10 minutes
[Timer]
OnBootSec=10min
OnUnitActiveSec=10min
Unit=test.service
[Install]
WantedBy=timers.target
Dans le détail :
OnBootSec
indique la durée à attendre après le démarrage de la machine.OnUnitActiveSec
définit la durée à attendre depuis la dernière activation du service que le timer contrôle.Unit
définit l'unité à contrôler. Il pourrait être implicite ici : par défaut,systemd
va chercher une unité de type service avec le même nom que le timer
On active uniquement le timer via :
# systemctl enable --now test.timer
Le --now
permet de démarrer immédiatement le timer. S'il est omis, il faut bien penser à le faire via :
# systemctl start test.timer
Voilà, on a un équivalent systemd
d'une entrée :
*/10 * * * * root /usr/local/bin/test-ip6
dans la crontab
. À la différence que le cron
est absolu : il s'exécute quoiqu'il arrive toutes les 10 min (18h00, 18h10, 18h20...), là où l'exécution du timer est relative à un évenement et peut être restreinte par des conditions (parmi celles que j'aime bien, le changement de fuseau horaire). Regardons un cas plus évolué : la rotation des journaux, gérées via logrotate.timer
:
[Unit]
Description=Daily rotation of log files
Documentation=man:logrotate(8) man:logrotate.conf(5)
[Timer]
OnCalendar=daily
AccuracySec=1h
Persistent=true
[Install]
WantedBy=timers.target
On a donc les conditions suivantes :
OnCalendar
permet de contrôler le timer via une base calendaire, ici un déclechement quotidien (à minuit). Comme pour lescron
, il est possible de définir des heures ou jours de la semaine (ou du mois) particulier. La documentation précise la syntaxe.AccuracySec
définit la précision de l'exécution. Ici, on définit une fenêtre de ±1h et le système exécutera la tâche durant cette dernière, à une valeur aléatoire mais fixe pour tous les timers, afin de ne pas réveiller le CPU inutilement (dixit le manuel).Persistent
est un booléen. Quand il est vrai (ce qui n'est pas le comportement par défaut), l'heure de la dernière exécution du service contrôlé par le timer est stockée. À l'activation du timer, le service est déclenché immédiatement s'il aurait du être déclenché durant l'inactivité du timer. Cela permet de rattraper des déclechements ratés (pour cause de machine éteinte par exemple). En gros,anacron
versionsystemd
.
D'autres conditions existent et sont listés dans le manuel. On va en voir une autre, que je trouve intéressante : OnUnitInactiveSec
.
Surveillance de service
L'idée de ce billet m'est venu en cherchant une solution au problème que je rencontre avec mon script unbound-adblock
et la combinaison NetworkManager et Wi-Fi (voir ce ticket pour le détail). J'ai trouvé une solution correcte ne nécessitant ni de modifier le script, ni de modifier le fichier .service
. Ce qui me satisfait, même si ce n'est pas parfait.
OnUnitInactiveSec
donc définit la durée (en secondes si aucune unité n'est précisée) à attendre depuis la dernière activation du service que le timer contrôle (l'inverse de OnUnitActiveSec
). Dans le cas de unbound-adblock
, on créé donc le adblock.timer
:
[Unit]
Description=Reload failed adblock
[Timer]
OnUnitInactiveSec=60
Unit=adblock.service
[Install]
WantedBy=timers.target
Une fois actif et démarré, ce timer va donc attendre 1 minute avant de relancer le service adblock
si ce dernier est inactif (le status failed
rentrant dans ce cas).
$ systemctl status adblock.* ● adblock.service - Unbound AdBlock List Making Loaded: loaded (/etc/systemd/system/adblock.service; enabled; vendor preset: enabled) Active: failed (Result: exit-code) since Mon 2019-09-09 19:59:06 CEST; 58s ago Process: 981 ExecStart=/usr/local/bin/unbound-adblock /home/john/liste-adblock.json (code=exited, status=1/FAILURE) Main PID: 981 (code=exited, status=1/FAILURE) sept. 09 19:59:06 SHAFT-NETBOOK systemd[1]: Starting Unbound AdBlock List Making... sept. 09 19:59:06 SHAFT-NETBOOK unbound-adblock[981]: grep: /var/log/adblock.log.1: Aucun fichier ou dossier de ce type sept. 09 19:59:06 SHAFT-NETBOOK unbound-adblock[981]: Le téléchargement de 2 listes sur 4 a échoué, voir /var/log/adblock.log. Arrêt du script sept. 09 19:59:06 SHAFT-NETBOOK systemd[1]: adblock.service: Main process exited, code=exited, status=1/FAILURE sept. 09 19:59:06 SHAFT-NETBOOK systemd[1]: adblock.service: Failed with result 'exit-code'. sept. 09 19:59:06 SHAFT-NETBOOK systemd[1]: Failed to start Unbound AdBlock List Making. ● adblock.timer - Reload failed adblock Loaded: loaded (/etc/systemd/system/adblock.timer; enabled; vendor preset: enabled) Active: active (waiting) since Mon 2019-09-09 19:58:59 CEST; 1min 5s ago Trigger: Mon 2019-09-09 20:00:06 CEST; 1s left
On voit donc adblock.service
planté, car démarré sans connexion Wi-Fi active et adblock.timer
expirant dans une seconde et relançant le service ce faisant. Si la nouvelle exécution du script échoue, le timer se redéclenchera. Sinon, il s'arrête. Cette solution est sans doute moins précise et efficace que le Restart
disponible pour les services (On ne peut pas exclure un code d'erreur ou définir une limite de tentative par exemple), mais il y a quelques avantages :
- Ne nécessite pas de modifier le fichier
.service
de l'unité. On peut donc surveiller un service installé via le gestionnaire de paquets (un serveur Web par exemple) sans crainte de voir la modification écrasée lors d'une mise à jour. - Cette indépendence du timer le rend optionnel et débrayable facilement.
Dans tous les cas, l'avantage comparé à une solution passant par la crontab
et de ne pas avoir à faire un script testant le service à surveiller.