Files
egress-proxies-operator/internal/provider/kvm2/xml_test.go
T

310 lines
10 KiB
Go

package kvm2
import (
"encoding/xml"
"errors"
"strings"
"testing"
"time"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
// checkedDomain is an independent view of the marshalled XML, so the tests
// assert on what libvirt would see rather than on the builder's own structs.
type checkedDomain struct {
Type string `xml:"type,attr"`
Name string `xml:"name"`
Memory struct {
Unit string `xml:"unit,attr"`
Value int64 `xml:",chardata"`
} `xml:"memory"`
VCPU int32 `xml:"vcpu"`
Metadata struct {
Inner string `xml:",innerxml"`
} `xml:"metadata"`
OS struct {
Firmware string `xml:"firmware,attr"`
Type struct {
Arch string `xml:"arch,attr"`
Machine string `xml:"machine,attr"`
Value string `xml:",chardata"`
} `xml:"type"`
FirmwareElems []struct {
Features []struct {
Enabled string `xml:"enabled,attr"`
Name string `xml:"name,attr"`
} `xml:"feature"`
} `xml:"firmware"`
} `xml:"os"`
Features struct {
ACPI []struct{} `xml:"acpi"`
APIC []struct{} `xml:"apic"`
} `xml:"features"`
CPU struct {
Mode string `xml:"mode,attr"`
} `xml:"cpu"`
OnPoweroff string `xml:"on_poweroff"`
OnReboot string `xml:"on_reboot"`
OnCrash string `xml:"on_crash"`
Devices struct {
Disks []struct {
Type string `xml:"type,attr"`
Device string `xml:"device,attr"`
Driver struct {
Name string `xml:"name,attr"`
Type string `xml:"type,attr"`
} `xml:"driver"`
Source struct {
File string `xml:"file,attr"`
Pool string `xml:"pool,attr"`
Vol string `xml:"volume,attr"`
} `xml:"source"`
Target struct {
Dev string `xml:"dev,attr"`
Bus string `xml:"bus,attr"`
} `xml:"target"`
ReadOnly []struct{} `xml:"readonly"`
} `xml:"disk"`
Interfaces []struct {
Type string `xml:"type,attr"`
Source struct {
Bridge string `xml:"bridge,attr"`
} `xml:"source"`
Model struct {
Type string `xml:"type,attr"`
} `xml:"model"`
} `xml:"interface"`
Serials []struct {
Type string `xml:"type,attr"`
} `xml:"serial"`
Consoles []struct {
Type string `xml:"type,attr"`
Target struct {
Type string `xml:"type,attr"`
} `xml:"target"`
} `xml:"console"`
Channels []struct {
Type string `xml:"type,attr"`
Target struct {
Type string `xml:"type,attr"`
Name string `xml:"name,attr"`
} `xml:"target"`
} `xml:"channel"`
RNGs []struct {
Model string `xml:"model,attr"`
Backend struct {
Model string `xml:"model,attr"`
Value string `xml:",chardata"`
} `xml:"backend"`
} `xml:"rng"`
Graphics []struct{} `xml:"graphics"`
} `xml:"devices"`
}
var testCreatedAt = time.Date(2026, 9, 16, 12, 0, 0, 0, time.UTC)
func testDomainParams() domainParams {
return domainParams{
Name: "proxy-abc",
UID: "0b1f9a2e-uid",
CreatedAt: testCreatedAt,
VCPUs: 2,
MemoryMiB: 2048,
Firmware: provider.KVMFirmwareUEFI,
Bridge: "br0",
DiskPath: "/var/lib/libvirt/images/proxy-abc.qcow2",
SeedPath: "/var/lib/libvirt/images/proxy-abc-seed.iso",
DiskFormat: provider.KVMFormatQcow2,
}
}
func mustBuildDomain(t *testing.T, p domainParams) (string, checkedDomain) {
t.Helper()
out, err := buildDomainXML(p)
if err != nil {
t.Fatalf("buildDomainXML: %v", err)
}
var d checkedDomain
if err := xml.Unmarshal([]byte(out), &d); err != nil {
t.Fatalf("unmarshalling produced XML: %v\n%s", err, out)
}
return out, d
}
func TestDomainXML_fileDisksWithResolvedPaths(t *testing.T) {
t.Parallel()
p := testDomainParams()
_, d := mustBuildDomain(t, p)
if d.Type != "kvm" || d.Name != p.Name {
t.Errorf("domain type=%q name=%q, want kvm/%s", d.Type, d.Name, p.Name)
}
if d.Memory.Unit != "MiB" || d.Memory.Value != p.MemoryMiB || d.VCPU != p.VCPUs {
t.Errorf("memory=%d %s vcpu=%d, want %d MiB / %d", d.Memory.Value, d.Memory.Unit, d.VCPU, p.MemoryMiB, p.VCPUs)
}
if len(d.Devices.Disks) != 2 {
t.Fatalf("got %d disks, want 2", len(d.Devices.Disks))
}
overlay, seed := d.Devices.Disks[0], d.Devices.Disks[1]
if overlay.Type != "file" || overlay.Device != "disk" {
t.Errorf("overlay type=%q device=%q, want file/disk", overlay.Type, overlay.Device)
}
if overlay.Source.File != p.DiskPath || overlay.Source.Pool != "" || overlay.Source.Vol != "" {
t.Errorf("overlay source = %+v, want file=%q only", overlay.Source, p.DiskPath)
}
if overlay.Driver.Name != "qemu" || overlay.Driver.Type != "qcow2" {
t.Errorf("overlay driver = %+v, want qemu/qcow2", overlay.Driver)
}
if overlay.Target.Dev != "vda" || overlay.Target.Bus != "virtio" {
t.Errorf("overlay target = %+v, want vda/virtio", overlay.Target)
}
if len(overlay.ReadOnly) != 0 {
t.Error("overlay is readonly")
}
if seed.Type != "file" || seed.Device != "cdrom" {
t.Errorf("seed type=%q device=%q, want file/cdrom", seed.Type, seed.Device)
}
if seed.Source.File != p.SeedPath || seed.Source.Pool != "" || seed.Source.Vol != "" {
t.Errorf("seed source = %+v, want file=%q only", seed.Source, p.SeedPath)
}
if seed.Driver.Name != "qemu" || seed.Driver.Type != "raw" {
t.Errorf("seed driver = %+v, want qemu/raw", seed.Driver)
}
if seed.Target.Dev != "sda" || seed.Target.Bus != "sata" {
t.Errorf("seed target = %+v, want sda/sata", seed.Target)
}
if len(seed.ReadOnly) != 1 {
t.Error("seed is not readonly")
}
if d.OnPoweroff != "destroy" || d.OnReboot != "restart" || d.OnCrash != "destroy" {
t.Errorf("lifecycle actions = %q/%q/%q, want destroy/restart/destroy", d.OnPoweroff, d.OnReboot, d.OnCrash)
}
}
func TestDomainXML_uefiDefaultSecureBootOff(t *testing.T) {
t.Parallel()
_, d := mustBuildDomain(t, testDomainParams())
if d.OS.Firmware != "efi" {
t.Errorf(`<os firmware> = %q, want "efi"`, d.OS.Firmware)
}
if len(d.OS.FirmwareElems) != 1 {
t.Fatalf("got %d <firmware> elements, want 1", len(d.OS.FirmwareElems))
}
feats := d.OS.FirmwareElems[0].Features
if len(feats) != 1 || feats[0].Name != "secure-boot" || feats[0].Enabled != "no" {
t.Errorf("firmware features = %+v, want one secure-boot enabled=no", feats)
}
if d.OS.Type.Value != "hvm" {
t.Errorf("os type = %q, want hvm", d.OS.Type.Value)
}
}
func TestDomainXML_biosEscape(t *testing.T) {
t.Parallel()
p := testDomainParams()
p.Firmware = provider.KVMFirmwareBIOS
out, d := mustBuildDomain(t, p)
if d.OS.Firmware != "" {
t.Errorf(`<os firmware> = %q, want absent`, d.OS.Firmware)
}
if len(d.OS.FirmwareElems) != 0 {
t.Errorf("got %d <firmware> elements, want none", len(d.OS.FirmwareElems))
}
if strings.Contains(out, "firmware") {
t.Errorf("BIOS domain XML still mentions firmware:\n%s", out)
}
if d.OS.Type.Value != "hvm" || d.OS.Type.Machine != "q35" {
t.Errorf("os type = %+v, want hvm on q35", d.OS.Type)
}
}
func TestDomainXML_metadataElement(t *testing.T) {
t.Parallel()
p := testDomainParams()
out, d := mustBuildDomain(t, p)
if !strings.Contains(out, `<instance xmlns="`+metadataURI+`">`) {
t.Errorf("domain XML lacks the namespaced instance element:\n%s", out)
}
m, err := parseInstanceMetadata(d.Metadata.Inner)
if err != nil {
t.Fatalf("parseInstanceMetadata(%q): %v", d.Metadata.Inner, err)
}
if !m.managed() {
t.Error("managed() = false, want true")
}
if m.UID != p.UID {
t.Errorf("UID = %q, want %q", m.UID, p.UID)
}
if m.CreatedAt != "2026-09-16T12:00:00Z" {
t.Errorf("CreatedAt = %q, want RFC3339 UTC", m.CreatedAt)
}
if !m.createdAt().Equal(p.CreatedAt) {
t.Errorf("createdAt() = %v, want %v", m.createdAt(), p.CreatedAt)
}
}
func TestDomainXML_guestAgentChannel(t *testing.T) {
t.Parallel()
p := testDomainParams()
_, d := mustBuildDomain(t, p)
if len(d.Devices.Channels) != 1 || d.Devices.Channels[0].Type != "unix" ||
d.Devices.Channels[0].Target.Type != "virtio" || d.Devices.Channels[0].Target.Name != "org.qemu.guest_agent.0" {
t.Errorf("channels = %+v, want one unix/virtio org.qemu.guest_agent.0", d.Devices.Channels)
}
if len(d.Devices.Interfaces) != 1 || d.Devices.Interfaces[0].Type != "bridge" ||
d.Devices.Interfaces[0].Source.Bridge != p.Bridge || d.Devices.Interfaces[0].Model.Type != "virtio" {
t.Errorf("interfaces = %+v, want one bridge %s with virtio model", d.Devices.Interfaces, p.Bridge)
}
if len(d.Devices.Serials) != 1 || d.Devices.Serials[0].Type != "pty" {
t.Errorf("serials = %+v, want one pty", d.Devices.Serials)
}
if len(d.Devices.Consoles) != 1 || d.Devices.Consoles[0].Type != "pty" || d.Devices.Consoles[0].Target.Type != "serial" {
t.Errorf("consoles = %+v, want one pty/serial", d.Devices.Consoles)
}
if len(d.Devices.RNGs) != 1 || d.Devices.RNGs[0].Model != "virtio" ||
d.Devices.RNGs[0].Backend.Model != "random" || d.Devices.RNGs[0].Backend.Value != "/dev/urandom" {
t.Errorf("rngs = %+v, want one virtio random /dev/urandom", d.Devices.RNGs)
}
if len(d.Devices.Graphics) != 0 {
t.Error("domain has <graphics>, want none")
}
if d.OS.Type.Arch != "x86_64" || d.OS.Type.Machine != "q35" {
t.Errorf("os type = %+v, want x86_64/q35", d.OS.Type)
}
if d.CPU.Mode != "host-passthrough" {
t.Errorf("cpu mode = %q, want host-passthrough", d.CPU.Mode)
}
if len(d.Features.ACPI) != 1 || len(d.Features.APIC) != 1 {
t.Errorf("features acpi=%d apic=%d, want one each", len(d.Features.ACPI), len(d.Features.APIC))
}
}
func TestDomainXML_rejectsBadInput(t *testing.T) {
t.Parallel()
tests := []struct {
name string
mutate func(*domainParams)
}{
{name: "unknown firmware", mutate: func(p *domainParams) { p.Firmware = "coreboot" }},
{name: "empty firmware", mutate: func(p *domainParams) { p.Firmware = "" }},
{name: "empty name", mutate: func(p *domainParams) { p.Name = "" }},
{name: "empty disk path", mutate: func(p *domainParams) { p.DiskPath = "" }},
{name: "empty seed path", mutate: func(p *domainParams) { p.SeedPath = "" }},
{name: "unknown disk format", mutate: func(p *domainParams) { p.DiskFormat = "vmdk" }},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := testDomainParams()
tc.mutate(&p)
out, err := buildDomainXML(p)
if !errors.Is(err, provider.ErrPermanent) {
t.Errorf("err = %v, want ErrPermanent", err)
}
if out != "" {
t.Errorf("got XML %q alongside the error", out)
}
})
}
}