Debian-Gastsysteme mit QEMU-ARM und libvirt virtualisieren
Wie die meisten Embedded-Systeme auf dem Markt ist auch unsere Spektralwerk-Plattform ARM-basiert. Sie arbeiten mit einer i.MX283 CPU von NXP (ehemals Freescale Semiconductor). ARM-Kerne sind besonders für Umgebungen geeignet, die niedrigen Energieaufwand und hohe Temperatur-Stabilität erfordern. Allerdings sind sie nicht gerade für ihre hohe Leistungsfähigkeit berühmt. Zwar greifen wir deshalb so weit wie möglich auf cross-compile toolchains zurück, doch benötigen wir dennoch regelmäßig tatsächliche ARM-Umgebungen für das Kompilieren von Bibliotheken und Anwendungen.
Bis jetzt haben wir in diesen Fällen auf echte Hardware zurückgegriffen, hauptsächlich ODROID-XU4- und Banana PI BPI-M3-SBCs, weil diese Boards relativ viel RAM und genug Rechenkraft mitbringen, um vollständige Kompilierungen der Firmware in akzeptablen Zeiträumen durchlaufen zu können. Auf jedem dieser Boards läuft ein Buildbot worker.
Mit dem Release von Debian Buster haben wir uns noch einmal mit dem Thema ARM-Virtualisierung unter QEMU gewidmet. Peter Maydell hat 2016 einen exzellenten Artikel über das virt-Board in QEMU veröffentlicht. Dieses Board emuliert im Gegensatz etwa zum versatilepb-Board keine häufig genutzte echte Hardware und kann deshalb mit viel RAM und einer variablen Anzahl an CPUs konfiguriert werden. Der Artikel bildete einen tollen Ausgangspunkt für unsere eigene Umsetzung, aber ordentliche libvirt-Integration entpuppte sich als einer der Stolpersteine. Libvirt ist ein Virtualisierungs-Daemon samt API, der einige Abstraktionen für übliche Virtualisierungs- und Prozess-Isolations-Techniken wie LXC, KVM und XEN definiert.
Von QEMU-Argumenten zu libvirt-XML
Peter Maydells Artikel endet mit folgendem qemu-system-arm-Aufruf:
shell
qemu-system-arm -M virt -m 1024 \
-kernel vmlinuz-3.16.0-4-armmp-lpae \
-initrd initrd.img-3.16.0-4-armmp-lpae \
-append 'root=/dev/vda2' \
-drive if=none,file=hda.qcow2,format=qcow2,id=hd \
-device virtio-blk-device,drive=hd \
-netdev user,id=mynet \
-device virtio-net-device,netdev=mynet \
-nographic
Das spezifische Problem, über das wir stolperten, war die Übersetzung des -device
-Arguments in
etwas, das libvirt versteht. virsh
, das Kommandozeilen-Verwaltungs-Tool, das mit libvirt geliefert
wird, hat eigens einen domxml-from-native
-Befehl, der entworfen wurde, um QEMU-Argumente in das
entsprechende libvirt-Format zu übersetzen. Leider befindet es sich, wie in dieser
Mail von Cole Robinson
beschrieben, nicht gerade in einem optimal gewarteten Zustand.
In unserem nächsten Ansatz haben wir deshalb die virtuelle Maschine mit den funktionierenden
QEMU-Argumenten gestartet und dann analysiert, welche Busse und Treiber innerhalb der Maschine
genutzt werden. So wollten wir herausfinden, was genau wir in der libvirt-Umgebung reproduzieren
müssen. Et voilá: der kritische Hinweis befand sich im Verzeichnis /dev/disk/by-path/
, das
die kürzesten physischen Pfade zu den Festplatten des Systems enthält. In unserem Fall führte ein
simpler Aufruf von ls /dev/disk/by-path/
zu folgender Ausgabe:
platform-a003c00.virtio_mmio platform-a003e00.virtio_mmio
virtio-mmio
ist ein gültiges type
-Attribut in der Dokumentation des
<device>
-Eintrags. Im Gegensatz dazu generiert libvirt standardmäßig pci
-Adresstypen,
die scheinbar nicht in aktuellen Versionen von Debian unterstützt werden. Letztendlich mussten
wir also einfach alle <address type="pci" .../>
-Zeilen durch <address type="virtio-mmio"/>
ersetzen, um die default-libvirt-Konfiguration unseres ARM-hosts in etwas tatsächlich lauffähiges
zu übersetzen. Das hat sowohl für lokale als auch für Netzerk-Laufwerke funktioniert.
So sieht unsere funktionierende libvirt-Konfiguration aus:
<domain type='qemu' id='38'>
<name>usain</name>
<uuid>3bf6e58f-e513-47b4-9b64-e00b32d9d9f4</uuid>
<memory unit='KiB'>3145728</memory>
<currentMemory unit='KiB'>3145728</currentMemory>
<vcpu placement='static'>3</vcpu>
<resource>
<partition>/machine</partition>
</resource>
<os>
<type arch='armv7l' machine='virt-3.1'>hvm</type>
<kernel>/var/lib/libvirt/boot/usain/vmlinuz</kernel>
<initrd>/var/lib/libvirt/boot/usain/initrd.img</initrd>
<cmdline>root=UUID=7a7f1855-2536-4342-a481-4853a125560f</cmdline>
<boot dev='hd'/>
</os>
<features>
<gic version='2'/>
</features>
<clock offset='utc'/>
<on_poweroff>destroy</on_poweroff>
<on_reboot>restart</on_reboot>
<on_crash>restart</on_crash>
<devices>
<emulator>/usr/bin/qemu-system-arm</emulator>
<disk type='block' device='disk'>
<driver name='qemu' type='raw'/>
<source dev='/dev/lvm-uhura/usain-boot'/>
<backingStore/>
<target dev='vda' bus='virtio'/>
<alias name='virtio-disk0'/>
<address type='virtio-mmio'/>
</disk>
<disk type='block' device='disk'>
<driver name='qemu' type='raw'/>
<source dev='/dev/lvm-uhura/usain-root'/>
<backingStore/>
<target dev='vdb' bus='virtio'/>
<alias name='virtio-disk1'/>
<address type='virtio-mmio'/>
</disk>
<controller type='pci' index='0' model='pcie-root'>
<alias name='pcie.0'/>
</controller>
<interface type='bridge'>
<mac address='52:54:00:79:39:16'/>
<source bridge='br-virt'/>
<target dev='vnet0'/>
<model type='virtio'/>
<alias name='net0'/>
<address type='virtio-mmio'/>
</interface>
<serial type='pty'>
<source path='/dev/pts/1'/>
<target type='system-serial' port='0'>
<model name='pl011'/>
</target>
<alias name='serial0'/>
</serial>
<console type='pty' tty='/dev/pts/1'>
<source path='/dev/pts/1'/>
<target type='serial' port='0'/>
<alias name='serial0'/>
</console>
</devices>
<seclabel type='dynamic' model='apparmor' relabel='yes'>
<label>libvirt-3bf6e58f-e513-47b4-9b64-e00b32d9d9f4</label>
<imagelabel>libvirt-3bf6e58f-e513-47b4-9b64-e00b32d9d9f4</imagelabel>
</seclabel>
<seclabel type='dynamic' model='dac' relabel='yes'>
<label>+64055:+64055</label>
<imagelabel>+64055:+64055</imagelabel>
</seclabel>
</domain>
Automatische Kernel-Updates
Als eine weitere Komplikation bei der ARM-Virtualisierung war die Notwendigkeit, das System
aktuell mit dem direct kernel boot
-Feature starten zu müssen. Libvirt unterstützt
direct kernel boot
out of the box, aber der Kernel und das initrd-Image müssen vom Dateisystem
des virtualisierenden Hosts aus erreichbar sein. Das führt dazu, dass nach einem Update zwar Kernel
und initrd innerhalb der virtuellen Maschine aktualisiert wurden, danach die neuen Versionen aber
zusätzlich in das Host-System kopiert werden müssen. Sobald also Kernel oder initrd verändert wurden,
mussten wir die boot-Partition mounten und die Datein an eine Stelle kopieren, wo libvirt auf sie
zugreifen konnte.
Glücklichweise unterstützt libvirt aber Hooks, die ausgeführt
werden, sobald ein qemu-Gast gestartet wird. Wir nutzen überlicherweise LVM als Speicher-backend
für libvirt. Jedem ARM-Gast wird ein $name-boot
- und ein $name-root
-Volume zugewiesen.
Wann immer wir nun einen ARM-Gast starten, können wir automatisch das entsprechende LVM-Volume
mounten, Kernel und initrd kopieren und libvirt den Rest erledigen lassen.
Das funktioniert bisher sehr gut und hat den Wartungsaufwand im Falle automatischer System-Updates
unserer ARM-Gäste deutlich verringert.
Dies ist der Hook, den wir dafür nutzen:
#!/bin/sh
set -eu
GUEST="$1"
ACTION="$2"
PHASE="$3"
BOOT_IMAGE_BASE_PATH=/var/lib/libvirt/boot
_is_mounted() {
grep -qwF "$1" /proc/mounts
}
_is_host_running() {
# calling virsh domstate here will cause the process to hang so we use ps instead
ps --no-headers -u libvirt-qemu -o cmd | grep -q -- "-name guest=$1"
}
_get_boot_volume() {
local guest="$1"
local configured_volume
local dm_path
# looks for a volume whose name ends with "-boot"
configured_volume="$(
xmllint --xpath 'string(/domain/devices/disk[@type="block"]/source[substring(@dev, string-length(@dev) - string-length("-boot") + 1) = "-boot"]/@dev)' \
"/etc/libvirt/qemu/$guest.xml"
)"
# the configured volume might contain any path that refers to a volume but /proc/mounts
# will contain paths from /dev/mapper so we need to find the path of the actual devices
# and then find the corresponding symlink in /dev/mapper
dm_path="$(realpath "$configured_volume")"
find /dev/mapper -type l | while read -r mapper_path; do
if [ "$(readlink -f "$mapper_path")" = "$dm_path" ]; then
echo "$mapper_path"
break
fi
done
}
update_guest_kernel_and_initrd() {
# ARM hosts cannot be booted like any x64_64 host.
# Instead we need libvirt to boot the kernel directly along with the guest’s generated initrd.
# We update the kernel and initrd on every guest startup, so that a system update will behave
# as expected on the next reboot.
local guest="$1"
local boot_image_path="$BOOT_IMAGE_BASE_PATH/$guest"
local boot_volume
local tmp_mount_path
boot_volume="${2:-$(_get_boot_volume "$guest")}"
if [ ! -z "$boot_volume" ]; then
echo "Boot volume for guest $guest not found." >&2
return 1
fi
if [ ! -e "$boot_volume" ]; then
echo "Boot volume for guest $guest does not exist in '$boot_volume'. Cannot extract kernel and initrd." >&2
return 1
fi
if _is_host_running "$guest"; then
# this should not happen, but maybe someone is calling this script manually
echo "Guest $guest is not shut down. Refusing to mount volumes." >&2
return 1
fi
if _is_mounted "$boot_volume"; then
echo "Boot volume '$boot_volume' is already mounted in the system. Mounting the volume twice may cause data loss." >&2
return 1
fi
mkdir -p --mode 750 "$boot_image_path"
chgrp libvirt-qemu "$boot_image_path"
tmp_mount_path="$(mktemp -d)"
trap "umount '$tmp_mount_path'; rmdir '$tmp_mount_path'" EXIT
mount -o ro "$boot_volume" "$tmp_mount_path"
cp "$tmp_mount_path/vmlinuz" "$tmp_mount_path/initrd.img" "$boot_image_path/"
}
if [ "$ACTION" = prepare ] && [ "$PHASE" = begin ]; then
# kernel and initrd of guests that use ARM emulation should be updated before being started
if grep -qwF /usr/bin/qemu-system-arm "/etc/libvirt/qemu/$GUEST.xml"; then
update_guest_kernel_and_initrd "$GUEST"
fi
fi
Fazit
ARM-Virtualisierung funktioniert mit Debian Buster sehr gut. Auch wenn die Performance nicht an echte ARM-Hardware heranreicht ist die Emulation dennoch schnell genug für unsere Anwendungsfälle. Die echten Boards, die wir zuvor genutzt haben, liefen oft nur mit uralten Kerneln und unfreier Firmware. Diese Probleme entfallen mit virtualisiserten Umgebungen. Gleichzeitig lassen sie sich im Vergleich zu den echten Boards viel leichter skalieren, verwalten und replizieren.