Files
egress-proxies-operator/internal/provider/kvm/domainxml.go
T
kacerrandClaude Fable 5.1 cd51886139 Reference KVM domain disks by host path so AppArmor-confined QEMU can open them
The first run against a real hypervisor (beelink, Ubuntu, libvirt 11.6)
failed at DomainCreate with "Could not open base image: Permission denied"
even with correct DAC ownership. libvirt's virt-aa-helper only grants QEMU
access to disk sources that carry a path; the <disk type="volume"> form
chosen in Step 2 gives it pool and volume names instead, so the overlay and
its backing image get no AppArmor rule on any stock Debian/Ubuntu host.

buildDomainXML now takes DiskPath/SeedPath and emits type="file" disks.
ensureOverlay and writeSeed return the volume handles and Create resolves
both paths with StorageVolGetPath before defining the domain. The domain
XML test rejects any volume/pool reference so this cannot drift back; the
Create test asserts the sources equal the paths libvirt reported.

Execution log: Step 2 note marked reverted, Step 7 entry opened with the
two failed runs and the diagnosis.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 19:13:51 +00:00

229 lines
5.9 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.
type domainParams struct {
Name string
UID string
CreatedAt time.Time
VCPUs int32
MemoryMiB int64
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.
func buildDomainXML(p domainParams) (string, error) {
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: osXML{
Type: osTypeXML{Arch: "x86_64", Machine: "q35", Value: "hvm"},
Boot: bootXML{Dev: "hd"},
},
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 {
Type osTypeXML `xml:"type"`
Boot bootXML `xml:"boot"`
}
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"`
}