Skip to main content

Module kvm

Module kvm 

Source
Expand description

The KVM half: the seam, and what a real guest has to do to fill it.

Behaviours 8 and 13–21 are not API behaviours. They are things a GUEST does, and no amount of JSON reproduces them: a PID-1 panic that is invisible except on the framebuffer is only invisible if there is a framebuffer, and an installer that loops only loops if something really boots twice. So the mock delegates the guest to a GuestEngine, and a real one is a qemu.

§Why the real engine is not in this crate’s dependencies

draupnir is the law’s door to a hypervisor and this crate starts no qemu of its own. But draupnir’s live KVM backend (backend-tunnr) depends on tunnr-vm by PATH, and tunnr is not published. An optional dependency is still resolved into the lockfile, so naming draupnir here would make a fresh clone of the open monetize workspace require a tunnr checkout beside it — the exact failure this workspace already paid for with Skidbladnir and wrote down in its root manifest.

So the seam is here and the binding is not. The binding is the detached kvm-guest crate beside this one (DraupnirGuest, rendered by draupnir’s own draupnir::qemu, not tunnr’s argv — see the measurement below for why tunnr’s could not express this machine), and its mock-upcloud-kvm binary serves this API with a real QEMU behind every server. cargo test -p mock-upcloud needs no hypervisor: the default engine runs nothing.

§What the real engine must do, and what each clause is for

clauseqemubehaviour
no serial console-serial none13 — a PID-1 panic reaches nobody
a framebuffer, and only that-vga std -display vnc=…13 — the panic IS visible, on the pixels
SeaBIOS, never OVMFno -bios OVMF*, no pflash15 — the BIOS path is the one that breaks
RTC two hours ahead-rtc base=<now+2h>19 — the skew gunnar-clock exists for
no inbound UDPa user-mode net with no hostfwd for udp18 — DNS and NTP are useless in there
the CD first while it is inserted-boot order=dc17 — the loop

A GuestEngine that cannot honour a clause must REFUSE it by name rather than quietly boot without it. A guest that silently got a serial console would make behaviour 13 untestable while reporting that it had been tested, which is the always-yes defect this estate has now found four times.

§MEASURED 2026-09-20 on oden: draupnir/tunnr cannot express this machine

tunnr::boot_primitive builds its argv unconditionally, and three of its lines contradict three of the clauses above:

  • args.push("-serial"); args.push("mon:stdio")always, with the comment “Serial → stdio so we capture the console and detect the BOOT-OK marker”. Behaviour 13 says there is no serial console, and the marker protocol is built on there being one.
  • // UEFI via OVMF pflash, exactly as the design mandates. — the firmware boot path is OVMF, unconditionally. Behaviour 15 says SeaBIOS, and UpCloud offers no knob.
  • if spec.headless { args.push("-nographic") } — which removes the framebuffer entirely. Behaviour 13 says the framebuffer is the ONLY place a PID-1 panic appears.

So the estate’s KVM proofs have been run on a machine that is not the machine UpCloud hands out — a UEFI box with a serial console and no screen, against a BIOS box with a screen and no serial. That is a large part of why an image that “booted fine locally” could panic invisibly up there.

The faithful argv, run against the real gunnar-installer-open-fullk.iso (59 496 448 bytes) on oden on 2026-09-20:

qemu-system-x86_64 -enable-kvm -m 2048 -smp 2 -no-reboot \
  -bios /usr/share/seabios/bios-256k.bin \
  -drive file=disk.qcow2,if=virtio,format=qcow2 -cdrom <iso> -boot order=dc \
  -serial none -vga std -display none -vnc 127.0.0.1:91 \
  -rtc base=<host time + 2h> \
  -netdev user,id=n0,dns=10.0.2.99 -device virtio-net-pci,netdev=n0

What it measured: zero bytes on stdout across every run, and the installer’s whole narration present on the framebuffer — INSTALLER: payload app.znippy materialized off the ISO volume (8580685 bytes), policy: require_signed=NO, and FAILED hwdetect: no usable target disk on the run with no disk attached. Two screendumps twenty-five seconds apart hashed differently, so the guest really progressed, and the qcow2 grew from 91 951 104 to 93 786 112 bytes, so the install really wrote. There is no log to quote for any of it. That is the point.

dns=10.0.2.99 is how behaviour 18 is modelled: slirp serves DNS on its own .3 and nothing else, so the guest’s queries leave and no reply ever arrives — which is what systemd-timesyncd experiences up there, and why gunnar-clock syncs from the front over a signed nonce instead.

What tunnr would need for draupnir to host this: a firmware (Bios/Uefi) choice on its BootSpec, a serial (Stdio/None) choice, and a headless mode that keeps -vga std and drops only the display. Three fields. Until they exist the seam below is the honest shape, and a DraupnirGuest would have to REFUSE no-serial-console and seabios by name rather than boot without them.

Structs§

GuestSpec
What the mock asks a hypervisor for.
Machine
One server’s machine, as the estate describes it at power-on.
MachineDisk
VirtualGuest
The guest as a state machine, for the runs that are not about the guest.

Enums§

GuestOutcome
What a guest did. Deliberately NOT a log: behaviour 20 is that nothing narrates the install window, so an engine that returned the guest’s output would be lying about the one thing this half exists to prove.

Traits§

GuestEngine
The seam. boot is the one-shot question a storm asks; the lifecycle methods below are what the estate calls when an API request changes a server, so that behind the JSON there is a machine.