Mon lab - Épisode 1 : Automatiser le socle
2025-11-08
Au tout début
Mon lab, je l'hérite de quand j'étais étudiant, c'était mon premier PC qui a évolué progressivement, nouveau boitier, nouvelle carte mère + CPU + RAM, nouveau disque... Bref, la machine de 2025 ne ressemble plus du tout à celle de 2012. Autant vous dire que ce serveur en a vu des horreurs, je me suis fait les dents dessus. Lorsqu'il a quitté sa fonction de PC pour devenir un NAS (Et oui, au début j'en avais fait un NAS !) c'était une Debian 8. Debian qui a connu que des mises à jour à coup de dist-upgrade.
Aujourd'hui le serveur tourne bien et porte 80% de mes usages d'internet, mais son aspect unique, centralisé, juste branché en direct sur la Freebox et à la merci de mes tests me dérange un peu. Surtout que, même si en termes de puissance il reste très bien équipé, des projets et tests gourmands sont à l'étroit et dégradent les autres applications disponibles. Je discute aussi avec des gens qui ont des labs plus intéressants que le mien et qui me font rêver. Rajoutez un changement de boulot dans une équipe orientée réseau (où j'ai tout à ré-apprendre) et vous avez tous les ingrédients pour me donner envie de redessiner intégralement mon infrastructure.
L'idée est que demain mon lab ressemble à ça :
Je n'ai certes pas prévu d'avoir autant de machines et non je n'ai pas simplement branché des machines sur un switch relié à un routeur (même si vous venez de grossièrement décrire le fonctionnement d'internet), les serveurs seront des hyperviseurs gérant eux-mêmes leurs réseaux comme ceci
Bref, beaucoup d'Overengineering pour héberger un serveur Minecraft (et deux trois autres trucs).
--------------------------------------------------------------------------------
J'ai envie que dans mon lab tout soit reproductible, de toute façon c'est la mode avec l'infrastructure as code et le "cloud". Il y a aussi la raison derrière qui est que si je fais une erreur assez grave comme modifier tous les droits de mon système, je peux tout remettre d'équerre facilement (en théorie). Ça amène aussi l'évolutivité de la plateforme et les mises à jour. Au revoir les binaires legacy en python2.7 qui trainent sur mon serveur, mes erreurs de jeunesse et autres configurations fantômes qui font tout tenir et que le jour où je modifie ça, tout explose.
Pour répondre à cette envie, j'ai pas mal creusé et réfléchi. La première question est : Comment automatiser l'installation du socle ?
Quel outil choisir ?
J'ai cherché un moyen pour installer automatiquement et à l'identique plusieurs serveurs en prenant en compte le partitionnement et les logiciels de base utilisés par le système et ce, sans aucune intervention manuelle (je n'ai pas d'IPMI pour intervenir).
MaaS, Foreman et d'autres outils entrent dans la pièce
J'ai découvert MaaS. MaaS pour Metal as a Service est un outil développé par Canonical pour déployer facilement et à grande échelle des serveurs. Par défaut l'outil embarque Ubuntu LTS ou CentOS. Pour avoir une image Debian (ou autre) il faut utiliser Packer (en soi c'est bien, c'est reproductible) mais sur la documentation Debian est flaggué en Beta. La création de cette image ne me convient pas. Il faut lancer Packer qui va exécuter des scripts shell qui vont configurer le serveur en ajoutant des PPA et des dépôts Ubuntu pour finir par fournir une sorte d'ISO déployable par MaaS. MaaS s'installe via Snap (😬) ou PPA et avec le changement de licence qu'ils ont fait sur LXD, je me méfie. Bref ça rajoute énormément de complexité (c'est curieux d'écrire ça au vu de ce que va être mon infra) pour une petite infrastructure ainsi que de surcouche alors que je ne maitrisais même pas la base à ce moment-là.
Pour les curieux⋅ses un petit article qui pourraient vous intéresser d'une personne qui a testé et poussé MaaS.
Il y a son pendant sous Fedora, mais il faut compiler les paquets pour Debian : Cobbler. J'ai testé de le faire tourner, mais sans grand succès, j'ai fini par lâcher l'affaire. J'ai également regardé du côté de Foreman, mais 4Go de RAM pour faire tourner un serveur PXE ça m'a un peu refroidi. Je comprends cependant l'intérêt dans un datacenter, mais pour un petit lab c'est un peu overkill. Il m'a été proposé iVentoy, du célèbre outil Ventoy, mais le fait qu'il ne soit que partiellement open-source me rebute. Il existe aussi Netboot.xyz qui, sur le papier est très bien mais qui ajoute une surcouche et je n'ai pas vu comment gérer l'installation sans brancher un clavier. Il m'a également été proposé Clonezilla qui, dans son approche, est pas mal, car on ne crée une image qu'une fois et on la restaure sur chaque machine. Dans mon cas j'ai très peu de machines similaires, certaines possèdent des NVME, d'autres des SSD SATA (Clonezilla fait la différence), de l'Intel, de l'AMD (là c'est Debian qui installe les paquets spécifiques au CPU)... Une en multi-disque. On finit par faire autant d'images qu'on a de machines, ce qui perd de son intérêt.
Mais seul PXE reste
Il me restait donc PXE et iPXE. Bon, iPXE c'est une évolution de PXE, soit il est présent sur la carte réseau, soit il faut la flasher (vu que la mienne est soudée sur la CM c'est pas possible), soit il faut boot en PXE, charger iPXE puis charger l'image ensuite. iPXE a de nombreux avantages, boot d'une ISO en HTTPS, support donc d'HTTPS, du scripting plus avancé et retire la dépendance à TFTP (Trivial File Transfert Protocol) qui n'est pas sécurisé (pas d'authentification, utilisation de l'UDP, pas de chiffrement). Dans mon cas je n'y vois pas de grand intérêt, tout est local et mon facteur de risque rend difficilement envisageable un ajout de backdoor sur mon serveur TFTP. Donc du PXE ça me va. Et ça me laisse des axes d'améliorations 😛
Donc je pars sur du PXE.
PXE c'est assez simple, lorsqu'il démarre le service fait un DHCPDISCOVER pour demander une IP, le serveur DHCP lui en donne une avec deux options :
- l'adresse IP d'un second serveur sur lequel il pourra récupérer sa configuration
- le chemin du fichier de configuration que le service doit récupérer.
Ensuite le PXE va télécharger le fichier de configuration qui est le bootloader, l'éxecute. Le bootloader quant à lui va lancer et télécharger les kernels, initramfs et tout ce qu'il a besoin.
Le netboot
Déjà pourquoi partir sur Debian ? J'utilise cet OS depuis bientôt 10 ans et tous mes playbooks sont configurés pour Debian. Il y a aussi le fait que je n'aime pas Ubuntu (vraiment avec Snap là brrrrrrrrrrr). Proxmox aurait sûrement pu répondre à mon projet qui devait s'appuyer sur 2 technos (que vous verrez dans un prochain épisode) mais pas sûr... Et il fallait faire beaucoup de réécriture de mes recettes terraform actuelles.
Je garde quand même un atout dans ma manche : PXE. Demain je veux installer Proxmox ou tout autre OS, je génère une nouvelle image et zou.
Debian avec preseed
Debian propose un installeur en PXE netboot. Mais l'installeur est interactif, ce qui est contraire à l'automatisation... Sauf si on utilise un fichier Preseed. En réalité le fichier Preseed va "simplement" répondre aux questions de l'installeur à notre place sauf s'il n'a pas réussi à résoudre une question.
--------------------------------------------------------------------------------
Je vous partage tout de même cette méthode que j'ai creusé mais je ne l'utilise pas. Vous pouvez directement passer à la méthode « Faire sa propre image Debian Netboot » si vous souhaitez un truc qui marche 😛
--------------------------------------------------------------------------------
Le Preseed, une fois la machine installée, se récupère avec la commande debconf-get-selections --installer . C'est illisible et très lourd pour ce que c'est. J'ai pris une autre option, créer mon fichier Preseed en m'appuyant notamment sur ce site preseed.valete.fr.
Pour le coup les commentaires, bien que en anglais, sont assez éloquents.
## locales # Preseeding only locale sets language, country and locale. d-i debian-installer/locale string fr_FR.UTF-8 d-i debian-installer/language string en d-i debian-installer/country string FR d-i keyboard-configuration/xkb-keymap string fr(latin9) ## Network # Disable network configuration entirely. This is useful for cdrom installations on non-networked devices where the network questions, warning and long timeouts are a nuisance. d-i netcfg/enable boolean true # netcfg will choose an interface that has link if possible. This makes it skip displaying a list if there is more than one interface. d-i netcfg/choose_interface select auto # Any hostname and domain names assigned from dhcp take precedence over values set here. However, setting the values still prevents the questions from being shown, even if values come from dhcp. d-i netcfg/get_hostname string kida # Any hostname and domain names assigned from dhcp take precedence over values set here. However, setting the values still prevents the questions from being shown, even if values come from dhcp. d-i netcfg/get_domain string srv.dryusdan.net # If non-free firmware is needed for the network or other hardware, you can configure the installer to always try to load it, without prompting. Or change to false to disable asking. d-i hw-detect/load_firmware boolean true ### Mirror settings # If you select ftp, the mirror/country string does not need to be set. d-i mirror/protocol string http d-i mirror/country string fr d-i mirror/http/hostname string deb.debian.org d-i mirror/http/directory string /debian d-i mirror/http/proxy string
Globalement, ici j'ai configuré mon clavier en FR, activé le réseau et le DHCP pour finir par nommer ma machine kida.srv.dryusdan.net. Puis j'ai configuré apt pour utiliser deb.debian.org.
La suite du fichier Preseed va créer un compte (avec comme mot de passe azerty :p ). Il est important de noter qu'il faut soit créer un compte utilisateur soit définir un mot de passe sur le compte root. Et si on veut uniquement se connecter en SSH... Il faut gérer ça plus tard. J'en profite pour régler l'heure et la timezone de la machine (qui de toute façon seront réécrites par mes playbooks).
## accounts # Skip creation of a root account (normal user account will be able to use sudo) d-i passwd/root-login boolean true # mkpasswd -s -m sha-512 d-i passwd/root-password-crypted password $6$fmioCke46GgEcazY$Orn2E3TT3Vbr17gSiFjgi0x5pbSQjL1S6uSlAdBUSGI/F/KtMDV2968H7ckj6NzLjEigpQmY4pMBtczXocRev. # Le mot de passe est bidon, c'est azerty :p # Skip creation of a normal user account. d-i passwd/make-user boolean false ## Clock and timezone # Controls whether or not the hardware clock is set to UTC. d-i clock-setup/utc boolean true d-i time/zone select
J'attaque maintenant le partitionnement. Tout d'abord si le disque n'est pas détecté (par exemple sur une machine virtuelle), on peut forcer son utilisation en décommentant la ligne d-i partman-auto/disk string /dev/vda. Je souhaite la création d'un LVM faisant 100% de l'espace restant (restant car je partage une partie du disque avec la partition EFI). Je confirme la suppression de tout block device type LVM ou MDADM encore présent sur le disque puis je confirme l'écriture sans remplir les disques de 0. Je demande ensuite que le fstab soit généré avec des UUID car ça marche bien et ça ne change pas.
## Partitioning #d-i partman-auto/disk string /dev/vda # use LVM to partition the disk d-i partman-auto/method string lvm # You can define the amount of space that will be used for the LVM volume group. It can either be a size with its unit (eg. 20 GB), a percentage of free space or the 'max' keyword. d-i partman-auto-lvm/guided_size string 100% # If one of the disks that are going to be automatically partitioned contains an old LVM configuration, the user will normally receive a warning. This can be preseeded away... d-i partman-lvm/device_remove_lvm boolean true # The same applies to pre-existing software RAID array. d-i partman-md/device_remove_md boolean true # And the same goes for the confirmation to write the lvm partitions. d-i partman-lvm/confirm boolean true # And the same goes for the confirmation to write the lvm partitions. d-i partman-lvm/confirm_nooverwrite boolean true # This makes partman automatically partition without confirmation. d-i partman-partitioning/confirm_write_new_label boolean true # This makes partman automatically partition without confirmation. d-i partman/choose_partition select finish # This makes partman automatically partition without confirmation. d-i partman/confirm boolean true # This makes partman automatically partition without confirmation. d-i partman/confirm_nooverwrite boolean true # Ensure the partition table is GPT - this is required for EFI d-i partman-partitioning/choose_label string gpt # Ensure the partition table is GPT - this is required for EFI d-i partman-partitioning/default_label string gpt # When disk encryption is enabled, skip wiping the partitions beforehand. d-i partman-auto-crypto/erase_disks boolean false # This makes partman automatically partition without confirmation. d-i partman-md/confirm boolean true # The default is to mount by UUID, but you can also choose "traditional" to use traditional device names, or "label" to try filesystem labels before falling back to UUIDs. d-i partman/mount_style select uuid
Là ça devient fun, si fun que j'ai rédigé l'autre méthode qui est présentée plus bas.
Tout d'abord je crée un volume logique nommé vg d'une taille de 30GB extensible jusqu'à 1TB, une partition efi de 768MB en fat32 qui sera bootable et une partition /boot de 1GB en ext4 qui sera également bootable.
Puis j'écris un lv-root de 5GB dans vg qui sera en ext4, monté sur /. Pareil pour /var, hormis qu'il s'agit d'un lv de 10GB.
Puis je passe sur incus qui sera un petit lv monté sur /var/lib/incus d'une taille allant de 10GB à 1TB.
Pour finir je crée un swap qui fait 200% de ma mémoire vive
d-i partman-auto-lvm/new_vg_name string vg
d-i partman-auto/expert_recipe string \
boot-root :: \
768 768 768 fat32 \
$primary{ } $bootable{ } \
method{ efi } format{ } \
. \
1024 1024 1024 ext4 \
$primary{ } $bootable{ } \
method{ format } format{ } \
use_filesystem{ } filesystem{ ext4 } \
ker ce mountpoint{ /boot } \
. \
30000 100 1000000 ext4 \
$defaultignore{ } \
$primary{} \
method{ lvm } format{ } \
vg_name{ vg } \
. \
5000 101 5000 ext4 \
$defaultignore{ } \
$lvmok{ } \
in_vg{ vg } lv_name{ lv-root } \
method{ format } format{ } \
use_filesystem{ } filesystem{ ext4 } \
mountpoint{ / } \
. \
10000 101 10000 ext4 \
$defaultignore{ } \
$lvmok{ } \
in_vg{ vg } lv_name{ lv-var } \
method{ format } format{ } \
use_filesystem{ } filesystem{ ext4 } \
mountpoint{ /var } \
. \
10000 101 -1 ext4 \
$defaultignore{ } \
$lvmok{ } \
in_vg{ vg } lv_name{ lv-incus } \
method{ format } format{ } \
use_filesystem{ } filesystem{ ext4 } \
mountpoint{ /var/lib/incus } \
. \
200% 100 200% linux-swap \
$defaultignore{ } \
$lvmok{ } \
in_vg{ vg } lv_name{ lv-swap } \
method{ swap } format{ } \
use_filesystem{ } filesystem{ linux-swap } \
. \
Vous avez dû tiquer sur la ligne 10000 101 10000 ext4. C'est théoriquement assez simple en surface. Le ext4 vous avez compris c'est le format de la partition. En revanche le premier nombre (ici 10000) c'est la taille minimale requise pour cette partition. Le second nombre c'est la priorité utilisée par l'algorithme pour attribuer la place restante sur le disque pour s'approcher de la taille maximum demandée par chaque partition. Le 3ème nombre c'est la taille maximale. Dans mon cas hormis Incus, toutes les partitions sont définies avec une taille fixe. Lorsqu'il y a un -1 c'est pour indiquer "prend toute la place qu'il te reste". Et je viens de l'apprendre, j'avais cru lire auparavant que c'était pour indiquer que la taille minimale suffisait...
C'est principalement le gros problème de Preseed, je n'ai pas trouvé de documentation claire. C'était que des bouts de documentation à droite à gauche, des posts sur des forums (qui sont en plus sur inscription only maintenant) et je ne vous ai même pas parlé d'utiliser BTRFS et ses subvolumes... L'histoire du -1 plus haut, je me suis dit qu'en cherchant à écrire cet article, je venais de comprendre un truc, donc j'ai de nouveau testé
Ça n'a pas fonctionné. La seule documentation trouvée c'est l'algorithme lui-même quand je cherchais à comprendre pourquoi il y a 3 nombres dans les recettes partman.
J'ai quand même décrit cette partie avec Partman, car j'ai passé trop de temps à essayer de comprendre pour ne pas en parler :D
Théoriquement c'est censé être simple à comprendre, mais cela m'a excessivement cassé les pieds. En plus de Partman, l'installeur de Debian me demandait toujours s'il devait analyser un CD en tant que repository. Sauf que non, no way. Mais impossible de le désactiver... J'ai trouvé la réponse sur un post de 2005, en fait la partie CD-ROM était une demande de "l'installeur du média d'installation" (apt-cdrom-setup) et non de "l'installeur Debian" (d-i)... Pour être sûr que ça ne me demande JAMAIS l'ajout d'un CD, j'ai tout collé.
## Apt setup # Choose, if you want to scan additional installation media # (default: false). d-i apt-setup/cdrom/set-first boolean false d-i apt-setup/cdrom/set-next boolean false d-i apt-setup/cdrom/set-double boolean false d-i apt-setup/cdrom/set-failed boolean false d-i apt-setup/disable-cdrom-entries boolean true # Faut-il analyser d'autres supports d'installation ? apt-cdrom-setup apt-setup/cdrom/set-double boolean false apt-cdrom-setup apt-setup/cdrom/set-failed boolean false apt-cdrom-setup apt-setup/cdrom/set-first boolean false apt-cdrom-setup apt-setup/cdrom/set-next boolean false
Ici ça reste classique, j'active tous les repos Debian, je ne choisis rien dans le run_tasksel (celui qui demande si vous voulez un serveur d'impression, ssh, web...). Je demande d'installer openssh-server (que je configure plus bas) et je désactive le popularity-contest. Puis je force Debian à installer grub en indiquant qu'il est le seul OS sur cette machine.
d-i apt-setup/use_mirror boolean false # You can choose to install non-free firmware. d-i apt-setup/non-free-firmware boolean true # You can choose to install non-free software. d-i apt-setup/non-free boolean true # You can choose to install contrib software. d-i apt-setup/contrib boolean true # If you don't want to have the sources.list entry for a DVD/ BD installation image active in the installed system(entries for netinst or CD images will be disabled anyway, regardless of this setting). ## Package selection # Or choose to not get the tasksel dialog displayed at all (and don't install any packages): d-i pkgsel/run_tasksel string # Individual additional packages to install d-i pkgsel/include string openssh-server # Some versions of the installer can report back on what software you have # installed, and what software you use. The default is not to report back, # but sending reports helps the project determine what software is most # popular and include it on CDs. popularity-contest popularity-contest/participate boolean false ## Boot loader # This is fairly safe to set, it makes grub install automatically to the UEFI partition/boot record if no other operating system is detected on the machine. d-i grub-installer/only_debian boolean true
Là y a deux trois petites astuces. Déjà Debian va forcément vous demander une confirmation pour reboot. Ça se désactive avec la ligne finish-install/reboot_in_progress. Ensuite un coup de exit/reboot ça fonctionne. Mais dans mon cas, je préfère un shutdown pour le remettre sur le réseau standard et lui claquer un WOL, pour éviter qu'il s'installe à l'infini.
Pour les late_command c'est là où ça devient intéressant. Je retire le mot de passe root, je bloque le compte. Puis je lui crée un répertoire .ssh pour autoriser ma clé SSH à entrer sur le serveur et je modifie la configuration d'OpenSSH pour autoriser la connexion à root.
## Finishing
# This is how to make the installer shutdown when finished, but not reboot into the installed system.
#d-i debian-installer/exit/reboot boolean true
d-i finish-install/reboot_in_progress note
## Advanced options
d-i preseed/late_command string \
in-target /usr/bin/passwd -dl root; \
in-target /usr/bin/mkdir -p /root/.ssh; \
in-target /usr/bin/echo 'ssh-ed25519 MASUPERCLESSH' > /root/.ssh/authorized_keys; \
in-target /usr/bin/sed -i "s/#PermitRootLogin prohibit-password/PermitRootLogin yes/" /etc/ssh/sshd_config;
Comme dit plus haut, j'ai privilégié une autre méthode. Mais vous pouvez tout de même accéder au preseed complet
Faire sa propre image Debian Netboot
Je suis assez content⋅e, car je commence à réussir à faire des liens entre mes articles. Il y a beaucoup de similitudes avec un précédemment écrit : l'installation d'une Debian sur un Raspberry PI
Pour réussir à faire l'installation que je souhaite, j'ai créé une image live qui va inclure un script d'installation et ce script va simplement partitionner la machine, puis lancer un debootstrap.
--------------------------------------------------------------------------------
Sur IRC, on m'a lancé le défi d'utiliser l'initramfs en PXE pour y lancer les instructions d'installation ... J'ai regardé mais pas réussi.
Cependant pour les curieux⋅ses j'ai un petit article de Lord :
--------------------------------------------------------------------------------
Déjà pour réussir ça, il faut saisir comment boot un Linux. C'est expliqué dans le lien de Lord juste au dessus, mais je vais très succinctement le résumer ici :
BIOS/EUFI → Bootloader → Initramfs → Linux
Le bootloader est un noyau Linux archi light et compressé mais qui aura plus de capacité pour lancer la suite que le BIOS/EUFI. Il est nommé vmlinuz. vmlinuz va ainsi pouvoir booter l'initramfs (en RAM) qui est plus complet, notamment, car il peut être amené à déchiffrer un disque. Ensuite là c'est peanut, initramfs a les clés nécessaires pour boot Linux en suivant les instructions transmises par l'initramfs qui l'a récupéré du bootloader. Bref, on démarre deux Linux avant d'avoir notre Linux final.
Je vais extrêmement grossir le trait, mais décompressez une Debian netinstall ou une iso Ubuntu et vous aurez grosso modo le même mécanisme qu'avec un PXE. Il y a un secteur d'amorçage, qui va lancer le grub puis de bootloader puis initramfs et pour finir par décompresser et exécuter le système de fichier en lecture seule.
Donc je vais créer un mini système que sera ensuite compressé pour boot dessus. Pour finir ce système va lancer une installation automatique créé avant. (En réalité ça me fait penser à Subiquity de Canonical... Mais en plus simple)
Pour créer ce système, il faut quelques paquets :
- debootstrap pour installer Debian
- squashfs-tools pour créer un système de fichier compressé
- rsync pour copier les données en conservant les permissions et envoyer le résultat sur notre serveur TFTP.
Et c'est tout (vous pensiez que c'était plus compliqué que ça ? :D). À vrai dire, là c'est ça va être la base, mais vous pouvez retrouver le script d'installation sur mon repository git.
je crée le système de base
ROOTFS="$(mktemp -d)" #Créer un dossier temporaire
debootstrap --arch=amd64 \
--components=main,non-free,non-free-firmware \
--variant=minbase \
--include=arch-install-scripts,btrfs-progs,ca-certificates,console-setup,curl,debootstrap,dbus,dosfstools,e2fsprogs,iproute2,iputils-ping,linux-image-amd64,live-boot,locales,openssh-server,parted,systemd-resolved,systemd-sysv,systemd-timesyncd,zstd \
--force-check-gpg \
trixie \
"${ROOTFS}" \
http://deb.debian.org/debian
Comme pour installer le Raspberry PI. J'ai rajouté arch-install-scripts, par pur fainéantise pour faire un chroot ou encore btrfs-progs pour créer et gérer un système de fichier en btrfs. J'inclus également live-boot qui est une composante nécessaire pour Debian pour se lancer sur un squashfs (j'ai testé sans, ça a fini en Kernel Panic). Et un openssh-server pour pouvoir se connecter sur la machine si, par hasard, le script d'installation échoue.
J'ai créé un répertoire rootfs comportant quelques fichiers et exécutables tels que mes clés SSH, ou encore le script d'installation. Ce répertoire est copié sur le système créé par debootstrap.
dryusdan@nemo:~/git/pxe-hv-install$ tree
rootfs/
├── etc
│ ├── hostname
│ ├── locale.gen
│ ├── ssh
│ │ └── sshd_config.d
│ │ └── pxe-boot.conf
│ └── systemd
│ ├── network
│ │ └── 10-dhcp.network
│ └── system
│ └── install.service
├── root
│ └── .ssh
│ └── authorized_keys
└── usr
└── local
└── bin
└── install.sh
J'explique en premier le script install.sh, le plus intéressant mais également le plus basique, pas d'installation compliquée avec des outils compliqués. Il reste cependant adapté à un petit lab, car il est basique mais n'offre que peu de personnalisation "facilement".
Le script install.sh
Tout d'abord le script commence par choisir un disque, plus ou moins arbitrairement. Soit je l'ai indiqué dans le serveur TFTP, soit il se débrouille. J'évite aussi les disques attachés à un soft raid (j'ai une machine qui a 3 disques /dev/sdX en soft-raid et un /dev/nvme0n1).
TFTP_SERVER=172.26.0.1
DISK_FORCE_INSTALL_FILE="config/install-disk"
disk=$(curl -s "tftp://${TFTP_SERVER}/${DISK_FORCE_INSTALL_FILE}")
rc=$?
if [ $rc -ne 68 ]; then
echo "File exist, choose disk ${disk}"
else
echo "No disk choosed. Fetch disks"
fi
if [ -z "$disk" ]; then
if [ ! -f /proc/mdstat ]; then
info "No mdadm found"
else
md=$(cat /proc/mdstat)
fi
for disk in $(lsblk --nodeps --exclude 7 -io NAME --noheadings --sort NAME)
do
if [[ "${md}" = *"${disk}"* ]]; then
echo "${disk} is in mdadm. Don't rewrite it"
echo "If you want to rewrite it, please fix in on tftp server ${DISK_FORCE_INSTALL_FILE}"
else
echo "Choosing ${disk}"
break
fi
done
fi
Une fois le disque choisi, je supprime ses entêtes et je crée une partition efi pour l'EUFI et une partition qui va comporter tous les subvolumes BTRFS. Pourquoi BTRFS ? On m'a dit que c'était bien, je ne connaissais pas donc je vais tester.
parted "/dev/${disk}" --script mktable gpt
parted "/dev/${disk}" --script mkpart EFI fat32 1MiB 512MiB
parted "/dev/${disk}" --script mkpart LINUX btrfs 512MiB 100%
# Petit trick parce que le nommage d'une partition nvme et sda n'est pas identique
if [[ "${disk}" = *"nvme"* ]]; then
bootpart="${disk}p1"
fspart="${disk}p2"
else
bootpart="${disk}1"
fspart="${disk}2"
fi
info "Formating disk"
mkfs.vfat -F 32 -n EFI "/dev/${bootpart}"
mkfs.btrfs -f -L LINUX "/dev/${fspart}"
Je monte la partition puis je crée les subvolumes
info "Mounting /dev/${fspart} on ${ROOTFS}"
mount /dev/${fspart} ${ROOTFS}
cd ${ROOTFS}
info "Creating subvolumes"
btrfs subvolume create @
btrfs subvolume create @var
btrfs subvolume create @incus
Je démonte tout ça pour monter progressivement les subvolumes en créant les points de montages et également pour créer les quotas.
echo "Unmount base"
cd /
umount /mnt
echo "Mount root subvolume into ${ROOTFS}"
mount -o subvol=@ "/dev/${fspart}" "${ROOTFS}"
btrfs quota enable "${ROOTFS}"
btrfs qgroup limit 10G "${ROOTFS}"
echo "Creating var"
mkdir -p "${ROOTFS}/var"
echo "Mounting var subvolume"
mount -o subvol=@var "/dev/${fspart}" "${ROOTFS}/var"
btrfs quota enable "${ROOTFS}/var"
btrfs qgroup limit 16G "${ROOTFS}/var"
echo "Creating incus"
mkdir -p "${ROOTFS}/var/lib/incus"
echo "Mounting incus subvolume"
mount -o subvol=@incus "/dev/${fspart}" "${ROOTFS}/var/lib/incus"
echo "Mounting efi partition"
mkdir -p ${ROOTFS}/boot/efi
mount "/dev/${bootpart}" ${ROOTFS}/boot/efi
Puis je lance debootstrap pour installer. Comme d'habitude, vous pouvez choisir vos propres paquets. Cependant gpg est quand même très utile pour apt.
echo "Debootstrap in ${ROOTFS}"
debootstrap --arch=amd64 \
--components=main,non-free,non-free-firmware \
--variant=minbase \
--include=btrfs-progs,ca-certificates,console-setup,curl,dbus,dosfstools,e2fsprogs,firmware-misc-nonfree,gpg,grub-efi,iproute2,iputils-ping,linux-image-amd64,lm-sensors,man-db,less,locales,openssh-server,python3,systemd-resolved,systemd-sysv,systemd-timesyncd,vim,zstd \
--force-check-gpg \
trixie \
"${ROOTFS}" \
http://deb.debian.org/debian
Je configure le fstab en utilisant le label parce que c'est plus simple que de récupérer l'UUID du disque ou le chemin vers ce disque.
info "Configure fstab" cat <"${ROOTFS}/etc/fstab" LABEL=LINUX / btrfs subvol=@,defaults,rw,noatime,autodefrag,compress=zstd 0 1 LABEL=LINUX /var btrfs subvol=@var,defaults,rw,noatime,autodefrag,compress=zstd 0 2 LABEL=LINUX /var/lib/incus btrfs subvol=@incus,defaults,rw,noatime,autodefrag,compress=zstd 0 2 EOF
Je configure le reste du serveur, qui ressemble étrangement (ou pas) au dossier rootfs vu plus haut. Peut-être un prochain axe d'amélioration.
echo "Configure server"
echo "fr_FR.UTF-8 UTF-8" >> "${ROOTFS}/etc/locale.gen"
echo "en_GB.UTF-8 UTF-8" >> "${ROOTFS}/etc/locale.gen"
echo "Configure network"
mkdir -pv "${ROOTFS}/etc/systemd/network/"
cat < "${ROOTFS}/etc/systemd/network/10-dhcp.network"
[Match]
Name=e*
[Network]
DHCP=ipv4
[DHCP]
UseDNS=yes
EOF
echo "Configure ssh server"
cat < "${ROOTFS}/etc/ssh/sshd_config.d/first-boot-conf.ssh"
PasswordAuthentication no
PermitEmptyPasswords no
PermitRootLogin prohibit-password
EOF
echo "Add ssh authorized_keys in root"
mkdir -vp "${ROOTFS}/root/.ssh/"
cat < "${ROOTFS}/root/.ssh/authorized_keys"
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPP4tMAPIWv4fMMI7xLfdVFe54weHoFe2ZVZU+ZVPl8d
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINF+bi103OW7hfuVXoO/MLzR9Ymi7wx0HGt7WvH8vrSa
EOF
echo "Change hostname"
echo "pxe-install" > "${ROOTFS}/etc/hostname"
echo "Fix /etc/hosts"
cat > "${ROOTFS}/etc/hosts" << EOF
127.0.0.1 localhost
127.0.1.1 pxe-install
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOF
Puis je lance les dernières configurations au travers du chroot, je régénère l'initramfs, le grub, je force l'écriture sur disque et j'éteins la machine !
Alors petit trick, le PATH de arch-chroot n'embarque pas /usr/sbin sauf que des binaires (notamment utilisés par locale-gen se trouvent dedans, j'ajoute ce chemin dans le PATH et je le retire après mes actions pour éviter de foutre la grouille sur mon terminal.
OLD_PATH=$PATH
export PATH="/usr/local/sbin:/usr/local/bin:/usr/bin:/usr/sbin"
info "Prepare nextboot"
arch-chroot "${ROOTFS}" locale-gen
arch-chroot "${ROOTFS}" timedatectl set-ntp true
arch-chroot "${ROOTFS}" timedatectl set-timezone Europe/Paris
arch-chroot "${ROOTFS}" systemctl enable systemd-networkd
arch-chroot "${ROOTFS}" systemctl enable systemd-resolved
arch-chroot "${ROOTFS}" update-initramfs -u
arch-chroot "${ROOTFS}" grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=debian --recheck --no-nvram --removable
arch-chroot "${ROOTFS}" update-grub
export PATH=$OLD_PATH
sync;sync;
info "Install completed. Shutdown"
shutdown -h 0
Les autres fichiers dans rootfs
Il n'y avait pas qu'un install.sh dans le rootfs 😉. On y trouve aussi le fichier pxe-boot.conf qui autorise la connexion au compte root en SSH.
# rootfs/etc/ssh/sshd_config.d/pxe-boot.conf PasswordAuthentication no PermitEmptyPasswords no PermitRootLogin prohibit-password
Le fichier locale.gen possède que deux locales, rien de folichon
# rootfs/etc/locale.gen fr_FR.UTF-8 UTF-8 en_GB.UTF-8 UTF-8
Le fichier 10-dhcp.network quant à lui va activer le DHCP sur toutes les interfaces commençant par e
# rootfs/etc/systemd/network/10-dhcp.network [Match] Name=e* [Network] DHCP=ipv4 [DHCP] UseDNS=yes
Le service install.service est la pièce maitresse, sans lui rien ne sera lancé automatiquement. Mais même s'il est basique il embarque deux trois dépendances liées au réseau
# rootfs/etc/systemd/system/install.service [Unit] Description=Install Requires=network.target network-online.target time-sync.target local-fs.target [Service] Type=simple ExecStart=/usr/local/bin/install.sh [Install] WantedBy=default.target
Il y a beaucoup de require, car le network.target attend uniquement que l'interface soit up, network-online.target attend que l'IP soit montée sur l'interface, time-sync.target attend que le client NTP lance une synchronisation et local-fs.target attend que les disques soient réellement montés.
Autant network-online.target est utile parce que le script se lance, DHCP ou non et c'est un peu ennuyeux. Autant time-sync.target c'est utile si la pile de votre machine est KO, fraichement remplacée et que vous utilisez les dépôts Debian en HTTPS.
En rajoutant des dépendances aux services, il va falloir les activer au lancement du serveur (et c'est là que arch-chroot va être utile :D)
#Je désactive le réseau de base Debian
arch-chroot "${ROOTFS}" /bin/systemctl disable networking
# J'active systemd-networkd et wait-online qui attend que le DHCP se soit lancé
arch-chroot "${ROOTFS}" /bin/systemctl enable systemd-networkd systemd-networkd-wait-online.service
# J'active systemd-timesyncd et wait-sync pour forcer la synchronisation de l'horloge système
arch-chroot "${ROOTFS}" /bin/systemctl enable systemd-timesyncd.service systemd-time-wait-sync.service
# J'active le script
arch-chroot "${ROOTFS}" /bin/systemctl enable install
Si vous voulez activer un mot de passe sur votre compte root (au cas où le réseau ne monte pas par exemple), attention le clavier sera en qwerty.
arch-chroot "${ROOTFS}" bash -c 'echo -e "azerty" | passwd root --stdin'
Et voilà, le système est prêt à être compressé. Ce qui se fait assez facilement d'ailleurs. Mais avant de continuer il faut récupérer l'initramfs et vmlinuz, car ils sont utiles au démarrage de la machine en PXE
NETSYSTEM_PATH="$(pwd)/netsystem"
mkdir -p "${NETSYSTEM_PATH}"
rsync -azvP "${ROOTFS}/boot/initrd.img"* "${NETSYSTEM_PATH}/initrd.img"
rsync -azvP "${ROOTFS}/boot/vmlinuz"* "${NETSYSTEM_PATH}/vmlinuz"
Puis je lance une compression excluant le /boot vu que ces fichiers ont été récupérés juste avant, mieux vaut éviter les données inutiles sinon ça ralentit le temps de téléchargement de l'image pour rien. La compression est du lzma. C'est plus gourmand en ressource pour compresser, mais j'ai quelques cores dans le boitier.
mksquashfs "${ROOTFS}" "${NETSYSTEM_PATH}/dry-debian-custom.squashfs" -comp xz -e boot
Puis j'envoie tout ça sur la machine qui va gérer le PXE.
Comme je vous l'ai dit plus haut, tout est disponible sur mon repository git, les fichiers sont un peu plus travaillés. Et vous pouvez les copier coller sans avoir besoin de les réassembler.
Maintenant le boot PXE
Maintenant que tout est prêt, je vais m'attaquer au boot. mais après un moment de réflexion.
Dans mon cas, j'ai un serveur DHCP géré par mon routeur. Si j'indique à mon routeur d'envoyer les options DHCP pour le PXE, à chaque reboot de la machine elle va se réinstaller en PXE (et au final former un boot reinstall loop). Je dois donc éviter ça. Et j'aimerais éviter les erreurs de manipulation aussi.
Je n'avais pas trouvé de meilleure solution que coller sa machine dans un VLAN spécifique, le serveur DHCP + TFTP dans ce même VLAN et boot.
Depuis j'ai constaté, en augmentant le verbose de mon tftp, qu'une ligne m'intéresse
RRQ from 172.26.0.19 filename efi64/pxelinux.cfg/01-ad-6a-a9-c8-8f-c0
Ça veut, théoriquement, dire qu'au lieu que syslinux utilise le fichier default (comme j'ai trouvé partout), je pourrais utiliser un fichier ayant comme nom l'adresse mac de la machine, ce qui me permettrait d'écrire les instructions de boot dans un fichier spécifique par machine.
Bon, aujourd'hui j'ai plutôt une machine que je branche sur un autre réseau avec un VLAN et comme ça ce requête via l'API du Mikrotik c'est automatisable.
Pour faire simple, au lieu de passer par le routeur je passe par le RPI qui fait office de routeur. Pas le même réseau, ça m'évite aussi de faire une connerie si jamais
Mais ça pourrait faire une nouvelle évolution à mener 😇
EDIT : En relisant une source de l'article j'ai vu que Debian l'expliquant, j'aurais pu économiser un peu de temps...
Then SYSLINUX/PXELINUX will try to search its configuration at different paths, from the most specific to the least:
>
* pxelinux.cfg/GUID
- pxelinux.cfg/MAC
- pxelinux.cfg/default
Maintenant que j'ai le comment, il va falloir passer à la configuration. Je vous laisse gérer votre configuration réseau, la mienne est un poil alambiquée pour d'autres raisons que ce projet.
Pour mettre en place ce boot PXE je vais avoir besoin de 3 choses :
- Un serveur DHCP
- Un serveur TFTP
- Un bootloader
Pour le serveur DHCP je pars sur isc-dhcp-server. La configuration est relativement simple, dans le fichier /etc/dhcp/dhcpd.conf je supprime tout pour y coller ces quelques lignes
default-lease-time 600;
max-lease-time 900;
ignore-client-uids true;
subnet 172.26.0.0 netmask 255.255.255.0 {
range 172.26.0.10 172.26.0.250;
option domain-name-servers 192.168.1.221;
option routers 172.26.0.1;
option broadcast-address 172.26.0.255;
deny unknown-clients;
}
group {
next-server 172.26.0.1;
filename "efi64/syslinux.efi";
host srv1 { hardware ethernet AD:6A:A9:C8:8F:C0; }
host srv2 { hardware ethernet 43:BD:7F:D8:3E:82; }
}
Le ignore-client-uids est là pour éviter d'assigner une nouvelle IP, ou d'assigner la même IP sur tous les clients qui ont le même UUID (ça m'est arrivé, c'est chiant). Le subnet défini le réseau. Au début, j'utilisais un /29. Mais j'ai de l'espace et aucun intéret à me comprimer dans un /29 (qui m'a apporté des doublons d'IP). La petite particularité est le deny unknown-clients; qui est là pour éviter qu'en cas de maladresse mon serveur principal, mon pc ou whatever démarre en PXE sur le même VLAN que mon RPI et se fasse réinstaller, je n'autorise les requêtes DHCP que des adresses MAC connues.
Adresses MAC qui sont définies dans la section group en dessous. L'option next-server c'est l'adresse du serveur qui a d'autres configurations à filer à la machine (aka notre serveur TFTP) et filename c'est le fichier de "configuration" que notre serveur va récupérer.
Ensuite on édite /etc/default/isc-dhcp-server pour faire écouter le serveur DHCP sur une seule interface.
INTERFACESv4="eth0.100"
On commente tout le reste et la configuration du serveur DHCP est terminé.
Côté serveur TFTP maintenant, c'est encore PLUS SIMPLE, déjà j'installe le paquet tftpd-hpa et modifie la configuration présente dans /etc/default/tftpd-hpa (Le verbosity n'est pas de trop je trouve)
TFTP_USERNAME="tftp" TFTP_DIRECTORY="/srv/tftp" TFTP_ADDRESS=":69" TFTP_OPTIONS="--secure --verbosity 4" RUN_DAEMON="yes"
Maintenant il ne reste plus qu'à aller créer un dossier /srv/tftp et y mettre les droits pour tftp et zou !
PXE avec le preseed
Sauf qu'il manque le bootloader ! Au tout départ j'ai pris celui de Debian netboot. Ça fonctionne, mais j'utilise plus le netboot de Debian. (Je vais quand même vous le montrer)
mkdir -p /srv/tftp cd /srv/tftp wget https://deb.debian.org/debian/dists/trixie/main/installer-amd64/current/images/netboot/netboot.tar.gz tar -xvf netboot.tar.gz ./ ln -s debian-installer/amd64/grubx64.efi . ln -s debian-installer/amd64/grub . chown -R tftp:tftp /srv/tftp
Dans la config DNS je modifie l'option filename pour pointer vers grubx64.efi. Pour indiquer où est l'URL du preseed et que Debian démarre tout seul correctement, je révise le fichier /srv/tftp/grub/grub.cfg pour remplacer tous les menuentry par celui ci-dessous en rajoutant au passage 2 options en plus : un timeout de 2 secondes avec une présélection par défaut sur l'entrée fraichement créée.
menuentry 'Install' {
set background_color=black
linux /debian-installer/amd64/linux vga=788 --- quiet auto=true preseed/url="http://preseed.dryusdan.net/preseed.cfg" netcfg/get_hostname=debian netcfg/get_domain=""
initrd /debian-installer/amd64/initrd.gz
}
set timeout=2
set default=""Install""
PXE avec le squashfs
Pour cette étape, il va falloir installer deux paquets et configurer le répertoire /srv/tftp en conséquence
apt install -y syslinux-common syslinux-efi
Une fois les bootloaders récupérés je les mets en place dans le répertoire tftp
mkdir -p /srv/tftp/{boot,pxelinux.cfg,efi64}
cp /usr/lib/SYSLINUX.EFI/efi64/syslinux.efi /srv/tftp/efi64/
cp /usr/lib/syslinux/modules/efi64/* /srv/tftp/efi64/
cd /srv/tftp/efi64
ln -s ../pxelinux.cfg/ ./
ln -s ../boot/ ./
Côté menu de démarrage je crée le fichier /srv/tftp/pxelinux.cfg/default en y ajoutant :
DEFAULT vesamenu.c32
TIMEOUT 100
MENU TITLE Starting menu
ALLOWOPTIONS 0
MENU AUTOBOOT Auto start in #
LABEL NETSYSTEM
MENU LABEL PXEBOOT
KERNEL boot/netsystem/vmlinuz
APPEND boot=live initrd=boot/netsystem/initrd.img vga=792 config noswap noprompt fetch=tftp://172.26.0.1/boot/netsystem/dry-debian.squashfs root=/dev/ram0 rw net.ifnames=0 showmounts toram quickreboot
TEXT HELP
Boot on PXE
ENDTEXT
Le menu va boot automatiquement sur vmlinuz qui va charger l'initramfs. L'initramfs lui va charger le squashfs en RAM sans swap et en lecture-écriture (pour les applications qui vont écrire leurs logs). L'instruction toram va charger tout l'OS en RAM (d'où la nécessité d'un OS léger). Le quickreboot va redémarrer sans demander à l'utilisateur de retirer sa clé et showmounts, quant à lui, va afficher le squashfs dans un point de montage sur la machine. Ces options nous viennent du paquet live-boot
Il y a 10 minutes, j'ai déplacé le dossier netsystem vers ce serveur. Ce dossier comporte un vmlinuz, un initrd.img et le squashfs. Il ne reste plus qu'à les déplacer du répertoire où ils nous attendent au serveur TFTP dans /srv/tftp/boot/netsystem/. Maintenant il faut tester en installant les serveurs via PXE ! 😊
Avec mon RPI 4 branchés sur une borne WIFI et faisant routeur (je vous ai dit que cette partie-là est spéciale) je boucle une installation en moins de 4 minutes.
À bientôt pour l'épisode 2 ! Et si vous ne voulez pas le louper, abonnez vous au flux /RSS
Sur ce, portez vous bien :)
Photo de Ildefonso Polo
--------------------------------------------------------------------------------
PS :systemd J'ai essayé d'être le plus clair et explicite possible. J'ai essayé de vulgariser un peu tout en étant le plus juste possible. Mais si j'ai fait une erreur, n'hésitez pas à me le dire par e-mail ou sur le fediverse (tout est sur ma page Hello ) 😊
Sources
--------------------------------------------------------------------------------