248 lines
7.8 KiB
Go
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, ",")
|
|
}
|
|
}
|