Tests moteur et exemple de configuration
This commit is contained in:
1 parent
30f770ed8f
commit
e5f3583086
2 files changed
+295
No files matched your search
@@ -0,0 +1,63 @@
|
|||||||
|
# pve-confsync - exemple de configuration (phase 0, CLI)
|
||||||
|
# Les secrets des tokens ne sont JAMAIS écrits ici : variable d'environnement
|
||||||
|
# (token_secret_env) ou fichier, par exemple un secret docker (token_secret_file).
|
||||||
|
|
||||||
|
reference:
|
||||||
|
name: ref
|
||||||
|
url: https://pve-ref.lab:8006
|
||||||
|
token_id: confsync@pve!ro # token en lecture seule (Sys.Audit, SDN.Audit)
|
||||||
|
token_secret_file: /run/secrets/pve_ref # secret docker (voir docker-compose.yml)
|
||||||
|
fingerprint: "AA:BB:CC:...:FF" # SHA-256 du certificat (pinning, certificat auto-signé)
|
||||||
|
|
||||||
|
clients:
|
||||||
|
- name: site-a
|
||||||
|
url: https://pve-a.lab:8006
|
||||||
|
token_id: confsync@pve!rw
|
||||||
|
token_secret_file: /run/secrets/pve_site_a
|
||||||
|
ca_file: /config/ca-site-a.pem # ou fingerprint, ou insecure: true (labo uniquement)
|
||||||
|
# Valeurs propres au cluster, prioritaires sur la référence :
|
||||||
|
# type d'objet -> id -> champ -> valeur
|
||||||
|
mapping:
|
||||||
|
sdn-zone:
|
||||||
|
vlanz:
|
||||||
|
bridge: vmbr1 # obligatoire pour créer une zone VLAN
|
||||||
|
nodes: pve-a1,pve-a2
|
||||||
|
vxz:
|
||||||
|
peers: 192.168.10.1,192.168.10.2
|
||||||
|
datacenter-options:
|
||||||
|
datacenter:
|
||||||
|
next-id: lower=2000,upper=2999
|
||||||
|
# Politiques propres à ce client (prioritaires sur les politiques globales)
|
||||||
|
policies:
|
||||||
|
sdn-zone:
|
||||||
|
mtu: ignore
|
||||||
|
|
||||||
|
- name: site-b
|
||||||
|
url: https://pve-b.lab:8006
|
||||||
|
token_id: confsync@pve!rw
|
||||||
|
token_secret_env: PVE_SITE_B_SECRET
|
||||||
|
insecure: true
|
||||||
|
mapping:
|
||||||
|
sdn-zone:
|
||||||
|
vlanz:
|
||||||
|
bridge: vmbr0
|
||||||
|
|
||||||
|
# Types synchronisés (défaut : tous)
|
||||||
|
kinds: [datacenter-options, sdn-zone, sdn-vnet, sdn-subnet]
|
||||||
|
|
||||||
|
# Sélection des objets par motif (glob) sur l'id
|
||||||
|
objects:
|
||||||
|
sdn-zone:
|
||||||
|
exclude: ["lab*"]
|
||||||
|
|
||||||
|
# Politique par champ : sync | ignore | override (valeur fournie par le mapping du client)
|
||||||
|
# Défauts : options datacenter = tag-style, registered-tags, user-tag-access, next-id en sync,
|
||||||
|
# le reste ignoré ; zones SDN = nodes, bridge, peers, fabric, dns, reversedns, dnszone en
|
||||||
|
# override, le reste en sync ; vnets et subnets = sync.
|
||||||
|
policies:
|
||||||
|
datacenter-options:
|
||||||
|
keyboard: sync
|
||||||
|
language: sync
|
||||||
|
|
||||||
|
# Suppression chez les clients des objets absents de la référence (défaut : non)
|
||||||
|
prune: false
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user