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 with secure boot off, and libvirt // picks the OVMF binary and creates the NVRAM file itself (removed again by // the nvram undefine flag). Under SeaBIOS, Debian 13's GRUB hangs on a // headless domain with a serial console (Debian bug #1111240: // "terminal_output gfxterm serial" with no video device); the same image // boots under UEFI with the serial console intact. 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"` }