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(` = %q, want "efi"`, d.OS.Firmware) } if len(d.OS.FirmwareElems) != 1 { t.Fatalf("got %d 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(` = %q, want absent`, d.OS.Firmware) } if len(d.OS.FirmwareElems) != 0 { t.Errorf("got %d 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, ``) { 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 , 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) } }) } }