Files
pve-confsync/internal/adapter/sdn.go
T

248 lines
7.8 KiB
Go

package adapter
import (
"context"
"fmt"
"net/url"
"sort"
"strings"
"pve-confsync/internal/model"
"pve-confsync/internal/pve"
)
// ---- zones: /cluster/sdn/zones ----
// Zone types handled in v1. EVPN, simple, QinQ, faucet are left aside (warning).
var supportedZoneTypes = map[string]bool{"vlan": true, "vxlan": true}
// Zone fields that describe the local cluster (bridges, nodes, underlay, DNS/IPAM
// plugins defined per cluster): taken from the client mapping, never copied.
var zoneLocalFields = map[string]bool{
"nodes": true, "bridge": true, "peers": true, "fabric": true,
"dns": true, "reversedns": true, "dnszone": true,
}
type zone struct{}
func (zone) Kind() model.Kind { return model.KindZone }
func (zone) SDN() bool { return true }
func (zone) Singleton() bool { return false }
func (zone) List(ctx context.Context, c *pve.Client, mode ReadMode, _ []model.Object) ([]model.Object, []string, error) {
var raw []map[string]any
if err := c.Get(ctx, "cluster/sdn/zones", modeQuery(mode), &raw); err != nil {
return nil, nil, err
}
var objs []model.Object
var warns []string
for _, r := range raw {
id := pve.Str(r["zone"])
f := normalizeAPI(r, "zone")
if t, _ := f["type"].(string); !supportedZoneTypes[t] {
warns = append(warns, fmt.Sprintf("zone %s: type %q not handled in v1, skipped", id, t))
continue
}
zone{}.Canonicalize(f)
objs = append(objs, model.Object{Kind: model.KindZone, ID: id, Fields: f})
}
return objs, warns, nil
}
func (zone) DefaultPolicy(f string) Policy {
if zoneLocalFields[f] {
return Override
}
return Sync
}
func (zone) Canonicalize(f map[string]any) { sortList(f, "nodes", "peers") }
func (zone) Forbidden(string) bool { return false }
func (zone) Immutable(f string) bool { return f == "type" }
func (zone) Required(o model.Object) []string {
if o.Fields["type"] == "vlan" {
return []string{"type", "bridge"}
}
return []string{"type"}
}
func (zone) Create(ctx context.Context, c *pve.Client, o model.Object, lock string) error {
f := form(o.Fields, nil, lock)
f.Set("zone", o.ID)
return c.Post(ctx, "cluster/sdn/zones", f, nil)
}
func (zone) Update(ctx context.Context, c *pve.Client, t model.Object, set map[string]any, unset []string, lock string) error {
return c.Put(ctx, "cluster/sdn/zones/"+url.PathEscape(t.ID), form(without(set, "type"), unset, lock), nil)
}
func (zone) Delete(ctx context.Context, c *pve.Client, o model.Object, lock string) error {
return c.Delete(ctx, "cluster/sdn/zones/"+url.PathEscape(o.ID), form(nil, nil, lock), nil)
}
// ---- vnets: /cluster/sdn/vnets ----
type vnet struct{}
func (vnet) Kind() model.Kind { return model.KindVNet }
func (vnet) SDN() bool { return true }
func (vnet) Singleton() bool { return false }
func (vnet) List(ctx context.Context, c *pve.Client, mode ReadMode, known []model.Object) ([]model.Object, []string, error) {
var raw []map[string]any
if err := c.Get(ctx, "cluster/sdn/vnets", modeQuery(mode), &raw); err != nil {
return nil, nil, err
}
zones := ids(known, model.KindZone)
var objs []model.Object
var warns []string
for _, r := range raw {
id := pve.Str(r["vnet"])
f := normalizeAPI(r, "vnet", "type")
if z, _ := f["zone"].(string); !zones[z] {
warns = append(warns, fmt.Sprintf("vnet %s: zone %q not handled, skipped", id, z))
continue
}
objs = append(objs, model.Object{Kind: model.KindVNet, ID: id, Fields: f})
}
return objs, warns, nil
}
func (vnet) DefaultPolicy(string) Policy { return Sync }
func (vnet) Canonicalize(map[string]any) {}
func (vnet) Forbidden(string) bool { return false }
func (vnet) Immutable(string) bool { return false }
func (vnet) Required(model.Object) []string { return []string{"zone"} }
func (vnet) Create(ctx context.Context, c *pve.Client, o model.Object, lock string) error {
f := form(o.Fields, nil, lock)
f.Set("vnet", o.ID)
return c.Post(ctx, "cluster/sdn/vnets", f, nil)
}
func (vnet) Update(ctx context.Context, c *pve.Client, t model.Object, set map[string]any, unset []string, lock string) error {
return c.Put(ctx, "cluster/sdn/vnets/"+url.PathEscape(t.ID), form(set, unset, lock), nil)
}
func (vnet) Delete(ctx context.Context, c *pve.Client, o model.Object, lock string) error {
return c.Delete(ctx, "cluster/sdn/vnets/"+url.PathEscape(o.ID), form(nil, nil, lock), nil)
}
// ---- subnets: /cluster/sdn/vnets/{vnet}/subnets ----
// The subnet id is "<zone>-<address>-<prefix length>" (SubnetPlugin.pm parse_subnet_id),
// the CIDR is given as "subnet" on creation only.
type subnet struct{}
func (subnet) Kind() model.Kind { return model.KindSubnet }
func (subnet) SDN() bool { return true }
func (subnet) Singleton() bool { return false }
func (subnet) List(ctx context.Context, c *pve.Client, mode ReadMode, known []model.Object) ([]model.Object, []string, error) {
var objs []model.Object
for v := range ids(known, model.KindVNet) {
var raw []map[string]any
if err := c.Get(ctx, "cluster/sdn/vnets/"+url.PathEscape(v)+"/subnets", modeQuery(mode), &raw); err != nil {
return nil, nil, err
}
for _, r := range raw {
id := pve.Str(r["subnet"])
// computed by PVE from the id
f := normalizeAPI(r, "subnet", "type", "cidr", "network", "mask", "zone", "id")
if f["vnet"] == nil {
f["vnet"] = v
}
subnet{}.Canonicalize(f)
objs = append(objs, model.Object{Kind: model.KindSubnet, ID: id, Fields: f})
}
}
return objs, nil, nil
}
func (subnet) DefaultPolicy(string) Policy { return Sync }
func (subnet) Forbidden(string) bool { return false }
func (subnet) Immutable(f string) bool { return f == "vnet" }
func (subnet) Required(model.Object) []string { return []string{"vnet"} }
// Canonicalize: dhcp-range is an array in the schema, even with a single range.
func (subnet) Canonicalize(f map[string]any) {
if s, ok := f["dhcp-range"].(string); ok {
f["dhcp-range"] = []string{s}
}
}
// SubnetCIDR derives the CIDR from a subnet id ("zone-10.0.0.0-24" -> "10.0.0.0/24").
func SubnetCIDR(id string) (string, error) {
i := strings.LastIndex(id, "-")
j := strings.Index(id, "-")
if i <= 0 || j == i {
return "", fmt.Errorf("invalid subnet id %q", id)
}
return id[j+1:i] + "/" + id[i+1:], nil
}
func (subnet) Create(ctx context.Context, c *pve.Client, o model.Object, lock string) error {
cidr, err := SubnetCIDR(o.ID)
if err != nil {
return err
}
v, _ := o.Fields["vnet"].(string)
f := form(without(o.Fields, "vnet"), nil, lock)
f.Set("subnet", cidr)
f.Set("type", "subnet")
return c.Post(ctx, "cluster/sdn/vnets/"+url.PathEscape(v)+"/subnets", f, nil)
}
func (subnet) Update(ctx context.Context, c *pve.Client, t model.Object, set map[string]any, unset []string, lock string) error {
v, _ := t.Fields["vnet"].(string)
return c.Put(ctx, "cluster/sdn/vnets/"+url.PathEscape(v)+"/subnets/"+url.PathEscape(t.ID),
form(without(set, "vnet"), unset, lock), nil)
}
func (subnet) Delete(ctx context.Context, c *pve.Client, o model.Object, lock string) error {
v, _ := o.Fields["vnet"].(string)
return c.Delete(ctx, "cluster/sdn/vnets/"+url.PathEscape(v)+"/subnets/"+url.PathEscape(o.ID),
form(nil, nil, lock), nil)
}
// ---- helpers ----
func ids(objs []model.Object, k model.Kind) map[string]bool {
m := map[string]bool{}
for _, o := range objs {
if o.Kind == k {
m[o.ID] = true
}
}
return m
}
func without(m map[string]any, keys ...string) map[string]any {
out := make(map[string]any, len(m))
for k, v := range m {
out[k] = v
}
for _, k := range keys {
delete(out, k)
}
return out
}
// sortList canonicalizes comma separated lists whose order is irrelevant.
func sortList(f map[string]any, keys ...string) {
for _, k := range keys {
s, ok := f[k].(string)
if !ok || s == "" {
continue
}
l := strings.Split(s, ",")
for i := range l {
l[i] = strings.TrimSpace(l[i])
}
sort.Strings(l)
f[k] = strings.Join(l, ",")
}
}