Boot KVM guests via UEFI by default; per-image firmware setting

Run 5 on the real host bisected the guest hang: the Debian 13 image
hangs after GRUB under SeaBIOS with or without our seed and with either
CPU model, and boots under OVMF. The image's legacy boot chain is the
problem, not the provider.

images.<key>.firmware is "uefi" (default) or "bios". The domain XML
emits <os firmware="efi"> with secure boot off, so libvirt selects the
OVMF binary and manages the NVRAM file, which the existing nvram undefine
flag removes. Direct-URL images default to UEFI too. OVMF joins the
hypervisor prerequisites; sample, config table, known gaps, decision log
and the execution log record the finding.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-09-10 19:13:51 +00:00
co-authored by Claude Fable 5.1
parent 5a6c889531
commit 9a393b1df8
12 changed files with 151 additions and 26 deletions
+7
View File
@@ -454,6 +454,13 @@ they read as choices, not accidents. Chronological by build step.
unknown statuses, `nostate` means libvirt could not determine the
state at all; answering that with delete-and-recreate is too drastic,
so the next poll decides.
- **KVM guests boot UEFI by default, secure boot off.** Found on the
first real host: Debian 13's generic image hangs after GRUB under
SeaBIOS regardless of CPU model or seed, and boots under OVMF. Every
current distro cloud image supports UEFI, so it is the default and
`firmware: bios` is the per-image escape; libvirt's firmware
autoselection (`<os firmware="efi">`) keeps the OVMF path out of the
operator's config. Secure boot is off so unsigned images boot too.
- **A KVM domain GC cannot read is skipped, a host it cannot reach fails
the sweep.** GC never deletes what it does not see, so leaving one
domain out is safe; one corrupt domain blocking GC for every other one
+12 -1
View File
@@ -23,7 +23,11 @@ Per host, once:
4. **A storage pool** (default `default`, usually `/var/lib/libvirt/images`)
with room for one base image per distro (a few hundred MB) plus the
per-VM overlays (`diskSizeGb` each, thin-provisioned).
5. Optionally, **outbound internet from the VMs**: the default
5. **UEFI firmware for guests**: the `ovmf` package on Debian/Ubuntu,
`edk2-ovmf` on Fedora/RHEL. VMs boot UEFI by default (libvirt picks
the OVMF binary itself); a domain start fails with "no firmware
found" without it. Only `firmware: bios` images skip this.
6. Optionally, **outbound internet from the VMs**: the default
vendor-data installs `qemu-guest-agent` from the distro mirrors on
first boot. Without it the VM still works — the operator falls back to
the host's ARP table to find the IP, which fills in once the guest
@@ -107,6 +111,7 @@ ssh virt@nas1.home.lan ip link show br0
| `images.<key>.url` | yes per entry | | http(s) URL of a cloud image (qcow2 or raw) |
| `images.<key>.sha256` | | | expected digest; verified after download, a change triggers a re-download |
| `images.<key>.format` | | `qcow2` | `qcow2` or `raw` |
| `images.<key>.firmware` | | `uefi` | `uefi` (OVMF, secure boot off) or `bios` (SeaBIOS) for images without an EFI system partition; Debian 13's BIOS path does not boot, see *Known gaps* |
| `machineTypes.<key>` | ≥ 1 | | `{vcpus, memoryMiB}` size profile |
| `hosts[].name` | yes | | the `placement.zone` value that selects this host; unique |
| `hosts[].address` | yes | | SSH endpoint, `host:port` |
@@ -228,6 +233,12 @@ The provider logs through controller-runtime's logger, named `kvm`, with
- **IP discovery needs either guest traffic or the agent.** A VM on a
bridge with no DHCP never gets an address and stays `Provisioning`
forever; check `virsh console`.
- **Debian 13 cloud images do not boot via BIOS on the tested host**
(Ryzen 8845HS, libvirt 11.6, QEMU 10.1): GRUB prints "Booting" and
the kernel never reaches the console, with or without a seed, with
host-passthrough and host-model CPUs alike. The same image boots
under UEFI, which is why `firmware` defaults to `uefi`. Images that
need `firmware: bios` are the exception, not the rule.
## Testing
@@ -477,3 +477,40 @@ outside the operator). Because the provider's own logs were invisible in
the test output, this run also produced a side plan:
[2026-09-10-0805-kvm-libvirt-tracing](2026-09-10-0805-kvm-libvirt-tracing.md),
which traces every libvirt RPC at V(1) and routes it into `-v` output.
### Run 5: the guest hang is Debian 13's BIOS boot path; UEFI fixes it
The cached image's sha512 matched Debian's published sum, and
`qemu-img info` showed a sound 3 GiB qcow2, which also confirms that
the raw upload over a libvirt-created volume plus pool refresh yields a
byte-identical file (the Step 3b note is answered). Bisected by hand on
beelink with `virt-install --import` on a fresh overlay of the same
image:
| variant | seed | CPU | firmware | result |
|---|---|---|---|---|
| operator's domain | ours | host-passthrough | SeaBIOS | hangs after GRUB "Booting", 100 % CPU |
| bare | none | host-passthrough | SeaBIOS | same hang |
| bare | none | host-model | SeaBIOS | same hang |
| bare | none | host-passthrough | **OVMF (`--boot uefi`)** | **boots to login** |
So neither the seed ISO, the domain XML nor the CPU model was the
problem: the image's legacy GRUB → kernel handoff does not work on this
host, while its EFI stub path does. Not diagnosed further; it is
Debian's chain, not ours, and every current cloud image boots UEFI.
Change: `images.<key>.firmware` (`uefi` default, `bios` escape),
threaded through `withDefaults` → `imageSource` → `domainParams`, and
`buildDomainXML` emits `<os firmware="efi"><firmware><feature
enabled="no" name="secure-boot"/></firmware>` so libvirt selects the
OVMF binary and creates the NVRAM file; the `nvram` undefine flag Delete
already passes removes it (seen on the host: `virsh undefine` without
`--nvram` refuses such a domain). Direct-URL images default to UEFI
too. Docs: OVMF is a new hypervisor prerequisite, config table row,
sample comment, known-gaps entry, decision-log entry.
Worth noting: the first image download went through with no checksum
configured, and the cache reuses a base image by key, so switching
`KVM_TEST_IMAGE_URL` requires deleting `base-test-image.qcow2` and its
marker by hand first. That is the reviewer's Step 4 finding showing up
in practice; still documented rather than fixed.