Introduction
Dans le monde de l’embarqué, une des questions récurrentes concerne la stratégie de mise à jour du système. Pour répondre à cette problématique plusieurs outils existent aujourd’hui (SWUpdate, Mender, Rauc, OSTree, ...), nous allons ici nous concentrer sur une solution peu courante : OSTree.
Qu’est ce que OSTree ?
OSTree est un système d’upgrade opensource pour les systèmes d'exploitation basés sur Linux, conçu et maintenu par Colin WALTERS et soumis à la LGPL-2.0.
Il opère dans le userspace, et fonctionne par dessus n’importe quel filesystem Linux.
Il effectue des mises à jour atomiques de filesystems complet mais n’est pas un système de paquets, il est plutôt destiné à les compléter. Le modèle principal de fonctionnement d'OSTree consiste à composer des packages sur un serveur, puis à les répliquer sur des clients.
Quel est son type d’architecture ?
Son architecture peut être résumée comme étant un “git pour les binaires de systèmes d’exploitation“, ainsi l’utilisateur peut réaliser des commits et des checkouts de rootfs bootable renseignés en tant que références au sein de l’arborescence OSTree.
Il est semblable à Git dans le sens où il stocke des fichiers avec des checksums (SHA256) dans un espace de stockage orienté contenu-addresse, mais il lui est différent au niveau des checkouts car les fichiers sont récupérés via hardlinks et sont immuables pour éviter la corruption de ces derniers. Un exemple d'arborescence d'un repository OSTree est présenté sur la Figure 1.
Principes de fonctionnement
Quelle est la stratégie de mise à jour de OSTree ?
Tout d’abord, il faut savoir qu’OSTree est conçu pour réaliser des mises à jour totalement sûres et atomiques, et de manière plus générique des transitions entre listes de déploiements bootables atomiques. Ce qui implique que si le système crash ou que l’alimentation est débranchée durant la procédure de mise à jour, le système démarrera soit sur l’ancienne version soit sur la nouvelle.
Ensuite, que OSTree se base sur un fonctionnement de mise à jour de type symétrique.
Lorsqu’une mise à jour est disponible, le système va la récupérer par l’intermédiaire d’un checkout ou d’un pull, ce qui aura pour effet de récupérer un patch contenant uniquement les différences entre les deux filesystems (celui sur lequel le système a démarré et celui mis à jour). Ce patch inclut également une signature qui sera utilisée pour valider la mise à jour.
Une fois le patch récupéré, OSTree va générer un nouveau répertoire avec une copie des fichiers à modifier afin d’y appliquer les modifications du patch. Les fichiers inchangés, quant à eux, seront directement réutilisés dans le nouveau filesystem, ce qui a pour conséquence de réduire drastiquement la taille de la mise à jour. On utilise également la terminologie mise à jour delta pour décrire ce principe.
Finalement, une fois les modifications appliquées aux copies des fichiers d’origine, OSTree va vérifier que la signature, calculée à partir des fichiers mis à jour, correspond à la signature reçue au sein du patch. Si les signatures ne correspondent pas, OSTree va décharger la mise à jour et effectuer un rollback automatique. Au contraire, si les signatures correspondent la mise à jour sera identifiée comme réussie et utilisée lors du prochain redémarrage du système.
Le fait de d’abord réaliser une copie des fichiers et ensuite mettre à jour cette copie permet d’être sûr que si une coupure de courant ou n’importe quel autre problème de ce genre intervient durant la mise à jour, le système démarrera avec un filesystem fonctionnel.
Exemple de mise à jour
Entre les versions 1 et 2, les fichiers A et C ont été modifiés. Quand la version 2 est récupérée, un patch incluant seulement les différences (entre A/A1 et C/C1) est reçu, ainsi qu'une signature de validation.
OSTree génère alors un nouveau répertoire contenant une copie du fichier A et applique les mises à jours contenues dans le patch pour passer à A1 (idem pour le fichier C). Le fichier B étant inchangé, il sera directement réutilisé dans la version 2.
Finalement OSTree va vérifier que la mise à jour s’est bien passée en comparant les signatures (celle calculée à partir des fichiers mis à jour et celle reçue au sein du patch).
Si la mise à jour s’est bien passée, alors la version 2 (A1, B et C1) sera utilisée lors du prochain redémarrage du système, sinon le système reviendra automatiquement à la version 1 (A, B et C).
Quels sont les déploiements gérés par OSTree ?
OSTree supporte seulement l’enregistrement et le déploiement de filesystems complets, c’est-à-dire bootable.
Sur chaque machine client il y a un répertoire OSTree situé sous /ostree/repo et un ensemble de déploiements sous /ostree/deploy/$STATEROOT/deploy/$CHECKSUM ($STATEROOT = osname).
Le répertoire $STATEROOT regroupe l’ensemble des déploiements qui partagent le même /var. Ainsi OSTree permet l’installation parallèle de différents OS, par exemple Debian sous /ostree/deploy/debian et Red Hat Entreprise Linux sous /ostree/deploy/rhel.
OSTree fournit également un ensemble d'outils pour permettre de créer les points de montage Linux qui garantissent que le déploiement utilisé voit la copie partagée de /var, mais ne touche pas à son contenu.
Dans les filesystems générés par OSTree il y a deux répertoires persistants, préservés entre les mises à jour et ouverts en écriture :
- /etc : fichiers/données de configuration pour l’OS de base, exemple network configuration.
- /var : lié aux applications. Données d’exécution créées ou utilisées par les applications.
Les données de ces deux répertoires sont conservées et réutilisées entre les déploiements.
Un déploiement n’a pas de dossier /etc Linux traditionnel, à la place il est inclus sous /usr/etc, c’est la configuration par défaut. Quand OSTree crée un déploiement, il réalise un merge 3-way en utilisant le déploiement actuellement booté (/etc), sa configuration par défaut et le nouveau déploiement (basé sur son /usr/etc).
En dehors de ces deux exceptions, le reste du contenu du déploiement est récupéré en tant que hardlink au sein du répertoire.
Pour réduire le nombre de montage "bind", OSTree recommende d'adopter le merge de /usr (UsrMerge) mis en place dans de nombreuse distributions.
Alors que OSTree installe des déploiements parallèles proprement à l’intérieur du répertoire /ostree, il doit au final contrôler le répertoire système /boot pour indiquer sur quel déploiement démarrer. Pour cela il se base sur la Boot Loader Specification qui est un standard pour les fichiers de fragments de configuration des bootloader.
Quand un filesystem est déployé, il va avoir un fichier de configuration généré de la forme /boot/loader/entries/ostree-$stateroot-$checksum.$serial.conf. Ce fichier inclut un argument kernel spécial (ostree=) qui permet au initramfs de trouver le déploiement spécifié et ainsi démarrer celui-ci.
Comment mettre à jour un filesystem via OSTree ?
Pour mettre à jour un filesystem via OSTree la solution la plus simple consiste en la réplication de filesystems pré-générés depuis un serveur via HTTP.
Afin de réaliser une mise à jour, OSTree récupère le contenu d’une ref depuis un serveur distant. Supposons que nous trackons une ref nommée exampleos/buildmain/x86_64-runtime, OSTree récupère alors le contenu de cette ref depuis l’URL http://example.com/repo/refs/heads/exampleos/buildmain/x86_64-runtime, lequel contient un checksum SHA256 qui détermine quel arbre déployer au prochain redémarrage.
En revanche, si nous n’avons pas le nom d’une ref en particulier, il est possible de récupérer le contenu de mise à jour graĉe à un simple pull, mais cela implique de récupérer tous les objets individuels que nous n’avons pas de manière asynchrone, chaque objets étant validés par un checksum et stockés au sein de /ostree/repo/objects. Une fois le pull terminé, nous avons récupéré tous les objets nécessaires à la réalisation d’un déploiement et donc d’une mise à jour du filesystem.
Quelles sont les particularités de OSTree pour les systèmes embarqués ?
Du fait qu’un système embarqué possède un stockage limité, pour chaque nouveau commit sur le serveur seulement les deux derniers commits seront stockés directement sur le système embarqué (le commit précédent étant gardé pour facilement effectuer un rollback vers l’ancienne version même si on a une perte de connectivité). De cette façon, la taille utilisée par OSTree pour stocker les différents fichiers est toujours limitée au minimum.
Intégration à Yocto
Comment s’intègre OSTree au sein de Yocto ?
Le projet OSTree est directement intégré à Yocto au sein du meta de base : meta-openembedded/meta-oe
Il s’agit dans ce layer seulement de l’outil brut sans aucune configuration pré-établie, c’est à dire que une fois intégrée à l’image l’utilisateur devra alors réaliser toute la configuration lui-même pour pouvoir déployer et booter des filesystems.
Pour simplement ajouter l’outil OSTree à notre image sans configuration, il suffit d’ajouter la ligne suivante au fichier conf/local.conf :
IMAGE_INSTALL_append = "ostree"
Cependant il existe un autre layer permettant les mises à jour over-the-air (OTA) avec OSTree et Aktualizr, il s’agit de meta-updater
En plus de permettre les mises à jour OTA, meta-updater va également configurer tout l’environnement Yocto pour permettre le déploiement et le boot de filesystem à l’aide de OSTree depuis une cible.
Ainsi, côté serveur de build, à l'issue de la génération d’une image par Yocto, un dossier tmp/deploy/images/$cible/ostree_repo est créé au sein du dossier de build. Il s'agit d'un dépôt ostree qui peut être utilisé comme dépôt de mise à jour.
Lors d'une nouvelle génération d’image, un commit dans tmp/deploy/images/$cible/ostree_repo sera généré, permettant par la suite de générer une mise à jour à partir d'une cible avec ostree.
Note : Il est possible de supprimer tout ce dossier, celui-ci pourra être régénéré par bitbake, mais l'historique précédent aura été perdu.
Côté cible, l’architecture déployée au sein de l’image sera celle détaillée dans la partie sur les déploiements gérés par Yocto. De plus à l’aide d’OSTree il sera possible de récupérer et déployer directement les nouvelles images générées côté serveur et stockées dans le dépôt ostree, sans avoir à flasher de nouveau la cible avec la nouvelle image.
Il existe tout de même un bémol à l’utilisation de ce layer concernant les mises à jour OTA. En effet, celles-ci se basent sur une structure de serveur bien particulière (uniquement compatible avec le standard Uptane) et la mise en place d’un tel serveur est plus que compliquée. Vous pouvez trouver la spécification ici : https://uptane.github.io/uptane-standard/uptane-standard.html.
Comment intégrer et utiliser meta-updater ?
Pour intégrer OSTree ainsi que la configuration pré-établie à la future image Yocto, il faut cloner le repository de meta-updater et l’ajouter à notre fichier bblayers.conf :
git clone https://github.com/uptane/meta-updater.git -m kirkstone
bitbake-layers add-layer meta-updater
Il faut ensuite récupérer (ou développer, exemple et documentation sur https://docs.ota.here.com/ota-client/latest/bsp-integration.html) le layer d’intégration de notre plateforme (meta-updater-${PLATFORM}, ex : meta-updater-qemux86-64) lié à meta-updater et l’ajouter à notre bblayers.conf.
La distro utilisée devra ensuite être configurée pour utiliser les fonctionnalités de meta-updater :
INHERIT += " sota"
DISTRO_FEATURES:append = " sota systemd usrmerge"
L’image contenant OSTree et la configuration minimale pour l’utiliser peut ainsi être générée à l’aide de bitbake et flashée sur notre plateforme.
Note : il existe une autre façon d’intégrer meta-updater au sein de Yocto qui est la suivante (supportée jusqu’à la version dunfell) :
repo init -u https://github.com/advancedtelematic/updater-repo.git -m dunfell.xml
repo sync
source meta-updater/scripts/envsetup.sh $PLATFORMrepo init -u https://github.com/advancedtelematic/updater-repo.git -m dunfell.xml
repo sync
source meta-updater/scripts/envsetup.sh $PLATFORM
De plus, il existe tout un ensemble de paramètres qui peuvent être modifiés afin d’affiner la configuration à la fois de OSTree et de meta-updater au sein de Yocto, la liste complète est disponible dans la documentation, mais voici quelques exemples :
- OSTREE_REPO: chemin du repository OSTree (${DEPLOY_DIR_IMAGE}/ostree_repo par défaut)
- OSTREE_BRANCHNAME: nom de la branche pour les commits (${SOTA_HARDWARE_ID} par défaut)
- OSTREE_OSNAME: nom du déploiement (poky par défaut)
- SOTA_HARDWARE_ID: nom du matériel (${MACHINE} par défaut)
Avantage et Inconvénients
Quels sont les avantages de l’outil OSTree ?
OSTree présente une multitude d’avantages :
- Développement et communauté open-source active
- Intégration Yocto native au sein du layer meta-openembedded et existence d’un layer réalisant la configuration (meta-updater)
- Réduction de la taille des mises à jour (application seulement des modifications et réutilisation des fichiers inchangés)
- Limitation de la bande passante utilisée
- Mise à jour rapide pour les systèmes embarqués
- Utilisation des artefacts signés via GPG nativement supportées
- Gestion de U-boot et grub natif
- Bon niveau de sécurité du processus de mise à jour
Quels sont les inconvénients de l’outil OSTree ?
OSTree présente également des inconvénients :
- Utilisation potentiellement importante de la RAM (dépend de la taille des mises à jour)
- Impossibilité de récupération après le déploiement d’un filesystem corrompu
- Absence d’interface utilisateur, uniquement utilisable par un outil CLI ou librairie C (libostree)
- Maintien et portabilité du meta-updater pour Yocto (supporté par advancedtelematic pour dunfell puis par Uptane pour kirkstone)
- Configuration dans un build Yocto compliquée si le meta-updater n’est pas utilisé
- Mises à jour OTA difficilent à mettre en place à cause du Standard Uptane à respecter
Fiche Produit Résumé
Nom du produit | OSTree |
Nom éditeur | OSTree Project |
Langage de programmation | C |
Open Source | Oui |
URL Documentation | https://ostreedev.github.io/ostree/ |
URL Git | https://github.com/ostreedev/ostree |
License | LGPL - 2.0 |
Type de mise à jour | Symétrique |
Origine de mise à jour | Dépôt local et distant |
Mise à jour du Kernel | Oui |
Mise à jour du Rootfs | Oui |
Rootfs | lecture/écriture, /etc et /var ouvert en écriture le reste est en read-only |
Structure du disque | Flexible ,mais ne supporte qu’un ensemble limité de bootloaders ceux qui supportent le standard The Boot Loader Specification. |
Bootloaders supportés | Grub / U-Boot |
OE Integration Yocto | meta-openembedded/meta-oe, meta-updater, meta-ostree (WIP) |
Besoin en ressource (côté client) | Update du répertoire local, hard link pour le partage des fichiers inchangés entre différents déploiement. |
Besoin en ressource (côté serveur) | Génération de commits basé sur de nouveaux builds, stockage des commits dans un répertoire. |
Sécurité (Installation) | Rollback intégré |
Sécurité (Communication) | HTTPS possible |
Sécurité (Image) | Commits signés, GPG nativement supporté |
Stabilité de Code | Relativement stable, utilisé par plusieurs distributions, soumis à un développement actif et à une communauté open-source. |
API côté serveur | Avec le projet pulp (plugin possible) |
API côté client | Outil CLI ressemblant à Git ou librairie en C |
Exemple : Utilisons OSTree pour mettre à jour notre système embarqué
Dans le cadre de cet exemple d’utilisation nous utilisons une plateforme cible de type Raspberry Pi 1 Model B+ V1.2 (nommée ci après RPI1).
La carte est connectée au réseau avec un accès internet via l’interface réseau.
La distribution utilisée est une distribution générée par Yocto dunfell.
La première chose à faire est d’initiliaser la distribution d’exemple (document de référence externe : https://docs.ota.here.com/getstarted/dev/raspberry-pi.html), pour cela nous créons un dossier nommé OSTree sur notre espace de travail afin de récupérer les sources :
mkdir -p ~/yocto-update/OSTree
cd ~/yocto-update/OSTree
repo init -u https://github.com/advancedtelematic/updater-repo.git -m dunfell.xml
repo sync
Ensuite, une fois les sources récupérées depuis le dépôt distant, un petit hack doit être réalisé afin de prendre en compte la RPI1 :
cp meta-updater/conf/include/bblayers/sota_raspberrypi2.inc meta-updater/conf/include/bblayers/sota_raspberrypi.inc
Pour la version Dunfell il faut également corriger un problème de syntaxe dans le fichier fit-conf.bb (ref https://git.yoctoproject.org/poky/commit/?id=ab6b5e97cebe19938baa403da6307ca320294b3a) :
sed -i 's/conf@/conf-/g' meta-updater/recipes-sota/fit-conf/fit-conf.bb
Ce problème a été corrigé pour les versions suivantes de Yocto.
Il faut désormais sourcer et configurer l’environnement Yocto ainsi que générer notre première image :
source meta-updater/scripts/envsetup.sh raspberrypi
echo "SOTA_MAIN_DTB = \"bcm2708-rpi-b.dtb\"" >> conf/local.conf
echo "OSTREE_KERNEL_ARGS_sota = \" 8250.nr_uarts=1 bcm2708_fb.fbwidth=656 bcm2708_fb.fbheight=614 bcm2708_fb.fbswap=1 vc_mem.mem_base=0x3ec00000 vc_mem.mem_size=0x40000000 dwc_otg.lpm_enable=0 console=ttyAMA0,115200 usbhid.mousepoll=0\"" >> conf/local.conf
bitbake core-image-minimal
A l’issue de la génération le fichier d’image est disponible à l’URI suivante :
~/yocto-update/OSTree/build/tmp/deploy/images/raspberrypi/core-image-minimal-raspberrypi.wic
Note: si un nouveau build a besoin d’être généré après le build initial, il est demandé d’utiliser la procédure suivante au sein d’un environnement déjà initié afin de ne pas corrompre les éléments contenus dans le fichier conf/local.conf :
cd ~yocto-update/OSTree
source sources/poky/oe-init-build-env build
MACHINE=raspberrypi bitbake core-image-minimal
On peut désormais se servir des fichiers images générés (core-image-minimal-raspberrypi.wic et core-image-minimal-raspberrypi.wic.bmap) afin de flasher notre carte RPI1.
Pour ce faire, après insertion d’une carte SD sur notre PC et démontage des partitions de la carte SD à l’aide de la commande umount et en considérant /dev/mmcblk0 comme étant le périphérique de la carte SD à flasher, voici les commandes à réaliser :
sudo bmaptool copy ~yocto-update/OSTree/build/tmp/deploy/images/raspberrypi/core-image-minimal-raspberrypi.wic /dev/mmcblk0
sudo parted -s /dev/mmcblk0 resizepart 2 '100%'
sudo e2fsck -f /dev/mmcblk0p2
sudo resize2fs -p /dev/mmcblk0p2
Maintenant que la carte SD est flashée nous pouvons l’insérer dans notre RPI1, démarrer notre carte et vérifier la présence de l’outil ostree afin de valider notre distribution d’exemple.
Nous souhaitons désormais mettre à jour notre distribution directement depuis la cible. Nous allons, dans ce but, commencer par démarrer un serveur HTTP depuis le dossier de build de Yocto (~/yocto-update/OSTree/build) :
cd tmp/deploy/images/raspberrypi/ostree_repo
python3 -m http.server
Ensuite, côté cible nous allons initialiser un dépot :
ostree remote add --no-gpg-verify my-remote http://notre_adr_ip:8000
Puis nous venons checker, récupérer et appliquer une mise à jour :
ostree pull my-remote raspberrypi
ostree admin deploy --os=poky my-remote:raspberrypi
On vérifie ensuite l’état de OSTree, pour voir quel distribution est installée et si une mise à jour est en attente :
ostree admin status
Si une mise à jour est en attente (pending) un reboot est nécessaire pour pouvoir installer la nouvelle version.
Maintenant qu’on a mis à jour notre distribution directement depuis la cible on veut pouvoir y ajouter un serveur ssh.
Pour cela, dans notre dossier de build (~/yocto-update/OSTree/build) nous générons une nouvelle version de notre distribution en y ajoutant le serveur ssh :
echo "EXTRA_IMAGE_FEATURES += \" ssh-server-dropbear\"" >> conf/local.conf
bitbake core-image-minimal
Depuis la cible, nous pouvons alors réitérer les étapes de récupération et d’application de la mise à jour :
ostree pull my-remote raspberrypi
ostree admin deploy --os=poky my-remote:raspberrypi
Nous vérifions ensuite que tout s’est bien passé :
ostree admin status
Et nous re-démarrons la cible si un pending est présent pour pouvoir prendre en compte le déploiement.
Une fois que la cible à bien redémarré, il faut vérifier que le serveur ssh est bien présent sur la cible et, si c’est le cas, qu’il fonctionne correctement :
systemctl start dropbear
systemctl status dropbear
netstat -ln
Il peut être intéressant à ce stade de réaliser un rollback pour explorer un peu plus les fonctionnalités de l’outil.
Pour ce faire, il faut récupérer le refspec de la version de rollback (celle identifié par un rollback entre parenthèses) :
ostree admin status
Ici le refspec de rollback est poky:raspberrypi
On peut alors réaliser le déploiement de la version de rollback :
ostree admin deploy --os=poky poky:raspberrypi
Puis vérifier que le rollback est bien en attente (pending), pour être appliquée après reboot :
ostree admin status
Désormais il n’y a plus qu’à rebooter la carte et vérifier que la commande dropbear n’est plus gérée pour valider le rollback.
Conclusion
OSTree est un outil puissant qui permet de réaliser des mises à jour de systèmes complets tout en limitant l’usage de la bande passante ainsi que de la RAM ce qui représente un avantage considérable dans le monde de l’embarqué.
En revanche, son utilisation et son intégration au sein de Yocto n’est pas si aisée du fait d’une documentation éparpillée et peu claire, principalement au sujet du meta-updater.
De plus la mise en place des mises à jour Over The Air se révèle être un vrai casse-tête de par sa complexité, ce qui est un gros point limitant comparé à ses concurrents que sont RAUC ou Mender.