239 lines
8.1 KiB
Go
239 lines
8.1 KiB
Go
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"
|
|
)
|
|
|
|
// client connects to a fake cluster like the web UI does: API as root@pam,
|
|
// pinned certificate, SSH with the pinned host key.
|
|
func client(t *testing.T, s *pvetest.Server, name string) *pve.Client {
|
|
t.Helper()
|
|
sum := sha256.Sum256(s.Certificate().Raw)
|
|
host, port := s.SSHAddr()
|
|
c, err := config.Cluster{Name: name, URL: s.URL, User: "root@pam", Password: pvetest.Password,
|
|
Fingerprint: hex.EncodeToString(sum[:]), SSHHost: host, SSHPort: port, SSHHostKey: s.SSHHostKey()}.PVE()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return c
|
|
}
|
|
|
|
// sdnKinds: the kinds of the SDN and datacenter tests.
|
|
var sdnKinds = []model.Kind{model.KindDatacenter, model.KindZone, model.KindVNet, model.KindSubnet}
|
|
|
|
// 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: sdnKinds,
|
|
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)
|
|
}
|
|
}
|