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 :


  1. stdio est configuré de façon à ne pas gérer les signaux. Ainsi, QEMU ne s’arrêtera pas s’il reçoit SIGINT. Comme on l’utilise comme console série, c’est un aspect important. ↩︎