Files
egress-proxies-operator/internal/provider/kvm/domainxml.go
T
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

252 lines
6.8 KiB
Go

package kvm
import (
"encoding/xml"
"fmt"
"time"
)
const guestAgentChannel = "org.qemu.guest_agent.0"
// domainParams is everything buildDomainXML needs, already resolved from the
// config and placement so the builder stays a pure function. DiskPath and
// SeedPath are the volumes' host paths as reported by libvirt; Firmware is
// firmwareUEFI or firmwareBIOS.
type domainParams struct {
Name string
UID string
CreatedAt time.Time
VCPUs int32
MemoryMiB int64
Firmware string
Bridge string
DiskPath string
SeedPath string
}
// buildDomainXML renders a q35 KVM guest: host-passthrough CPU, a virtio
// overlay disk and a SATA cdrom for the seed, one virtio NIC on the host
// bridge, a serial console, the guest-agent channel, a virtio RNG, no
// graphics, and the operator's metadata element.
//
// Disks are type="file" with host paths, not type="volume" with pool/volume
// names: libvirt's AppArmor helper (virt-aa-helper) only grants QEMU access
// to sources that carry a path, so a volume-referenced disk fails to start
// with "Permission denied" on any AppArmor-confined host (stock Debian and
// Ubuntu). The paths cost one StorageVolGetPath round-trip each per Create.
//
// UEFI is expressed as <os firmware="efi"> with secure boot off, and libvirt
// picks the OVMF binary and creates the NVRAM file itself (removed again by
// the nvram undefine flag). Debian 13's BIOS boot path was found to hang
// after GRUB on a real host, while the same image boots under UEFI.
func buildDomainXML(p domainParams) (string, error) {
os := osXML{
Type: osTypeXML{Arch: "x86_64", Machine: "q35", Value: "hvm"},
Boot: bootXML{Dev: "hd"},
}
if p.Firmware == firmwareUEFI {
os.Firmware = "efi"
os.FirmwareFeatures = &firmwareXML{Features: []firmwareFeatureXML{{Enabled: "no", Name: "secure-boot"}}}
}
d := domainXML{
Type: "kvm",
Name: p.Name,
Metadata: domainMetadataXML{Instance: newInstanceMetadata(p.UID, p.CreatedAt)},
Memory: sizeXML{Unit: "MiB", Value: p.MemoryMiB},
VCPU: p.VCPUs,
OS: os,
Features: featuresXML{ACPI: &struct{}{}, APIC: &struct{}{}},
CPU: cpuXML{Mode: "host-passthrough"},
Clock: clockXML{Offset: "utc"},
OnPoweroff: "destroy",
OnReboot: "restart",
OnCrash: "destroy",
Devices: devicesXML{
Disks: []diskXML{
{
Type: "file",
Device: "disk",
Driver: driverXML{Name: "qemu", Type: formatQcow2},
Source: diskSourceXML{File: p.DiskPath},
Target: targetXML{Dev: "vda", Bus: "virtio"},
},
{
Type: "file",
Device: "cdrom",
Driver: driverXML{Name: "qemu", Type: formatRaw},
Source: diskSourceXML{File: p.SeedPath},
Target: targetXML{Dev: "sda", Bus: "sata"},
ReadOnly: &struct{}{},
},
},
Interfaces: []interfaceXML{{
Type: "bridge",
Source: ifaceSourceXML{Bridge: p.Bridge},
Model: modelXML{Type: "virtio"},
}},
Serials: []serialXML{{Type: "pty", Target: portXML{Port: 0}}},
Consoles: []consoleXML{{Type: "pty", Target: consoleTargetXML{Type: "serial", Port: 0}}},
Channels: []channelXML{{
Type: "unix",
Target: channelTargetXML{Type: "virtio", Name: guestAgentChannel},
}},
RNG: rngXML{Model: "virtio", Backend: rngBackendXML{Model: "random", Value: "/dev/urandom"}},
},
}
out, err := xml.Marshal(d)
if err != nil {
return "", fmt.Errorf("marshalling domain xml: %w", err)
}
return string(out), nil
}
type domainXML struct {
XMLName xml.Name `xml:"domain"`
Type string `xml:"type,attr"`
Name string `xml:"name"`
Metadata domainMetadataXML `xml:"metadata"`
Memory sizeXML `xml:"memory"`
VCPU int32 `xml:"vcpu"`
OS osXML `xml:"os"`
Features featuresXML `xml:"features"`
CPU cpuXML `xml:"cpu"`
Clock clockXML `xml:"clock"`
OnPoweroff string `xml:"on_poweroff"`
OnReboot string `xml:"on_reboot"`
OnCrash string `xml:"on_crash"`
Devices devicesXML `xml:"devices"`
}
type domainMetadataXML struct {
Instance instanceMetadata
}
type sizeXML struct {
Unit string `xml:"unit,attr"`
Value int64 `xml:",chardata"`
}
type osXML struct {
Firmware string `xml:"firmware,attr,omitempty"`
Type osTypeXML `xml:"type"`
FirmwareFeatures *firmwareXML `xml:"firmware"`
Boot bootXML `xml:"boot"`
}
type firmwareXML struct {
Features []firmwareFeatureXML `xml:"feature"`
}
type firmwareFeatureXML struct {
Enabled string `xml:"enabled,attr"`
Name string `xml:"name,attr"`
}
type osTypeXML struct {
Arch string `xml:"arch,attr"`
Machine string `xml:"machine,attr"`
Value string `xml:",chardata"`
}
type bootXML struct {
Dev string `xml:"dev,attr"`
}
type featuresXML struct {
ACPI *struct{} `xml:"acpi"`
APIC *struct{} `xml:"apic"`
}
type cpuXML struct {
Mode string `xml:"mode,attr"`
}
type clockXML struct {
Offset string `xml:"offset,attr"`
}
type devicesXML struct {
Disks []diskXML `xml:"disk"`
Interfaces []interfaceXML `xml:"interface"`
Serials []serialXML `xml:"serial"`
Consoles []consoleXML `xml:"console"`
Channels []channelXML `xml:"channel"`
RNG rngXML `xml:"rng"`
}
type diskXML struct {
Type string `xml:"type,attr"`
Device string `xml:"device,attr"`
Driver driverXML `xml:"driver"`
Source diskSourceXML `xml:"source"`
Target targetXML `xml:"target"`
ReadOnly *struct{} `xml:"readonly"`
}
type driverXML struct {
Name string `xml:"name,attr"`
Type string `xml:"type,attr"`
}
type diskSourceXML struct {
File string `xml:"file,attr"`
}
type targetXML struct {
Dev string `xml:"dev,attr"`
Bus string `xml:"bus,attr"`
}
type interfaceXML struct {
Type string `xml:"type,attr"`
Source ifaceSourceXML `xml:"source"`
Model modelXML `xml:"model"`
}
type ifaceSourceXML struct {
Bridge string `xml:"bridge,attr"`
}
type modelXML struct {
Type string `xml:"type,attr"`
}
type serialXML struct {
Type string `xml:"type,attr"`
Target portXML `xml:"target"`
}
type portXML struct {
Port int `xml:"port,attr"`
}
type consoleXML struct {
Type string `xml:"type,attr"`
Target consoleTargetXML `xml:"target"`
}
type consoleTargetXML struct {
Type string `xml:"type,attr"`
Port int `xml:"port,attr"`
}
type channelXML struct {
Type string `xml:"type,attr"`
Target channelTargetXML `xml:"target"`
}
type channelTargetXML struct {
Type string `xml:"type,attr"`
Name string `xml:"name,attr"`
}
type rngXML struct {
Model string `xml:"model,attr"`
Backend rngBackendXML `xml:"backend"`
}
type rngBackendXML struct {
Model string `xml:"model,attr"`
Value string `xml:",chardata"`
}