Linux Embedded

Le blog des technologies libres et embarquées

Mise à jour d'un système embarqué : la voie de systemd

Introduction

La mise à jour des systèmes embarqués est un aspect important lors de la conception de ces derniers. En effet, que ce soit pour corriger des bugs, des problèmes de performances, des failles de sécurité ou pour y ajouter de nouvelles fonctionnalités, il est nécessaire de réaliser une mise à jour du système. Il existe déjà des solutions permettant de le faire, e.g. RAUC ou SWUpdate, cependant cela impose d'installer des logiciels supplémentaires sur le système. De son côté, systemd qui est (très souvent) nativement intégré aux systèmes Linux, propose aussi une solution de mise à jour du système : systemd-sysupdate.

Il s'agit d'une composante logicielle de systemd qui permet de mettre à jour des fichiers, des dossiers ou des partitions en suivant une logique symétrique (A/B) et de façon atomique. De plus, systemd-boot, le bootloader fourni par systemd, propose des fonctionnalités qui permettent de répondre aux exigences des mises à jour des systèmes embarqués (fallback automatique et compteurs de boot) et donc de compléter systemd-sysupdate dans le processus de mise à jour.

Dans la suite de cet article, nous nous concentrerons sur la construction d'un layer Yocto qui permet d'intégrer un système de mise à jour complet par USB (i.e. système de fichiers racine, noyau, device tree) basé sur systemd en utilisant la méthode par partition symétrique avec une partition de récupération (A/B/R).

Ce layer devra être capable de :

  • Créer une image initiale à installer sur la carte.
  • Générer des bundles de mise à jour prêts à être installés sur le système.

Au premier démarrage, l'image initiale sera composée d'une partition contenant le bootloader et d'une partition de récupération. Cette dernière sera chargée de créer les partitions qui accueilleront les systèmes de fichiers racine et d'en installer une première version, comme cela est schématisé sur le diagramme de séquence suivant :

Intégration dans Yocto : meta-systemd-update

Avant de commencer l'intégration du système de mise à jour dans Yocto, il est important de noter qu'il n'est pas possible d'utiliser n'importe quelle version de ce dernier. En effet, systemd-sysupdate n'est disponible qu'à partir de la version 251 de systemd ce qui impose d'utiliser Yocto Scarthgap.

Étape 1 : Activation de systemd

Par défaut lorsqu'une distribution est créée avec Yocto, systemd n'est pas activé. Depuis la version Scarthgap, pour l'activer il suffit d'utiliser la variable INIT_MANAGER dans le fichier local.conf

# local.conf
INIT_MANAGER = "systemd"

Étape 2 : Créer l'image initiale

Comme vu en introduction, l'image initiale sera composée de deux partitions : une partition pour systemd-boot et une partition de récupération. Pour créer des images partitionnées avec Yocto, on va utiliser une image de type WIC. Il faut donc définir un fichier kickstart (.wks) permettant de décrire les propriétés des partitions à créer ainsi que les dépendances à compiler avant la création de l'image.

# sdu-image-minimal.bb
include recipes-core/images/core-image-minimal.bb
IMAGE_FSTYPES = "wic"
WKS_FILE = "sdu-image-minimal.wks"
WKS_FILE_DEPENDS = "systemd-boot e2fsprogs-native"

Étape 3 : Créer le fichier kickstart

Comme évoqué dans l'étape précédente, le fichier kickstart permet de définir les propriétés des partitions à créer. Le schéma de partition à utiliser est imposé par systemd-repart et systemd-sysupdate qui ne sont compatibles qu'avec le schéma GPT.

Ensuite, l'image initiale sera composée des partitions suivantes :

  • Une partition pour systemd-boot, de type EFI System Partition (GUID = C12A7328-F81F-11D2-BA4B-00A0C93EC93B) avec un système de fichier VFAT qui sera peuplé grâce à un plugiciel personnalisé que l'on créera dans la suite de l'article.
  • Une partition de récupération contiendra le système de fichier racine de type ext4 avec le label recovery.

Enfin, il n'y a pas besoin d'ajouter ces partitions dans le fstab car elles seront détectées par systemd-gpt-auto-generator qui les montera automatiquement lors du démarrage du système.

# sdu-image-minimal.wks
part --source custom --fstype=vfat --part-type C12A7328-F81F-11D2-BA4B-00A0C93EC93B --no-fstab-update
part --source rootfs --fstype=ext4 --label recovery --no-fstab-update
bootloader --ptable gpt

Étape 4 : Configuration de systemd

Étape 4a : Activer les modules de systemd

Pour mettre à jour un système embarqué avec systemd nous avons besoin des composantes logicielles suivantes :

  • systemd-repart : permet de créer des partitions
  • systemd-sysupdate : permet de mettre à jour le système
  • systemd-gpt-auto-generator : permet de monter les partitions
  • systemd-bless-boot-generator : permet de valider une entrée de systemd-boot

Or, ces composantes ne sont pas présentes dans les options de configuration par défaut du paquet systemd. Pour les activer, on utilise les configuration suivantes

# systemd_%.bbappend
PACKAGECONFIG:append = " openssl repart journal-upload bzip2 zlib xz importd efi"

Étape 4b : Configurer systemd-repart

Ensuite, on configure systemd-repart de telle sorte qu'au premier démarrage de la partition de récupération les deux partitions qui accueilleront les rootfs soient créées. Pour ce faire, systemd-repart utilise une approche déclarative dans le sens où, pour chaque partition que l'on souhaite créer, on écrit un fichier de configuration décrivant les propriétés de la partition.

Dans notre cas, il s'agira de deux partitions identiques car elles accueilleront des évolutions d'un même système de fichiers racine. Le label associé "_empty" permet de signaler à systemd-sysupdate que la partition est vide. Dans l'image créée, il devra donc y avoir deux fois le fichier suivant.

# 10-rootfs.conf
[Partition]
Type=root
Label=_empty
Format=ext4
SizeMinBytes=200M
SizeMaxBytes=200M

Une fois les fichiers de configuration de systemd-repart créés, il faut que Yocto les ajoute dans le système de fichiers racine de l'image. Comme les deux fichiers sont identiques, pour économiser de l'espace disque, on pourrait faire un lien symbolique entre le deuxième et le premier fichier.

# systemd_%.bbappend
FILESEXTRAPATHS:prepend := "${THISDIR}/systemd:"
SRC_URI += "file://10-rootfs.conf"
do_install:append() {
	repartd_dir=${rootlibexecdir}/repart.d
	install -d ${D}${repartd_dir}
	install -m 0644 ${WORKDIR}/10-rootfs.conf ${D}${repartd_dir}/rootfsA.conf
	install -m 0644 ${WORKDIR}/10-rootfs.conf ${D}${repartd_dir}/rootfsB.conf
}
FILES:${PN} += "${repartd_dir}/10-rootfsA.conf ${repartd_dir}/10-rootfsB.conf"

Étape 4c : Configurer systemd-sysupdate

Enfin, on configure systemd-sysupdate de telle sorte qu'il fasse la mise à jour complète du système (rootfs et noyau). Pour ce faire, systemd-sysupdate, de la même façon que systemd-repart, utilise une approche déclarative dans le sens où, pour chaque ressource à mettre à jour, il faut créer un fichier de configuration permettant de spécifier les propriétés de la mise à jour, des sources de la mise à jour et de la ressource à mettre à jour.

Dans notre cas, on souhaite faire une mise à jour via une clé USB montée dans le répertoire /mnt. Pour cela, il faut que le nom des fichier sources correspondent au schéma présent dans "MatchPattern" de la section "Source" et aient le même numéro de version récupéré avec l'identificateur "@v". Cette version est ensuite comparée avec la version du système démarré stockée dans le champ IMAGE_VERSION du fichier "os-release" pour déterminer s'il faut appliquer la mise à jour. Enfin, on souhaite avoir deux versions concurrentes de chaque ressources et ne pas écraser celle qui est en cours d'utilisation. Cela se fait en la protégeant avec l'identifiant "%A" qui contient la version du système démarré. Lors de la mise à jour, le label de la partition ou le nom du fichier affecté est le champ "MatchPattern" de la section "Target" (ils seront utilisés plus tard dans cet article).

On peut à présent écrire les fichiers de transfert du système de fichiers racine et du noyau. 

# 10-rootfs.conf
[Transfer]
ProtectVersion=%A
[Source]
Type=regular-file
Path=/mnt
MatchPattern=rootfs_@v.ext4.xz
[Target]
Type=partition
Path=auto
MatchPattern=rootfs_@v
MatchPartitionType=root
InstancesMax=2
# 20-kernel.conf
[Transfer]
ProtectVersion=%A
[Source]
Type=regular-file
Path=/mnt
MatchPattern=bzImage_@v.xz
[Target]
Type=regular-file
Path=/boot
MatchPattern=bzImage_@v
Mode=0644
InstancesMax=2

Enfin, lors du redémarrage du système, il faut permettre à systemd-boot de lancer le bon système. Cela peut se faire en mettant à jour ses entrées afin qu'elles contiennent les informations nécessaires pour démarrer le nouveau système installé. Avec systemd-boot, il y a aussi la possibilité de compter le nombre de tentative de démarrage. Ces compteurs sont stockés dans le nom des fichiers des entrées de la façon suivante : "<basename>+<tries_left>-<tries_done>.conf". systemd-sysupdate est capable d'initialiser ces compteurs grâce aux champs "TriesLeft" et "TriesDone". À partir de ces compteurs, systemd-boot détermine l'état des entrées avant de les trier par ordre alphanumérique pour permettre de faire du fallback automatiquement en cas d'échec répété d'une entrée (pour plus de détails voir l'article automatic boot assessment [4]).

# 99-sd-boot-entry.conf
[Transfer]
ProtectVersion=%A
[Source]
Type=regular-file
Path=/mnt
MatchPattern=entry_@v.conf.xz
[Target]
Type=regular-file
Path=/boot/loader/entries
MatchPattern=entry_@v+@l-@d.conf entry_@v+@l.conf entry_@v.conf
Mode=0644
TriesLeft=3
TriesDone=0
InstancesMax=2

Une fois tous les fichiers de configuration de systemd-sysupdate créés, il faut que Yocto les ajoute dans le système de fichiers racine de l'image.

# systemd_%.bbappend
FILESEXTRAPATHS:prepend := "${THISDIR}/systemd:"
SRC_URI += "file://10-rootfs.conf file://20-kernel.conf file://99-sd-boot-entry.conf"
do_install:append() {
	sysupdated_dir=${rootlibexecdir}/sysupdate.d
	install -d ${D}${sysupdated_dir}
	install -m 0644 ${WORKDIR}/10-rootfs.conf ${D}${sysupdated_dir}
	install -m 0644 ${WORKDIR}/20-kenrel.conf ${D}${sysupdated_dir}
	install -m 0644 ${WORKDIR}/99-sd-boot-entry.conf ${D}${sysupdated_dir}
}
FILES:${PN} += "${sysupdated_dir}/10-rootfs.conf ${sysupdated_dir}/20-kernel.conf ${sysupdated_dir}/99-sd-boot-entry.conf"

Étape 5 : La gestion des version - os-release

Comme vu dans l'étape précédente, la version du système démarré est stockée dans le champs IMAGE_VERSION du fichier os-release qui est ensuite utilisé par systemd-sysupdate pour déterminer si la version des sources reçues est plus récente que la version démarrée. Si c'est le cas la mise à jour est alors appliquée.

Il faut donc modifier la recette de os-release pour ajouter le champ IMAGE_VERSION. On aimerait mettre dans ce champs une valeur dont on est sûr qu'elle soit incrémentée à chaque build. La variable DATETIME, qui correspond au moment où le build est lancé, est un bon candidat.

OS_RELEASE_FIELDS += "IMAGE_VERSION"
OS_RELEASE_UNQUOTED_FIELDS += "IMAGE_VERSION"
IMAGE_VERSION = "${DATETIME}"
IMAGE_VERSION[vardepsexclude] = "DATETIME"
do_compile[nostamp] = "1"

Attention, après avoir compilé la recette os-release, cette dernière se retrouve en cache et ne sera plus recompilée à moins d'un changement. Pour ne pas se retrouver avec la même version dans toutes les images compilées, il faut exclure la mise en cache de la tache do_compile pour qu'elle soit exécutée à chaque build.

Étape 6 : Peupler la partition de systemd-boot

Lors de la création du fichier kickstart (étape 3), pour peupler la partition systemd-boot il faut créer un plugiciel custom qui permettra de recréer l'arborescence de fichier suivante

Ce plugin sera chargé de déplacer les fichiers déployés par Yocto dans la partition et de générer la configuration systemd-boot permettant de démarrer la partition de récupération. Pour implémenter le plugiciel il y a deux méthodes de classes à définir :

  • do_configure_partition : pour générer les fichiers de configuration
  • do_prepare_partition : pour créer l'arborescence de fichiers.

Étape 6a : Implémentation de do_configure_partition

Dans cette méthode, on va dans un premier temps créer les dossiers de l'arborescence de fichier. Ensuite on génère la configuration de systemd-boot afin de démarrer la partition de récupération en définissant le noyau à charger ainsi que la kernel command line. Pour monter le système de fichier racine, on peut utiliser le label "recovery" qui a été affecté à la partition de récupération.

@classmethod
def do_configure_partition(cls, part, source_params, creator, cr_workdir,
							oe_builddir, bootimg_dir, kernel_dir,
							native_sysroot):
	kernel = get_bitbake_var("KERNEL_IMAGETYPE")
	
	#
	# Création des dossier
	#
	hdddir = "%s/hdd/boot" % cr_workdir
	install_cmd = "install -d %s/EFI" % hdddir
	exec_cmd(install_cmd)
	install_cmd = "install -d %s/EFI/BOOT" % hdddir
	exec_cmd(install_cmd)
	install_cmd = "install -d %s/loader" % hdddir
	exec_cmd(install_cmd)
	install_cmd = "install -d %s/loader/entries" % hdddir
	exec_cmd(install_cmd)

	#
	# Configuration de systemd-boot loader.conf pour définir le timeout
	#
	loader_conf = "timeout 3\n"
	cfg = open("%s/hdd/boot/loader/loader.conf" % cr_workdir, "w")
	cfg.write(loader_conf)
	cfg.close()

	#
	# Configuration de l'entrée de la partition de récupération
	#
	recovery_conf = "title Recovery\n" # % (title if title else "boot")
	recovery_conf += "linux /%s\n" % kernel
	recovery_conf += "options root=PARTLABEL=recovery rw rootwait console=tty1 panic=3\n"
	cfg = open("%s/hdd/boot/loader/entries/entry.conf" % cr_workdir, "w")
	cfg.write(recovery_conf)
	cfg.close()

Étape 6b : Implémentation de do_prepare_partition

Dans cette méthode, on va récupérer les fichiers déployés par Yocto (noyau et systemd-boot) pour les installer dans la partition. Ensuite, on calculera la taille de la partition sachant qu'il faut prévoir de l'espace disponible pour les fichiers supplémentaires installés au cours de mises à jour. Pour cela, on prévoit la taille de l'arborescence de fichiers à laquelle on ajoute deux fois la taille du noyau et deux fois la taille de l'entrée systemd-boot. En plus de cela, on prévoit de l'espace supplémentaire au cas où le noyau grandit significativement entre deux mises à jour. Enfin, on créer la partition et on y copie l'arborescence de fichiers créés.

@classmethod
def do_prepare_partition(cls, part, source_params, creator, cr_workdir,
							oe_builddir, bootimg_dir, kernel_dir,
							rootfs_dir, native_sysroot):
	hdddir = "%s/hdd/boot" % cr_workdir

	#
	# Récupérer les fichiers déployés par Yocto
	#
	kernel = get_bitbake_var("KERNEL_IMAGETYPE")
	deploy_dir_image = get_bitbake_var("DEPLOY_DIR_IMAGE")
	install_cmd = "install -m 0644 %s/%s %s/%s" % (deploy_dir_image, kernel, hdddir, kernel)
	exec_cmd(install_cmd)
	cp_cmd = "cp %s/systemd-bootx64.efi %s/EFI/BOOT/bootx64.efi" % (deploy_dir_image, hdddir)
	exec_cmd(cp_cmd, True)
	
	#
	# Calculer la taille de la partition
	#
	du_cmd = "du -bks %s" % hdddir
	out = exec_cmd(du_cmd)
	blocks = int(out.split()[0])
	extra_blocks = 0
	du_cmd = "du -bks %s/%s" % (hdddir, kernel)
	out = exec_cmd(du_cmd)
	extra_blocks += 2 * int(out.split()[0])
	du_cmd = "du -bks %s/loader/entries/entry.conf" % hdddir
	out = exec_cmd(du_cmd)
	extra_blocks += 2 * int(out.split()[0])
	blocks += extra_blocks
	
	#
	# Créer la partition
	#
	label = part.label if part.label else "esp"
	bootimg = "%s/esp.img" % cr_workdir
	dosfs_cmd = "mkdosfs -n %s -i %s -C %s %d" % (label, part.fsuuid, bootimg, blocks)
	exec_native_cmd(dosfs_cmd, native_sysroot)
	mcopy_cmd = "mcopy -i %s -s %s/* ::/" % (bootimg, hdddir)
	exec_native_cmd(mcopy_cmd, native_sysroot)
	chmod_cmd = "chmod 644 %s" % bootimg
	exec_cmd(chmod_cmd)
	du_cmd = "du -Lbks %s" % bootimg
	out = exec_cmd(du_cmd)
	part.size = int(out.split()[0])
	part.source_file = bootimg

Étape 7 : Générer des bundles de mise à jour

Maintenant que l'image initiale est prête, pour pouvoir installer une mise à jour il ne reste plus qu'à générer le bundle de mise à jour, i.e. l'ensemble des fichiers qui constituent la mise à jour. Pour cela, il faut récupérer les différents fichiers déployés par Yocto, les compresser, les nommer de telle sorte à ce que le nom corresponde au "MatchPattern" des sources dans la configuration de systemd-sysupdate et qu'elles contiennent toutes le même numéro de version. On crée donc une classe qui permet d'ajouter la fonctionnalité de création d'un bundle de mise à jour si elle est héritée par la recette d'une image.

Enfin, il faut générer une entrée systemd-boot permettant de démarrer cette nouvelle version du système. Pour ce faire, on fait correspondre le "MatchPattern" de la section "Target" du fichier de configuration de la mise à jour de la partition rootfs (qui représente le label affecté à la partition après mise à jour) à l'argument "root=PARTLABEL" de la kernel command line. De plus, on prendra soin de charger la bonne version (DATETIME) du noyau dans la configuration de l'entrée.

inherit image-artifact-names

BUNDLEDIR = "${WORKDIR}/deploy-${PN}-bundle"
SSTATETASKS += "do_bundle"
do_bundle[dirs] = "${BUNDLEDIR}"
do_bundle[cleandirs] += "${BUNDLEDIR}"
do_bundle[sstate-inputdirs] = "${BUNDLEDIR}"
do_bundle[sstate-outputdirs] = "${DEPLOY_DIR_IMAGE}"
do_bundle[vardepsexclude] = "DATETIME"

do_bundle() {
	#
	# Déplacer, renommer et compresser les fichiers déployés par Yocto.
	#
	install -d ${BUNDLEDIR}/bundle
	rootfs_src=${IMGDEPLOYDIR}/${IMAGE_LINK_NAME}.ext4
	rootfs_dst=${BUNDLEDIR}/bundle/rootfs_${DATETIME}.ext4
	install rootfs_src rootfs_dst
	xz rootfs_dst
	kernel_src=${IMGDEPLOYDIR}/${KERNEL_IMAGETYPE}
	kernel_dst=${BUNDLEDIR}/bundle/${KERNEL_IMAGETYPE}_${DATETIME}
	install kernel_src kernel_dst
	xz kernel_dst
	#
	# Générer l'entrée systemd-boot
	#
	cat << EOF > ${BUNDLEDIR}/bundle/entry_${DATETIME}.conf
	title Linux ${DATETIME}
	linux /${KERNEL_IMAGETYPE}_${DATETIME}
	options root=PARTLABEL=rootfs_${DATETIME} console=tty1 rootwait rw panic=3
	xz ${BUNDLEDIR}/bundle/entry_${DATETIME}.conf
}
addtask bundle before do_image_complete after do_image_ext4

Test sur une machine QEMUx86-64

Avant de tester le layer créé sur une cible embarquée, nous allons le tester sur une machine virtuelle x86 avec de l'UEFI. Pour cela, on crée une machine basée sur la machine qemux86-64 fourni par Yocto à laquelle on ajoute la fonctionnalité EFI.

include conf/machine/qemux86-64.conf
MACHINE_FEATURES:append = " efi"
EFI_PROVIDER = "systemd-boot"

La première chose que l'on peut constater est qu'après l'exécution de systemd-repart les partitions qui accueilleront les rootfs ont bien été créées comme attendu.

Ensuite, pour tester le système de mise à jour, on utilise une image contenant un script "helloworld" suivi d'un numéro qui hérite de la classe permettant de créer le bundle. On remarque qu'après l'installation de l'image, une entrée de systemd-boot a été créée et permet de démarrer sur la nouvelle partition (/dev/sda3) et elle contient bien le script "helloworld".

Enfin, lorsque l'on fait une mise à jour du système, on remarque un nouvelle entrée systemd-boot qui permet de démarrer le système mis à jour (/dev/sda4) et qui contient la nouvelle version du script "helloworld".

Test sur beaglebone-black

Pour finir, nous allons tester le layer sur une cible embarqué,e la beaglebone-black. De la même façon que QEMU, on commence par définir une machine.

include conf/machine/beaglebone-yocto.conf
MACHINE_FEATURES:append = " efi"
EFI_PROVIDER = "systemd-boot"
DTB_FILES = "am335x-boneblack.dtb"
IMAGE_BOOT_FILES = "u-boot.${UBOOT_SUFFIX} ${SPL_BINARY}"

On remarque que u-boot est utilisé pour démarrer la carte car systemd-boot n'est pas capable de le faire, cependant il est possible de lancer systemd-boot à partir de u-boot avec la configuration suivante.

CONFIG_BOOTCOMMAND="fatload mmc 0:2 ${loadaddr} EFI/BOOT/bootarm.efi ; bootefi ${loadaddr}"

Pour pouvoir utiliser u-boot, il faudra lui définir une partition dédiée dans le fichier kickstart. De plus, la carte SD doit être formatée avec un schéma GPT hybride qui permet d'enregistrer la partition u-boot dans le MBR pour des raisons de compatibilité tout en ayant un schéma GPT nécessaire pour que systemd-repart et systemd-sysupdate fonctionnent.

part --source bootimg-partition --fstype=vfat --active --fixed-size 32 --mbr --no-fstab-update
part --source custom --fstype=vfat --part-type C12A7328-F81F-11D2-BA4B-00A0C93EC93B --no-fstab-update
part / --source rootfs --fstype=ext4 --label recovery --no-fstab-update
bootloader --ptable=gpt-hybrid

Enfin, la dernière adaption à faire est de prendre en compte le device tree dans le processus de mise à jour. Pour cela on commence par définir un fichier de configuration de systemd-sysupdate pour la mise à jour du device tree:

# 20-devicetree.conf
[Transfer]
ProtectVersion=%A
[Source]
Type=regular-file
Path=/mnt
MatchPattern=am335x-boneblack_@v.dtb.xz
[Target]
Type=regular-file
Path=/boot
MatchPattern=am335x-boneblack_@v.dtb
Mode=0644
InstancesMax=2
# systemd_%.bbappend
SRC_URI += "file://20-devicetree.conf"
do_install:append() {
	sysupdated_dir=${rootlibexecdir}/sysupdate.d
	install -m 0644 ${WORKDIR}/20-devicetree.conf ${D}${sysupdated_dir}
}
FILES:${PN} += "${sysupdated_dir}/20-devicetree.conf"

Ensuite, il faut ajouter ce device tree dans la partition de systemd-boot et définir le champ "devicetree" dans les entrées systemd-boot. Pour cela on complète les méthodes do_configure_partition et do_prepare_partition.

# do_configure_partition
#
# Configuration de l'entrée de la partition de récupération
#
recovery_conf += "devicetree /am335x-boneblack.dtb\n"
# do_prepare_partition
#
# Récupérer les fichiers déployés par Yocto
#
dtb = am335x-boneblack.dtb
install_cmd = "install -m 0644 %s/%s %s/%s" % (deploy_dir_image, dtb, hdddir, dtb)
exec_cmd(install_cmd)
...
#
# Calculer la taille de la partition
#
du_cmd = "du -bks %s/%s" % (hdddir, dtb)
out = exec_cmd(du_cmd)
extra_blocks += 2 * int(out.split()[0])

Enfin, lors de la génération du bundle de mise à jour, il faut prendre en compte l'ajout du device tree en l'ajoutant dans les sources mais aussi dans l'entrée systemd-boot générée.

# do_bundle
#
# Déplacer, renommer et compresser les fichiers déployés par Yocto.
#
dtb_src=${IMGDEPLOYDIR}/${DTB_FILE}
dtb_name="$(basename ${DTB_FILES} .dtb)"
dtb_dst=${BUNDLEDIR}/bundle/${dtb_name}_${DATETIME}.dtb
install dtb_src dtb_dst
xz dtb_dst
...
#
# Générer l'entrée systemd-boot
#
cat << EOF > ${BUNDLEDIR}/bundle/entry_${DATETIME}.conf
title Linux ${DATETIME}
linux /${KERNEL_IMAGETYPE}_${DATETIME}
devicetree /${dtb_name}_${DATETIME}.dtb
options root=PARTLABEL=rootfs_${DATETIME} console=tty1 rootwait rw panic=3
xz ${BUNDLEDIR}/bundle/entry_${DATETIME}.conf

Avec cette configuration, le système arrive à démarrer. Cependant, lorsque l'on redémarre après une mise à jour le système n'est pas capable de décrémenter le compteur de boot.

Failed to rename '\loader\entries\entry+3-0.conf' to 'entry+2-1.conf', ignoring: Access denied

Les attributs du fichier ne semblent pas initialisés correctement par u-boot car ,d'après la documentation UEFI, "0" n'est pas une valeur possible d'attribut.

Pour remédier à ce problème, on peut alors démarrer le système uniquement avec u-boot et essayer de reproduire le comportement de SWUpdate avec systemd-sysupdate. Dans ce cas, nous n'aurons plus que deux partitions : une pour u-boot et une pour le rootfs. Voici le nouveau fichier kickstart

part /boot/uboot --source bootimg-partition --fstype=vfat --fixed-size 32 --mbr --no-fstab-update
part / --source rootfs --fstype=ext4 --label recovery --no-fstab-update
bootloader --ptable=gpt-hybrid

Ensuite, comme u-boot est moins bien intégré avec systemd-sysupdate que systemd-boot, il faut introduire des variables u-boot pour établir une communication entre le bootloader et le système de mise à jour. Pour cela on introduit les variables suivantes :

  • DEFAULT : version par défaut du système à démarrer
  • FALLBACK : version du système en cas de fallback
  • TRIES_LEFT : compteur de boot

Ces variables seront mises à jour à la fin d'une mise à jour juste avant le redémarrage du système avec le patch de systemd-sysupdate suivant :

static int verb_update(int argc, char **argv, void *userdata)
{
	...
	if (strverscmp_improved(applied->version, booted_version)) {
		libuboot_initialize(&ctx, NULL);
		libuboot_read_config(ctx, "/etc/fw_env.config");
		libuboot_open(ctx);
		if (libuboot_get_env(ctx, "DEFAULT")) {
			libuboot_set_env(ctx, "FALLBACK", booted_version);
			libuboot_set_env(ctx, "FALLBACK_TRY_LEFT", "3");
		}
		libuboot_set_env(ctx, "DEFAULT", applied->version);
		libuboot_set_env(ctx, "DEFAULT_TRY_LEFT", "3");
		libuboot_env_store(ctx);
		libuboot_close(ctx);
		libuboot_exit(ctx);
	}
	...
}

Une fois que les variables sont définies, u-boot peut les utiliser dans un script qui permettra de choisir quel système démarrer.

if DEFAULT et FALLBACK non définie then
	Démarrer partition récupération
end if
if DEFAULT définie et FALLBACK non définie then
	if TRIES_LEFT non nul then
		Décrémenter TRIES_LEFT
		Démarrer partition DEFAULT
	else
		Démarrer partition récupération
	end if
end if
if DEFAULT et FALLBACK définie then
	if TRIES_LEFT non nul then
		Décrémenter TRIES_LEFT
		Démarrer partition DEFAULT
	end if
	if TRIES_LEFT non nul then
		Décrémenter TRIES_LEFT
		Démarrer partition FALLBACK
	else
		Démarrer partition récupération
	end if
end if

Enfin, la variable "TRIES_LEFT" doit être ré-initialisée lorsque le système arrive à démarrer pour valider la mise à jour et ne pas la considérer échouée en cas de réussite en décrémentant continuellement le compteur. On créer alors un service systemd-boot qui permet de reset la variable à 3.

[Unit]
Description=Reset boot count for the booted partition
[Service]
ExecStart=/usr/bin/fwsetenv TRIES_LEFT 3
[Install]
WantedBy=multi-user.target

Une fois ces modifications faites, on réalise les mêmes tests que sur QEMU. Il est alors possible de faire des mises à jour du système. Mais la mise en place de cette solution est plus compliquée qu'avec UEFI parce qu'il faut modifier le comportement de systemd avec des patchs pour le faire ressembler à SWUpdate.

Conclusion

La solution proposée par systemd permet de réaliser la mise à jour de systèmes embarqués pour les machines supportant l'UEFI. En effet, systemd-sysupdate et systemd-boot sont bien intégrés entre eux. systemd-sysupdate permet d'installer la mise à jour et systemd-boot s'occupe de choisir automatiquement la bonne entrée à démarrer tout en garantissant un fallback automatique et robuste en cas d'échec répétés d'une entrée. 

En revanche, pour les machines qui ne supportent pas l’UEFI, la solution proposée par systemd est nettement moins intéressante parce qu'on ne peut plus utiliser systemd-boot. On perd ainsi les interactions entre systemd-boot et systemd-sysupdate qui étaient la force de la solution systemd. Dans ce cas là on se retrouve à devoir patcher systemd et des scripts u-boot pour qu'ils aient un comportement similaire à SWUpdate ou RAUC.

Pour terminer voici un tableau comparatif des fonctionnalités entre RAUC SWUpdate et systemd-boot.

Liens

  1. https://www.freedesktop.org/software/systemd/man/latest/systemd-repart.html
  2. https://www.freedesktop.org/software/systemd/man/latest/systemd-sysupdate.html
  3. https://www.freedesktop.org/software/systemd/man/latest/systemd-boot.html
  4. https://systemd.io/AUTOMATIC_BOOT_ASSESSMENT/
  5. https://www.freedesktop.org/software/systemd/man/latest/systemd-gpt-auto-generator.html
  6. https://www.freedesktop.org/software/systemd/man/latest/systemd-bless-boot-generator.html
  7. https://uefi.org/specs/UEFI/2.10/05_GUID_Partition_Table_Format.html

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée.