From c235cb62beefd03087e7cfc7f7f55f0c7c6601c9 Mon Sep 17 00:00:00 2001 From: Lena Date: Sat, 1 Aug 2026 00:00:00 +0000 Subject: Enter vmm A QEMU/KVM virtual machine manager in one POSIX shell script. Each VM is a directory of plain files under $VMMDIR, configured by a hand-edited KEY=VALUE file that is parsed rather than sourced, so nothing in it can inject a raw QEMU argument. Control is a QMP FIFO pair inside that directory, guarded by its permissions alone; nothing binds a TCP port. The tests are TAP without a framework and boot real guests where the host allows. --- tests/boot | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 tests/boot (limited to 'tests/boot') diff --git a/tests/boot b/tests/boot new file mode 100644 index 0000000..2f778eb --- /dev/null +++ b/tests/boot @@ -0,0 +1,250 @@ +# A real guest, booted for real. Skipped without a usable /dev/kvm, so the +# suite still exits 0 on a machine with no virtualisation. + +if [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then + skip 1 "no usable /dev/kvm, skipping the boot tests" + return 0 +fi + +"$VMM" create bt1 64M >/dev/null 2>&1 +cfg bt1 </dev/null || echo) +if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then + ok "qemu is running" +else + notok "qemu is running" "$(tail -n 5 "$VMMDIR/bt1/stderr" 2>/dev/null)" +fi + +holds "qemu's stdout lands in a file" test -f "$VMMDIR/bt1/stdout" +holds "qemu's stderr lands in another" test -f "$VMMDIR/bt1/stderr" +outputs '==> .*stderr <==' "logs shows every stream" logs bt1 + +exits 0 "status of a running VM exits 0" status bt1 +outputs 'STATE=running' "status reports running" status bt1 +outputs 'running' "list reports running" list + +# The lock must be free after start, or every later verb reports busy. +holds "start releases its lock" flock -n "$VMMDIR/bt1" true + +exits 0 "start is idempotent" start bt1 +outputs 'already running' "a second start says so" start bt1 + +# restart swallows exit 5 from a stop that had to force the guest, and +# nothing else. A guest with no OS ignores ACPI, so this is that path, +# and it must come back as a different process. +old=$(cat "$VMMDIR/bt1/pid" 2>/dev/null || echo) +exits 0 "restart survives a stop that had to force" restart bt1 +new=$(cat "$VMMDIR/bt1/pid" 2>/dev/null || echo) +if [ -n "$new" ] && [ "$new" != "$old" ]; then + ok "restart comes back as a new process" +else + notok "restart comes back as a new process" "was [$old] now [$new]" +fi +outputs 'STATE=running' "the restarted guest is running" status bt1 + +# A restart that cannot start again must not report success: the config +# is broken under the guest, so stop works and start refuses. +cp "$VMMDIR/bt1/config" "$WORK/bt1.config" +cfg bt1 <&1) && got=0 || got=$? +n=$(printf '%s\n' "$out" | grep -c '"return"' || :) +if [ "$got" = 0 ] && [ "$n" = 2 ]; then + ok "monitor answers every command it reads from stdin" +else + notok "monitor answers every command it reads from stdin" \ + "exit $got, $n replies" "$out" +fi + +# A final line with no newline is a command too: dropped, it would be +# exit 0 and nothing done. +out=$(printf 'info status' | "$VMM" monitor bt1 2>&1) && got=0 || got=$? +if [ "$got" = 0 ] && printf '%s\n' "$out" | grep -q '"return"'; then + ok "monitor sends a final line with no newline" +else + notok "monitor sends a final line with no newline" "exit $got: $out" +fi + +# The command is interpolated into a JSON string. This payload closes +# that string and the object around it, so an unescaped one reaches +# QEMU's parser as a second complete command and stops the guest. An +# unescaped backslash instead makes the object unparseable, and QEMU +# refuses it with an error carrying no id, so no reply arrives at all. +outputs 'unknown command' "a quote cannot close the monitor's own object" \ + monitor bt1 'x"}},{"execute":"quit' +exits 0 "and the quit it carried never ran" status bt1 +outputs '"return"' "a backslash still leaves the monitor a parseable object" \ + monitor bt1 'info \ block' + +exits 5 "stop escalates and exits 5 on a guest with no OS" stop bt1 +holds "stop removes the pidfile once dead" test ! -e "$VMMDIR/bt1/pid" +exits 0 "stop is idempotent" stop bt1 + +# SNAPSHOT=yes must build a real overlay, never rely on -snapshot. +cp "$VMMDIR/bt1/disk.qcow2" "$WORK/bt1-before-snapshot.qcow2" +cfg bt1 </dev/null 2>&1 || : +holds "snapshot writes do not change the durable disk" qemu-img compare -q \ + "$WORK/bt1-before-snapshot.qcow2" "$VMMDIR/bt1/disk.qcow2" +exits 0 "kill is idempotent" kill bt1 + +# The same write without SNAPSHOT must survive a complete QEMU restart. +cfg bt1 </dev/null 2>&1 || : +exits 0 "restart after a durable disk write" start bt1 +outputs '"return": ""' "an ordinary disk write survives restart" monitor bt1 \ + 'qemu-io -d /machine/peripheral/blk0/virtio-backend "read -P 0x5a 2097152 4096"' +"$VMM" kill bt1 >/dev/null 2>&1 || : + +# A running VM must be undeletable and unclonable. +cfg bt1 </dev/null 2>&1 || : +exits 4 "delete refuses a running VM" delete -f bt1 +exits 4 "clone refuses a running source" clone bt1 bt9 +"$VMM" kill bt1 >/dev/null 2>&1 || : +exits 0 "delete -f removes it once stopped" delete -f bt1 + +# status answers about the process, so an unparseable config must not stop +# it, and a config edited under a live guest must not make it lie. +"$VMM" create bt2 64M >/dev/null 2>&1 +cfg bt2 </dev/null 2>&1 || : +cfg bt2 </dev/null || echo) +mkdir -p "$VMMDIR/bt3" +cp "$VMMDIR/bt2/config" "$VMMDIR/bt3/config" +printf '%s\n' "$P2" > "$VMMDIR/bt3/pid" +exits 4 "a different live qemu is reported as unknown state" stop bt3 +holds "state owned by an unrecognised live qemu is left alone" \ + test -f "$VMMDIR/bt3/pid" +state=$("$VMM" list | awk '$1 == "bt3" { print $2 }') +if [ "$state" = unknown ]; then + ok "list exposes unrecognised live qemu state" +else + notok "list exposes unrecognised live qemu state" "got [$state]" +fi +rm -rf "$VMMDIR/bt3" + +# A wedged monitor must be reported, not silently reported as success. +P=$(cat "$VMMDIR/bt2/pid" 2>/dev/null || echo) +if [ -n "$P" ]; then + kill -STOP "$P" 2>/dev/null || : + out=$(MONITOR_TIMEOUT=1 "$VMM" monitor bt2 info status 2>&1) && got=0 || got=$? + kill -CONT "$P" 2>/dev/null || : + if [ "$got" != 0 ] && printf '%s' "$out" | grep -q 'no reply'; then + ok "a silent monitor is an error, not exit 0" + else + notok "a silent monitor is an error, not exit 0" "exit $got: $out" + fi + # The shell reports the job timeout kills unless it is reaped where + # nobody can see it, and that report is not vmm's to print. + if printf '%s' "$out" | grep -q 'Terminated'; then + notok "a timed out call prints nothing but vmm's own error" "$out" + else + ok "a timed out call prints nothing but vmm's own error" + fi +else + skip 2 "bt2 did not start, cannot test the wedged monitor" +fi + +"$VMM" kill bt2 >/dev/null 2>&1 || : +exits 3 "status of the killed guest exits 3" status bt2 + +# GRAPHICS=vnc listens on a unix socket in the VM directory, and status +# reports the socket that is really there rather than what the config says. +"$VMM" create bt4 64M >/dev/null 2>&1 +cfg bt4 </dev/null 2>&1; then + holds "GRAPHICS=vnc creates the socket" test -S "$VMMDIR/bt4/vnc.sock" + outputs "VNC=$VMMDIR/bt4/vnc.sock" "status names the VNC socket" status bt4 + "$VMM" kill bt4 >/dev/null 2>&1 || : + holds "the socket is removed once stopped" test ! -e "$VMMDIR/bt4/vnc.sock" + out=$("$VMM" status bt4 2>&1) && got=0 || got=$? + if [ "$got" = 3 ] && ! printf '%s\n' "$out" | grep -q '^VNC='; then + ok "a stopped VM has no VNC line" + else + notok "a stopped VM has no VNC line" "exit $got: $out" + fi +else + skip 4 "this qemu cannot do GRAPHICS=vnc" +fi + +# One directory per VM means two guests are independent of each other: +# each names its own process, and killing one leaves the other alone. +"$VMM" create bt5 64M >/dev/null 2>&1 +"$VMM" create bt6 64M >/dev/null 2>&1 +for v in bt5 bt6; do + cfg "$v" </dev/null 2>&1 || : +done +p5=$(cat "$VMMDIR/bt5/pid" 2>/dev/null || echo) +p6=$(cat "$VMMDIR/bt6/pid" 2>/dev/null || echo) +holds "two guests run at once, as two processes" \ + test -n "$p5" -a -n "$p6" -a "$p5" != "$p6" +"$VMM" kill bt5 >/dev/null 2>&1 || : +exits 3 "killing one stops that one" status bt5 +exits 0 "and leaves the other running" status bt6 +"$VMM" kill bt6 >/dev/null 2>&1 || : -- cgit v1.2.3