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 :

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 :

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 :

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.