2020-03-28
md
Chien de garde au goût de framboise
<-Démarrage à chaud et à froid du Raspberry Pi
<-Revoir le chien de garde matériel du Raspberry Pi

Il s'agit du premier des « nouveaux » chiens de garde matériels « améliorés » pour l'ordinateur monocarte Raspberry Pi hôte du système domotique. Le chien de garde est est lui-même un Raspberry Pi. Étant donné que Raspbian Buster fonctionne sur tous les modèles du Raspberry Pi, tout modèle pourrait être le chien de garde et tout modèle pourrait être le serveur Pi surveillé. Pour réduire les coûts, un Raspberry Pi Zero s'impose. Cependant, si le chien de garde doit envoyer un courriel lors du redémarrage du serveur surveillé, il doit pouvoir communiquer sur le réseau. Dans ce cas, un Raspberry Pi Zero W est probablement le choix le plus économique. Comme je n'ai pas de Pi Zero, un ancien Raspberry Pi 1 est utilisé comme succédané.

Table of contents

  1. Montage du chien de garde
  2. Réglage du serveur surveillé
    1. Alimentation du chien de garde
    2. Nourir le chien de garde à la dure
    3. Service signe de vie
    4. Module d'arrêt
  3. Réglage du chien de garde
    1. Chien de garde rudimentaire
    2. Chien de garde obéissant
    3. Chien de garde obéissant et aboyeur
    4. Déchaîner le chien de garde
  4. Minutage
  5. Validation

Montage du chien de garde toc

Pour faire mieux qu'avec le chien de garde pour plateforme d'extraction de cryptomonnaie, le nouveau chien de garde doit être en mesure d'arrêter correctement le serveur et d'effectuer une réinitialisation matérielle uniquement si cet arrêt a échoué. Cela signifie que notre temporisateur de surveillance aura une première connexion de sortie pour lancer un arrêt correct lorsque nécessaire. Il lui faut une seconde connexion pour déclencher une réinitialisation matérielle quand le module d'arrêt n'a pu être lancé. Si l'arrêt est effectué correctement, le second signal de sortie est encore nécessaire pour relancer le serveur et ainsi terminer le redémarrage. Bien sûr, le chien de garde doit être « alimenté » par le serveur. En d'autres termes, le chien de garde doit surveiller une connexion d'entrée pour capter le « signe de vie » du serveur pour utiliser l'autre analogie au sujet des temporisateurs de surveillance. Ainsi, en plus d'une masse commune entre les deux Raspberry Pi, trois autres connexions doivent être effectuées comme indiqué dans le schéma suivant.

Connexion Temporisateur Serveur
signe de vie GPIO17 (broche 11) - entrée GPIO17 (broche 11) - sortie
arrêt GPIO27 (broche 13) - sortie GPIO27 (broche 13) - entrée
réinitialisation GPIO25 (broche 22) - sortie RUN - entrée
masse masse (broche 20) masse (broche 20)

Les trois connexions doivent utiliser des broches d'entrée-sortie à usage général en évitant les broches à usage spécial qui pourraient être utilisées par un Pi ou l'autre pour communiquer avec d'autres périphériques. Par exemple, les broches GPIO2 et GPIO3 (signal de données et signal d'horloge I2C respectivement) du serveur ne peuvent être utilisé car une horloge matérielle y est connectée. Même avec cette contrainte, il existe de nombreuses connexions possibles; mon choix fut dicté par des considérations esthétiques pour que montage soit simple et symétrique.

Il existe de nombreuses broches de masse sur le connecteur E/S du Raspberry Pi. Peu importe quelles broches sont utilisée, il faut connecter la masse des deux Raspberry Pi. Sauf pour le Raspberry Pi 3 A+ et B+, le petit connecteur à deux broches nommé P2, P6, ou RUN ou le connecteur de trois broches libellé RUN GLOBAL_EN du modèle 4 ont également une broche mise à terre qui pourrait être utilisée. En aucun cas faut-il relié la broche 1 (3,3 volts) des deux appareils. De même, si les deux Raspberry Pi sont alimentés à partir de sources différentes, ne connectez pas ensemble les broches 2 et 4 (5 volts) des deux appareils.

Un bouton-poussoir normalement ouvert est également placé entre le signal d'arrêt et la masse. De cette façon, il est possible d'initier manuellement un arrêt du serveur Pi. Un bouton-poussoir semblable est entre le signal de réinitialisation et la masse pour redémarrer manuellement le serveur si tout le reste échoue ou pour redémarrer manuellement le serveur Pi s'il a été arrêté avec une commande telle que halt ou poweroff.

De même, deux boutons poussoirs normalement ouverts sont connectés au chien de garde Pi. Le bouton d'alimenatation, étiquetté 0/1, sera expliquée plus loin. Le bouton-poussoir de réinitialisation entre RUN et la masses redémarre le chien de garde Pi si cela devient nécessaire. Le bouton d'alimentation n'est pas utilisé dans le premier chien de garde dit maigre et moyen, mais c'est un ajout utile, comme expliqué ci-dessous.

Réglage du serveur surveillé toc

La préparation du Raspberry Pi qui est surveillé par le chien de garde matériel sera divisée en deux parties. Il faut créer un service (un démon ou procéssus) qui s'exécute en arrière plan pour signaler régulièrement au chien de garde que tout fonctionne correctement. Ensuite, le module d'arrêt GPIO doit être inclus dans le noyau et doit être lié à la broche E/S relié au signal d'arrêt. En activant cette broche, le chien de garde pourra arrêter le serveur de manière ordonnée.

Nourir le chien de garde toc

Le Pi qui est surveillé doit « alimenter » régulièrement le chien de garde. Il le fait en basculant l'état de la broche de sortie « signe de vie ». J'utilise un script Python pour ce faire. Tout d'abord, certains modules requis doivent être installés dans l'environnement virtuel Python pour les utilitaires système.

woopi@goldserver:~ $ ve .systempy (.systempy) woopi@goldserver:~ $ pip install --upgrade rpi.gpio gpiozero Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple ... Successfully installed colorzero-1.1 gpiozero-1.5.1 rpi.gpio-0.7.0 (.systempy) woopi@goldserver:~ $ (.systempy) woopi@goldserver:~ $ pip freeze colorzero==1.1 gpiozero==1.5.1 pkg-resources==0.0.0 RPi.GPIO==0.7.0
Environnements Python virtuels :

Je préfère utiliser des environnements virtuels pour le développement Python. Il y a plus d'une façon de le faire, celle utilisée ici a été décrite dans un billet plus ancien: Python 3 virtual environments. Dernièrement, je créé systématiquement un environnement virtuel pour les scripts « système » comme décrit dans la section sur les répertoires de travail d'un récent billet sur l'installation de Raspbian. Sur goldserver cet environnement est appelé .systempy.

Pour activer l'environnement virtuel, la commande ve est utilisée. Il s'agit d'un alias de source $1/bin/activate, ce qui signifie que ve .systempy est la même chose que source .systempy/bin/activate.

Il est très simple de créer un objet LED avec le module gpiozero qui clignote nominalement une DEL mais qui est en fait le signe de vie requis.

(.systempy) woopi@goldserver:~ $ nano .systempy/wdfeed.py

#!/home/woopi/.systempy/bin/python # Python 3 script that toggles a GPIO output pin on at regular interval. # This is the heartbeat signal meant to feed a hardware watchdog. ### User settable values ########################################## HEARTBEAT_PIN = 17 # GPIO17 = header pin 11 HEARTBEAT_INTERVAL = 5 # seconds wait between heartbeats ################################################################### from gpiozero import LED from signal import pause led = LED(HEARTBEAT_PIN, initial_value=False) led.blink(on_time=0.2, off_time=HEARTBEAT_INTERVAL) pause()

Nourrir le chien de garde à la dure toc

Déçu que le script pour nourrir le chien de garde soit si simple ? Si trois petites lignes de code vous suffisent, passez à la section suivante, mais si retrousser les manches ne vous fait pas peur, lisez la suite.

Les vrais programmeurs n'utilisent pas de nouveautés comme gpiozero. Donc, si vous l'aviez installé en suivant les instruction de la section précédente, vous pouvez supprimer le module de l'environnement virtuel. Autant supprimer colorzero ce qui a été installé avec gpiozero.

(.systempy) woopi@goldserver:~ $ pip uninstall gpiozero colorzero Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple ...

Si vous n'avez pas suivi la démarche de la section précédente, alors démarrez l'environnement virtuel et installez le module RPi.GPIO à ce stade.

woopi@goldserver:~ $ ve .systempy (.systempy) woopi@goldserver:~ $ pip install --upgrade rpi.gpio ...

Qu'importe comment on est arrivé ici, l'environnement virtuel doit maintenant contenir le module RPi.GPIO.

(.systempy) woopi@goldserver:~ $ pip freeze pkg-resources==0.0.0 RPi.GPIO==0.7.0

L'idée est de créer une minuterie qui activera la broche de sortie à des intervalles réguliers. Évidemment, ce cycle doit se répèter indéfiniment. Cependant, les minuteries Python sont des objets plutôt simples ne s'exécutant qu'une fois. Enfin si je dit simple c'est en comparaison aux temporisateurs de Free Pascal qui peuvent déclancher une action qu'une fois ou à répétion. Heureusement, right2clicky a créé RepeatTimer, une classe élégante sous-classe de Timer qui est exactement ce dont nous avons besoin (voir StackOverflow.)

(.systempy) woopi@goldserver:~ $ nano .systempy/wdfeed.py

#!/home/woopi/.systempy/bin/python ''' Python 3 script that toggles a GPIO output pin on/off twice at regular interval. This is the heartbeat signal meant to feed a hardware watchdog. ''' ### User settable values ########################################## HEARTBEAT_PIN = 17 # GPIO17 = header pin 11 HEARTBEAT_INTERVAL = 5 # seconds wait between heartbeats ################################################################### import atexit from time import sleep import RPi.GPIO as GPIO from threading import Timer from signal import pause # Subclassed Timer that will restart itself after executing the function # specified when created. It will execute the same function over and over # at the specified interval. Reference: # right2clicky on StackOverflow: https://stackoverflow.com/a/48741004 # class RepeatTimer(Timer): def run(self): while not self.finished.wait(self.interval): self.function(*self.args, **self.kwargs) def gpioCleanup(): #print("gpioCleanup") GPIO.cleanup() def toggleHeartbeat(): #print("toggleHeartbeat") GPIO.output(HEARTBEAT_PIN, GPIO.HIGH) sleep(0.2) GPIO.output(HEARTBEAT_PIN, GPIO.LOW) atexit.register(gpioCleanup) GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(HEARTBEAT_PIN, GPIO.OUT, initial=GPIO.LOW) timer = RepeatTimer(HEARTBEAT_INTERVAL, toggleHeartbeat) timer.start() pause()

Service signe de vie toc

Une DEL peut être connectée à la sortie GPIO17 (broche physique 11) pour tester l'un et l'autre des deux scripts élaborés dans la section précédente. N'oubliez pas la résistance de limitation de courant et respectez la polarité de la DEL. Avec la deuxième version, on peut remplacer la DEL en activant l'instruction print dans la fonction toggleHeartbeat. Rendez le script exécutable puis exécutez-le et vérifiez que la DEL clignote pendant deux dixièmes de seconde toutes les cinq secondes ou que toggleHeartbeat est imprimée sur la console toutes les cinq secondes.

(.systempy) woopi@goldserver:~ $ ev woopi@goldserver:~ $ sudo chmod +x .systempy/wdfeed.pv woopi@goldserver:~ $ .systempy/wdfeed.pv toggleHeartbeat toggleHeartbeat ...

Appuyez sur la combinaison de touches CtrlC pour arrêter l'exécution du script. Remarquez comment l'environnement virtuel a été désactivé, mais le script s'est exécuté correctement même si les modules Python requis ne sont pas installés dans le répertoire Python par défaut. C'est parce que la ligne "shebang" #!/home/woopi/.systempy/bin/python, au début du script informe le système qu'il doit utiliser l'interpréteur Python de l'environnement virtuel .systempy pour exécuter le script. Cet interpréteur se trouve dans le répertoire home/woopi/.systempy/bin. Il est important d'ajuster le shebang pour identifier le bon répertoire. Si l'on se trompe, alors bash émettra un message d'erreur.

woopi@goldserver:~ $ .systempy/wdfeed.pv -bash: .systempy/wdfeed.py: home/woopi/.systempy/bin/python: No such file or directory

N'oubliez pas de régler la constante HEARTBEAT_PIN = 17 sur la bonne broche E/S si le montage du chien de garde et du serveur est différent de celui présenté ci-dessus. À mon avis, l'identité de la broche E/S et le chemin de répertoires vers Python sont les deux seules erreurs possibles, à part une faute de frappe, bien sûr. Si le script fonctionne correctement, vous souhaiterez peut-être le protéger en écriture.

woopi@goldserver:~ $ ls -l .systempy/wdfeed.py -rwxr-xr-x 1 woopi woopi 605 Feb 6 15:40 .systempy/wdfeed.py woopi@goldserver:~ $ sudo chmod -w .systempy/wdfeed.py woopi@goldserver:~ $ ls -l .systempy/wdfeed.py -r-xr-xr-x 1 woopi woopi 605 Feb 6 15:40 .systempy/wdfeed.py

Le script doit être exécuté automatiquement à chaque départ du système d'exploitation du Raspberry Pi. Cela peut être réalisé avec une tâche cron effectuée à chaque redémarrage du serveur.

woopi@goldserver:~ $ woopi@goldserver:~ $ sudo crontab -e

Ajoutez la dernière ligne qu'on peut voir ci-dessous.

... # For more information see the manual pages of crontab(5) and cron(8) # # m h dom mon dow command @reboot /home/woopi/.systempy/bin/python /home/woopi/.systempy/wdfeed.py &

Bien que cela soit simple à mettre en place, il est préférable d'exécuter le script en arrière-plan en tant que démon (selon l'ancienne terminologie ou service selon la nouvelle nomeclature Linux). Voici une unité de base pour faire cela dans le système d'initialisation systemd utilisé dans Raspbian.

[Unit] Description=Hardware watchdog feeding service After=network.target [Service] Type=simple Restart=always RestartSec=1 User=root ExecStart=/home/woopi/.systempy/wdfeed.py [Install] WantedBy=multi-user.target

Créez ce fichier en tant que super utilisateur et enregistrez-le dans le répertoire /etc/systemd/system. Un moyen simple de le faire est de démarrer l'éditeur nano, pour y coller le texte affiché ci-dessus.

woopi@goldserver:~ $ sudo nano /etc/systemd/system/wdfeed.service

Utilisez l'utilitaire systemctl pour démarrer le service, puis pour l'activer afin qu'il soit automatiquement démarré au redémarrage du serveur.

woopi@goldserver:~ $ sudo systemctl start wdfeed.service woopi@goldserver:~ $ sudo systemctl enable wdfeed.service

L'intérêt de cette approche est qu'il sera dorénavant facile d'arrêter le système.

woopi@goldserver:~ $ sudo systemctl stop wdfeed.service

Ce sera un bon moyen de tester le chien de garde plus tard. De plus, il est facile de vérifier que le service fonctionne correctement.

woopi@goldserver:~ $ sudo systemctl status wdfeed.service ● wdfeed.service - Hardware watchdog feeding service Loaded: loaded (/etc/systemd/system/wdfeed.service; disabled; vendor preset: enabled) Active: active (running) since Wed 2020-02-05 20:18:35 AST; 6s ago Main PID: 720 (wdfeed.py) Tasks: 2 (limit: 2319) Memory: 4.6M CGroup: /system.slice/wdfeed.service └─720 /home/woopi/.systempy/bin/python /home/woopi/.systempy/wdfeed.py Feb 05 20:18:35 goldserver systemd[1]: Started Hardware watchdog feeding service.

Module d'arrêt toc

Le module d'arrêt, gpio-shutdown a déjà été longuement discuté dans la section 5 d'un article précédent : Démarrage à chaud et à froid du Raspberry Pi. Il n'est pas nécessaire de ressasser le sujet. J'ai apporté trois modifications au fichier de configuration config.txt.

woopi@goldserver:~ $ sudo nano /boot/config.txt
... # Uncomment some or all of these to enable the optional hardware interfaces #dtparam=i2c_arm=on #dtparam=i2s=on #dtparam=spi=on # For access to I2C RTCand other I2C devices on hardware I2C bus (SDA on GPIO2, SCL on GPIO3) dtoverlay=i2c-rtc,ds3231 ## - compatible with gpio-shutdown as long as gpio_pin is not 2 or 3 dtoverlay=gpio-shutdown,gpio_pin=27 ... # Connect mini-UART to the GPIO header # This implies core_freq=250, a performance hit so disable this if not needed # Tx on BCM GPIO 14, Rx on BCM GPIO 15 [pins 8 and 10 on the GPIO header respectively]. # Refence: https://www.raspberrypi.org/documentation/configuration/uart.md enable_uart=1 ...

Deux modifications, affichées en bleu, étaient facultatives. Elles activent le mini-UART ainsi que le contrôleur matériel I2C et installe le pilote I2C pour une horloge matérielle utilisant la puce DS3231. Il est réconfortant de voir que l'utilisation de ces périphériques est compatible avec l'ajout nécessaire du module gpio-shutdown indiqué en rouge. Ce dernier surveillera l'entrée GPIO27 et déclenchera un arrêt ordonné chaque fois que cette broche est activée (basculement de HIGH vers LOW) soit avec le bouton-poussoir soit par le chien de garde. Le mini-UART rend possible la connexion série sur les broches physiques 8 et 10 du connecteur E/S du Raspberri Pi. Avec cette connexion on peut voir plus facilement si un arrêt ordonné se produit ou non. Une fois que le chien de garde fonctionne correctement, l'UART est désactivé car il ralentit légèrement le système.

Cela termine les modifications qui doivent être apportées au serveur.

Réglage du chien de garde toc

Je présenterai trois versions du script de surveillance Python. La première version rudimentaire émule le chien de garde pour plateforme d'extraction de cryptomonnaie tout en corrigeant les problèmes associés au chien de garde matériel sans en faire plus. Avec la deuxième version, le « chien de garde obéissant », il sera possible d'utiliser son bouton d'alimentation pour arrêter le serveur sans que le chien de garde ne le redémarre. Dans la version finale, le « chien de garde obéissant et aboyeur », enregistre ses actions dans le journal du système et si possible, envoie une notification par courriel lors du redémarrage du serveur.

Comme mentionné dans l'introduction de cette série d'articles, un ancien Raspberry Pi est utilisé comme proxy pour un Raspberry Pi Zero ou Raspberry Pi Zero W qui sont de meilleurs choix en raison de leur taille et de leur prix.

pi@wdog:~ $ cat /proc/device-tree/model Raspberry Pi Model B Rev 2 pi@wdog:~ $ cat /proc/cpuinfo | grep Revision Revision : 000e

C'était le dernier Raspberry Pi modèle 1 avec seulement deux ports USB et un connecteur E/S à 26 broches. Comme le Zero, il a 512 Mo de mémoire vive. Les deux ont le même système sur puce Broadcom (BCM2835) avec le même processeur ARM à un cœur (ARM1176JZF-S). Le Zero fonctionne à 1 GHz tandis que le modèle 1 a une vitesse d'horloge inférieure de 700 MHz dont la cadence peut être accélérée.

Le système d'exploitation est Rasbian Buster Lite (kernel 4.19) version 2019-09-26 auquel seules quelques modifications ont été apportées. Le nom d'hôte est wdog plutôt que raspberrypi alors que l'utilisateur par défaut est encore nommé pi. L'environnement virtuel Python pour les utilitaires système tels que le chien de garde est nommé .systempy. Pour plus de détails, consultez l'article intitulé Installation and Configuration of Raspbian Buster Lite.

Un adaptateur USB Wi-Fi facilite la vie car il n'y a pas de moyen simple de se connecter au réseau local avec Ethernet dans la pièce où cette expérience est exécutée. Heureusement, l'adaptateur est basé sur la puce RTL8188CUS de Realtek qui prise en charge par Buster.

pi@wdog:~ $ lsusb Bus 001 Device 004: ID 0bda:8176 Realtek Semiconductor Corp. RTL8188CUS 802.11n WLAN Adapter

Le Raspberry Pi Zero n'a pas de capacités réseau conventionnelles, mais je comprends qu'il est possible d'ouvrir des sessions SSH en utilisant une connexion USB entre le Raspberry Pi Zero et un ordinateur de bureau.

Comme toujours, le système d'exploitation a été mis à niveau juste avant de démarrer ce projet.

pi@wdog:~ $ sudo apt update; sudo apt upgrade -y

Comme de plus en plus de modifications sont apportées au système d'exploitation après la mise à jour de l'image de téléchargement par la Fondation Raspberry, la mise à jour prendra plus de temps. Étant donné que l'image avait quatre mois et que le Raspberry Pi a un processeur relativement sous-alimenté, j'ai eu le temps de déjeuner rapidement à ce stade. N'oubliez pas le paramètre -y si vous souhaitez que cette mise à niveau se poursuive sans surveillance.

Chien de garde rudimentaire toc

L'objectif ici est de reproduire le chien de garde pour plateforme d'extraction de cryptomonnaie tout en surmontant ses principaux inconvénients. Dans cette mesure, ce chien de garde minimal doit

Le chien de garde sera mis en oeuvre avec un script Python. Encore une fois, le module RPi.GPIO est prérequis qui doit être ajouté dans l'environnement virtuel Python pour les utilitaires système.

pi@wdog:~ $ ve .systempy (.systempy) pi@wdog:~ $ pip install --upgrade rpi.gpio Looking in indexes: https://pypi.org/simple, https://www.piwheels.org/simple ... Successfully installed rpi.gpio-0.7.0 (.systempy) pi@wdog:~ $ pip freeze pkg-resources==0.0.0 RPi.GPIO==0.7.0

Après avoir désactivé l'environnement virtuel, le script a été créé.

(.systempy) pi@wdog:~ $ ev pi@wdog:~ $ nano .systempy/wdog.py

#!/home/pi/.systempy/bin/python # coding: utf-8 ### User settable values ##################################################### # Timing constants CHECK_INTERVAL = 10 # seconds between checks of the last alive signal WATCHDOG_TIMEOUT = 45 # seconds without an alive signal before rebooting the server PULSE_TIME = 0.3 # length (seconds) of pulses sent to the server shutdown and reset pins SHUTDOWN_DELAY = 25 # time allowed (seconds) for the server to shut down RESET_DELAY = 5 # time allowed (seconds) for the server to cold boot START_COUNT = 4 # number of initial heartbeats to start watchdog # Watchdog GPIO connections HEARTBEAT_GPIO = 17 # watchdog input connected to server's alive pin SERVER_SHUTDOWN_GPIO = 27 # watchdog output connected to server's shutdown pin SERVER_RESET_GPIO = 22 # watchdog output connected to server's RUN pin ############################################################################## ## Global variables ## startCount = 0 # Count of initial heartbeats aliveTime = 0.0 # last time the alive signal received from server watchdogActive = False # True = watchdog was started by initial hearbeat ## Required modules ## import RPi.GPIO as GPIO from threading import Timer from signal import pause from subprocess import check_call import os import time # Common routine to assert a normally HIGH GPIO pin LOW for a short # period of time and to optionally sleep for a specified amount of time # def pulseServerPin(aPin, sleepTime=None): global watchdogActive watchdogActive = False #print("pulseServerPin {} low".format(aPin)) GPIO.output(aPin, GPIO.LOW) time.sleep(PULSE_TIME) GPIO.output(aPin, GPIO.HIGH) if sleepTime: #print("waiting {} seconds after pulse".format(sleeptime)) time.sleep(sleepTime) # Restarts the watchdog so that it resumes waiting for an initial # feeding before starting # def initWatchdog(): global watchdogActive global startCount print("Watchdog - resetting watchdog") watchdogActive = False startCount = 0 # Shuts down the server by activating its shutdown pin and, # after a delay to allow a proper shutdown of the OS, it # restarts the server by activating its RUN pin. Sleeps # to allow the boot process to complete on the server # def rebootServer(): global aliveTime global watchdogActive global startCount print('Watchdog - rebooting server') if not watchdogActive: print('Watchdog - already rebooting') return # Shut down the server properly and then wake it up pulseServerPin(SERVER_SHUTDOWN_GPIO, sleepTime=SHUTDOWN_DELAY) pulseServerPin(SERVER_RESET_GPIO, sleepTime=RESET_DELAY) # Reset the watchdog initWatchdog() # Subclassed Timer that will restart itself after executing the function # specified when created. It will execute the same function over and over # at the specified intervals. # Reference: # right2clicky on StackOverflow: https://stackoverflow.com/a/48741004 # class RepeatTimer(Timer): def run(self): while not self.finished.wait(self.interval): self.function(*self.args, **self.kwargs) # Routine called by the timer at regular intervals (CHECK_INTERVAL) # to check last time the server sent heartbeat. Reboots the server if # the alive signal has not been received for too long a period # def checkAlive(): if watchdogActive and (time.time() - aliveTime > WATCHDOG_TIMEOUT): print("Watchdog - watchdog timed out after {0:.2f} seconds".format(time.time() - aliveTime)) rebootServer() # This is the call back routine for the interrupt generated by the # server heartbeat signal. It updates the time at which the signal was # received. If the watchdog has not been started then it increments # the number of times a heartbeat has been detected and if it is # now large enough, the watchdog is started. # def aliveCallback(channel): global watchdogActive global startCount global aliveTime aliveTime = time.time() if not watchdogActive: startCount += 1 #print("Watchdog - startCount: ", startCount) if startCount > START_COUNT: print("Watchdog - watchdog started") watchdogActive = True # Setup the GPIO pins GPIO.setwarnings(False) GPIO.setmode(GPIO.BCM) GPIO.setup(SERVER_SHUTDOWN_GPIO, GPIO.OUT, initial=GPIO.HIGH) GPIO.setup(SERVER_RESET_GPIO, GPIO.OUT, initial=GPIO.HIGH) GPIO.setup(HEARTBEAT_GPIO, GPIO.IN, pull_up_down=GPIO.PUD_UP) GPIO.add_event_detect(HEARTBEAT_GPIO, GPIO.FALLING, callback=aliveCallback) # Setup the timer aliveTime = time.time() timer = RepeatTimer(CHECK_INTERVAL, checkAlive) # Run the watchdog timer.start() print("Watchdog - watchdog loaded") try: pause() finally: GPIO.cleanup() if timer.is_alive(): timer.cancel() print("Watchdog - watchdog terminated")

Le script, renommé wdog_lm.py pour distinguer les trois versions, peut être téléchargé en cliquant sur le lien, mais voici un moyen rapide d'obtenir le script, de le renommer wdog.py et de le rendre exécutable.

pi@wdog:~ $ wget -O .systempy/wdog.py https://sigmdel.ca/michel/ha/rpi/dnld/wdog_lm.py pi@wdog:~ $ sudo chmod +x ./syspy/wdog.py

Si le répertoire de l'environnement virtuel n'est pas nommé .systempy les commandes ci-dessus devront être ajustées ainsi que la première ligne "shebang" du script.

S'il n'y avait pas de commentaires, il serait plus évident qu'il s'agit d'un court script avec peu de choses. Chaque fois que le serveur donne un signe de vie, une interruption se produit et son gestionnaire, aliveCallback met à jour l'heure de réception du signal. Le minuteur checkAlives'exécute régulièrementqui et il redémarrera le serveur si le dernier signe de vie est trop vieux.

Les lecteurs attentifs auront remarqué que le code de nettoyage n'a pas été enregistré auprès de atexit comme cela a été fait dans le script précédent. Au lieu de cela, l'instruction pause est insérée dans un bloc try... finally et le code de nettoyage est effectué dans la clause finally dont l'exécution est assurée. Le nettoyage inclut désormais l'annulation du minuteur, sinon la combinaison de touches CtrlC n'arrêtera pas le minuteur. Notons que la classe RepeatTimer présentée ci-dessus est à nouveau utilisée.

Le redémarrage du serveur se fait en deux étapes. Tout d'abord, il est arrêté correctement en activant la broche GPIO du serveur prise en charge par le module gpio-shutdown. Le chien de garde attend alors que l'arrêt soit effectué. Après un délai approprié, le serveur est redémarré en activant sa broche RUN. Cette approche en deux étapes fournit un mécanisme de sécurité intégrée. Si le serveur avait déraillé au point que l'activation de la broche GPIO liée à gpio-shutdown n'a pu arrêter correctement le système d'exploitation, la deuxième étape réinitialisera le système, quite à ce que l'arrêt du système ne soit pas fait dans les règles de l'art.

Notez que lorsque le chien de garde est démarré et même après qu'il redémarre le serveur, le chien de garde n'est pas actif et il ne redémarrera pas le serveur même en l'absence de signes de vie. Le chien de garde doit être activé, ce qui se produit lorsqu'il a reçu un nombre spécifique de pulsations du serveur. C'est exprès. Il est possible de désactiver le service de signe de viesur le serveur et après un redémarrage initial par le chien de garde, ce dernier ne tentera plus de redémarrer le serveur. De même, il est possible de changer le système d'exploitation sur le serveur et le chien de garde n'interférera pas lors de la mise à jour du système d'exploitation et de l'installation des services. Cette fonctionnalité de démarrage implique qu'il n'est pas nécessaire d'attendre la fin du processus de redémarrage du serveur Pi avant de redémarrer le chien de garde. Il attendra patiemment que le « battement cardiaque » du serveur reprenne avant de démarrer la surveillance du serveur. Je pense que c'est une idée astucieuse, mais ce n'est pas la mienne. Il peut être trouvé dans l'utilitaire watchdog de Linux (voir Chien de garde pour le Raspberry Pi et Domoticz ou la documentation man de watchdog).

Une conséquence heureuse de ne pas démarrer le chien de garde tant qu'il n'a pas reçu au moins un signal du système surveillé est qu'il n'a pas d'importance quel appareil est démarré en premier. Malheureusement, il importe de savoir qui est arrêté en premier. Si le serveur Raspberry Pi est fermé en premier et si le chien de garde a été démarré, ce dernier redémarrera le serveur après le délai d'expiration. Peu importe que l'arrêt du serveur soit fait avec un utilitaire de ligne de commande ou avec les boutons de redémarrage ou de réinitialisation. C'était également un problème avec l'ancien chien de garde matériel. La solution consiste à arrêter d'abord le script wdog.py ou d'arrêter le Pi qui agit comme chien de garde en premier.

Chien de garde obéissant toc

Il existe un moyen d'éviter partiellement le dernier problème. Au lieu d'utiliser les boutons d'arrêt ou de réinitialisation du serveur, le bouton d'alimentation connecté au Pi chien de garde sera utilisé. En effet, au cours de cette phase expérimentale, le bouton marche / arrêt pourra effectuer quatre fonctions différentes selon le nombre de pressions successives.

Nombre de fois actionné Fonction Note
1 Redémarrage du serveur Le chien de garde reste en marche
2 Arrêt du serveur Le chien de garde est en suspens jusqu'à ce que le serveur donne signe de vie
3 Redémarrage du chien de garde Sans effet sur le serveur
4 Arrêt du chien de garde

Le module gpiozero est ajouté à l'environnement virtuel pour son objet bouton pratique.

pi@wdog:~ $ ve .systempy (.systempy) pi@wdog:~ $ pip install gpiozero ... Successfully installed colorzero-1.1 gpiozero-1.5.1 (.systempy) pi@wdog:~ $ pip freeze colorzero==1.1 gpiozero==1.5.1 pkg-resources==0.0.0 RPi.GPIO==0.7.0

Seuls les ajouts apportés au script ~/.syspy/wdog.py sont affichés ci-dessous. Étant donné que le nombre de fois que le bouton est enfoncé rapidement de façon successive doit être compté, une minuterie (la variable globale appelée buttonTimer) est mise à jour chaque fois que le bouton est relâché. Si le bouton est enfoncé avant la fin du délai imparti, le compteur du nombre de fois que le bouton a été enfoncé est incrémenté et la minuterie est redémarrée.

#!/home/pi/.systempy/bin/python # coding: utf-8 ### User settable values ### # Timing constants ... BUTTON_WAIT = 0.5 # seconds to wait for a repeat power button press BUTTON_BOUNCE = 0.08 # seconds of debounce time for power button # Watchdog GPIO connections POWER_BUTTON_GPIO = 3 # watchdog input connected to watchdog power button ... ## Global variables ## ... buttonCount = 0 buttonTimer = None ## Required modules ## from gpiozero import Button ... # Routine performed when the power button timer times out # What is done depends on the number of times the power button was # pressed. The button count is reset. # def doButton(): global buttonCount count = buttonCount buttonCount = 0 if count &lt; 1: return elif count == 1: rebootServer() elif count == 2: print("Watchdog - shutdown server") pulseServerPin(SERVER_SHUTDOWN_GPIO, sleepTime=5) initWatchdog() elif count == 3: print("Watchdog - reboot watchdog") check_call(['/sbin/reboot']) # must be root for this to work else: print("halt watchdog") check_call(['/sbin/poweroff']) # must be root for this to work # Button released callback. It increments the power button release # count and starts a one shot timer that will call on doButton when it # times out. If a timer was already running, it is cancelled before # being # restarted. This is the mechanism to take care of multiple button # presses. # def buttonUpCallback(): global buttonCount global buttonDownTime global buttonTimer buttonCount += 1 if not buttonTimer is None: buttonTimer.cancel() buttonTimer = Timer(BUTTON_WAIT, doButton) buttonTimer.start() # Setup the power button button = Button(POWER_BUTTON_GPIO, bounce_time=BUTTON_BOUNCE) button.when_released = buttonUpCallback # Setup the GPIO pins ...

Si le délai imparti est expiré sans que le bouton soit activé, alors doButton est exécuté. Notez qu'il sera nécessaire d'exécuter le script en tant que root, ce qui sera le cas de toute falçon lorsque le script sera démaré comme service.

Pour essayer cette version, récupérez le script complet et rendez-le exécutable. Vous souhaiterez peut-être conserver l'ancien script comme indiqué dans la première ligne ci-dessous. Le script, wdog_o.py, peut également être téléchargé.

pi@wdog:~ $ mv ./syspy/wdog.py wdog_lm.py pi@wdog:~ $ wget -O .systempy/wdog.py https://sigmdel.ca/michel/ha/rpi/dnld/wdog_o.py pi@wdog:~ $ sudo chmod +x ./syspy/wdog.py

Bien sûr, cette solution n'empêche pas le chien de garde de redémarrer le serveur lorsque ce dernier est arrêté avec une commande bash. Voici quelques solutions possibles:

C'est amusant de spéculer sur ces solutions pour un problème somme tout mineure dans mon estimation. Avant de passer plus de temps à les examiner, il serait préférable de vérifier l'efficacité du chien de garde matériel.

Le chien de garde décrit ci-dessus a les capacités minimales qui seront requises de tous les autres appareils devant être évalués en tant que chiens de garde matériels. Cela se fera dans les prochains articles, comme annoncé dans l' introduction à cette série d'articles.

Chien de garde et aboyeur toc

Jusqu'à présent, le chien de garde a fait son travail très discrètement. Cela sera particulièrement vrai lorsque le chien de garde est exécuté en tant que service, car les messages des scripts wdog.py, destinés à faciliter les tests initiaux, ne seront pas visibles lorsque les scripts sont lancés par un service. Mais même le modeste Raspberry Pi Zero a des capacités de journalisation. J'ai donc converti les instructions d'impression en instructions de journalisation. Voici un exemple.

# Shuts down the server by activating its shutdown pin and, # after a delay to allow a proper shutdown of the OS, it # restarts the server by activating its RUN pin. Sleeps # to allow the boot process to complete on the server # def rebootServer(): global aliveTime global watchdogActive global startCount log(LOG_INFO, "Rebooting server") if not watchdogActive: log(LOG_INFO, "Already rebooting") return sendNotification(REBOOT_MSG) # Shut down the server properly and then wake it up pulseServerPin(SERVER_SHUTDOWN_GPIO, sleepTime=SHUTDOWN_DELAY) pulseServerPin(SERVER_RESET_GPIO, sleepTime=RESET_DELAY) # Reset the watchdog initWatchdog()

La fonction log ne fait qu'appeler la fonction syslog du module Python du même nom. Cette fonction imprime éventuellement le message destiné au journal sur la console fait dans le script précédent sauf pour l'ajout d'un horodatage. La fonction sendNotification fait appel à une fonction postmail pour envoyer un courrielchaque fois que le serveur Pi est sur le point d'être redémarré ou arrêté.

# Routine to send messages to syslog and echo it to the console # def log(level, msg): syslog(level, msg) if (VERBOSE) and (level <= CONSOLELOG_LEVEL): print(time.strftime('%Y-%m-%d %H:%M:%S ', time.localtime()) + msg) # Routine to send a notification (e-mail) # def sendNotification(msg): if SEND_NOTIFICATION: try: log(LOG_INFO, 'Sending e-mail notification') postmail(EMAIL_SUBJECT, msg.format(time.strftime('%Y-%m-%d %H:%M:%S ', time.localtime())), EMAIL_DESTINATION) log(LOG_INFO, 'E-mail notification sent') except BaseException as error: log(LOG_ERR, 'An exception occurred in postmail: {}'.format(str(error)))

Bien sûr, vous aurez besoin du script pymail.py contenant la fonction postmail. Le module et un fichier de « secrets » pymail_secrets.py, sont dans une archive qui peut être obtenue ici: pymail_0-2-0.zip. Les valeurs dans le fichier des secrets et dans pymail.py devront être ajustées. Si le chien de garde n'a pas accès à Internet, basculez la macro SEND_NOTIFICATION à false.

Si une session SSH peut être ouverte sur le Raspberry Pi qui agit comme chien de garde, il sera alors possible de voir les messages de journalisation en temps réel.

pi@wdog:~ $ journalctl -f SYSLOG_IDENTIFIER=PiWatchdog -- Logs begin at Fri 2020-02-07 16:10:03 AST. -- Feb 08 02:28:25 wdog PiWatchdog[3975]: Watchdog loaded Feb 08 02:28:49 wdog PiWatchdog[3975]: Watchdog started Feb 08 02:31:55 wdog PiWatchdog[3975]: Watchdog timed out after 51.29 seconds Feb 08 02:31:55 wdog PiWatchdog[3975]: Rebooting server Feb 08 02:32:28 wdog PiWatchdog[3975]: Resetting watchdog Feb 08 02:33:01 wdog PiWatchdog[3975]: Watchdog started

Enfin, la fonction du bouton d'alimentation a été simplifiée. Un simple clic sur le bouton redémarrera à la fois le chien de garde et le serveur. Un appui long sur le bouton d'alimentation éteindra le chien de garde sans rien faire au serveur. Une fois que le chien de garde sera arrêté, il sera possible de le redémarrer en appuyant à nouveau sur le bouton car ce dernier est connecté à la broche E/S GPIO3. Je pense qu'il est beaucoup plus probable que je me souvienne de ces deux actions possibles au lieu des quatre.

buttonPressedTime = None # Time when power button was pressed # Callback routine when power button is pressed # def buttonPressed(): global buttonPressedTime buttonPressedTime = time.time() # Callback routine when power button is released # def buttonReleased(): elapsed = time.time()-buttonPressedTime log(LOG_DEBUG, "Power button pressed for {0:.2f}".format(elapsed)) if elapsed > 3: log(LOG_INFO, "Power button pressed to shut down the server and watchdog") sendNotification(SHUTDOWN_MSG) pulseServerPin(SERVER_SHUTDOWN_GPIO) check_call(['/sbin/poweroff']) # must be root for this to work else: log(LOG_INFO, "Rebooting the server and watchdog") rebootServer() check_call(['/sbin/reboot']) # must be root for this to work ... # Setup the power button button = Button(POWER_BUTTON_GPIO, bounce_time=BUTTON_BOUNCE) button.when_pressed = buttonPressed button.when_released = buttonReleased

Lorsque les deux appareils sont en arrêtés, il sera alors possible de les redémarrer sans couper puis rétablir l'alimentation. Appuyez sur le bouton d'alimentation du chien de garde pour redémarrer ce dernier, le serveur peut être redémarré en appuyant sur son bouton de réinitialisation.

Pour essayer cette dernière version des scripts de surveillance présentés dans cet article, téléchargez-le et rendez-le exécutable. Encore une fois, vous voudrez peut-être enregistrer toute version précédente avant de télécharger cette version du script.

pi@wdog:~ $ mv ./syspy/wdog.py wdog_bak.py pi@wdog:~ $ wget -O .systempy/wdog.py https://sigmdel.ca/michel/ha/rpi/dnld/wdog_ob.py pi@wdog:~ $ sudo chmod +x ./syspy/wdog.py

Il faudra adapter quelques constantes au début du script.

Déchaîner le chien de garde toc

Tout ce qui doit être fait maintenant est de s'assurer que le script du chien de garde est exécuté automatiquement au démarrage du Raspberry Pi qui serveille le serveur. Cela se fait avec un fichier unité qui est presque identique à celui créé pour le signe de vie du serveur.

pi@wdog:~ $ sudo nano /etc/systemd/system/piwdog.service

[Unit] Description=Raspberry Pi Server Watchdog After=network.target [Service] Type=simple Restart=always RestartSec=1 User=root ExecStart=/home/pi/.systempy/wdog.py [Install] WantedBy=multi-user.target

Comme précédemment, il est facile d'effectuer les tâches usuelles.

  1. Démarrez le service :
    pi@wdog:~ $ sudo systemctl start piwdog.service
  2. Arrêter le service :
    pi@wdog:~ $ sudo systemctl stop piwdog.service
  3. Vérifier l'état du service :
    pi@wdog:~ $ sudo systemctl status piwdog.service
  4. Activer le lancement automatique du service au démarrage :
    pi@wdog:~ $ sudo systemctl enable piwdog.service
  5. Désactiver le lancement automatique du service au démarrage :
    pi@wdog:~ $ sudo systemctl disable piwdog.service

Horaire toc

How quickly should the hardware watchdog reboot the server when it no longer receives the heartbeat signal? It would be preferable that the home automation system be on line all the time to execute scheduled tasks as planned. That could lead one to decide on a fast response from the watchdog. However, provisions have been made in the circuit shown above for manual reboots of the server. Furthermore, I have rebooted the server from outside the house using one of the functions of the home automation system Domoticz more than once. When rebooting, the server will not be sending out heartbeat signals and it would be unfortunate if the starved watchdog were to reboot the server while it is in the process of booting. It would not be a catastrophe because when the watchdog itself reboots the server it knows to wait long enough for the server to reboot before trying to restart it. Actually, the watchdog does not know much of anything, the script contains no less than seven timing constants that will probably need to be adjusted in actual use.

# Timing constants CHECK_INTERVAL = 10 # seconds between checks of the last alive signal WATCHDOG_TIMEOUT = 45 # seconds without an alive signal before rebooting the server PULSE_TIME = 0.3 # length (seconds) of pulses sent to the server shutdown and reset pins SHUTDOWN_DELAY = 25 # time allowed (seconds) for the server to shut down RESET_DELAY = 5 # time allowed (seconds) for the server to cold boot START_COUNT = 4 # number of initial heartbeats to start watchdog BUTTON_BOUNCE = 0.08 # seconds of debounce time for power button

The script is basically event driven. One event is when the repeat timer expires. The CHECK_INTERVAL is the time between timeouts. When that happens, the event handler checks how long it has been since the last time a heartbeat was received from the server Pi. If that time exceeds WATCHDOG_TIMEOUT, then the watchdog tries to reboot the server. That time period should be greater than the time the server needs to reboot as explained above. Right now the timeout is set at 45 seconds, but the test server, a Raspberry Pi 3 B, is just a skeleton. It is important to actually time a few reboots and set the timeout in accordance with the measured time plus a safety margin just in case adding another piece of software adds to the boot time. The constant SHUTDOWN_DELAY is related, because it should correspond to the time needed by the server Pi to shut down which should be in approximately half the time needed for a complete reboot. It is important not to underestimate this time because when that interval is over the watchdog will activate the server RUN signal. If that happens too soon, the effect would be to stop the whole shutdown process instead of restarting a machine. The RESET_DELAY may seem very short at 5 seconds, but this is not a very important value. After all, the watchdog is reset after initiating a reboot of the server and it will then wait however long it takes for the server to send a few initial heartbeats (as specified by the START_COUNT constant) before beginning to function.

If the pulse activating the server shutdown GPIO pin is too short, it will not work. No doubt because of the debounce delay in the gpio-shutdown module. A 3 tenths of a second pulse seems ok, but if the shutdowns initiated by the watchdog do not seem to work, it may be worthwhile to increase the value of PULSE_TIME. The BUTTON_BOUNCE constant is not too critical. It matters more in the previous version of the script when the number of consecutive button presses was being counted. In this version, all that needs to be distinguished is a short versus a long button press and the debounce delay could easily be 3 or 4 times greater without creating much difficulty.

Testing toc

Testing of the watchdog was done with two approaches. The simplest is to turn on both Raspberry Pi and, after the watchdog has started, to stop feeding it.

pi@wdog:~ $ journalctl -f SYSLOG_IDENTIFIER=PiWatchdog -- Logs begin at Tue 2020-02-25 18:49:05 AST. -- Feb 25 18:49:37 wdog PiWatchdog[386]: Watchdog loaded Feb 25 18:50:14 wdog PiWatchdog[386]: Watchdog started

To help with the timing, the current time will be obtained just before halting the wdfeed service.

woopi@goldserver:~ $ date; sudo systemctl stop wdfeed.service Tue 25 Feb 18:58:24 AST 2020

Feb 25 18:59:13 wdog PiWatchdog[386]: Watchdog timed out after 51.26 seconds Feb 25 18:59:13 wdog PiWatchdog[386]: Rebooting server Feb 25 18:59:13 wdog PiWatchdog[386]: Sending e-mail notification Feb 25 18:59:16 wdog PiWatchdog[386]: E-mail notification sent Feb 25 18:59:46 wdog PiWatchdog[386]: Resetting watchdog Feb 25 19:00:19 wdog PiWatchdog[386]: Watchdog started

If the 51-seconds timeout seems excessive, remember that the watchdog checks the last time it was fed once every 10 seconds. Only if it has been more than 45 seconds since the last heartbeat was received will the watchdog initiate a reboot. So the time-out could be anywhere between 45 and 55 seconds. The SSH session opened with the server Pi was closed and the following message was received.

Rebooting the home automation server at 2020-02-25 18:59:13 local time.

This confirmed that the watchdog performed as expected. I ran the test overnight by adding the following cron task.

woopi@goldserver:~ $ crontab -e

... # For more information see the manual pages of crontab(5) and cron(8) # # m h dom mon dow command */15 * * * * sudo systemctl stop wdfeed.service

The server stops feeding the watchdog every fifteen minutes triggering the watchdog whcih reboots the server Pi. This was verified by looking at the incoming emails the following morning. To be pedantic, the task only happens once, 15 minutes after the server Pi boots up, but then the cycle repeat. That verifies the mechanics of the watchdog. But to see it in action, it was necessary to "crash" the server Pi. In the past I have used the forkbomb.sh script.

!/bin/bash # forkbomb swapoff -a :(){ :|:&amp; };:

It has the disadvantage of taking a relatively long while to use up all the resources. Others have come up with a similar script, let's call it crash.sh

#!/usr/bin/env bash # crash echo c | sudo tee /proc/sysrq-trigger # Reference: How to cause kernel panic with a single command? # Answer by artyom and Stephen Kitt # https://unix.stackexchange.com/a/66205

So that is my arsenal for testing.

woopi@goldserver:~ $ crontab -e

... # For more information see the manual pages of crontab(5) and cron(8) # # m h dom mon dow command */15 * * * * /home/pi/crash.sh #*/15 * * * * /home/pi/forkbomb.sh #*/15 * * * * sudo systemctl stop wdfeed.service

Do not forget to make the scripts executable, and remember that there is no point in enabling more than one of these tasks because the watchdog will reboot the server Pi when one of these tasks is first performed. (That's not exactly true, if crash.sh were started right after either of the other two, it would probably crash the Linux kernel before the previous task could be completed.

<-Démarrage à chaud et à froid du Raspberry Pi
<-Revoir le chien de garde matériel du Raspberry Pi