#Les conteneurs Linux
Un conteneur Linux est un terme générique pour une implémentation d'une forme de virtualisation au niveau du système d'exploitation. Il s'agit en effet d'une manière d'isoler un ou plusieurs processus qui tournent en leur créant un environnement qui sera différent, parfois à l'extrême, des autres processus tournant sur le même système.
À la différence de la virtualisation classique, une seule copie du système d'exploitation tourne sur la machine, les conteneurs peuvent ainsi réclamer moins de ressources en termes de mémoire.
##A propos de conteneurs
###Les conteneurs LXC
Le terme Linux Container (LXC) désigne à la fois les applications virtualisées basées sur Linux. A la différence des machines virtuelles qui fonctionnent au niveau du matériel, les conteneurs fonctionnent au niveau du système d’exploitation, Les conteneurs se partagent le système d’exploitation et isolent les processus d’application du reste du système tandis que les systèmes de virtualisation classique permettent l’exécution de plusieurs systèmes d’exploitation sur un seul système.
Les conteneurs Linux nécessitent en général moins de ressources qu’une machine virtuelle et possèdent une interface standard qui permet de gérer facilement plusieurs conteneurs simultanément.
Un inconvénient majeur de LXC se situe au niveau de la gestion de la mémoire : bien que différents backend de mémoire (ivm, overlayfs, zfs et btrfs) soient pris en charge, par défaut, la mémoire est stockée directement sur le rootfs.
###Le RootFS
Un "rootfs" est "le système de fichiers que la partition racine" (le /). La structure d'un RootFS peut servir de base à des conteneurs:
- sur une machine, lorsque l'installation normale ne peut pas fonctionner (ex. hardware trop récent pour le kernel d'installation)
- LXC
- OpenVZ
- VServer
- UML
- coLinux
##Préparation d'un RootFS
###RootFS prêts à l'emploi
linuxcontainers.org proposent des RootFS en téléchargement gratuit sur:
-> https://jenkins.linuxcontainers.org/
-> https://images.linuxcontainers.org/
Les distributiopns proposent également des rootfs:
-> https://github.com/oracle/container-images
###Les image disques
Par exemple Les images pour Raspberry Pi3 et Pi4 disponibles depuis https://raspi.debian.net/, sont utilisées pour graver une carte SD directement amorçable. On peut donc extraire un RootFS depuis ces images.
Télécharger l'erchive:
```
wget https://raspi.debian.net/tested/20231111_raspi_4_trixie.img.{xz,xz.sha256.asc}
```
Les images pour Raspberry Pi3 et Pi4 sont mises à disposition par Gunnar Wolf / gwolf, mainteneur Debian. Rechercher l'empreinte de sa clé à partir https://nm.debian.org/public/people/ puis https://nm.debian.org/person/gwolf/
```
gpg --keyserver keyring.debian.org --recv-keys 4D14050653A402D73687049D2404C9546E145360
```
Vérifier la signature des checksums et la checksum des fichiers images compressés
```
gpg --verify 20231111_raspi_4_trixie.img.xz.sha256.asc
sha256sum -c 20231111_raspi_4_trixie.img.xz.sha256.asc
```
Extraire l'image et les partitions:
```
unxz 20231111_raspi_4_trixie.img.xz
7z x 20231111_raspi_4_trixie.img
```
La première partition est le boot et contient le noyau et les firms, elle peut être extraite avec 7z.
La seconde est le FS ext4 et doit être montée pour en récupérer le contenu:
```
7z x 0.fat
fuse2fs -o ro 1.img /mnt
cp -a /mnt/* /tmp/rootfs/
umount /mnt
```
On peut récupérer des RootFS directement sur le site https://downloads.raspberrypi.com/
###Les conteneurs LXC
Il est possible d'amorcer un RootFS avec lxc-create:
```
lxc-create -t download -n debian11 -- -d debian -r bullseye -a arm64
echo "root:$(echo 'raspberry' | openssl passwd -6 -stdin)" | sed -i -e "s|^root:[^:]\+:|$(awk '{print $1}'):|" /var/lib/lxc/debian11/rootfs/etc/shadow
sed -i 's/^\[Resolve\]/[Resolve]\nDNS=8.8.8.8 8.8.4.4\nFallbackDNS=1.1.1.1 1.0.0.1/g' /var/lib/lxc/debian11/rootfs/etc/systemd/resolved.conf
```
###Les images turnkey
Le site mirrors.dotsrc.org propose des images turnkey qui peuvent être utilisées dans les conteneurs lxc
Cependant les services **inithooks** déployés avec le package **inithooks** exécute des scripts d'initialisation du système qui régénèrent les clés secrètes (par exemple, SSH, certificat SSL par défaut), définit des mots de passe (par exemple, root, base de données, application) et configure les paramètres d'application de base (par exemple, domaine, e-mail d'administrateur). Ces services doivent être désactivés afin de gérer manuellement ces configurations.
```
mkdir -pv /tmp/{iso,rootfs}
wget https://mirrors.dotsrc.org/turnkeylinux/images/proxmox/debian-11-turnkey-jenkins_17.1-1_amd64.tar.gz -P /tmp/iso
tar xf /tmp/iso/debian-11-turnkey-jenkins_17.1-1_amd64.tar.gz -C /tmp/rootfs/
cd /var/lib/lxc/jenkins/rootfs
rsync -aPHxv /tmp/rootfs/etc/systemd/system/multi-user.target.wants/ /var/lib/lxc/jenkins/rootfs/etc/systemd/system/multi-user.target.wants/
rm /tmp/rootfs/etc/systemd/system/multi-user.target.wants/inithooks.service
rm /tmp/rootfs/etc/systemd/system/basic.target.wants/inithooks-*.service
rsync -aPHxv /tmp/rootfs/etc/ssh /var/lib/lxc/jenkins/rootfs/etc/ssh/
echo "root:$(echo 'raspberry' | openssl passwd -6 -stdin)" | sed -i -e "s|^root:[^:]\+:|$(awk '{print $1}'):|" /tmp/rootfs/etc/shadow
cp -a /lib/systemd/system/ssh.service /tmp/rootfs/lib/systemd/system/
echo "PermitRootLogin yes" >> /tmp/rootfs/etc/ssh/sshd_config
echo "net.ipv6.conf.all.disable_ipv6 = 1" >> /tmp/rootfs/etc/sysctl.d/99-sysctl.conf
```
##Virtualisation avec qemu
À la différence de la virtualisation classique, une seule copie du système d'exploitation tourne sur la machine, les conteneurs peuvent ainsi réclamer moins de ressources en termes de mémoire.
Pour émuler un autre noyau Linux sur lequel un conteneur poura fonctionner, QEMU peut être utilisé pour l'exécuter. Cela permet d'isoler les applications et de les exécuter de manière plus légère que les machines virtuelles traditionnelles.
###Configuration du système hôte
Il convient d'installer l'émulateur qemu ainsi que libvirt:
```
sudo apt install -y git qemu-system qemu-system-arm qemu-system-aarch64 qemu-utils lxc bridge-utils lxc lxc-templates libvirt0 libpam-cgfs bridge-utils uidmap unzip cpio
sudo apt install -y libvirt-clients libvirt-daemon-system virtinst virt
```
Pour pouvoir démarrer sur un RootFS, un serveur NFS est également requis :
```
sudo apt install -y nfs-kernel-servel
```
Lorsque le système hôte est lui même émulé dans un namespace (un conteneur), il faut utiliser un serveur NFS qui s'exécute en user namespace, par exemple **unfs3**:
* Télécharger le paquet `https://cloudfront.debian.net/debian-archive/debian/pool/main/u/unfs3/unfs3_0.9.21+dfsg-1_amd64.deb`
* Installer le paquet manuellement `dpkg -i unfs3_0.9.21+dfsg-1_amd64.deb`
* Télécharger le scripte unfsd.init `wget https://raw.githubusercontent.com/unfs3/unfs3/refs/heads/master/unfsd.init -P /etc/init.d'
* Modifier la commande start `/usr/sbin/unfsd -u -n 2049 -m 2049 -i ${pidfile}'
* Editer le nfs-user-server.service pour définir l'unité systemd
```
[Unit]
Description=NFS server and services
DefaultDependencies=no
Requires= network.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/etc/init.d/unfsd start
ExecStop=/etc/init.d/unfsd stop
ExecReload=/usr/sbin/exportfs -r
[Install]
WantedBy=multi-user.target
```
virsh net-define default.xml
###Chargement du rootfs dans une archive portable cpio
Lorsque le rootfs n'est pas trop gros, on peut le charger directement en mémoire avec l'option **-initrd** et fournir un fichier cpio (compressé) avec le rootfs. Donc, si on a un fichier tar, il faut le décompresser et créer un cpio à la place.
```
wget http://cdimage.debian.org/mirror/turnkeylinux/images/iso/turnkey-jenkins-17.1-bullseye-amd64.iso
isoinfo -R -X turnkey-jenkins-17.1-bullseye-amd64.iso
7z x turnkey-jenkins-17.1-bullseye-amd64.iso
7z x live/initrd.gz
sqfs2tar /tmp/iso/live/10root.squashfs > /tmp/iso/rootfs.tar
cd /tmp/rootfs
cpio -idv < /tmp/iso/mkinitramfs-MAIN_HXrvlm
echo "root:$(echo 'raspberry' | openssl passwd -6 -stdin)" | sed -i -e "s|^root:[^:]\+:|$(awk '{print $1}'):|" etc/shadow
sed -i 's/^\[Resolve\]/[Resolve]\nDNS=8.8.8.8 8.8.4.4\nFallbackDNS=1.1.1.1 1.0.0.1/g' etc/systemd/resolved.conf
tar -uvf /tmp/iso/rootfs.tar .
```
Il faut utiliser le format '-H newc' pour cpio.
```
cd /tmp/iso
find . | cpio --quiet -o -H newc | bzip2 -c > /qemu-rpi-kernel/rootfs.cpio
```
Il est nécessaire ,au minimum, de définir un utilisateur admin ou de définir un mot de passe pour l'utilisateur root (non recomandé) pour pouvoir se connecter:
```
echo "root:$(echo 'raspberry' | openssl passwd -6 -stdin)" | sed -i -e "s|^root:[^:]\+:|$(awk '{print $1}'):|" etc/shadow
```
ou
```
echo 'password' |openssl passwd -1 -salt QidasQKZ -stdin | cut -d“ ” -f1 | xargs -I{} sed -i '/^root/s/\([^:]*\):[^:]*:\(.*\)/\1:'"{}"':\2/' /tmp/rootfs/etc/shadow
```
Il est important de noter que la propriété et les autorisations de tous les fichiers du système de fichiers racine doivent être préservées. Pour cela, il faut exécuter tar en tant que root (via sudo) et utilisez l'argument --same-owner dans la commande.
La décomprsesion de cerains items spéciaux (/dev) peuvent ressortir en erreur, il n'est pas nécessaire de décompresser toute l'archive tar afin de la covertir au format cpio. **bsdtar** permet de convertir un fichier tar au format cpio:
```
cd /tmp/iso
bsdtar --format=newc -cf @rootfs.tar > /qemu-rpi-kernel/rootfs.cpio
```
Pour modifier un fichier spécifique il suffit de l'extraire:
````
tar -xf rootfs.tar.xz "/etc/shadow"
````
Puis de mettre à jour l'archive avec le fichier modifié:
```
tar -uvf rootfs.tar etc/shadow
```
rsync -aPHxv /tmp/rootfs/ /var/lib/lxc/jenkins/rootfs/
###boot nfsroot
Il est possible de passer les options au noyau linux pour démarrer directement depuis un export NFS '-append "ip=dhcp root=/dev/nfs rw nfsroot=10.0.3.1:/tmp/rootfs/,vers=3"`.
Mais pour cela les options de démarrage des stacks IP sont requises (l'option « Racine du système de fichiers sur NFS » est également requise), car toutes les autres machines configurent le réseau dans leurs scripts de démarrage. La compilationdu noyau avec les options suivantes permet la configuration automatique des adresses IP des périphériques et de la table de routage lors du démarrage du noyau:
```
CONFIG_IP_PNP -- IP: kernel-level configuration support
CONFIG_IP_PNP_ENABLE
CONFIG_IP_PNP_DHCP -- DHCP support
CONFIG_NFS_FS -- NFS files ystem support
CONFIG_NFS_V3 -- NFS version 3 support
CONFIG_ROOT_NFS -- Root file system on NFS
```
###initrd avec nfsroot
Si le noyau utilisé ne dispose pas des options de démarrage des stacks IP requises on peut utiliser un initrd busybox pour démarrer en NFS.
Le processus est le suivant:
- démarrage sur un initrd minimal (busybox)
- montage du rootfs en nfs
- switch\_root((la commande **switch\_root** permet de passer à un autre système de fichiers racine de l'arbre de montage)) vers le point de montage
```
ARCH=arm64
URL="http://deb.debian.org/debian/pool/main/b/busybox/busybox-static_1.37.0-4_${ARCH}.deb"
package=$(basename "$URL")
wget -nv -nc "$URL"
rm -rf "initramfs.${ARCH}.cpio" "initramfs.${ARCH}.cpio.gz"
dpkg-deb -x "$package" tmp/
mkdir -pv tmp/etc/udhcpc
cat > tmp/etc/udhcpc/default.script << 'EOL'
#!/bin/sh
# cargo cult from buildroot
[ -z "$1" ] && echo "Error: should be called from udhcpc" && exit 1
case "$1" in
renew|bound)
/sbin/ifconfig $interface $ip $BROADCAST $NETMASK
if [ -n "$router" ] ; then
while route del default gw 0.0.0.0 dev $interface 2>/dev/null; do
:
done
for i in $router ; do
route add default gw $i dev $interface
done
fi
;;
esac
RESOLV_CONF="/etc/resolv.conf"
echo -n > $RESOLV_CONF
[ -n "$domain" ] && echo domain $domain >> $RESOLV_CONF
for i in $dns
do
echo adding dns $i
echo nameserver $i >> $RESOLV_CONF
done
EOL
cat > tmp/init << 'EOL'
#!/bin/busybox sh
echo "Loading, please wait..."
mkdir -p /newroot /proc /sbin /sys /tmp /usr/bin /usr/sbin /var/lock
/bin/busybox --install
mount -t sysfs -o nodev,noexec,nosuid none /sys
mount -t proc -o nodev,noexec,nosuid none /proc
depmod -a||true
# Modprobe all drivers/net/ethernet modules
for module in $(find /lib/modules/$(uname -r)/kernel/drivers/net/ethernet -type f -name '*.ko') ; do
module_name=$(basename "${module}")
modprobe ${module_name%.ko}||true
done
modprobe bridge||true
echo "Bringing up bridge ..."
ifconfig eth0 0.0.0.0 up
brctl addbr br0
brctl addif br0 eth0
ifconfig br0 0.0.0.0 up
udhcpc -i br0 -q
echo "Sleep 5 ..."
sleep 5
# parsing stolen from initramfs-tools
for x in $(cat /proc/cmdline); do
case "$x" in
rootdelay=*)
ROOTDELAY="${x#rootdelay=}"
case ${ROOTDELAY} in
*[![:digit:].]*)
ROOTDELAY=
;;
esac
;;
nfsroot=*)
NFSROOT="${x#nfsroot=}"
;;
esac
done
# get nfs root from dhcp
if [ "x${NFSROOT}" = "xauto" ]; then
# check if server ip is part of dhcp root-path
if [ "${ROOTPATH#*:}" = "${ROOTPATH}" ]; then
NFSROOT="${ROOTSERVER}:${ROOTPATH}"
else
NFSROOT="${ROOTPATH}"
fi
# nfsroot=[:][,]
elif [ -n "${NFSROOT}" ]; then
# nfs options are an optional arg
if [ "${NFSROOT#*,}" != "${NFSROOT}" ]; then
NFSOPTS="-o ${NFSROOT#*,}"
fi
NFSROOT=${NFSROOT%%,*}
if [ "${NFSROOT#*:}" = "$NFSROOT" ]; then
NFSROOT="${ROOTSERVER}:${NFSROOT}"
fi
fi
echo "Mount NFS ..."
mount -t nfs -o nolock ${NFSOPTS} ${NFSROOT} /newroot||/bin/busybox sh -i /dev/console 2>&1
ln -sf /run/systemd/resolve/resolv.conf /newroot/etc/resolv.conf
# populate interfaces to keep NM and ifupdown away
cat > /newroot/etc/network/interfaces < "../initramfs.${ARCH}.cpio"
gzip -9 "initramfs.${ARCH}.cpio"
rm -rf tmp/
```
Le groupe apertis propose des kits complets (noyau + initrd) permettant d'amorcer un RootFS en NFS. Ces kits sont disponibles dans les dossiers nfs des release sur le site https://images.apertis.org/
##Exéxution de l'image
###Depuis une image cpio
```
qemu-system-x86_64 -machine virt -m 4G -kernel /qemu-rpi-kernel/Image-5.16.1 -initrd /qemu-rpi-kernel/etherpad.cpio -device virtio-net-pci,netdev=user0 -netdev bridge,id=user0,br=virbr0 -nographic
```
Cette méthode charge l'image du rootfs en mémoire RAM, donc tous les changements sont volatiles et disparaitront quand la machine sera arrêtée.
Pour garder les modifications il faut mettre à jour le rootfs source par exemple avec un rsync, puis recréer l'image cpio.
```
rsync -aPHxv -e 'ssh -o StrictHostKeyChecking=no' --exclude-from=/root/exclude-files.txt root@192.168.122.76:/ /var/lib/lxc/debian11/rootfs/
```
Il faut impérativement exclure de la synchronization les dossiers proc,tmp,sys,boot et dev:
```
vi /root/exclude-files.txt
/boot
/dev
/tmp
/sys
/proc
/backup
/etc/mtab
/etc/mdadm.conf
```
###Architecture native (x86)
* En mode non graphique avec sortie directe sur le sysout par défaut (terminal)
```
qemu-system-aarch64 -machine virt -cpu cortex-a72 -smp 6 -m 2G -kernel /qemu-rpi-kernel/Image-5.16.1 -initrd /qemu-rpi-kernel/rootfs.cpio -device virtio-net-pci,netdev=user0 -netdev bridge,id=user0,br=virbr0 -nographic -append "ip=dhcp rdinit=/sbin/init root=/dev/ram rootfstype=ramfs rw nokaslr"
```
* En mode daemon sans sortie sur le sysout
```
qemu-system-aarch64 -machine virt -cpu cortex-a72 -smp 6 -m 2G -kernel /qemu-rpi-kernel/Image-5.16.1 -initrd /qemu-rpi-kernel/rootfs.cpio -device virtio-net-pci,netdev=user0 -netdev bridge,id=user0,br=virbr0 -display none -daemonize -append "ip=dhcp rdinit=/sbin/init root=/dev/ram rootfstype=ramfs rw nokaslr"
```
* depuis un livecd
```
qemu-system-x86_64 -cdrom turnkey-jenkins-17.1-bullseye-amd64.iso -m 2G -device virtio-net-pci,netdev=user0 -netdev bridge,id=user0,br=virbr0 -display none
```
* avec un dossier du system-host comme rootfs
```
qemu-system-x86_64 -machine virt -m 2G -kernel /tmp/rootfs/vmlinuz -initrd /tmp/rootfs/initrd.img -device virtio-net-pci,netdev=user0 -netdev bridge,id=user0,br=virbr0 -nographic -append "ip=dhcp rdinit=/sbin/init root=/dev/nfs rw nfsroot=192.168.122.1:/tmp/rootfs/"
```
* avec un kernel spécifique
```
qemu-system-x86_64 -smp 6 -m 4G -kernel /qemu-rpi-kernel/x86_64-image-6.1.1 -initrd /qemu-rpi-kernel/jenkins.cpio -device virtio-net-pci,netdev=user0 -netdev bridge,id=user0,br=virbr0 -nographic -curses -append "ip=dhcp rdinit=/sbin/init root=/dev/nfs rw nfsroot=192.168.122.1:/tmp/rootfs/,tcp,vers=3"
```
###Foreign architecture
Les sites suivants proposent des kernels pour les architectures arm:
->https://images.apertis.org/
-> https://github.com/dhruvvyas90/qemu-rpi-kernel.git
-> https://l4re.org/download/Linux-kernel/ (recommandé car le kernel est compilé avec les options IP et NFS)
On suppose qemu installé pour l'architecture arm 64 bits (aarch64)
```
qemu-system-aarch64 --version
QEMU emulator version 5.0.0 (Debian 1:5.0-14~bpo10+1)
Copyright (c) 2003-2020 Fabrice Bellard and the QEMU Project developers
```
Vérifier la présence du modèle virt:
```
qemu-system-aarch64 -machine help |grep virt
....
virt QEMU 5.0 ARM Virtual Machine (alias of virt-5.0)
virt-5.0 QEMU 5.0 ARM Virtual Machin
```
On peut utiliser le noyau téléchargé afin d'émuler une architecture arm64 dans un système x86:
```
qemu-system-aarch64 -machine virt -cpu cortex-a53 -smp 6 -m 2G -kernel /qemu-rpi-kernel/vmlinuz-5.7.0-qcomlt-arm64 -initrd /qemu-rpi-kernel/initrd-5.0.4-img -device virtio-net-pci,netdev=user0 -netdev bridge,id=user0,br=lxcbr0 -nographic -curses -append "ip=dhcp rdinit=/sbin/init root=/dev/ram rootfstype=ramfs rw nokaslr"
```
```
qemu-system-aarch64 -machine virt -cpu cortex-a72 -smp 6 -m 2G -kernel /qemu-rpi-kernel/l4re/kernel/arm64/Image-6.6.8 -initrd /qemu-rpi-kernel/initramfs.arm64.cpio -device virtio-net-pci,netdev=user0 -netdev bridge,id=user0,br=lxcbr0 -nographic -append "earlycon rootdelay=5 console=ttyAMA0 ip=dhcp root=/dev/nfs rw nfsroot=10.0.3.1:/tmp/rootfs/"
```
```
qemu-system-aarch64 -machine virt -cpu cortex-a72 -smp 2 -m 4G -kernel /qemu-rpi-kernel/l4re/kernel/arm64/Image-6.6.8 -device virtio-net-pci,netdev=user0 -netdev bridge,id=user0,br=lxcbr0 -nographic -append "earlycon rootdelay=5 console=ttyAMA0 ip=dhcp root=/dev/nfs rw nfsroot=10.0.3.1:/tmp/rootfs/,vers=3"
```