Mise à jour d’un système Linux embarqué « Over The Air » : comment intégrer et utiliser « Mender » pour vos déploiements

GNU/Linux Magazine n° 219 | octobre 2018 | Pierre-Jean TEXIER
Creative Commons
  • Actuellement 0 sur 5 étoiles
0
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Afin de mieux comprendre les enjeux liés à la mise à jour d’un système embarqué connecté (nous parlons bien d’(I)IoT !), nous mettrons en œuvre dans cet article, Mender, une solution OTA permettant la gestion des déploiements sur des systèmes Linux embarqués.

Smartphone (e.g Android :)), enceinte connectée, robot de cuisine, accessoire domotique, distributeur automatique de carburant, box internet et bien d’autres encore. Autant de produits connectés plus ou moins industriels qui rythment la vie de notre quotidien. Face à cette forte croissance, nombreux sont les objets où un accès physique pour déployer les mises à jour n’est absolument pas envisageable (coûts de déplacement, objet difficilement accessible…). C’est pourquoi, au travers de cet article, nous verrons comment intégrer une solution robuste et sécurisée pour la gestion des mises à jour en environnement Linux embarqué. Pour ce faire, nous mettrons en œuvre la solution Mender qui se veut être une des références open source pour la gestion du déploiement OTA (« Mender is an end-to-end open source updater for connected devices and IoT »), nous verrons ainsi comment faire le support sur la plateforme de référence, la WaRP7. Attachez votre ceinture !

1. Introduction

1.1 Pourquoi un système de mise à jour ?

Et pourquoi pas ! Malheureusement, bien que la présence d’un mécanisme de mise à jour soit parfois négligée dans les systèmes embarqués, il est pourtant un élément crucial pour la réussite de ce dernier sur le marché. Il permet entre autres le déploiement de nouvelles fonctionnalités logicielles, l’application de correctifs ou encore la correction des failles de sécurité. Voici quelques exemples :

  • Le logiciel est quelque chose de perfectible (oui désolé de vous l’apprendre) ! En effet, face à l’évolution constante (complexité), il n’est pas rare de devoir mettre à jour une flotte de périphériques suite à la découverte d’un bug logiciel. Il est donc primordial d’avoir un moyen de mettre à jour le système une fois celui-ci sur le marché, ceci afin de garantir la correction de défauts dans les plus brefs délais (pensez aux coûts de déplacement d’un technicien sur site !).
  • Le deuxième point concerne les ajouts de fonctionnalités au sein du produit. Être capable de déployer de nouvelles « features » après livraison, permet la mise en service sur le marché beaucoup plus tôt qu’un système dépourvu d’un mécanisme de mise à jour.
  • La sécurité ! Pour un système connecté, il est primordial de garantir une protection maximale afin de rendre l’objet non vulnérable aux différentes attaques/exploits venant de l’extérieur (injection de code, extraction de code propriétaire…). En effet, du noyau GNU/Linux aux différentes applications « userspace » (curl, syslog, openssl et bien d’autres encore), il est fréquent de découvrir des failles de sécurité. De plus, les exploits sont souvent connus publiquement, ceci rendant d’autant plus vulnérable le produit en question. Il est donc nécessaire de pouvoir déployer des mises à jour afin de se protéger des différentes attaques possibles. Pour information, CVE permet de lister les différentes vulnérabilités de sécurité, chaque vulnérabilité ayant son propre identifiant unique [1]. La figure 1 montre un exemple impactant libcurl.

Fig. 1 : CVE libcurl.

1.2 Oui, mais quels composants ?

De par sa conception, un système Linux embarqué « industriel » possède de façon générale les éléments suivants :

  • Bootloader : u-boot (ou barebox) pour l’embarqué. C’est le premier élément de la chaîne à être opérationnel lors de la mise sous tension. C’est lui qui s’occupe de l’initialisation des éléments vitaux, mais aussi de charger l’image noyau. De par son rôle important pour le bon fonctionnement de la cible, c’est donc un élément qui n’est que rarement mis à jour.
  • Image noyau : élément majeur du système que l’on retrouve généralement au sein de la partition du système de fichiers racine (/boot). On notera aussi la présence de(s) fichier(s) device-tree, et des parties non statiques à notre Image (les modules), dans /lib/modules/<kernel version>. À l’inverse du bootloader, l’image noyau fait partie des éléments à mettre à jour de façon régulière (correctifs, mises à jour de pilotes de périphériques, nouvelles fonctionnalités…).
  • Système de fichiers racine (Root Filesystem) : qui contiendra l’ensemble des applications, librairies, scripts de démarrage, ainsi que l’image noyau. Lors d’une mise à jour, il sera naturellement nécessaire de pouvoir remplacer l’ensemble des fichiers.
  • Données utilisateurs : configuration réseau, certificats, fichiers de logs. Ces données ne doivent  pas être mises à jour. Celles-ci seront donc stockées sur une partition spécifique (/data par exemple pour les données que l’on qualifiera de persistantes).

1.3 Mise à jour « Over the Air »

Derrière cette expression à la mode (je vous l’accorde) se cachent de nombreux concepts intéressants dans un contexte industriel autour des objets connectés.

En effet, à l’inverse des mises à jour sur site, qui se font localement et au cas pas cas, par un technicien en charge de réaliser cette tâche. Les mises à jour distantes seront réalisées sur la base des artefacts générés par le build system (image système + informations relatives à la version logicielle) et mis à disposition sur le serveur de déploiement. Ainsi, chaque périphérique sera en capacité de vérifier de façon régulière (polling du serveur), si une nouvelle version logicielle est disponible. La phase d’update du périphérique aura lieu si et seulement si :

  • la version diffère ;
  • celle-ci lui est destinée. Soyez rassuré, nous aborderons l’aspect artefact plus en détail lors de la découverte de Mender. Il faudra retenir que la principale force d’une solution OTA est de pouvoir gérer un parc d’objets connectés, tout en connaissant pour chacun, la version logicielle, le statut courant (update success, update failed) avec des logs à disposition et les informations systèmes (IP, MAC, CPU...).

Afin de schématiser la présentation sur les mises à jour OTA, la figure 2 montre la représentation graphique que propose Mender.

Fig. 2 : Architecture OTA.

1.4 Un peu de théorie !

Même si la mise à jour d’un système embarqué peut paraître à première vue une tâche somme toute facile (ce n’est ni plus ni moins que remplacer des fichiers me diriez-vous), c’est en réalité tout autre chose dans la pratique. Voici par exemple quelques cas de figure pouvant apparaître pendant ou après un déploiement d’une nouvelle version, questions auxquels il faudra répondre en amont de vos projets afin d’assurer une totale maîtrise du produit.

1.4.1 « Comment assurer la robustesse face aux coupures de courant lors d’une mise à jour logicielle ? »

Afin d’éviter de rendre un périphérique « inutilisable » suite à un déploiement interrompu, on parlera de mise à jour « atomique » (ensemble complet d’une image système), on oubliera donc les mises à jour incrémentales (par fichiers, gestionnaire de paquets ou via une simple archive avec ses scripts associés !) qui ne garantissent en rien la bonne installation d’une mise à jour. De plus, la solution incrémentale devient un mécanisme difficilement maintenable dans un environnement de production où différentes versions logicielles coexistent (gestion des dépendances, fichiers corrompus lors d’une installation, et bien d’autres encore). Il faudra de ce fait qu’aucun état intermédiaire inconnu n’existe lors d’une nouvelle installation, on parlera de succès de la mise à jour ou échec de la mise à jour, ceci afin de ne pas avoir de comportement tendancieux.

Pour garantir l’atomicité lors d’un nouveau déploiement et ainsi rester relativement sûr face aux coupures de courant, l’installation devra se faire sur une partition autre que celle en cours de fonctionnement, ceci dans le but ne pas rendre inutilisable le périphérique sur l’apparition d’un problème. Cela permettra une fois celle-ci installée, de redémarrer sur la partition fraîchement disponible (dans le cas d’une mise à jour avec succès bien entendu), rendant de surcroît l’ancienne partition inactive et disponible pour accueillir une prochaine mise à jour. Ce schéma tout à fait classique porte le nom de mise à jour symétrique (aussi schéma A/B ou schéma active/inactive voire même appelé « seamless updates » par Android [2]) où le principe est de posséder 2 rootfs identiques, 1 actif et l’autre inactif pour la gestion des mises à jour (voir figure 3).

Fig. 3 : Mise à jour symétrique. Partition A active.

Une fois la nouvelle mise à jour installée, le bootloader choisira la dernière partition à jour active et valide pour démarrer le système comme le montre la figure 4 (gestion par flag de l’updater).

Fig. 4 : Mise à jour symétrique. Partition B active.

Dans le cas d’une mise à jour interrompue (coupure de courant), aucun changement ne sera observé par le chargeur d’amorçage, car aucun flag mis à jour. Ainsi, aucune action ne sera entreprise. Ce qui aura pour effet de garder la dernière partition active pour démarrer le système, et donc de nous donner la possibilité de relancer une nouvelle séquence de mise à jour. Le système ne sera donc pas « brické » !

Un des avantages de cette technique est qu’elle réduit considérablement le « down time » (temps durant lequel le produit est inutilisable) sur site, où un redémarrage suffit. De plus, l’ensemble du système, ainsi que les fonctions vitales de celui-ci seront toujours opérationnels le temps de la mise à jour.

Une autre solution, appelé asymétrique, permet pour les systèmes ne disposant que d’un espace réduit, de faire cohabiter avec la partition principale, une partition que l’on appellera partition recovery, mécanisme que l’on pouvait retrouver sur les anciennes versions Android, laissant place maintenant aux mises à jour symétriques. Bien qu’une technique intéressante, celle-ci nécessite néanmoins :

  • de placer la cible dans un mode spécifique (mode upgrade) afin de pouvoir mettre à jour la partition principale ;
  • puis de redémarrer sur la nouvelle installation, ce qui allonge le « down time » du produit.

1.4.2 « L’application principale ne fonctionne pas comme attendu après la nouvelle mise à jour, le produit est inutilisable, que faire ? »

Parfois mal testée en amont, une nouvelle mise à jour logicielle peut parfois être installée de façon correcte, mais avoir un comportement inattendu une fois le système démarré. Prenons ici l’exemple de la serrure « intelligente » du fabricant Lockstate utilisée pour les locations Airbnb (mécanisme permettant à l’hôte de générer des codes d’accès pour le client lors de l’enregistrement). Malheureusement, suite à une mise à jour logicielle (assez hasardeuse je vous l’accorde, car celle-ci incluait un sous-ensemble logiciel pour un ancien modèle de serrure), l’application principale était dans l’incapacité de se reconnecter au serveur principal, rendant celle-ci inutilisable pour sa fonction principale [3]. Entraînant par la suite, le retour des serrures chez le fabricant et/ou le déplacement d’un technicien (€€€).

Afin d’éviter ce genre de mésaventure (retour produit pour analyse, déplacement d’un technicien et mauvaise pub !), il est primordial de disposer d’un mécanisme pour revenir en arrière (« Rollback ») dans le but de garantir un environnement fonctionnel et utilisable par l’utilisateur à tout moment. Pour ce faire, le mécanisme de mise à jour symétrique (schéma A/B) nous offre la possibilité de retourner sur une installation N-1 lors de la détection d’un quelconque problème. Il nous faudra en conséquence, après l’installation, une étape permettant de s’assurer que l’ensemble est conforme aux attentes. C’est ce qu’on appellera l’étape de « sanity checks ». C’est ici qu’il conviendra par exemple de s’assurer que la totalité des services a bien démarré, que l’accès au serveur de mise à jour est fonctionnel, que les fonctions critiques du système embarqué sont toujours opérationnelles, mais surtout de valider ou invalider la mise à jour ! Voici ci-après, la cinématique d’une séquence de mise à jour avec rollback :

  1. Démarrage sur la partition A ;
  2. Mise à jour de la partition B ;
  3. Reboot ;
  4. Démarrage sur la partition B ;
  5. Étape de « Sanity checks » → « Problème configuration application » → mise à jour non valide ;
  6. Déclenchement d’un redémarrage, car erreur sur le périphérique ;
  7. Rollback ! (on revient sur la partition de départ), car partition défaillante ;
  8. Démarrage sur la partition A ;
  9. Nouvelle mise à jour sur la partition B.

Naturellement, le mécanisme de « Rollback » n’est pas de facto intégré lors de l’utilisation d’un système embarqué. Pour ce faire, il faudra se reposer sur plusieurs fondamentaux.

1.4.2.1 La notion de « Watchdog »

C'est un mécanisme sous GNU/Linux permettant de redémarrer le système en cas de défaillance d’une application (à travers l’utilisation de /dev/watchdog).  Dans ce cas de figure, il est à la charge du développeur de faire sa propre application pour faire l’interface/dialogue avec /dev/watchdog. Mais pour les partisans du moindre effort, sachez que sur les systèmes embarqués récents il n’est pas rare de retrouver systemd comme système d’init qui, de par sa conception, intègre sa propre implémentation (une sorte de keep alive) que l’on peut associer à chaque application lancée au démarrage, plutôt intéressant, non ? Pour une utilisation via systemd, il faudra dans un premier temps, prévoir dans l’application principale, un appel régulier au bus via la fonction sd_notify(0, "WATCHDOG=1"), ceci pour signaler que le démon est toujours fonctionnel :

sd_notify(0, "READY=1\n");

[...]

while(1) {

 [...]

 sd_notify(0, "WATCHDOG=1");

}

Dans un second temps, il nous faudra paramétrer le fichier d’unité associé à l’application en question, où il faudra bien sûr renseigner les champs relatifs à l’utilisation du « keep alive » :

[Unit]

Description=Awesome GLMF application

[Service]

ExecStart=/usr/bin/awesome-glmf-app

WatchdogSec=10s

Restart=on-failure

StartLimitInterval=5min

StartLimitBurst=2

StartLimitAction=reboot

Dans cet exemple :

  • l’application awesome-glmf-app est lancée de façon automatique lors de la phase de démarrage ;
  • WatchdogSec permet de configurer le temps limite entre 2 appels de la fonction sd_notify(0, "WATCHDOG=1") ;
  • Restart, permet de spécifier le fait que notre application devra redémarrer de façon automatique lors d’un timeout ou si un signal anormal est apparu ;
  • StartLimitInterval et StartLimitBurst spécifient le nombre de redémarrages maximum dans l’intervalle en question. Ici, nous autorisons 2 restarts sur un maximum de 5 minutes ;
  • StartLimitAction permet d’indiquer l’action à effectuer lors des échecs successifs, dans notre cas, il est souhaitable de redémarrer la cible.

Voici ci-après un exemple de déclenchement d’une séquence de « reboot » après les 2 échecs de démarrage de l’application :

root@imx7s-warp:~#

[  OK  ] Stopped target Timers.

[  345.238339] imx2-wdt 30280000.wdog: Device shutdown: Expect reboot!

[  345.295169] reboot: Restarting system

Nous voici maintenant avec un mécanisme permettant de redémarrer un système possédant une anomalie logicielle durant la phase de sanity check, mais comme annoncé plus haut, il nous faudra intégrer une deuxième partie importante. En l’état actuel des choses, vous l’aurez compris, le système ne fera que des cycles de « reboot », ce qui n’est aucunement souhaitable. Pour ce faire, il est recommandé d’utiliser des mécanismes intégrés au chargeur d’amorçage et qui plus est u-boot.

1.4.2.2 CONFIG_BOOTCOUNT_LIMIT

Via cette option, il nous sera possible de détecter les cycles de redémarrage et ainsi effectuer des actions en conséquence (rollback !). Pour profiter de ce mécanisme dans notre environnement, il suffira d’ajouter l’option Devices Drivers > Enable support for checking boot count limit lors de la configuration de u-boot, maintenant disponible depuis Kconfig sur les dernières versions (voir figure 5).

Fig. 5 : Ajout de l’option BOOTCOUNT_LIMIT.

Une fois intégrée, celle-ci nous donne accès à 3 variables essentielles afin de réaliser une séquence de « rollback » lors d’un redémarrage continu :

  • bootcount : variable qui sera incrémentée à chaque redémarrage du processeur (valeur qui est remise à 1 sur un power-on-reset) ;
  • bootlimit : permet de spécifier le nombre maximal de redémarrages autorisé. Si bootcount est supérieur à cette valeur, alors la variable altbootcmd sera exécutée avant la commande classique bootcmd. Exemple avec l’implémentation de Mender : « altbootcmd=run mender_altbootcmd; run bootcmd » ;
  • altbootcmd : qui contiendra la séquence afin de réaliser un « rollback » sur une version précédente. Reprenons l’exemple précédent : mender_altbootcmd=if test ${mender_boot_part} = 2; then setenv mender_boot_part 3; else setenv mender_boot_part 2;. Cette commande permet le changement de partition. Il faut voir que si la partition courante est égale à /dev/mmcblk2p2 (rootfs A), alors la partition courante est maintenant égale à /dev/mmcblk2p3 (rootfs B). Aussi simple que ça !
Note

La commande bootcmd permet de définir un ensemble de commandes à exécuter de façon automatique une fois le décompte u-boot terminé (le fameux bootdelay). On retrouvera par exemple : le chargement du script boot.scr, le chargement du fichier device-tree, le chargement de l’image Noyau et dans le cas d’un System on Chipi.MX7, pourquoi pas, l’étape de flashage pour le cœur cortex m4. Pour de plus amples informations, le lecteur pourra se référer à la page suivante : https://www.denx.de/wiki/DULG/UbootEnvVariables.

1.4.2.3 CONFIG_BOOTCOUNT_ENV

Très utile pour nos besoins, elle permet de stocker dans l’environnement u-boot, la valeur de bootcount. Celle-ci introduit aussi une autre variable nommée upgrade_available. Elle permet par exemple, d’éviter les exécutions de la commande saveenv à chaque démarrage (action qui peut réduire le cycle de vie de la flash à disposition). Son principe est le suivant :

  • Si upgrade_available est égale à 0, alors la variable bootcount, n’est pas incrémentée. C’est cette commande qui permettra de valider la bonne installation d’une mise à jour ;
  • À l’inverse, si upgrade_available est égale à 1, bootcount sera alors incrémentée et stockée dans l’environnement. Pour les curieux, voici le code permettant de gérer cette partie (drivers/bootcount/bootcount_env.c) :

void bootcount_store(ulong a)

{

        int upgrade_available = env_get_ulong("upgrade_available", 10, 0);

        if (upgrade_available) {

                env_set_ulong("bootcount", a);

                env_save();

        }

}

La figure 6 nous présente l’option à implémenter pour avoir accès à cette fonctionnalité (ici aussi disponible depuis Kconfig sur les dernières versions, commit : « Convert CONFIG_BOOTCOUNT_ENV to Kconfig ») :

Fig. 6 : Sauvegarde du « boot counter » dans l’environnement u-boot.

Pour l’instant, nous ne disposons d’aucun moyen, côté espace utilisateur, pour positionner les différentes variables (mise à zéro de la variable bootcount ou encore mise à jour du flag permettant de spécifier la partition active (*boot_part), voire le mécanisme permettant la validation de la mise à jour (upgrade_available)). C’est donc ici qu’il sera important de mettre en œuvre les utilitaires u-boot permettant l’interaction entre la partie userspace GNU/Linux ↔ environnement u-boot. Pour ce faire, nous avons à disposition 2 commandes dans les sources de u-boot (<u-boot>/tools/env) :

  • fw_printenv, qui permet d’afficher la configuration stockée au sein de l’environnement u-boot :

root@imx7s-warp:~# fw_printenv --help

Usage: fw_printenv [OPTIONS]... [VARIABLE]...

Print variables from U-Boot environment

 -h, --help           print this help.

 -v, --version        display version

 -c, --config         configuration file, default:/etc/fw_env.config

[...]

  • fw_setenv, qui, comme son nom l’indique, permet de mettre à jour notre environnement (une simple variable par exemple) :

root@imx7s-warp:~# fw_setenv --help

Usage: fw_setenv [OPTIONS]... [VARIABLE]...

Modify variables in U-Boot environment

 -h, --help           print this help.

 -v, --version        display version

 -c, --config         configuration file, default:/etc/fw_env.config

[...]

Il faudra bien entendu que les commandes sachent où et comment l’environnement u-boot est stocké au niveau mémoire. Pour ce faire, il convient d’éditer le fichier /etc/fw_env.config, prenons un exemple simple :

/dev/mmcblk2            0x80000         0x2000

/dev/mmcblk2            0xA0000         0x2000

Ici, nous pourrons retrouver à l’offset 0x8000 notre environnement, avec une partie redondante (via l’utilisation de CONFIG_ENV_OFFSET_REDUND) à l’offset 0xA0000. Faisons un premier test en créant une variable au sein de notre environnement :

root@imx7s-warp:~# fw_setenv glmf mender

Il nous est maintenant possible de vérifier sa valeur :

root@imx7s-warp:~# fw_printenv glmf

glmf=mender

Si on reprend l’exemple vu un peu plus haut, la séquence de mise à jour deviendra la suivante :

  1. Démarrage sur la partition A ;
  2. Mise à jour de la partition B ;
  3. Mise à 1 de la variable upgrade_available → fw_setenv upgrade_available 1 ;
  4. Mise à zéro de la variable bootcount → fw_setenv bootcount 0 ;
  5. Mise à jour du flag par l’updater pour indiquer de démarrer sur la partition B ;
  6. Reboot automatique ;
  7. Démarrage sur la partition B ;
  8. Si mise à jour OK (sanity checks !), réinitialisation de la variable upgrade_availablefw_setenv upgrade_available 0 afin de valider l’installation ;
  9. La partition B est maintenant la partition active, A devient disponible pour une nouvelle mise à jour.

C’est donc sur ce concept que repose beaucoup de solutions de mise à jour OTA, et Mender en fait partie.

 

Redundant environment

Pour se prémunir des erreurs d’écritures ou des problèmes de perte d’alimentation, il est fortement recommandé d’utiliser la fonctionnalité de redondance d’environnement offerte par u-boot. Celle-ci, un peu comme la stratégie à double partition pour les mises à jour (A/B), permet de disposer de deux environnements u-boot, l’un actif et l’autre inactif. Ainsi, lors d’une mise à jour d’une variable (bloc de données), la mise à jour se fera sur l’environnement inactif, qui deviendra ensuite l’environnement actif, permettant à l’utilisateur de disposer à chaque moment, d’un environnement de secours (e.g corruption).

bootchooser

Pour les personnes hermétiques à l’utilisation de u-boot, sachez que Barebox propose aussi un mécanisme pour les systèmes embarqués ayant un schéma à plusieurs partitions. Celuise basant sur des notions de priorité. Afin de vous faire une idée, rien de mieux que de lire la documentation : https://barebox.org/doc/latest/user/bootchooser.html.

1.4.3 « Que faire vis-à-vis de la sécurité ? »

De plus en plus présente (et d’actualité avec les attaques DDoS par exemple !), la question de la sécurité est en effet assez récurrente dans le monde des systèmes embarqués. Il va s’en dire que les solutions de mises à jour « Over the Air » n’y échappent pas , hormis les différents aspects de sécurité plutôt généralistes à l’égard d’un développement produit :

  • notion de firewall (iptables) ;
  • une gestion correcte des mots de passe ;
  • des applicatifs à jour (dropbear, openssl, curl…).

L’utilisation d’une solution de mise à jour « Over the Air » demandera des efforts supplémentaires sur la question de la sécurité. Les exigences reposeront principalement sur deux axes :

  • Disposer d’une communication sécurisée entre le serveur de mise à jour et la cible embarquée (TLS par exemple). Ceci permettant de se prémunir des différents problèmes liés à l’analyse des différents flux du canal de communication par l’attaquant ;
  • La mise en œuvre d’un mécanisme d’authentification lors des mises à jour. On parlera ici de signature/vérification (via des mécanismes de clé privée, clé publique par exemple). C’est un moyen de garantir que la mise à jour est en provenance d’un serveur de confiance (et de rejeter un « faux » serveur).

1.5 Quelques noms !

Comme présenté plus tôt dans l’article, il est aujourd’hui très fortement déconseillé de créer sa propre solution de mise à jour (sur le sujet, l’auteur conseillera d’ailleurs la lecture du document en [4]). En effet, depuis quelques années, de nombreux travaux ont vu le jour autour de cette thématique qu’est la mise à jour logicielle en environnement Linux embarqué, tout ceci dans l’optique de fournir des solutions stables, robustes et éprouvées pour l’utilisateur final.

Même si nous allons parler de Mender dans la suite de l’article, faisons ici un petit tour sur l’état de l’art des solutions de mises à jour. Il existe aujourd’hui, un certain nombre de solutions open source envisageables, allant du simple framework hyper flexible aux solutions clés en main. Citons libostree, SWUpdate, RAUC, updatehub, Mender, resin.io, swupd et bien d’autres encore.

Prêtons ici, dans un premier temps, attention aux solutions plutôt orientées frameworks, solutions qui se veulent plus flexibles et entièrement configurables par l’utilisateur. RAUC et SWUpdate en font par exemple partie. Le premier se veut être un projet très complet et universel dans son intégration (compatible u-boot, barebox, GRUB, EFI). Il permet aussi la gestion des mises à jour symétriques et asymétriques, avec une granularité très intéressante suivant les cas de figure (voir figure 7).

Fig. 7 : Rauc et la gestion des mises à jour.

On notera aussi la compatibilité avec le très intéressant projet casync [5] (par le créateur de systemd), permettant la réalisation de mises à jour basées sur delta (support expérimental pour l’instant). Et puis, le projet propose aussi une interface D-Bus pour l’interface entre applications (hawkBit par exemple). Il est donc fortement recommandé d’aller jeter un œil à la documentation du projet pour en apprendre plus sur celui-ci : http://rauc.readthedocs.io/en/latest/index.html.

Très similaire au projet RAUC, SWUpdate (créé entre autres par Stefano Babic de la société Denx) permet aussi le support de plusieurs schémas de mise à jour, il propose l’intégration avec le projet psplash [6] (afficher la progression d’une mise à jour par exemple), la gestion de handlers customisés où on notera l’implémentation récente de SWUforwader, un handler permettant de mettre à jour d’autres cibles faisant tourner le client swupdate (une sorte de gateway), très utile dans une architecture maître/esclave où seul le maître profite d’une véritable connexion internet à l’inverse des esclaves, en connectivité restreinte. Là aussi, rendez-vous sur la page principale du projet http://sbabic.github.io/swupdate/index.html afin de découvrir l’ensemble des possibilités.

De plus, les 2 projets permettent de s’interfacer avec le back-end hawkBit pour la gestion des déploiements OTA (https://www.eclipse.org/hawkbit/). Malheureusement, SWUpdate et RAUC n’intègrent pas la fonctionnalité de rollback (il est à la charge du développeur d’intégrer cette fonctionnalité). Rassurez-vous, des exemples existent dans les projets respectifs (notamment avec l’utilisation de u-boot).

Enfin, pour finir sur la présentation des différentes solutions, parlons du projet updatehub créé et maintenu par la société brésilienne OSSystems (le CTO étant Otavio Salvador, mainteneur du BSP Yocto/OE de la communauté Freescale/NXP). Celui-ci est plutôt orienté « clé en main » et se rapproche un peu plus de Mender, avec un service de déploiement à disposition depuis l’interface web, comme présenté en figure 8.

Fig. 8 : Interface principale du projet updatehu.

Un mécanisme de rollback directement intégré, un support Yocto/OE très complet avec un grand nombre d’exemples d’intégration (raspberry-pi, beaglebone, odroid, warp7, pico-pi i.MX7, nitrogen6x…), ce qui facilite grandement l’ajout d’une nouvelle carte. Autre point intéressant, le projet updatehub met à disposition plusieurs SDKs afin de s’interfacer avec la partie client (affichage sur un écran déporté par exemple). On retrouvera du Python, du C++/Qt, du Go et d’autres en cours de développement. Ce projet  hérite aussi d’une bonne documentation, donc à vous de jouer : https://docs.updatehub.io.

2. Mender : « Over-the-air software updates for embedded Linux »

Après avoir longuement présenté les fondamentaux, nous allons dans cette partie, rentrer dans le monde merveilleux de Mender. Il sera question ici de présenter l’architecture et les différents fonctionnements de cette solution.

2.1 Faisons les présentations

Basé sur une architecture client/serveur, Mender est composé :

  • d’un agent en charge des mises à jour (client), situé sur le périphérique Linux embarqué, il est sous la forme d’une simple application (écrit en Go !) en espace utilisateur ;
  • un serveur de mise à jour : serveur qui sera en charge de gérer les différents déploiements, lister les périphériques (avec l’inventaire associé).

Pour parler en quelques mots de la partie serveur mise à disposition par Mender, il faut savoir que deux choix s’offrent à l’utilisateur afin d’utiliser l’outil. En effet, étant sous licence libre (Apache 2.0), la partie serveur peut tout à fait être hébergée sur une infrastructure personnelle, plutôt intéressant, non ? Pour des questions de maintenabilité et de gain de temps, nous utiliserons jusqu’à la fin de l’article la deuxième solution, qui consiste à utiliser le service mis à disposition par le projet Mender : Hosted Mender. Hosted Mender (figure 9) permettra donc de gérer l’aspect déploiement et interactions avec le(s) périphérique(s).

Fig. 9 : Hosted Mender.

2.1.1 Caractéristiques

Afin de garantir une mise à jour robuste avec le maximum de flexibilité, Mender utilise une stratégie de mise à jour reposant sur l’utilisation du schéma A/B, ce qui permet à l’utilisateur de profiter des mécanismes de Rollback (car 2 rootfs identiques, rappelons-le). Un exemple de l’ensemble des partitions mises à disposition lors d’une intégration avec Mender est visible en figure 10.

Fig. 10 : Partitions.

On y retrouvera 4 partitions nécessaires :

  • une partition boot (bootloader, environnement, scripts, …) ;
  • une partition data, permettant de stocker les données persistantes (comme évoqué précédemment) ;
  • une partition RootFS, la première qui sera active au démarrage de la cible, elle contient l’image noyau (zImage) et le fichier device-tree dans le répertoire /boot ;
  • une deuxième partition RootFS, la première qui sera inactive au démarrage de la cible, elle contient elle aussi l’image noyau (zImage) et le fichier device-tree dans le répertoire /boot.

Enfin, Mender se voulant être une solution clé en main, celle-ci propose au sein de son écosystème, un nombre non négligeable de fonctionnalités :

  • la partie serveur, comme précédemment évoqué ;
  • une communication basée TLS entre le client et le serveur ;
  • un mécanisme de rollback natif (basé sur les concepts présentés !) ;
  • le support eMMC/SD et UBI ;
  • la gestion des déploiements en local ;
  • une gestion pour la signature des artefacts ;
  • une gestion de scripts personnalisés (state script)...
  • ...et encore un grand nombre de possibilités ! Dont certaines que nous découvrirons en fin d’article.

2.1.2 Artefacts

Comme abordé lors de la présentation sur l’aspect « OTA », il est nécessaire d’approvisionner notre serveur de mise à jour avec ce qu’on appelle des artefacts : ce n’est ni plus ni moins qu’une image Filesystem qui servira à la partie client pour réaliser une mise à jour sur la partition non active. Afin d’être le plus robuste possible, Mender propose son propre format, facilement identifiable par l’extension .mender ; celui-ci permettra d’encapsuler :

  • notre image au format ext4, avec une somme de contrôle associée (pour se prémunir d’une éventuelle corruption) ;
  • ainsi que des métadonnées permettant :
  • d’identifier la version, non déployée si déjà installée sur la cible ;
  • de définir les cibles compatibles, un artefact pour BeagleBone ne pourra pas être installé sur la WaRP7 et inversement. Ceci permettra d’éviter le fameux problème de mise à jour auquel a dû faire face la société Lockstate.

Pour les plus curieux, sachez que la documentation de Mender est très riche (ce qui n'est pas le cas de tous les projets open source !), ainsi le lecteur désireux d’en apprendre davantage sur les artefacts, pourra se rendre à l’adresse suivante : https://docs.mender.io/1.4/architecture/mender-artifacts.

2.1.3 Le mode « standalone »

Bien que prévu pour fonctionner en mode client/serveur, Mender permet pour les systèmes dépourvus d’une connectivité internet ou d’une connectivité restreinte, de bénéficier d’un mécanisme de mise à jour local via USB ou par serveur HTTP. Par exemple, voici 2 commandes permettant de lancer une mise à jour depuis le périphérique lui-même :

root@imx7s-warp:~# mender -rootfs <chemin vers artefact>.mender

Ou :

root@imx7s-warp:~# mender -rootfs http://adresse-ip-du-serveur/artefact.mender

2.2 Yocto Project, encore et toujours !

Initialement développé autour du Projet Yocto (pour le plus grand bonheur de l’auteur !), Mender permet de s’intégrer facilement au sein de vos projets grâce aux différentes couches de métadonnées mises à disposition par la communauté (et l’entité juridique derrière le projet Mender, northern.tech). De plus, il existe plusieurs exemples d’intégrations. On retrouvera par exemple :

  • un exemple autour de l’utilisation de QEMU :  meta-mender-qemu ;
  • la Raspberry Pi (encore elle !) : meta-mender-raspberrypi ;
  • la plateforme i.MX7 Colibri de Toradex : meta-mender-toradex-nxp ;
  • un support pour les cartes Orange-pi : meta-mender-orangepi.

Enfin, on retrouvera la couche principale donnant accès aux fonctions principales de mender meta-mender-core.

Pour finir, c’est via notre build system favori (Yocto/OE), que nous génèrerons :

  • l’image système, avec les 4 partitions (au format .sdimg) ;
  • ainsi que l’artefact (.mender), qui nous servira à approvisionner notre serveur de mise à jour (Hosted Mender ici).

3. Mender : Intégration sur i.MX7

Respirez un grand coup ! Nous voilà dans la dernière partie de l’article, et c’est ici que nous allons faire l’intégration de Mender sur notre carte d’évaluation à disposition, la WaRP7 encore et toujours (oui, l’auteur a une forte préférence pour la série i.MX de NXP). Nous commencerons l’étude sur une grosse partie avec le bootloader u-boot, nous verrons quelles sont les modifications à apporter pour mener à bien le support. Nous ferons ensuite notre premier déploiement d’artefact, ceci via le mode standalone proposé par Mender, cette étape nous servira à explorer sur cible, les différents mécanismes abordés durant la première partie (flag, rollback...). S’ensuivra une partie avec Hosted Mender, pour la découverte de l’interface de déploiement, suivi d’une dernière partie où nous explorerons les fonctionnalités additionnelles que propose Mender (données persistantes).

3.1 Préparation de l’environnement

Afin de poser les bases de notre étude, commençons par télécharger l’ensemble des sources requises pour la bonne construction de notre image. Pour ce faire, nous utiliserons la couche BSP dédiée à l’utilisation de la WaRP7, celle-ci permet de disposer des couches de métadonnées suivantes :

  • poky ;
  • meta-freescale (3rdparty et distro) ;
  • meta-qt5 ;
  • meta-openembedded ;
  • meta-warp7-distro ;
  • meta-mender (incluant les sous-ensembles : core, raspberrypi, ...).

3.1.1 Téléchargement des sources

Assurons-nous de disposer de l’outil repo, puis exécutons les commandes ci-après pour lancer la récupération des sources :

$ mkdir ~/bin

$ curl http://commondatastorage.googleapis.com/git-repo-downloads/repo > ~/bin/repo

$ chmod a+x ~/bin/repo

$ PATH=${PATH}:~/bin

$ mkdir warp7_bsp

$ cd warp7_bsp

$ repo init -u https://github.com/texierp/yocto-warp7-bsp-repo -b rocko

$ repo sync -j8

Après quelques (longues) minutes, il nous est maintenant possible de configurer notre environnement. Spécifions le type de machine visée (imx7s-warp), la distribution (warp7, spécialement développée pour l’utilisation de notre carte) et notre répertoire de construction (warp7-build) :

$ MACHINE=imx7s-warp DISTRO=warp7 source setup-environment warp7-build/

...

Welcome to WaRP7 BSP !

3.1.2 Configuration

Disposant maintenant de tout le nécessaire, commençons à intégrer les éléments requis par Mender pour générer un ensemble cohérent et fonctionnel. La configuration qui suit pourra se faire dans le fichier définissant la distribution utilisée : le fichier warp7.conf (meta-warp7-distro/conf/distro/warp7.conf), néanmoins, pour des phases de tests et autres expérimentations, l’utilisation du fichier local.conf conviendra tout aussi bien.

La première exigence concerne l’implémentation des fonctionnalités de Mender. Du point de vue de notre Build system (Yocto/OE !), cela consistera à faire hériter (utilisation de la directive INHERIT) un ensemble de classes concernant chacune des fonctionnalités disponibles (meta-mender/meta-mender-core/classes/*.bbclass). Dans notre cas, nous choisirons d’implémenter ici l’ensemble des possibilités :

INHERIT += "mender-full"

Ceci nous donnera par exemple accès à la gestion/génération automatique des partitions pour le mode A/B (mender-image), un support pour l’utilisation de systemd par Mender (mender-systemd), et bien d’autres classes encore. Celles-ci sont documentées ici : https://docs.mender.io/1.4/artifacts/image-configuration/features.

Poky se trouve de base configurée pour fonctionner avec un système d’init basé sur sysVinit. Il nous faudra, afin d’avoir notre distribution au même niveau que la configuration de Mender, un système d’init basé sur systemd, pour ce faire, rien de plus simple, il suffit d’ajouter les lignes suivantes dans notre fichier :

# Use systemd as init system

VIRTUAL-RUNTIME_init_manager = "systemd"

DISTRO_FEATURES_BACKFILL_CONSIDERED = "sysvinit"

VIRTUAL-RUNTIME_initscripts = ""

DISTRO_FEATURES_append = " systemd"

Ensuite, l’artefact étant généré par notre outil de construction préféré, il sera nécessaire de définir le nom de celui-ci (le nom qui permettra de faire la différentiation au niveau des versions lors des déploiements) :

MENDER_ARTIFACT_NAME = "warp7-mender"

Plutôt facile, non ? Allez continuons ! Mender prenant en charge la génération des partitions, il devra être informé du périphérique de stockage cible lors de la construction, pour notre utilisation, la configuration sera la suivante :

MENDER_STORAGE_DEVICE = "/dev/mmcblk2"

Il nous faudra aussi spécifier au sein de quel périphérique est stocké l’environnement u-boot :

MENDER_UBOOT_STORAGE_INTERFACE = "mmc"

MENDER_UBOOT_STORAGE_DEVICE = "0"

MENDER_UBOOT_STORAGE_DEVICE correspondant à la configuration u-boot CONFIG_SYS_MMC_ENV_DEV.

Nous devrons aussi spécifier la recette spécifique à prendre en compte pour u-boot-fw-utils :

PREFERRED_PROVIDER_u-boot-fw-utils = "u-boot-fslc-fw-utils"

PREFERRED_RPROVIDER_u-boot-fw-utils = "u-boot-fslc-fw-utils"

Enfin, pour finir, il faudra s’assurer que l’image noyau, ainsi que le(s) fichier(s) device-tree soient déployés dans la partition Root Filesystem (/boot). Une fois de plus, la tâche nous est grandement facilitée par Yocto/OE où il suffira d’ajouter : kernel-image et kernel-devicetree (IMAGE_INSTALL += "kernel-image kernel-devicetree" ou MACHINE_ESSENTIAL_EXTRA_RDEPENDS = "kernel-image …").

3.2 u-boot : patch me if you can !

Mender propose une intégration grandement simplifiée pour l’interfaçage avec u-boot, qui plus est avec la version liée au projet Yocto (u-boot_2017.09 pour une utilisation avec rocko). En effet, il permet pour les cibles n’utilisant pas de « fork », une gestion automatique (patch) pour l’implémentation des différents mécanismes obligatoires (BOOTCOUNT_*). Malheureusement, étant liés au fork u-boot de la communauté Freescale (u-boot-fslc) de par la configuration de notre machine, il nous faudra procéder à une intégration manuelle, mais rassurez-vous, rien de bien compliqué. Let’s go !

Pour mieux appréhender les explications liées au portage, partons de l’implémentation déjà existante (faite par l’auteur), en parcourant les différents fichiers. Regardons dans un premier temps l’arborescence du dossier u-boot contenu au sein de la couche distribution (meta-warp7-distro) :

recipes-bsp/u-boot (rocko) $ tree .

.

├── files

│   ├── 0001-warp7-added-mender-requirements.patch

│   └── fw_env.config

├── u-boot-fslc_%.bbappend

├── u-boot-fslc-fw-utils_%.bbappend

└── u-boot-mender-warp7.inc

Intéressons-nous en premier lieu au fichier .patch pour y analyser les éléments importants :

diff --git a/include/configs/warp7.h b/include/configs/warp7.h

index 11f1bc3..eeaa988 100644

--- a/include/configs/warp7.h

+++ b/include/configs/warp7.h

@@ -113,11 +113,11 @@

 /* environment organization */

 #define CONFIG_ENV_SIZE   SZ_8K

-#define CONFIG_ENV_OFFSET  (8 * SZ_64K)

+#define CONFIG_BOOTCOUNT_ENV

+#define CONFIG_BOOTCOUNT_LIMIT

 

-#define CONFIG_SYS_MMC_ENV_DEV  0

-#define CONFIG_SYS_MMC_ENV_PART  0

[...]

On remarquera d’abord la suppression de plusieurs configurations. CONFIG_ENV_OFFSET doit en effet être maintenant définie dans la configuration du build system et non plus de façon statique (pour les aspects d’auto-configuration), mettons simplement à jour le fichier warp7.conf :

MENDER_UBOOT_ENV_STORAGE_DEVICE_OFFSET_1 = "0x80000"

Profitons-en aussi pour déclarer un environnement redondant :

MENDER_UBOOT_ENV_STORAGE_DEVICE_OFFSET_2 = "0xA0000"

Concernant les 2 autres suppressions, vous remarquerez que nous les avons surchargés en fin de configuration de l’environnement (variables MENDER_UBOOT_STORAGE_*).

Pour ce qui est des ajouts (CONFIG_BOOTCOUNT_ENV et CONFIG_BOOTCOUNT_LIMIT), c’est naturellement qu’il faudra les implémenter (gestion retour en arrière) comme vu plus tôt dans l’article. Mais ici dans la définition de la carte et non via Kconfig. Le fork uboot-fslc sur la branche rocko étant basé sur la version 2017.11, celle-ci n’intègre donc pas les dernières modifications (2018.03 par exemple).

Au niveau du fichier fw_env.config, comme déjà présenté en introduction, il permettra de spécifier les différents emplacements mémoires de l’environnement u-boot, il faudra donc lui spécifier les différents paramètres que nous venons de configurer au sein du fichier définissant notre distro :

/dev/mmcblk2 0x80000 0x2000

/dev/mmcblk2 0xA0000 0x2000

Il nous faudra par la suite, créer un fichier permettant de faire le lien avec les recettes dérivées qu’il faudra implémenter. Il sera donc primordial de créer dans un premier temps, un fichier u-boot-mender-warp7.inc, puis d’y renseigner les paramètres suivants ;

FILESEXTRAPATHS_prepend := "${THISDIR}/files:"

SRC_URI += "\

 file://0001-warp7-added-mender-requirements.patch \

 file://fw_env.config \

"

MENDER_UBOOT_AUTO_CONFIGURE = "0"

BOOTENV_SIZE = "0x2000"

  • FILESEXTRAPATHS_prepend := "${THISDIR}/files:" : permet de spécifier à Bitbake, d’étendre sa recherche en incluant le répertoire passé en paramètre, ici, le dossier files ;
  • SRC_URI : afin de définir la liste des fichiers pris en compte ;
  • MENDER_UBOOT_AUTO_CONFIGURE : cette variable permet de contrôler le comportement lors de l’intégration de Mender avec u-boot. Une mise à   de celle-ci, indique que l’intégration s’effectuera de manière manuelle ;
  • BOOTENV_SIZE : valeur qui doit être identique à celle de u-boot (CONFIG_ENV_SIZE), celle-ci étant utilisée lors des différents tests effectués par Mender lors de la configuration : (meta-mender/meta-mender-core/recipes-bsp/u-boot/patches/0002-Generic-boot-code-for-Mender.patch).

+#if MENDER_BOOTENV_SIZE != CONFIG_ENV_SIZE

+# error 'CONFIG_ENV_SIZE' define must be equal to bitbake variable 'BOOTENV_SIZE' set in U-Boot build recipe.

+#endif

Dans un second temps, il faudra créer 2 recettes dérivées :

  • La première concerne celle de notre bootloader (u-boot-fslc). Dans le fichier u-boot-fslc_%.bbappend, il faudra donc :
    • intégrer le fichier u-boot-mender.inc (fichier dans les sources de meta-mender), ceci afin d’inclure les différents patchs liés à l’utilisation de Mender ;
    • inclure le fichier précédemment créé, afin de prendre en compte le patch relatif à la WaRP7.
    • ajouter un mécanisme permettant de surcharger la séquence de démarrage initiale de la carte pour la prise en compte des différentes actions que Mender intègre. L’idée étant de ne pas modifier les sources de notre bootloader. Pour ce faire, nous allons profiter des capacités que propose u-boot, à savoir la gestion par script. Ainsi, il nous est facile de venir placer l’ensemble des commandes dans un simple fichier texte, qui lors de la phase de construction de notre image, se verra être « compilé » via la commande mkimage et positionné sur la première partition (boot). Le fichier sera donc sous la forme :

run mender_setup

mmc dev ${mender_uboot_dev}

setenv fdt_file imx7s-warp.dtb

setenv bootargs 'console=${console},${baudrate} root=${mender_kernel_root} rootwait rw'

load ${mender_uboot_root} ${kernel_addr_r} /boot/zImage

load ${mender_uboot_root} ${fdt_addr} /boot/${fdt_file}

bootz ${kernel_addr_r} - ${fdt_addr}

run mender_try_to_recover

mender_setup permettra de gérer les enregistrements de l’environnement (sur une nouvelle installation par exemple), mender_kernel_root fera référence à la partition RootFilesystem active (via l’utilisation du flag mender_boot_part), mender_uboot_root pour spécifier le périphérique et la partition de boot valide (exemple : mmc 0:2). Enfin, mender_try_to_recover facilitera les séquences de rollback.

La recette dérivée sera au final sous la forme suivante :

require recipes-bsp/u-boot/u-boot-mender.inc

require u-boot-mender-warp7.inc

RDEPENDS_${PN}_append_imx7s-warp = " u-boot-scr"

Puis, il conviendra ensuite de créer un dernier fichier bbappend pour la recette u-boot-fslc-fw-utils, celui qui nous donnera entre autres accès aux commandes fw_printenv et fw_setenv :

require u-boot-mender-warp7.inc

Dans celui-ci, seule l’inclusion de notre premier fichier suffira, le but étant de venir surcharger notre fichier fw_env.config en lieu et place de l’original.

3.3 Votre clé SVP !

Gérée « out-of-the-box » par Mender, la signature/validation d’artefact est une étape obligatoire dans un processus de mise à jour à distance. Notre étude n’y échappera donc pas ! Il nous faudra pour ceci générer notre paire de clé privée/clé publique. Vigilance tout de même quant à la gestion de la clé privée : étant une partie critique, elle ne devra en aucun cas se trouver au sein du périphérique et devra donc rester en dehors de notre système de construction, l’étape de signature se voudra donc être manuelle, mais nous y reviendrons !

Commençons par générer les clés (RSA 3072 bits) :

$ openssl genpkey -algorithm RSA -out private.key -pkeyopt rsa_keygen_bits:3072

$ openssl rsa -in private.key -out private.key

Puis procédons à l’extraction de la clé publique :

$ openssl rsa -pubout -in private.key -out artifact-verify-key.pem

Enfin, c’est donc cette clé publique (artifact-verify-key.pem) que nous pourrons approvisionner au sein de notre image. Pour réaliser cette étape, une fois de plus il conviendra de créer une recette dérivée, cette fois-ci pour Mender :

$ cat meta-warp7-distro/recipes-mender/mender/mender_%.bbappend

FILESEXTRAPATHS_prepend := "${THISDIR}/files:"

SRC_URI_append = " file://artifact-verify-key.pem"

Assez simple, non ?

3.4 shell oui chef !

Maintenant en possession de tous les éléments nécessaires à l’utilisation de Mender, il est grand temps de faire le grand pas et de générer notre première image.

3.4.1 Génération

Commençons par lancer la commande suivante:

$ bitbake qt5-image

Cette commande permettra de lancer le processus de génération de notre image et des éléments associés (merci à bitbake !). Une fois celle-ci terminée, nous retrouverons dans le répertoire de construction (tmp/deploy/images/imx7s-warp), plusieurs fichiers, l’image noyau ou encore notre image bootloader ; mais intéressons-nous aux deux des plus importants produits issus de la construction de notre étude :

  • qt5-image-imx7s-warp.sdimg : qui représente l’image disque complète avec l’intégration des exigences de Mender. Cette image contiendra les 4 partitions essentielles au bon fonctionnement (boot, rootfsA, rootfsB, data). La première étape consistera logiquement à copier celle-ci sur notre périphérique afin de rendre ce dernier entièrement compatible et prêt pour les mises à jour distantes. Pour ce faire :

$ dd if=qt5-image-imx7s-warp.sdimg of=/dev/sdX status=progress

  • qt5-image-imx7s-warp.mender : c’est notre artefact ! Le fichier qui nous sera utile pour nos mises à jour.

3.4.2 Artefact & signature

Après la génération des clés et la création de l’artefact par notre build system, il nous est, à ce niveau-ci de notre étude, possible de procéder à l’utilisation du mécanisme de signature proposé par Mender. Encore une fois, de par la gestion très intelligente du projet, nous avons à disposition un petit utilitaire : mender-artifact qui nous permettra de modifier, écrire, signer, vérifier un artefact. Ce dernier est disponible via le dépôt GitHub du projet (https://github.com/mendersoftware/mender-artifact), ou sous forme compilée en [7].

Let’s do it ! Utilisons l’outil magique :

$ mender-artifact \

 sign qt5-image-imx7s-warp.mender \

 -k ./private.key

Avec ici en paramètre notre artefact tout juste généré, ainsi que notre clé privée préalablement créée. Chose intéressante, il nous est possible après cette étape de signature, de vérifier celle-ci avec l’utilisation de notre clé publique. Faisons l’essai sur notre artefact :

$ mender-artifact \

 read qt5-image-imx7s-warp.mender \

 -k ./artifact-verify-key.pem

Mender artifact:

  Name: warp7-mender

  Format: mender

  Version: 2

  Signature: signed and verified correctly

  Compatible devices: '[imx7s-warp]'

[...]

Vous l’aurez sans doute remarqué, nous retrouvons ici le nom de notre artefact (valeur mise à jour durant notre étape de configuration), les périphériques compatibles et le résultat de l’étape de signature.

3.5 do_deploy_append()

Il est maintenant temps pour nous d’effectuer notre toute première mise à jour. Branchons notre WaRP7 et allons examiner les entrailles de notre client.

Dans un premier temps, jetons un œil au fichier contenant l’information principale de notre artefact :

root@imx7s-warp:~# cat /etc/mender/artifact_info

artifact_name=warp7-mender

Nous retrouvons bien l’information passée au build system (artifact_name). Il serait aussi intéressant de lister les différentes partitions de notre périphérique pour valider l’ensemble nos actions précédentes :

root@imx7s-warp:~# fdisk -l /dev/mmcblk2

Disk /dev/mmcblk2: 7566 MB, 7566524416 bytes

4 heads, 32 sectors/track, 115456 cylinders

Units = cylinders of 128 * 512 = 65536 bytes

        Device Boot      Start         End      Blocks  Id System

/dev/mmcblk2p1   *         385         640       16384   c Win95 FAT32 (LBA)

/dev/mmcblk2p2             641        7296      425984  83 Linux

/dev/mmcblk2p3            7297       13952      425984  83 Linux

/dev/mmcblk2p4           13953       16000      131072  83 Linux

Puis assurons-nous d’être sur une partition active correspondant au rootfs A :

root@imx7s-warp:~# mount | grep mmc

/dev/mmcblk2p2 on / type ext4 (rw,relatime,data=ordered)

/dev/mmcblk2p4 on /data type ext4 (rw,relatime,data=ordered)

[...]

Et comparons avec la configuration du client au travers de son fichier de configuration :

root@imx7s-warp:~# cat /etc/mender/mender.conf

{

    [...]

    "RootfsPartA": "/dev/mmcblk2p2",

    "RootfsPartB": "/dev/mmcblk2p3",

    [...]

}

Maintenant que l’environnement nous apporte entière satisfaction (c’est vrai, non ?!), nous allons pouvoir préparer notre premier déploiement. Coté hôte, nous pourrons, par l’utilisation du module SimpleHTTPServer, instancier un serveur HTTP (sur le port 8000) au sein de notre répertoire de déploiement (au sens Yocto/OE) :

$ cd tmp/deploy/images/imx7s-warp/

$ python -m SimpleHTTPServer

Serving HTTP on 0.0.0.0 port 8000 ...

Puis côté cible, nous pourrons ainsi lancer aisément une phase de déploiement :

root@imx7s-warp:~# mender -log-level info -rootfs http://192.168.1.17:8000/qt5-image-imx7s-warp.mender

INFO[0000] Performing remote update from: [http://192.168.1.17:8000/qt5-image-imx7s-warp.mender].  module=rootfs

Installing update from the artifact of size 106301440

INFO[0000] opening device /dev/mmcblk2p3 for writing     module=block_device

INFO[0000] partition /dev/mmcblk2p3 size: 436207616      module=block_device

................................   0% 1024 KiB

................................  99% 103424 KiB

............INFO[0208] wrote 436207616/436207616 bytes of update to device /dev/mmcblk2p3  module=device

                     100% 103810 KiB

INFO[0210] Enabling partition with new image installed to be a boot candidate: 3  module=device

Comme nous le constatons, l’installation s’effectue directement sur la partition inactive (ici /dev/mmcblk2p3), qui en fin de processus deviendra éventuellement candidate pour être active.

Il est maintenant intéressant de regarder les différentes variables u-boot mises à jour par le client Mender depuis l’espace utilisateur. Comparons 3 variables après mise à jour :

root@imx7s-warp:~# fw_printenv mender_boot_part bootcount upgrade_available

mender_boot_part=3

bootcount=0

upgrade_available=1

Nous voyons que la variable mender_boot_part fait référence à la 3e partition, ce qui correspond en effet à ce que nous venons d’observer. Le compteur bootcount est lui, initialisé à   et upgrade_available vient de passer à 1.

Sans plus attendre, lançons une phase de redémarrage afin de migrer sur notre nouvelle installation :

root@imx7s-warp:~# mount | grep mmc

/dev/mmcblk2p3 on / type ext4 (rw,relatime,data=ordered)

/dev/mmcblk2p4 on /data type ext4 (rw,relatime,data=ordered)

[...]

Bingo ! Nous sommes bien sur la partition fraîchement installée. Maintenant, il nous restera à valider celle-ci (mise à   de la variable upgrade_available), pour ce faire, le client mender nous met à disposition le paramètre commit :

root@imx7s-warp:~# mender -commit

INFO[0000] Commiting update                              module=device

Ce qui aura pour effet de rendre notre installation persistante (valide) :

root@imx7s-warp:~# fw_printenv upgrade_available

upgrade_available=0

Et voilà, nous venons de valider l’ensemble d’un déploiement en utilisant Mender dans son mode local (standalone) sur notre WaRP7, le tout en validant les concepts abordés jusqu’ici. Il nous reste maintenant à intégrer la partie serveur à notre étude. Mais le plus dur est fait !

Pour information, sans la validation de l’installation (via mender -commit), nous aurions eu au prochain redémarrage (sur un problème sanity check par exemple), un dépassement du compteur bootcount, provoquant ainsi une séquence de rollback et l’utilisation de la commande contenue dans altbootcmd :

Writing to redundant MMC(0)... done

Warning: Bootlimit (1) exceeded. Using altbootcmd.

3.6 Hosted Mender

Pour une utilisation avec Hosted Mender, l’ensemble de notre configuration restera entièrement valide, néanmoins, il nous faudra spécifier quelques paramètres au sein de celle-ci pour une utilisation en mode client/serveur. Premièrement, spécifions l’adresse de notre serveur :

MENDER_SERVER_URL = "https://hosted.mender.io"

Ensuite, pour permettre l’identification du périphérique par la partie serveur, il est requis de spécifier le TENANT_TOKEN :

MENDER_TENANT_TOKEN="ici votre tenant token"

Celui-ci étant disponible au niveau de l’interface, dans My organization > Token.

Enfin, le déploiement d’une mise à jour distante étant réalisé si, est seulement si, la version logicielle du client diffère de la version logicielle du serveur (basé sur le nom de l’artefact), nous devrons générer une nouvelle version de celui-ci :

MENDER_ARTIFACT_NAME = "artifact-glmf-demo"

Ensuite, il conviendra de télécharger ce dernier sur le serveur de mise à jour (figure 11).

Fig. 11: Upload de notre artefact.

...et nous pourrons avec sérénité, nous mettre dans la peau du « Release Manager » afin de créer une séquence de déploiement pour notre périphérique (voir figure 12). Choisissons notre Artefact et les cibles visées (nous pourrions imaginer ici un déploiement sur N périphériques sur des zones géographiques totalement différentes).

Fig. 12 : Création d’un déploiement.

Celui-ci deviendra actif et en attente d’une connexion avec le périphérique afin de procéder à la mise à jour comme le montre la figure 13.

Fig. 13 : Déploiement maintenant en attente.

Sans plus attendre, générons une nouvelle image (via bitbake), copions celle-ci sur la cible et mettons notre WaRP7 sous tension. Et regardons côté configuration les différents changements apportés :

root@imx7s-warp:~# cat /etc/mender/mender.conf

{

    [...]

    "ServerURL": "https://hosted.mender.io",

    "TenantToken": "c’est secret!!",

}

Une fois la connexion établie entre les deux parties (étape d’autorisation côté serveur), nous pourrons observer le début de notre premier déploiement avec Hosted Mender (figure 14).

Fig. 14 : Notre premier déploiement avec Hosted Mender.

Après un certain temps (download + installation), la cible se verra être redémarrée de façon automatique, comme le montre l’interface web sur la figure suivante :

Fig. 15 : Phase de Reboot.

Une fois redémarrée, si toutes les étapes de sanity check ont été validées avec succès, notre déploiement se verra ainsi être validé (voir figure 16).

Fig. 16 : Success !

Nous remarquerons ici la mise à jour du champ Current software avec notre dernière version !

Et comme déjà évoqué un peu plus haut dans cet article, une des exigences d’une solution OTA est de disposer d’un moyen permettant de gérer une flotte de périphériques et qui plus est, lister la totalité des informations de chaque périphérique. Devinez quoi... Mender intègre cette fonctionnalité comme le montre la figure 17.

Fig. 17 : Inventaire du périphérique.

Et voilà, nous avons réussi notre déploiement OTA via Hosted Mender ! Plutôt sympa, non ?

3.7 Features

De par sa maturité, Mender propose de nombreuses fonctionnalités très intéressantes. Et comme vous vous en doutez, il est assez compliqué d’en faire entièrement le détail dans les colonnes de GNU/Linux Magazine. Ainsi, nous n'en présenterons ici qu’une petite partie.

3.7.1 Données persistantes

Cruciale dans bien des scénarios de mise à jour, la possibilité de ne pas perdre des éléments de configuration durant celle-ci est un réel plus. Nous en avons déjà parlé et Mender permet de par son partitionnement, de gérer les données persistantes (partition /data). Mais chose intéressante, il nous est possible d’inclure des fichiers dans cette partition via Yocto/OE. Cela nous assure la garantie que chaque fichier de l’image sera déployé au bon endroit.

Mender met à disposition la variable MENDER_DATA_PART_DIR afin de spécifier le répertoire à copier dans la partition /data. Prenons ici un exemple :

MENDER_DATA_PART_DIR_append = " ${DEPLOY_DIR_IMAGE}/user-data"

Cette configuration aura pour effet d’ordonner à Mender la récupération de l’ensemble des fichiers contenus dans user-data.

Afin de donner un peu de concret à cette présentation, prenons ici l’exemple de notre configuration wifi gérée par l’utilisation de wpa-supplicant. Par défaut, la configuration qui permet de définir le réseau wifi utilisé (ssid, passphrase), se trouve dans /etc/wpa-supplicant/wpa_supplicant-wlan0.conf. Et vous le comprendrez, cette solution n’est que peu viable dans un système avec mise à jour atomique où il n’est pas concevable qu’un utilisateur reconfigure l’accès wifi après chaque mise à jour. Pour ce faire, il conviendra par exemple de procéder à une création de lien symbolique dans la recette dérivée du paquet wpa-supplicant :

do_install_append() {

 [...] 

 ln -sf /data/hub/etc/wpa_supplicant/wpa_supplicant-wlan0.conf \

  ${D}${sysconfdir}/wpa_supplicant/wpa_supplicant-wlan0.conf

}

Pour ensuite s’assurer de déployer le tout dans notre répertoire de déploiement dans l’étape do-deploy() :

do_deploy() {

 install -d ${DEPLOYDIR}/user-data/hub/etc/wpa_supplicant

}

Puis, dans tmp/deploy/images/imx7s-warp, nous aurons la chance d’observer la création du répertoire suivant :

$ tree user-data/

user-data/

└── hub

    └── etc

        └── wpa_supplicant

Ensemble qui sera par la suite copié dans l’image système (.sdimg), image qu’il faudra une fois de plus copier sur notre cible. Voici par exemple, une configuration sauvegardée sur la partition /data, après paramétrage via wpa-supplicant :

root@imx7s-warp:~# ls -l /etc/wpa_supplicant/wpa_supplicant-wlan0.conf

... /etc/wpa_supplicant/wpa_supplicant-wlan0.conf -> /data/hub/etc/wpa_supplicant/wpa_supplicant-wlan0.conf

Conclusion

Toutes les bonnes choses ont une fin et il est maintenant temps de conclure. Nous aurons découvert via cet article, comment mettre en place une solution de mise à jour OTA (robuste et sécurisée) de bout en bout grâce à Mender (dans sa version 1.4), qui plus est sur une plateforme industrielle !

Malheureusement, nous n’avons pas pu découvrir l’ensemble des fonctionnalités que propose cet excellent projet. C’est donc pour cette raison qu’il est très fortement recommandé par l’auteur, de regarder de plus près ce que Mender nomme les « state scripts » (https://docs.mender.io/1.4/artifacts/state-scripts). C’est globalement une fonction permettant à l’utilisateur de placer des scripts personnalisables entre chaque étape du processus de mise à jour (idle, download, reboot, commit...). On pourrait imaginer ici avoir une interface graphique (en Qt/QML), permettant à l’utilisateur de valider chaque étape (exemple : repousser l’étape de download ou de reboot), un peu comme nos smartphones. Un projet est d’ailleurs en cours de réalisation par l’auteur (D-Bus, Qt et state scripts) : https://github.com/texierp/mender-qt-updater (voir figure 18).

Fig. 18 : Utilisation mender-state-script avec Qt & D-Bus.

Et voilà, il ne vous reste plus qu’à tester Mender sur votre périphérique ! Mais suivant vos besoins, il est également probable qu’une solution plus flexible comme RAUC ou SWUpdate fasse l’affaire, n’ayez pas peur de tester les différentes solutions en évaluant les fonctionnalités de chacun !

Références

[1] Site web CVE : https://cve.mitre.org/about/

[2] Présentation « seamless update » par Android : https://source.android.com/devices/tech/ota/ab/

[3] Article sur la serrure Lockstate : https://techcrunch.com/2017/08/14/wifi-disabled/

[4] Article « Hidden Costs of Homegrown Updaters » par Mender.io : https://mender.io/resources/guides-and-whitepapers/_resources/hidden-costs-of-homegrown

[5] Dépôt GitHub du projet casync : https://github.com/systemd/casync

[6] Dépôt GitHub du projet psplash : http://git.yoctoproject.org/cgit/cgit.cgi/psplash/

[7] Lien de téléchargement de mender-artifact : https://d1b0l86ne08fsf.cloudfront.net/mender-artifact/2.1.2/mender-artifact

Note

Toutes les images ont été utilisées avec l’accord de northern.tech pour la partie Mender.