Files
kacerrandClaude Fable 5.1 9a393b1df8 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>
2026-09-10 19:13:51 +00:00

138 lines
5.0 KiB
Go

package kvm
import (
"encoding/xml"
"strings"
"testing"
"time"
)
func testDomainParams() domainParams {
return domainParams{
Name: "proxy-abc123def456ghij",
UID: "11111111-2222-3333-4444-555555555555",
CreatedAt: time.Date(2026, 9, 4, 12, 0, 0, 0, time.UTC),
VCPUs: 2,
MemoryMiB: 2048,
Firmware: firmwareUEFI,
Bridge: "br0",
DiskPath: "/var/lib/libvirt/images/proxy-abc123def456ghij.qcow2",
SeedPath: "/var/lib/libvirt/images/proxy-abc123def456ghij-seed.iso",
}
}
func TestBuildDomainXML_fieldByField(t *testing.T) {
t.Parallel()
raw, err := buildDomainXML(testDomainParams())
if err != nil {
t.Fatalf("buildDomainXML() error = %v", err)
}
var d domainXML
if err := xml.Unmarshal([]byte(raw), &d); err != nil {
t.Fatalf("output is not well-formed XML: %v\n%s", err, raw)
}
if d.Type != "kvm" || d.Name != "proxy-abc123def456ghij" {
t.Errorf("type/name = %s/%s", d.Type, d.Name)
}
if d.VCPU != 2 || d.Memory.Value != 2048 || d.Memory.Unit != "MiB" {
t.Errorf("vcpu/memory = %d/%d %s, want 2/2048 MiB", d.VCPU, d.Memory.Value, d.Memory.Unit)
}
if d.OS.Type.Value != "hvm" || d.OS.Type.Arch != "x86_64" || d.OS.Type.Machine != "q35" || d.OS.Boot.Dev != "hd" {
t.Errorf("os = %+v", d.OS)
}
if d.OS.Firmware != "efi" || d.OS.FirmwareFeatures == nil || len(d.OS.FirmwareFeatures.Features) != 1 ||
d.OS.FirmwareFeatures.Features[0] != (firmwareFeatureXML{Enabled: "no", Name: "secure-boot"}) {
t.Errorf("os firmware = %q %+v, want efi with secure-boot disabled", d.OS.Firmware, d.OS.FirmwareFeatures)
}
if d.Features.ACPI == nil || d.Features.APIC == nil {
t.Errorf("features = %+v, want acpi and apic", d.Features)
}
if d.CPU.Mode != "host-passthrough" {
t.Errorf("cpu mode = %q", d.CPU.Mode)
}
if d.OnPoweroff != "destroy" || d.OnReboot != "restart" || d.OnCrash != "destroy" {
t.Errorf("lifecycle = %s/%s/%s", d.OnPoweroff, d.OnReboot, d.OnCrash)
}
m := d.Metadata.Instance
if !m.managed() || m.UID != "11111111-2222-3333-4444-555555555555" || m.CreatedAt != "2026-09-04T12:00:00Z" {
t.Errorf("metadata = %+v", m)
}
if !strings.Contains(raw, `xmlns="`+metadataURI+`"`) {
t.Errorf("metadata element is not namespaced; libvirt rejects that:\n%s", raw)
}
if len(d.Devices.Disks) != 2 {
t.Fatalf("disks = %d, want 2", len(d.Devices.Disks))
}
disk, seed := d.Devices.Disks[0], d.Devices.Disks[1]
if disk.Type != "file" || disk.Device != "disk" || disk.Driver.Type != "qcow2" ||
disk.Source.File != "/var/lib/libvirt/images/proxy-abc123def456ghij.qcow2" ||
disk.Target.Dev != "vda" || disk.Target.Bus != "virtio" || disk.ReadOnly != nil {
t.Errorf("overlay disk = %+v", disk)
}
if seed.Type != "file" || seed.Device != "cdrom" || seed.Driver.Type != "raw" ||
seed.Source.File != "/var/lib/libvirt/images/proxy-abc123def456ghij-seed.iso" ||
seed.Target.Bus != "sata" || seed.ReadOnly == nil {
t.Errorf("seed cdrom = %+v", seed)
}
if strings.Contains(raw, `type="volume"`) || strings.Contains(raw, " pool=") {
t.Errorf("disks reference pool volumes; virt-aa-helper cannot grant QEMU access to those:\n%s", raw)
}
if len(d.Devices.Interfaces) != 1 {
t.Fatalf("interfaces = %d, want 1", len(d.Devices.Interfaces))
}
if nic := d.Devices.Interfaces[0]; nic.Type != "bridge" || nic.Source.Bridge != "br0" || nic.Model.Type != "virtio" {
t.Errorf("interface = %+v", nic)
}
if len(d.Devices.Channels) != 1 || d.Devices.Channels[0].Target.Name != guestAgentChannel || d.Devices.Channels[0].Target.Type != "virtio" {
t.Errorf("channels = %+v, want one virtio %s channel", d.Devices.Channels, guestAgentChannel)
}
if len(d.Devices.Serials) != 1 || len(d.Devices.Consoles) != 1 || d.Devices.Consoles[0].Target.Type != "serial" {
t.Errorf("serial/console = %+v/%+v", d.Devices.Serials, d.Devices.Consoles)
}
if d.Devices.RNG.Model != "virtio" || d.Devices.RNG.Backend.Value != "/dev/urandom" {
t.Errorf("rng = %+v", d.Devices.RNG)
}
if strings.Contains(raw, "<graphics") {
t.Errorf("domain has a graphics device; headless VMs must not:\n%s", raw)
}
}
func TestBuildDomainXML_escapesUserValues(t *testing.T) {
t.Parallel()
p := testDomainParams()
p.Bridge = `br0" onload="x`
raw, err := buildDomainXML(p)
if err != nil {
t.Fatalf("buildDomainXML() error = %v", err)
}
var d domainXML
if err := xml.Unmarshal([]byte(raw), &d); err != nil {
t.Fatalf("output is not well-formed XML: %v", err)
}
if d.Devices.Interfaces[0].Source.Bridge != p.Bridge {
t.Errorf("bridge = %q, want the raw value back", d.Devices.Interfaces[0].Source.Bridge)
}
}
func TestBuildDomainXML_biosFirmware(t *testing.T) {
t.Parallel()
p := testDomainParams()
p.Firmware = firmwareBIOS
raw, err := buildDomainXML(p)
if err != nil {
t.Fatalf("buildDomainXML() error = %v", err)
}
var d domainXML
if err := xml.Unmarshal([]byte(raw), &d); err != nil {
t.Fatalf("output is not well-formed XML: %v", err)
}
if d.OS.Firmware != "" || d.OS.FirmwareFeatures != nil || strings.Contains(raw, "firmware") {
t.Errorf("bios domain still carries firmware settings:\n%s", raw)
}
}