Files
kacerrandClaude Fable 5.1 a114201ad3 Add KVM provider methods: Create/Get/Delete/ListByTag over the libvirt seam
Create looks the domain up first and only starts it when it exists;
otherwise it caches the image, reuses or creates the overlay, rebuilds
the seed ISO unconditionally (a cancelled upload can leave a truncated
seed that no later attempt would repair), defines and starts the domain.
Get maps libvirt state plus the first routable IPv4 from ARP or the
guest agent onto the provider states. Delete destroys, undefines and
removes the per-VM volumes by name, tolerating every already-gone case.
ListByTag keeps managed domains across all hosts, skipping domains it
cannot read and failing the sweep only for an unreachable host.

Also corrects the Step 2 seed determinism test: the iso9660 writer
stamps the build time into the volume descriptor, so builds differ by a
few bytes; the test now compares label, files and size.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 10:48:59 +00:00

101 lines
2.9 KiB
Go

package kvm
import (
"bytes"
"io"
"maps"
"strings"
"testing"
"github.com/kdomanski/iso9660"
)
func readSeed(t *testing.T, iso []byte) (label string, files map[string]string) {
t.Helper()
img, err := iso9660.OpenImage(bytes.NewReader(iso))
if err != nil {
t.Fatalf("OpenImage() error = %v", err)
}
label, err = img.Label()
if err != nil {
t.Fatalf("Label() error = %v", err)
}
root, err := img.RootDir()
if err != nil {
t.Fatalf("RootDir() error = %v", err)
}
children, err := root.GetChildren()
if err != nil {
t.Fatalf("GetChildren() error = %v", err)
}
files = make(map[string]string, len(children))
for _, c := range children {
body, err := io.ReadAll(c.Reader())
if err != nil {
t.Fatalf("reading %s: %v", c.Name(), err)
}
files[c.Name()] = string(body)
}
return label, files
}
func TestBuildSeedISO_contents(t *testing.T) {
t.Parallel()
userData := "#cloud-config\npackages: [squid]\n"
iso, err := buildSeedISO("proxy-abc123def456ghij", userData, defaultVendorData)
if err != nil {
t.Fatalf("buildSeedISO() error = %v", err)
}
label, files := readSeed(t, iso)
if !strings.EqualFold(label, seedLabel) {
t.Errorf("label = %q, want %q", label, seedLabel)
}
if len(files) != 3 {
t.Errorf("files = %v, want exactly user-data, meta-data, vendor-data", files)
}
if files["user-data"] != userData {
t.Errorf("user-data = %q, want verbatim cloud-init", files["user-data"])
}
if files["vendor-data"] != defaultVendorData {
t.Errorf("vendor-data = %q", files["vendor-data"])
}
if got, want := files["meta-data"], "instance-id: proxy-abc123def456ghij\nlocal-hostname: proxy-abc123def456ghij\n"; got != want {
t.Errorf("meta-data = %q, want %q", got, want)
}
}
// The writer stamps the build time into the volume descriptor, so two
// builds are not byte-identical; what must not change between them is
// what cloud-init reads: the label and the three files.
func TestBuildSeedISO_deterministicContent(t *testing.T) {
t.Parallel()
a, err := buildSeedISO("proxy-x", "#cloud-config\n", "")
if err != nil {
t.Fatalf("first buildSeedISO() error = %v", err)
}
b, err := buildSeedISO("proxy-x", "#cloud-config\n", "")
if err != nil {
t.Fatalf("second buildSeedISO() error = %v", err)
}
labelA, filesA := readSeed(t, a)
labelB, filesB := readSeed(t, b)
if labelA != labelB || !maps.Equal(filesA, filesB) {
t.Errorf("two builds for the same inputs differ: %q %v vs %q %v", labelA, filesA, labelB, filesB)
}
if len(a) != len(b) {
t.Errorf("sizes differ: %d vs %d; the seed volume is created with the ISO's exact size", len(a), len(b))
}
}
func TestBuildSeedISO_emptyUserData(t *testing.T) {
t.Parallel()
iso, err := buildSeedISO("proxy-x", "", defaultVendorData)
if err != nil {
t.Fatalf("buildSeedISO() error = %v", err)
}
_, files := readSeed(t, iso)
if body, ok := files["user-data"]; !ok || body != "" {
t.Errorf("user-data = %q (present=%v), want an empty file to still exist", body, ok)
}
}