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>
138 lines
5.0 KiB
Go
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)
|
|
}
|
|
}
|