J’avais un guide d’installation NixOS à vérifier, et une machine incapable d’exécuter Linux. Pas de constructeur Linux, pas de serveur sous la main, un Mac.
Le guide en question promettait deux SSD en miroir et un démarrage qui survit à la mort d’un disque. Pour le prendre au sérieux il fallait répondre à des questions comme « un micrologiciel UEFI sait-il lire cette partition » ou « cette machine redémarre-t-elle avec un disque en moins », et aucune ne se tranche en relisant du texte.
Quatre paliers, donc. Et la partie intéressante n’est pas qu’ils coûtent de plus en plus cher, c’est qu’ils ne répondent pas à la même question.
Quatre questions, pas quatre degrés de réalisme
La tentation, en montant un banc de test, est d’y voir une échelle de fidélité : plus c’est cher, plus c’est proche du réel, et on s’arrête quand le budget est épuisé. Mauvaise façon de penser. Elle laisse croire qu’un palier cher sait faire tout ce que fait le palier bon marché, en mieux.
Chaque palier interroge un interlocuteur différent :
| Palier | À qui on pose la question | Ce qu’il sait répondre |
|---|---|---|
| Évaluation Nix | au système de modules | cette configuration est-elle cohérente |
| Conteneur privilégié | au noyau Linux | que fait vraiment le pilote md, que fait btrfs |
| QEMU et vrai micrologiciel | à EDK2 ou SeaBIOS | que voit une machine avant tout système |
| Installation réelle | à l’assemblage complet | est-ce que ça démarre |
Un palier ne peut pas répondre à la place d’un autre. Demander à Linux si un micrologiciel sait lire une partition n’a aucun sens : Linux a le pilote md, il répondra oui, et il aura raison pour lui. C’est le défaut central du guide que j’examinais, et c’est aussi le piège dans lequel tombe n’importe quel banc de test monté sur un seul étage.
Palier 1 : évaluer sans rien exécuter
Le moins cher, et de loin le plus rentable. NixOS est un système de modules : soumettez-lui une configuration et lisez ce qu’il en pense, sans rien construire ni exécuter.
nix-instantiate --eval --strict -E '
let n = import <nixpkgs/nixos> { configuration = ./configuration.nix; };
in {
drv = n.config.system.build.toplevel.drvPath;
warnings = n.config.warnings;
failedAssertions = builtins.filter (a: !a.assertion) n.config.assertions;
}'
Trois champs, trois natures de défaut. drvPath force l’évaluation complète, donc une option inconnue ou un type incorrect explose ici. warnings ramasse ce que les modules signalent sans bloquer, c’est-à-dire précisément ce que personne ne lit dans un terminal qui défile. assertions donne les incompatibilités que nixpkgs a pris la peine d’interdire.
Quelques secondes, sur n’importe quelle machine où Nix s’évalue, macOS compris, puisque rien n’est jamais construit pour Linux.
Sur le guide examiné, ce palier a sorti un avertissement mdadm que son auteur n’avait pas vu, deux options renommées depuis des mois, deux paquets qui n’existent plus sous ce nom. Rien de spectaculaire, mais c’est ce qu’un lecteur rencontre au premier essai.
Une variante vaut le détour : plutôt qu’une copie de la configuration, j’extrais le bloc du Markdown publié et j’évalue ce texte-là. Trois lignes de Python, et toute la classe des erreurs où l’article et le fichier testé ont divergé disparaît.
block = re.search(r'```nix\n(.*?)\n```', article[i:], re.S).group(1)
Palier 2 : un noyau Linux sans provisionner de machine virtuelle
Pour la couche bloc, il faut un vrai noyau Linux avec md et btrfs. Pas forcément une machine virtuelle à installer : si vous avez podman sur un Mac, vous avez déjà une machine Linux qui tourne.
podman run --rm --privileged -v "$PWD/experiment.sh:/x.sh:z" alpine:latest sh /x.sh
Il manque deux choses dans ce conteneur, et elles se contournent.
Il n’y a pas de périphériques loop, donc on les crée. Le nombre majeur des loop est 7, et le mineur est le numéro :
for i in 0 1 2 3; do [ -e /dev/loop$i ] || mknod /dev/loop$i b 7 $i; done
Il n’y a pas d’udev non plus, donc personne ne crée les nœuds de partition : après un parted, aucun /dev/loop0p1 n’apparaît. La parade est plus élégante que de relancer un démon : un périphérique loop peut viser exactement la région d’une partition.
losetup -o $((1024*1024)) --sizelimit $((511*1024*1024)) /dev/loop2 disk.raw
Ce /dev/loop2 est, octet pour octet, ce que mdadm verrait d’un sda1. On peut créer une grappe dessus, la formater, la monter, mesurer où atterrit le superbloc.
Deux pièges m’ont coûté du temps. Le premier : /nix/store n’est pas partagé avec la machine podman, donc tout ce qui vient du store doit d’abord être copié sous le répertoire de travail avant d’être monté dans un conteneur. Le second : podman met ses images en cache par nom, pas par plateforme, donc une image amd64 récupérée plus tôt est silencieusement réutilisée quand on demande de l’arm64. Il faut l’expliciter, sous peine de voir mdadm échouer sans raison apparente :
podman run --rm --privileged --platform linux/arm64 ...
Ce palier a répondu à tout ce qui concerne le disque : la position du superbloc selon la version de métadonnées, le refus de btrfs de monter une grappe incomplète, l’échec de btrfs device remove sur un miroir à deux disques, et le fait qu’un wipefs sur le disque entier laisse derrière lui le superbloc md d’une installation précédente.
Palier 3 : poser la question au micrologiciel
Vient la question que Linux ne peut pas trancher. Un micrologiciel UEFI lit la partition EFI avant qu’aucun noyau n’existe, et il n’a pas de pilote md. Savoir s’il y arrive demande un vrai micrologiciel.
QEMU en livre deux, et ils sont déjà dans le paquet : EDK2 pour l’UEFI, SeaBIOS pour le BIOS hérité. On construit une image disque, on la démarre sans aucun système d’exploitation, et on lit la console série.
qemu-system-x86_64 -machine q35 -m 512 -nodefaults -no-reboot \
-drive if=pflash,format=raw,unit=0,readonly=on,file="$QEMU/share/qemu/edk2-x86_64-code.fd" \
-drive if=pflash,format=raw,unit=1,file=vars.fd \
-drive file=disk-a.raw,format=raw,if=virtio \
-display none -serial file:serial.log -net none
Le micrologiciel annonce lui-même ce qu’il fait. S’il trouve un chargeur il écrit starting Boot0001, sinon failed to load ... Not Found puis il tombe dans son shell, dont la table de correspondance dit exactement quels systèmes de fichiers il a su reconnaître. C’est un témoin très direct, et qui n’a aucune complaisance.
Restait un obstacle que je croyais rédhibitoire : il me fallait un binaire EFI x86_64 à poser sur l’image, et je n’avais pas de constructeur Linux.
C’est là que j’avais tort, et c’est le déclic de tout ce banc. Nix substitue les fermetures de n’importe quelle plateforme. Construire pour x86_64-linux sur un Mac est impossible ; le télécharger tout construit depuis le cache ne l’est pas.
nix build --no-link --print-out-paths 'nixpkgs#legacyPackages.x86_64-linux.systemd'
Et surtout, ce binaire, je n’ai jamais besoin de l’exécuter. Je dois seulement le copier dans une partition FAT. C’est le micrologiciel émulé qui l’exécutera, et lui est x86_64. La distinction entre construire, exécuter et simplement copier m’avait échappé, et elle changeait tout.
L’émulation a quand même ses limites, et il faut savoir les contourner plutôt que de conclure trop vite. qemu-user traduit mal les ioctl de md : sous émulation, mdadm --create répond
failed to set array info: Not a tty
ce qui n’a aucun rapport avec le problème réel. La même chose arrive à grub-install, qui sonde la grappe avec un ioctl RAID_VERSION. J’ai d’abord déclaré le mode BIOS intestable à cause de ça. C’était faux : la chaîne se coupe en deux. grub-mkimage ne fait qu’empaqueter des modules et ne touche aucun périphérique bloc, donc elle tourne très bien en émulation ; grub-bios-setup écrit des secteurs mais se contente d’un répertoire ordinaire et ne sonde jamais la grappe. L’affirmation que je voulais tester, elle, reste intacte, parce que c’est le préfixe embarqué dans l’image qui enverra GRUB lire la grappe au démarrage.
Quand une étape échoue sous émulation, la question utile n’est pas « comment émuler mieux » mais « quelle partie de cette commande a réellement besoin du matériel ».
Palier 4 : installer pour de bon
Les trois premiers paliers avaient répondu à toutes mes questions, et j’ai failli m’arrêter là. J’ai eu tort, et c’est le palier qui m’a le plus appris.
Je croyais avoir besoin d’une machine x86_64-linux. En fait non : le Mac est en aarch64, et un invité ARM64 y tourne avec l’accélération matérielle, donc à vitesse quasi native. Il me fallait une ISO d’installation aarch64, et NixOS en publie une qu’il suffit de télécharger au lieu de la construire.
curl -L -o nixos.iso \
https://channels.nixos.org/nixos-26.05/latest-nixos-minimal-aarch64-linux.iso
qemu-system-aarch64 -machine virt,accel=hvf -cpu host -smp 4 -m 4096 ...
À partir de là, plus rien n’est émulé au sens approximatif du terme : un vrai udev, un vrai initrd systemd, un vrai nixos-install, de vrais redémarrages. Et débrancher un disque se fait en retirant une ligne de la ligne de commande QEMU, ce qui est nettement plus pratique qu’un tournevis.
Le coût réel, chiffres en main : une ISO de 1,7 Go, deux disques de 16 Gio créés creux, 16 démarrages de machine enregistrés, et 77 minutes entre le téléchargement et le dernier test. Un cycle démarrage plus vérification tient en une à trois minutes.
C’est peu pour ce que ça rapporte. Ce palier a trouvé trois erreurs, et deux d’entre elles étaient dans ma version corrigée du guide, pas dans l’original. J’allais publier un correctif d’une ligne qui ne corrige rien.
Piloter une console série sans y laisser sa santé
Une ISO d’installation est interactive, et il n’y a pas d’autre canal que la console. J’ai écrit un petit pilote façon expect sur la socket série de QEMU.
qemu-system-aarch64 ... -serial unix:serial.sock,server,nowait
Deux pièges méritent d’être connus, parce qu’ils m’ont coûté une heure à eux deux et qu’aucun ne ressemble à ce qu’il est.
Ne jamais cesser de lire. QEMU écrit sur la série de manière bloquante. Si le pilote s’arrête de lire, ne serait-ce que pour un sleep de cinq secondes, le tampon se remplit et c’est l’invité qui se fige, en plein milieu d’une séquence d’échappement. Le symptôme est déroutant : le journal s’arrête net, et ses derniers octets sont une séquence de titre de terminal coupée en deux.
b'[?2004h\r\r\n\x1b[1;31m[\x1b]0;root@n'
L’invite allait s’écrire, le shell attendait, et il ne manquait que les octets suivants. Il faut donc vider la socket en continu, y compris entre deux frappes de clavier.
Ne jamais se synchroniser sur un marqueur qu’on a tapé. Mon premier pilote envoyait commande ; echo FINI puis attendait FINI. Il l’a trouvé immédiatement, parce que le terminal renvoie en écho la ligne qu’on tape, marqueur compris, avant même que la commande ne s’exécute. Le pilote partait alors en avance d’une commande et tout le reste se désynchronisait. La bonne synchronisation est l’invite du shell elle-même, qui n’apparaît qu’une fois la commande terminée.
Dernier conseil, qui supprime beaucoup de frappe : en mode réseau utilisateur, l’invité joint l’hôte à l’adresse 10.0.2.2. Un serveur HTTP d’une ligne sur l’hôte suffit à livrer les scripts à l’invité, qui n’a plus qu’à faire curl ... | sh. On itère alors sur un fichier local au lieu de retaper deux cents lignes dans une console série.
Ce que chaque palier a trouvé
| Palier | Trouvailles |
|---|---|
| Évaluation Nix | options renommées, paquets disparus, avertissement mdadm ignoré |
| Conteneur privilégié | décalage du superbloc, refus de montage dégradé, device remove impossible, nettoyage de disque incomplet |
| QEMU et micrologiciel | la partition EFI illisible par EDK2, le mode BIOS fonctionnel sous SeaBIOS |
| Installation réelle | conflit sur les fileSystems, options de montage perdues, correctif degraded inopérant |
La répartition m’a surpris. Les paliers bon marché ont trouvé le défaut spectaculaire, celui qui fait l’article : une machine parfaitement installée et incapable de démarrer. Le palier cher, lui, a trouvé les défauts discrets, ceux qu’on publie sans les voir parce qu’ils ressemblent à du bon sens.
Et c’est bien la raison de monter le dernier étage. Un correctif qui a l’air évident, qui s’évalue sans erreur et que tout le monde recommande peut malgré tout ne rien corriger. Il n’y avait qu’une manière de le savoir.
Ce que j’en retiens
Les paliers ne forment pas une échelle qu’on gravit jusqu’à épuisement du budget. Ce sont quatre interlocuteurs, et on va voir celui qui détient la réponse.
La question à se poser devant un banc de test n’est donc pas « est-ce assez réaliste », mais « est-ce que cette mesure traverse la frontière où vit le problème ». Un guide qui vérifie son installation en interrogeant Linux ne saura jamais ce qu’un micrologiciel en pense. Un banc qui s’arrête à l’évaluation ne saura jamais si la machine démarre.
C’est exactement ce qui m’est arrivé, d’ailleurs, à une frontière que je n’avais pas vue. J’ai failli publier que degraded dans les options de montage suffisait à redémarrer sur un disque. L’option est correcte. Elle arrive simplement après le moment où tout se joue, et seul un vrai démarrage pouvait me le dire.
Pour reproduire
- QEMU pour la machine
virtARM64 et ses options de firmware - Les ISO NixOS, dont les images aarch64, à télécharger plutôt qu’à construire
- losetup(8), pour
--offsetet--sizelimit - Le manuel NixOS, pour
nixos-installetnixos-generate-config