Labo virtuel avec QEMU
Vincent Bernat
J’utilisais jusqu’ici des machines virtuelles UML pour toutes mes expérimentations relatives au réseau. De nombreuses alternatives existent : GNS3, Netkit, Marionnet ou Cloonix. Malgré leurs qualités, je considère encore ma propre solution comme la plus adaptée à mes attentes :
- Je ne veux pas utiliser d’images disques. Elles prennent beaucoup de place et il faut les maintenir. Elles ont aussi parfois tendance à l’embonpoint si elles sont beaucoup utilisées. Elles sont de plus difficiles à partager.
- L’accès à mon répertoire personnel (
/home
) depuis chacune des machines virtuelles m’est aussi essentiel. Cela facilite l’échange de fichiers avec les VM et je peux disposer de tous les fichiers de configuration important pour le labo dans un répertoire dédié. - Je ne veux pas démarrer un système complet. Cela permet d’économiser de la mémoire et de démarrer le labo en quelques secondes.
L’utilisation d’UML me posait quelques soucis :
- Il est peu utilisé et sujet à certains bugs. Il n’est par exemple pas possible d’utiliser un gdbserver depuis UML sans un correctif. Parfois, le noyau peut simplement ne pas compiler.
- Il est plutôt lent.
Toutefois, l’un des points forts d’UML est la présence de HostFS,
un système de fichiers donnant accès à une partie du système de
fichiers de l’hôte. C’est une fonctionnalité clé pour mon usage qui
me permet de me passer d’images disques et d’accéder à mon /home
depuis chaque machine invitée.
J’ai découvert que QEMU fournissait 9P, un système de fichiers similaire se basant sur VirtIO, un système d’IO paravirtualisé.
Mise en place du labo#
La mise en palce du labo est effectuée à travers un unique fichier de script. La procédure est très similaire à ce que je faisais avec UML. Je décris ici les étapes les plus intéressantes.
Démarrer QEMU avec un noyau minimal#
Mon principal objectif était de tester le
patch de Nicolas Dichtel concernant le support IPv6 d’ECMP. J’ai
donc besoin d’un noyau adhoc. Concernant la configuration de celui-ci,
je suis parti du résultat de make defconfig
, j’ai retiré tout ce qui
n’était pas nécessaire, ajouté ce dont j’avais spécifiquement besoin
pour le labo (des options concernant principalement la pile réseau) et
ajoutés les pilotes pour utiliser VirtIO :
CONFIG_NET_9P_VIRTIO=y CONFIG_VIRTIO_BLK=y CONFIG_VIRTIO_NET=y CONFIG_VIRTIO_CONSOLE=y CONFIG_HW_RANDOM_VIRTIO=y CONFIG_VIRTIO=y CONFIG_VIRTIO_RING=y CONFIG_VIRTIO_PCI=y CONFIG_VIRTIO_BALLOON=y CONFIG_VIRTIO_MMIO=y
Aucun module. Jetez un œil sur la configuration complète si besoin.
Il est ensuite possible de faire démarrer ce noyau :
qemu-system-x86_64 \ -m 256m \ -display none \ -nodefconfig -no-user-config -nodefaults \ \ -chardev stdio,id=charserial0,signal=off \ -device isa-serial,chardev=charserial0,id=serial0 \ \ -chardev socket,id=con0,path=$TMP/vm-$name-console.pipe,server,nowait \ -mon chardev=con0,mode=readline,default \ \ -kernel $LINUX \ -append "init=/bin/sh console=ttyS0"
$LINUX
correspond au fichier bzImage
. Bien sûr, sans disque, le
noyau s’arrête net quand il cherche la racine. La
configuration n’utilise pas d’écran virtuel (-display none
). Un port
série est défini et utilise l’entrée/sortie standard1. Le
noyau est configuré pour utiliser ce dernier comme console
(console=ttyS0
). Il existe des consoles VirtIO, mais elles ne sont
pas fonctionnelles au moment du démarrage du noyau.
Le moniteur de QEMU est configuré pour écouter sur une socket Unix. On
s’y connecte avec la commande socat UNIX:$TMP/vm-$name-console.pipe -
.
Disque mémoire (ramdisk)#
Mise à jour (10.2012)
Initialement, j’utilisais un initrd car je ne
parvenais pas à convaincre le noyau de monter directement le système
de fichiers de l’hôte comme racine pour le système invité. Dans un
commentaire, Josh Triplett m’a mis sur la
bonne voie en m’indiquant d’utiliser /dev/root
comme étiquette de
montage. À titre documentaire, je continue d’utiliser ici un initrd,
mais j’ai déjà mis à jour le labo sur GitHub pour ne plus en
faire usage.
Voici comment construire un ramdisk minimal:
# Setup initrd setup_initrd() { info "Build initrd" DESTDIR=$TMP/initrd mkdir -p $DESTDIR # Setup busybox copy_exec $($WHICH busybox) /bin/busybox for applet in $(${DESTDIR}/bin/busybox --list); do ln -s busybox ${DESTDIR}/bin/${applet} done # Setup init cp $PROGNAME ${DESTDIR}/init cd "${DESTDIR}" && find . | \ cpio --quiet -R 0:0 -o -H newc | \ gzip > $TMP/initrd.gz }
La fonction copy_exec
provient du paquet initramfs-tools
dans
Debian. Elle permet de copier un exécutable ainsi que toutes les
bibliothèques nécessaires pour son exécution. Une autre approche
aurait été d’exploiter une version statique de busybox.
Le script de configuration du labo est copié dans le ramdisk sous le
nom /init
. Il détectera qu’il est invoqué ainsi. Si ce script est
omis, le noyau démarrera un shell à la place.
L’option -initrd
indique à QEMU de charger ce ramdisk en mémoire.
Système de fichiers racine#
Mettons maintenant en place le système de fichiers racine à l’aide de 9P. Indiquons d’abord à QEMU d’exporter le système de fichiers de l’hôte au système invité :
qemu-system-x86_64 \ ${PREVIOUS_ARGS} \ -fsdev local,security_model=passthrough,id=fsdev-root,path=${ROOT},readonly \ -device virtio-9p-pci,id=fs-root,fsdev=fsdev-root,mount_tag=rootshare
${ROOT}
peut soit être /
, soit un répertoire contenant un système
complet. Dans l’invité, il est alors possible de monter le répertoire
ainsi partagé :
mkdir -p /target/ro mount -t 9p rootshare /target/ro -o trans=virtio,version=9p2000.u
On se retrouve alors avec un système de fichiers racine complet dans
/target/ro
. Ce dernier a été monté en lecture seule car nous ne
disposons pas des droits pour le modifier (il ne faut pas lancer le
labo avec les droits du superutilisateur). Comme pour un Live CD,
nous allons superposer un système de fichiers en mémoire qui recevra
les modifications effectuées pendant la vie du système. Les noyaux
Debian fournissent actuellement AUFS tandis que Ubuntu et OpenWRT
sont passés à overlayfs. J’ai par le passé rencontré quelques
erreurs lors de l’utilisation d’AUFS. Il n’est pas encore très clair
lequel des deux sera officiellement intégré dans le noyau.
Essayons overlayfs.
Il n’existe à ma connaissance pas de patch tout fait à appliquer sur son noyau pour overlayfs. Je travaille avec la branche net-next de David Miller. Voici comme appliquer overlayfs par dessus celle-ci :
$ git remote add torvalds git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git $ git fetch torvalds $ git remote add overlayfs git://git.kernel.org/pub/scm/linux/kernel/git/mszeredi/vfs.git $ git fetch overlayfs $ git merge-base overlayfs.v15 v3.6 4cbe5a555fa58a79b6ecbb6c531b8bab0650778d $ git checkout -b net-next+overlayfs $ git cherry-pick 4cbe5a555fa58a79b6ecbb6c531b8bab0650778d..overlayfs.v15
Ne pas oublier d’activer CONFIG_OVERLAYFS_FS
dans le
.config
. Voici comment est configuré le système de fichiers racine :
info "Setup overlayfs" mkdir /target mkdir /target/ro mkdir /target/rw mkdir /target/overlay # Version 9p2000.u allows one to access /dev, /sys and mount new # partitions over them. This is not the case for 9p2000.L. mount -t 9p rootshare /target/ro -o trans=virtio,version=9p2000.u mount -t tmpfs tmpfs /target/rw -o rw mount -t overlayfs overlayfs /target/overlay -o lowerdir=/target/ro,upperdir=/target/rw mount -n -t proc proc /target/overlay/proc mount -n -t sysfs sys /target/overlay/sys info "Mount home directory on /root" mount -t 9p homeshare /target/overlay/root -o trans=virtio,version=9p2000.L,access=0,rw info "Mount lab directory on /lab" mkdir /target/overlay/lab mount -t 9p labshare /target/overlay/lab -o trans=virtio,version=9p2000.L,access=0,rw info "Chroot" export STATE=1 cp "$PROGNAME" /target/overlay exec chroot /target/overlay "$PROGNAME"
Il faut configurer QEMU pour exporter ${HOME}
et le répertoire
contenant le labo :
qemu-system-x86_64 \ ${PREVIOUS_ARGS} \ -fsdev local,security_model=passthrough,id=fsdev-root,path=${ROOT},readonly \ -device virtio-9p-pci,id=fs-root,fsdev=fsdev-root,mount_tag=rootshare \ -fsdev local,security_model=none,id=fsdev-home,path=${HOME} \ -device virtio-9p-pci,id=fs-home,fsdev=fsdev-home,mount_tag=homeshare \ -fsdev local,security_model=none,id=fsdev-lab,path=$(dirname "$PROGNAME") \ -device virtio-9p-pci,id=fs-lab,fsdev=fsdev-lab,mount_tag=labshare
Réseau#
N’oublions pas la partie réseau. Pour chaque réseau L2 nécessaire au labo, mettons en place un switch VDE :
# Setup a VDE switch setup_switch() { info "Setup switch $1" screen -t "sw-$1" \ start-stop-daemon --make-pidfile --pidfile "$TMP/switch-$1.pid" \ --start --startas $($WHICH vde_switch) -- \ --sock "$TMP/switch-$1.sock" screen -X select 0 }
Et pour y attacher une interface :
mac=$(echo $name-$net | sha1sum | \ awk '{print "52:54:" substr($1,0,2) ":" substr($1, 2, 2) ":" substr($1, 4, 2) ":" substr($1, 6, 2)}') qemu-system-x86_64 \ ${PREVIOUS_ARGS} \ -net nic,model=virtio,macaddr=$mac,vlan=$net \ -net vde,sock=$TMP/switch-$net.sock,vlan=$net
L’utilisation d’un switch VDE permet de se passer des droits
superutilisateur. Il est possible de donner accès à Internet à chaque
VM, soit en utilisant -net user
, soit en utilisant un switch VDE et
en y attachant un processus slirpvde
. Cette dernière solution permet
aux VM de communiquer entre elles via Internet si besoin.
Debug#
Le but principal de ce labo était de débugguer à la fois le noyau et Quagga, ce qui peut se faire « à distance » dans les deux cas.
Debug noyau#
KGDB est le débuggueur, compatible avec GDB, inclus dans le noyau. Cependant, QEMU embarque un serveur GDB qui est plus simple d’utilisation.
qemu-system-x86_64 \ ${PREVIOUS_ARGS} \ -gdb unix:$TMP/vm-$name-gdb.pipe,server,nowait
Pour se connecter à ce serveur, il faut d’abord localiser le fichier
vmlinux
, présent à la racine des sources d’un noyau compilé. Pour en
faire bon usage, le noyau doit avoir été compilé avec l’option
CONFIG_DEBUG_INFO=y
. Il est ensuite possible d’utiliser socat
pour
s’attacher au débuggueur distant :
$ gdb vmlinux GNU gdb (GDB) 7.4.1-debian Reading symbols from /home/bernat/src/linux/vmlinux...done. (gdb) target remote | socat UNIX:$TMP/vm-r1-gdb.pipe - Remote debugging using | socat UNIX:/tmp/tmp.W36qWnrCEj/vm-r1-gdb.pipe - native_safe_halt () at /home/bernat/src/linux/arch/x86/include/asm/irqflags.h:50 50 } (gdb)
Le noyau se débuggue alors comme un programme classique.
Le debug est plus simple si les optimisations sont
désactivées. Cependant,
il n’est pas possible de les désactiver globalement. On
peut toutefois le faire de manière sélective. Par exemple, si on
est intéressé par net/ipv6/route.c
, il suffit d’ajouter
CFLAGS_route.o = -O0
à la fin de net/ipv6/Makefile
, de supprimer
le fichier net/ipv6/route.o
et de lancer make
.
Debug Quagga#
Pour débugguer un programme classique, le plus simple est de
simplement utiliser gdb
dans le système invité. La présence des
sources dans le /home
rend cette approche tout à fait
supportable. Nonobstant, il est assez simple de débugguer à
distance. Ajoutons un port série à QEMU :
qemu-system-x86_64 \ ${PREVIOUS_ARGS} \ -chardev socket,id=charserial1,path=$TMP/vm-$name-serial.pipe,server,nowait \ -device isa-serial,chardev=charserial1,id=serial1
Puis lançons gdbserver
dans le système invité :
$ libtool execute gdbserver /dev/ttyS1 zebra/zebra Process /root/code/orange/quagga/build/zebra/.libs/lt-zebra created; pid = 800 Remote debugging using /dev/ttyS1
Depuis l’hôte, attachons-nous au processus distant :
$ libtool execute gdb zebra/zebra GNU gdb (GDB) 7.4.1-debian Reading symbols from /home/bernat/code/orange/quagga/build/zebra/.libs/lt-zebra...done. (gdb) target remote | socat UNIX:/tmp/tmp.W36qWnrCEj/vm-r1-serial.pipe - Remote debugging using | socat UNIX:/tmp/tmp.W36qWnrCEj/vm-r1-serial.pipe - Reading symbols from /lib64/ld-linux-x86-64.so.2...(no debugging symbols found)...done. Loaded symbols for /lib64/ld-linux-x86-64.so.2 0x00007ffff7dddaf0 in ?? () from /lib64/ld-linux-x86-64.so.2 (gdb)
Démonstration#
Voici une courte démonstration du labo ainsi obtenu :
-
stdio
est configuré de façon à ne pas gérer les signaux. Ainsi, QEMU ne s’arrêtera pas s’il reçoitSIGINT
. Comme on l’utilise comme console série, c’est un aspect important. ↩︎