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>
229 lines
5.9 KiB
Go
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"`
|
|
}
|