package engine import ( "context" "crypto/sha256" "encoding/hex" "strings" "testing" "pve-confsync/internal/adapter" "pve-confsync/internal/config" "pve-confsync/internal/model" "pve-confsync/internal/pve" "pve-confsync/internal/pve/pvetest" ) func client(t *testing.T, s *pvetest.Server, name string) *pve.Client { t.Helper() sum := sha256.Sum256(s.Certificate().Raw) c, err := pve.New(pve.Options{Name: name, BaseURL: s.URL, TokenID: "confsync@pve!t", TokenSecret: "secret", Fingerprint: hex.EncodeToString(sum[:])}) if err != nil { t.Fatal(err) } return c } // reference: two zones (vlan, vxlan), one unsupported (evpn), vnets, subnets, tags. func reference(t *testing.T) *pvetest.Server { ref := pvetest.New("9.1.4") t.Cleanup(ref.Close) ref.Options = map[string]string{ "registered-tags": "prod;dev", "tag-style": "color-map=prod:ff0000,shape=full", "next-id": "lower=100,upper=9999", "keyboard": "fr", } ref.Pending.Zones["vlanz"] = map[string]any{"type": "vlan", "bridge": "vmbr0", "mtu": 1500, "ipam": "pve"} ref.Pending.Zones["vxz"] = map[string]any{"type": "vxlan", "peers": "10.0.0.2,10.0.0.1", "ipam": "pve"} ref.Pending.Zones["evpnz"] = map[string]any{"type": "evpn", "controller": "c1"} ref.Pending.VNets["v100"] = map[string]any{"zone": "vlanz", "tag": 100, "alias": "prod"} ref.Pending.VNets["vx1"] = map[string]any{"zone": "vxz", "tag": 10001} ref.Pending.VNets["ev1"] = map[string]any{"zone": "evpnz", "tag": 5000} ref.Pending.Subnets["vlanz-10.1.0.0-24"] = map[string]any{"vnet": "v100", "gateway": "10.1.0.1", "dhcp-range": []any{"start-address=10.1.0.100,end-address=10.1.0.200"}} ref.Commit() return ref } func testConfig() *config.Config { return &config.Config{ Kinds: model.Kinds, Clients: []config.Client{{ Cluster: config.Cluster{Name: "site-a"}, Mapping: map[model.Kind]map[string]map[string]any{ model.KindZone: { "vlanz": {"bridge": "vmbr1", "nodes": "a2,a1"}, "vxz": {"peers": "192.168.0.1,192.168.0.2"}, }, }, }}, } } func TestSyncEndToEnd(t *testing.T) { ctx := context.Background() ref := reference(t) cli := pvetest.New("9.0.10") defer cli.Close() cli.Options = map[string]string{"registered-tags": "old", "keyboard": "de"} cfg := testConfig() cl := &cfg.Clients[0] rc, cc := client(t, ref, "ref"), client(t, cli, "site-a") src, err := Read(ctx, rc, cfg.Kinds, adapter.ReadRunning) if err != nil { t.Fatal(err) } if len(src.Warnings) != 2 { // evpn zone + its vnet t.Fatalf("warnings = %v", src.Warnings) } cur, err := Read(ctx, cc, cfg.Kinds, adapter.ReadCurrent) if err != nil { t.Fatal(err) } plan := MakePlan(cfg, cl, src.Objects, cur.Objects) if len(plan.Errors) > 0 { t.Fatal(plan.Errors) } // 1 datacenter update + 2 zones + 2 vnets + 1 subnet if len(plan.Changes) != 6 { t.Fatalf("changes = %+v", plan.Changes) } dc := plan.Changes[0] if dc.Kind != model.KindDatacenter || dc.Set["registered-tags"] != "dev;prod" || dc.Set["keyboard"] != nil { t.Fatalf("datacenter change = %+v", dc) } res := Apply(ctx, cc, plan) if res.Err != nil { t.Fatalf("apply: %v (%+v)", res.Err, res.Steps) } if cli.Lock != "" { t.Fatal("SDN lock not released") } z := cli.Running.Zones["vlanz"] if z["bridge"] != "vmbr1" || z["nodes"] != "a1,a2" || z["mtu"] != "1500" { t.Fatalf("vlan zone = %v (mapping not applied)", z) } if cli.Running.Zones["evpnz"] != nil { t.Fatal("evpn zone must not be synchronized") } if cli.Options["keyboard"] != "de" || cli.Options["tag-style"] != "color-map=prod:ff0000,shape=full" { t.Fatalf("options = %v", cli.Options) } if cli.Running.Subnets["vlanz-10.1.0.0-24"]["gateway"] != "10.1.0.1" { t.Fatalf("subnets = %v", cli.Running.Subnets) } replan := func(actual []model.Object) Plan { return MakePlan(cfg, cl, src.Objects, actual) } if err := Verify(ctx, cc, replan, cfg.Kinds); err != nil { t.Fatalf("verify: %v", err) } } func TestMissingMappingIsAnError(t *testing.T) { ref := reference(t) cli := pvetest.New("9.1.0") defer cli.Close() cfg := testConfig() cfg.Clients[0].Mapping = nil ctx := context.Background() src, _ := Read(ctx, client(t, ref, "ref"), cfg.Kinds, adapter.ReadRunning) cur, _ := Read(ctx, client(t, cli, "c"), cfg.Kinds, adapter.ReadCurrent) plan := MakePlan(cfg, &cfg.Clients[0], src.Objects, cur.Objects) if len(plan.Errors) != 1 || !strings.Contains(plan.Errors[0], `"bridge"`) { t.Fatalf("errors = %v", plan.Errors) } if res := Apply(ctx, client(t, cli, "c"), plan); res.Err == nil || len(cli.Calls) != 0 { t.Fatalf("a plan in error must not be applied: %v %v", res.Err, cli.Calls) } } func TestRollbackOnFailure(t *testing.T) { ref := reference(t) cli := pvetest.New("9.1.0") defer cli.Close() cli.FailOn = "POST cluster/sdn/vnets" cfg := testConfig() ctx := context.Background() src, _ := Read(ctx, client(t, ref, "ref"), cfg.Kinds, adapter.ReadRunning) cc := client(t, cli, "c") cur, _ := Read(ctx, cc, cfg.Kinds, adapter.ReadCurrent) res := Apply(ctx, cc, MakePlan(cfg, &cfg.Clients[0], src.Objects, cur.Objects)) if res.Err == nil { t.Fatal("expected an error") } if len(cli.Pending.Zones) != 0 || cli.Lock != "" { t.Fatalf("pending not rolled back or lock kept: %v lock=%q", cli.Pending.Zones, cli.Lock) } last := cli.Calls[len(cli.Calls)-1] if last != "POST cluster/sdn/rollback" { t.Fatalf("last call = %s", last) } } func TestRefuseNonPVE9(t *testing.T) { s := pvetest.New("8.4.1") defer s.Close() if _, err := client(t, s, "old").CheckVersion(context.Background()); err == nil { t.Fatal("PVE 8 must be refused") } } func TestPruneAndFieldRemoval(t *testing.T) { ref := reference(t) cli := pvetest.New("9.1.0") defer cli.Close() cli.Pending.Zones["vlanz"] = map[string]any{"type": "vlan", "bridge": "vmbr1", "mtu": "1500", "ipam": "pve", "vlan-protocol": "802.1q"} cli.Pending.Zones["local"] = map[string]any{"type": "vlan", "bridge": "vmbr9"} cli.Pending.VNets["lv"] = map[string]any{"zone": "local", "tag": 9} cli.Commit() cfg := testConfig() cfg.Prune = true cfg.Objects = map[model.Kind]config.Selection{model.KindZone: {Exclude: []string{"keep-*"}}} ctx := context.Background() src, _ := Read(ctx, client(t, ref, "ref"), cfg.Kinds, adapter.ReadRunning) cur, _ := Read(ctx, client(t, cli, "c"), cfg.Kinds, adapter.ReadCurrent) plan := MakePlan(cfg, &cfg.Clients[0], src.Objects, cur.Objects) var ops []string for _, c := range plan.Changes { ops = append(ops, string(c.Op)+" "+c.ID) if c.ID == "vlanz" && (len(c.Unset) != 1 || c.Unset[0] != "vlan-protocol") { t.Fatalf("vlanz unset = %v", c.Unset) } } got := strings.Join(ops, "|") // deletions at the end, vnet before its zone if !strings.HasSuffix(got, "delete lv|delete local") { t.Fatalf("order = %s", got) } } func TestDatacenterCanonicalForm(t *testing.T) { s := pvetest.New("9.1.0") defer s.Close() s.Options = map[string]string{"user-tag-access": "user-allow=list,user-allow-list=b;a", "next-id": "upper=500,lower=200"} snap, err := Read(context.Background(), client(t, s, "c"), []model.Kind{model.KindDatacenter}, adapter.ReadCurrent) if err != nil { t.Fatal(err) } f := snap.Objects[0].Fields if f["user-tag-access"] != "user-allow=list,user-allow-list=a;b" || f["next-id"] != "lower=200,upper=500" { t.Fatalf("fields = %v", f) } if _, ok := f["mac_prefix"]; ok { t.Fatal("default mac_prefix injected by PVE must be dropped") } } func TestSubnetCanonicalForm(t *testing.T) { a, _ := adapter.ByKind(model.KindSubnet) f := model.Normalize(map[string]any{"dhcp-range": "start-address=10.0.0.10,end-address=10.0.0.20"}) a.Canonicalize(f) if l, ok := f["dhcp-range"].([]string); !ok || len(l) != 1 { t.Fatalf("dhcp-range = %#v", f["dhcp-range"]) } if cidr, err := adapter.SubnetCIDR("vxz-fd00:1::-64"); err != nil || cidr != "fd00:1::/64" { t.Fatalf("cidr = %q %v", cidr, err) } }