Aujourd’hui c’est déjà demain : systemd dans l’initrd sous Arch Linux

23
16
oct.
2014
Technologie

Nous allons voir dans ce petit article comment utiliser systemd dans l’initrd. La construction d’un initrd étant spécifique à la distribution, nous verrons comment l’utiliser avec Arch Linux, mais avec un peu de travail cela devrait pouvoir donner le principe général de fonctionnement et être adaptable sur d’autres distributions.

Sommaire

ATTENTION : nous allons modifier une partie très importante du démarrage de la machine, et il est probable qu’elle ne puisse plus démarrer. Prévoyez donc un live CD quelconque pour pouvoir réparer en cas de souci !

Quelques généralités sur l’initrd

L’initrd, c’est une archive cpio qui est chargée en mémoire vive par le noyau juste après son lancement. Elle est montée sur /, puis le fichier /init est exécuté. Ce dernier est censé s’occuper de lancer tout ce qui est nécessaire au montage de du vrai système de fichiers racine /, et qui n’est pas en dur dans le noyau, comme le chargement de modules noyau : accès au LVM, le déchiffrement de partitions, montage de /usr s’il est sur une partition séparée…

Traditionnellement, ce fichier init est un script shell construit par des outils spécifiques à la distribution.

La construction classique de l’initrd sous Arch Linux

Sous Arch Linux, la construction d’un initrd se fait à l’aide du logiciel mkinitcpio. Il se configure en utilisant le fichier /etc/mkinitcpio.conf. La variable HOOKS est celle qui nous intéresse présentement.

Pour chaque hook présent dans cette variable, deux fichiers seront utilisés, un script install qui sera chargé d’ajouter les fichiers nécessaires à l’initrd lors de la construction, et un script hook qui sera lancé par le script shell init lors de l’amorçage du système. L’ordre d’apparition des hooks dans la variable HOOKS est important, car c’est l’ordre utilisé pour lancer les scripts lors de l’amorçage.

L’utilisation de systemd dans l’initrd

Lorsqu’on utilise systemd dans l’initrd, le fichier /init de l’initrd n’est plus un script shell, mais directement systemd.

Ainsi, lors de la construction de l’initrd avec mkinitcpio, le script hook n’a plus aucun effet, et le script install est chargé, outre les fichiers nécessaires, tels les démons et modules noyaux, d’installer les unités systemd nécessaires à monter la vraie racine. L’ordre d’apparition des hooks dans la variable HOOKS n’a plus d’importance (à une exception près que nous verrons plus bas), car ce sont les unités systemd qui détermineront l’ordre de lancement des différentes unités (et donc de tous les binaires lancés, car tout est lancé via systemd).

systemd dans l’initrd, ça sert à quoi ?

Il y a sûrement des avantages secondaires, comme la parallèlisation, une configuration de démarrage unique et centralisée, et peut‐être d’autres, mais l’avantage principal est quand même de faire rager les anti‐systemd en le mettant partout et à toutes les sauces.

Configuration du système pour utiliser systemd

Le problème principal est que le support d’Arch Linux pour systemd dans l’initrd est un peu balbutiant, et il faut donc mettre un peu les mains dans le cambouis pour que tout fonctionne. Nous verrons ici comment configurer un système utilisant une partition chiffrée contenant du LVM et Plymouth, puisque c’est ma configuration. L’ajout du RAID ne doit pas être très compliqué.

La base

Les hooks base, usr, udev et timestamp ne sont plus nécessaires et sont remplacés par le hook systemd. Les hooks autodetect, block, filesystems, btrfs et keyboard sont toujours nécessaires (au moins sur ma machine). Les autres hooks (lvm, encrypt, plymouth, etc.) seront par la suite remplacés par des équivalents sd-machin.

Les hooks sd-machin doivent toujours être après le hook systemd. En effet, ce hook ajoute une fonction utilisable dans les autres hooks, add_systemd_unit. Cette fonction ajoute dans l’initrd l’unité passée en paramètre, ainsi que toutes ses dépendances.

Correctif 1 : unités dans /etc

Cette fonction a un problème : elle n’utilise que le dossier /usr/lib/systemd/system et ignore donc totalement les unités créées par l’utilisateur, situées dans le dossier /etc/systemd/system (voir le rapport de bogue).

Nous allons donc modifier le hook, en copiant /usr/lib/initcpio/install/systemd vers /etc/initcpio/install/systemd. Nous allons donc pouvoir effectuer nos modifications sans risquer de les voir écrasées par une mise à jour.

Le correctif est le suivant :

--- /usr/lib/initcpio/install/systemd   2014-09-02 00:11:28.000000000 +0630
+++ /etc/initcpio/install/systemd       2014-10-16 13:39:11.291460470 +0630
@@ -51,7 +51,7 @@

     local unit= rule= entry= key= value= binary= dep=

-    unit=$(PATH=/usr/lib/systemd/system:/lib/systemd/system type -P "$1")
+    unit=$(PATH=/etc/systemd/system:/usr/lib/systemd/system:/lib/systemd/system type -P "$1")
     if [[ -z $unit ]]; then
         # complain about not found unit file
         return 1
@@ -78,18 +78,23 @@
     done <"$unit"

     # preserve reverse soft dependency
-    for dep in {/usr,}/lib/systemd/system/*.wants/${unit##*/}; do
+    for dep in {{/usr,}/lib,/etc}/systemd/system/*.wants/${unit##*/}; do
         if [[ -L $dep ]]; then
             add_symlink "$dep"
         fi
     done

     # add hard dependencies
-    if [[ -d $unit.requires ]]; then
-        for dep in "$unit".requires/*; do
-            add_systemd_unit ${dep##*/}
-        done
-    fi
+    for dir in {{/usr,}/lib,/etc}/systemd/system/${unit##*/}.requires; do
+        if [[ -d "$dir" ]]; then
+            for dep in "$dir"/*; do
+                if [[ -L $dep ]]; then
+                    add_symlink "$dep"
+                fi
+                add_systemd_unit ${dep##*/}
+            done
+        fi
+    done
 }

 build() {

Correctif 2 : emergency.target

Second problème de ce hook, il n’ajoute pas les utilitaires nécessaires au fonctionnement d’emergency.target, une unité spéciale qui est lancée lorsque l’amorçage plante et donne un shell à l’utilisateur pour essayer de sauver les meubles (voir un rapport de bogue et un autre). On va donc le modifier pour rajouter sulogin, ainsi que les utilitaires de busybox.

--- /etc/initcpio/install/systemd.old   2014-10-16 13:44:23.135993657 +0630
+++ /etc/initcpio/install/systemd       2014-10-16 13:43:23.657715653 +0630
@@ -98,12 +98,22 @@
 }

 build() {
-    local rules unit
+    local rules unit applet

     # from base
     add_binary /bin/mount
     add_binary /usr/bin/kmod /usr/bin/modprobe

+    add_binary /usr/lib/initcpio/busybox /bin/busybox
+    for applet in $(/usr/lib/initcpio/busybox --list); do
+      add_symlink "/usr/bin/$applet" busybox
+    done
+
+    # sulogin is needed for emergency target
+    add_binary /sbin/sulogin
+    add_file /etc/shadow
+    add_file /etc/gshadow
+
     # systemd
     add_binary /usr/lib/systemd/systemd /init
     add_binary /usr/bin/systemd-tmpfiles

À ce stade, si l’on a une partition racine simple, on a terminé.

Partition chiffrée

S’il y a une partition chiffrée nécessaire au démarrage, il faut ajouter le hook sd-encrypt.

Attention, les paramètres de ligne de commande du noyau (probablement configurés dans /etc/default/grub) pour indiquer les partitions et leurs options ont changé. Le paramètre cryptdevice est remplacé par luks.machin (man systemd-cryptsetup-generator pour plus d’infos).

Cependant, plutôt que ces paramètres, il est plus utile d’utiliser le fichier /etc/crypttab.initramfs, qui suit la syntaxe de /etc/crypttab.

LVM

Il suffit d’ajouter le fichier sd-lvm. Aucune configuration particulière, car un détection est lancée dès l’apparition d’un nouveau block device, et activé si celui‐ci est un LVM (donc on peut avoir une partition chiffrée dans un LVM et un LVM dans une partition chiffrée, sans avoir à préciser d’ordre particulier ; et même avoir une partition chiffrée dans un LVM dans une partition chiffrée).

Résumé

Il n’y a rien dans la version stable de systemd pour s’occuper de sortir le système de l’hibernation. C’est prévu pour la prochaine version, mais en attendant, il va falloir faire les choses à la main.

On crée donc une unité /etc/systemd/system/resume.target :

[Unit]
Description=Resume from disk
Before=initrd-root-fs.target sysroot.mount

[Install]
WantedBy=initrd.target

Et on l’active avec systemctl enable resume.target.

Puis une unité générique /etc/systemd/system/resume@.service :

[Unit]
Description=Resume from disk using %I
Before=resume.target
DefaultDependencies=no
BindsTo=%i.device
After=%i.device

[Service]
Type=oneshot
ExecStart=/bin/sh -c "echo $(mountpoint -x %I) > /sys/power/resume"

[Install]
RequiredBy=resume.target

On l’active en utilisant le chemin de sa partition d’échange (swap), dans mon cas : systemctl enable resume@dev-main-swap.service.

On crée enfin le hook mkinitcpio /etc/initcpio/install/sd-resume :

#!/bin/bash

build() {
    add_systemd_unit resume.target
}

help() {
    cat <<HELPEOF
This hook adds resume capabilities to the initramfs.
HELPEOF
}

# vim: set ft=sh ts=4 sw=4 et:

Et on l’active en ajoutant sd-resume dans la variable HOOKS.

Plymouth

Si l’on utilise le AUR de Plymouth, cela demande un peu de travail.

Création du hook

Il n’y a pas de hook sd-plymouth, donc il va falloir en créer un à partir du hook plymouth. On copie donc /usr/lib/initcpio/install/plymouth vers /etc/initcpio/install/sd-plymouth, puis on applique le correctif suivant pour ajouter les unités systemd :

--- /usr/lib/initcpio/install/plymouth  2014-10-16 09:38:06.000000000 +0630
+++ /etc/initcpio/install/sd-plymouth   2014-10-16 14:05:01.237713438 +0630
@@ -1,7 +1,6 @@
 build() {
        add_dir /dev/pts
        add_dir /usr/share/plymouth/themes
-       add_dir /var/run/plymouth

        DATADIR="/usr/share"
        PLYMOUTH_LOGO_FILE="${DATADIR}/plymouth/arch-logo.png"
@@ -49,7 +48,15 @@
        add_binary "$(readlink -e /lib/libnss_files.so.2)"
        add_file /lib/libnss_files.so.2

-       add_runscript
+       add_systemd_unit systemd-ask-password-plymouth.path
+       add_systemd_unit systemd-ask-password-plymouth.service
+
+       map add_systemd_unit plymouth-switch-root.service \
+           plymouth-start.service \
+           plymouth-reboot.service \
+           plymouth-kexec.service \
+           plymouth-poweroff.service \
+           plymouth-halt.service
 }

 help() {

Utilisation avec systemd-ask-password

Lorsqu’on utilise Plymouth avec un système ayant besoin d’un mot de passe (comme une partition chiffrée), il faut que le mot de passe soit demandée via Plymouth et non sur la console. systemd possède un système plutôt malin pour gérer ce genre de choses, sauf que l’unité plymouth-start.service est cassée, et donc la détection du l’utilisation de Plymouth ne marche pas. On copie donc l’unité /usr/lib/systemd/system/plymouth-start.service vers /etc/systemd/system/plymouth-start.service et on applique le correctif suivant pour corriger le chemin du fichier contenant l’identifiant de processus :

--- /usr/lib/systemd/system/plymouth-start.service      2014-10-16 14:15:55.005953827 +0630
+++ /etc/systemd/system/plymouth-start.service  2014-10-16 14:06:53.054169994 +0630
@@ -7,7 +7,8 @@
 ConditionKernelCommandLine=!plymouth.enable=0

 [Service]
-ExecStart=/usr/bin/plymouthd --mode=boot --pid-file=/var/run/plymouth/pid --attach-to-session
+ExecStartPre=/bin/mkdir /run/plymouth
+ExecStart=/usr/bin/plymouthd --mode=boot --pid-file=/run/plymouth/pid --attach-to-session
 ExecStartPost=-/usr/bin/plymouth show-splash
 Type=forking
 KillMode=none

Création de l’initrd

On crée l’initrd avec mkinitcpio -p linux, on redémarre et on croise les doigts. :)

Pour s’amuser un peu

Pour s’amuser un peu, on peut créer des unités et supprimer de son système /etc/crypttab, /etc/cryttab.initramfs, /etc/fstab et certains paramètres de ligne de commande du noyau (comme root, par exemple).

Ainsi, toute la configuration de démarrage du système est centralisée dans des unités systemd, et non plus éparpillée dans différents endroits.

Suppression de /etc/crypttab et des paramètres luks.machin du noyau

Le plus simple est d’utiliser directement le générateur /usr/lib/systemd/system-generators/systemd-cryptsetup-generator /etc/systemd/system /etc/systemd/system /etc/systemd/system, puis d’analyser les fichiers pour éventuellement les simplifier.

On peut ensuite déplacer /etc/crypttab.initramfs vers /etc/crypttab, puis relancer la commande. On peut maintenant se débarrasser des ces deux fichiers.

Pour les paramètres luks.machin du noyau, le plus simple est de les transformer en une entrée de /etc/crypttab avant de lancer le générateur.

Suppression de /etc/fstab

Même principe en utilisant le fichier /usr/lib/systemd/system-generators/systemd-fstab-generator.

Suppression du paramètre root du noyau

Cette partie est normalement gérée par le générateur fstab, mais celui‐ci génère l’unité correspondant au paramètre root du noyau seulement s’il est lancé dans l’initrd. Il faudra donc créer l’unité à la main.

Cette unité doit s’appeler /etc/systemd/system/sysroot.mount et doit contenir les options habituelles d’une unité [Mount] (man systemd.mount pour les détails). Cependant, les lignes suivantes sont indispensables :

[Install]
RequiredBy=initrd-root-fs.target

[Unit]
Before=initrd-root-fs.target

[Mount]
Where=/sysroot

Il faut bien sûr ajouter les lignes spécifiques à votre système, notamment What et peut‐être Type dans la section [Mount].

Ne pas oublier d’activer l’unité avec systemctl enable sysroot.mount.

Conclusion

Cette solution de gestion de l’initrd, qui en est à ses débuts, sera sûrement une solution standard à l’avenir et par défaut sur les distributions majeures.

À vos trolls !

  • # /var/run -> /run

    Posté par (page perso) . Évalué à 4.

    Pourquoi dois-tu patcher ceci :

    -ExecStart=/usr/bin/plymouthd --mode=boot --pid-file=/var/run/plymouth/pid --attach-to-session
    +ExecStartPre=/bin/mkdir /run/plymouth
    +ExecStart=/usr/bin/plymouthd --mode=boot --pid-file=/run/plymouth/pid --attach-to-session

    … sachant que /var/run est un lien symbolique vers /run ?

    Et pourquoi donc supprimer cette ligne ?

    -       add_dir /var/run/plymouth

    <mode pour_vendredi>
    Merci pour ton article, ça donne une vue d'ensemble plus précise de ce qu'il est nécessaire de savoir en plus pour mettre en place un initrd : savoir scripter + savoir alimenter initcpio via son vocabulaire + ajouter des unités via le vocabulaire systemd + patcher l'existant)
    </mode>

    C'est néanmoins très complet et ça prend en compte LVM + le chiffrement + Plymouth, merci donc.

    • [^] # Re: /var/run -> /run

      Posté par (page perso) . Évalué à 4.

      Il est possible que le lien de /var/run vers /run ne soit pas encore en place à ce moment la ?

    • [^] # Re: /var/run -> /run

      Posté par (page perso) . Évalué à 4.

      Pourquoi dois-tu patcher ceci :
      […]
      sachant que /var/run est un lien symbolique vers /run ?

      Comme l'a supposé Misc au dessus, /var/run n'est un lien symbolique vers /run que plus tard.

      Et pourquoi donc supprimer cette ligne ?

      La création du dossier /run/plymouth ne fonctionne pas dans le script install, car cela créé un dossier /run/plymouth dans l'initrd, mais un tmpfs est monté sur /run, donc ce qu'il y a dans /run dans l'initrd est caché.

      Il faut donc créer le dossier au boot, donc dans l'unité systemd.

      Merci pour ton article, ça donne une vue d'ensemble plus précise de ce qu'il est nécessaire de savoir en plus pour mettre en place un initrd

      Il faut espérer que les bug reports soient corrigés et qu'utiliser systemd dans l'initrd se fasse en 5 minutes en changeant juste les hooks.

  • # Chargement de l'initrd

    Posté par . Évalué à 5.

    L'initrd, c'est une archive cpio qui est chargée en RAM par le noyau juste après son lancement.

    J'aurais plutôt dit que c'était chargé par le chargeur de démarrage. Sinon, je vois pas trop comment il fait pour la récupérer sur le disque.

    • [^] # Re: Chargement de l'initrd

      Posté par (page perso) . Évalué à 2.

      Tout à fait. D'après man initrd, le chargeur de démarrage charge l'initrd en mémoire dans /dev/initrd, puis lors du démarrage du noyau, celui-ci décompresse et copie le contenu dans /dev/ram0 puis libère le contenu de la mémoire de /dev/initrd.

      • [^] # Re: Chargement de l'initrd

        Posté par . Évalué à 6.

        Oui en fin ça c'est l'ancienne méthode. La « nouvelle » (depuis 10 ans peut-être ?) c'est l'initramfs, copié en RAM par le chargeur de démarrage également (il ne connaît pas la différence, d'ailleurs), qui est décompressé dans un tmpfs (indépendant du système de bloc, contrairement au /dev/ramX).

  • # Pour éviter le live-cd

    Posté par (page perso) . Évalué à 5.

    Pour éviter d'avoir à sortir le live-cd pendant les tests ou suite à une mise à jour fâcheuse d'une version du noyau, je conseille :

    • l'installation du noyau LTS en plus du noyau classique (paquet linux-lts) ;
    • la création de plusieurs initrd pour chaque noyau, un avec et un sans systemd, au cas où.

    Tout ceci est très facile à faire puisqu'il suffit de copier /etc/mkinitcpio.conf vers /etc/mkinitcpio-systemd.conf par exemple, de faire les modifications désirées, puis de modifier les fichiers /etc/mkinitcpio.d/linux*.preset pour ajouter un nouveau PRESET utilisant la config systemd ;

    # mkinitcpio preset file for the 'linux' package
    
    ALL_config="/etc/mkinitcpio.conf"
    ALL_kver="/boot/vmlinuz-linux"
    
    PRESETS=('default' ' fallback' 'systemd')
    
    default_config="/etc/mkinitcpio.conf"
    default_image="/boot/initramfs-linux.img"
    default_options=""
    
    fallback_config="/etc/mkinitcpio.conf"
    fallback_image="/boot/initramfs-linux-fallback.img"
    fallback_options="-S autodetect"
    
    systemd_config="/etc/mkinitcpio-systemd.conf"
    systemd_image="/boot/initramfs-linux-systemd.img"
    systemd_options=""
    

    Un petit mkinitcpio -p linux && mkinitcpio -p linux-lts et hop, il ne reste plus qu'à créer les entrées dans GRUB/gummiboot pour avoir le choix au boot en cas d'imprévu.

    Seul inconvénient : il faut avoir de la place dans sa partition /boot.

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.