310 lines
10 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|