two/internal/lab/topology/validate.go
GnomeZworc f3515ca370
f-50: lab: arguments QEMU et seeds cloud-init des VM #50
Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
2026-10-05 20:15:46 +02:00

175 lines
4.5 KiB
Go

package topology
import (
"errors"
"fmt"
"net/netip"
"regexp"
"sort"
"strings"
)
const (
MinMTU = 1280
MaxMTU = 9000
MinMemory = 256
MaxPrefix = 30
MinPrefix = 8
ReservedInterface = "mgmt0"
)
var (
namePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{0,14}$`)
segmentPattern = regexp.MustCompile(`^[a-z][a-z0-9]{0,11}$`)
)
func (t *Topology) Validate() error {
var errs []error
add := func(format string, args ...any) {
errs = append(errs, fmt.Errorf(format, args...))
}
if !namePattern.MatchString(t.Name) {
add("name %q: must match %s", t.Name, namePattern)
}
images := map[string]bool{}
for _, i := range t.Images {
images[i.Name] = true
if !strings.HasPrefix(i.URL, "https://") {
add("image %s: url must be an https:// URL", i.Name)
}
if !strings.HasPrefix(i.Sums, "https://") {
add("image %s: sums must be an https:// URL", i.Name)
}
}
nodes := map[string]Node{}
for _, n := range t.Nodes {
nodes[n.Name] = n
}
if len(t.Nodes) == 0 {
add("nodes: at least one node is required")
}
if len(t.Segments) > maxSegments {
add("segments: %d declared, at most %d", len(t.Segments), maxSegments)
}
if len(t.Nodes) > maxNodes {
add("nodes: %d declared, at most %d", len(t.Nodes), maxNodes)
}
segments := map[string]Segment{}
for _, s := range t.Segments {
segments[s.Name] = s
if !segmentPattern.MatchString(s.Name) {
add("segment %s: name must match %s", s.Name, segmentPattern)
}
if s.Name == ReservedInterface {
add("segment %s: name is reserved for the administration interface", s.Name)
}
sw, ok := nodes[s.Switch]
switch {
case s.Switch == "":
add("segment %s: switch is required", s.Name)
case !ok:
add("segment %s: switch %s is not a declared node", s.Name, s.Switch)
case sw.Role != RoleSwitch:
add("segment %s: %s is a %s, not a switch", s.Name, s.Switch, sw.Role)
}
if s.MTU < MinMTU || s.MTU > MaxMTU {
add("segment %s: mtu %d out of range [%d, %d]", s.Name, s.MTU, MinMTU, MaxMTU)
}
prefix, err := netip.ParsePrefix(s.CIDR)
switch {
case err != nil:
add("segment %s: cidr %q: %v", s.Name, s.CIDR, err)
case !prefix.Addr().Is4():
add("segment %s: cidr %s is not IPv4", s.Name, s.CIDR)
case prefix.Masked() != prefix:
add("segment %s: cidr %s has host bits set, network is %s", s.Name, s.CIDR, prefix.Masked())
case prefix.Bits() < MinPrefix || prefix.Bits() > MaxPrefix:
add("segment %s: cidr %s prefix length out of range [/%d, /%d]", s.Name, s.CIDR, MinPrefix, MaxPrefix)
}
}
attached := map[string]int{}
hosting := map[string]int{}
for _, s := range t.Segments {
hosting[s.Switch]++
}
for _, n := range t.Nodes {
if !namePattern.MatchString(n.Name) {
add("node %s: name must match %s", n.Name, namePattern)
}
switch n.Role {
case RoleSwitch, RoleRR, RoleHypervisor:
default:
add("node %s: role %q must be one of %s, %s, %s", n.Name, n.Role, RoleSwitch, RoleRR, RoleHypervisor)
}
if !images[n.Image] {
add("node %s: image %q is not declared", n.Name, n.Image)
}
if n.CPUs < 1 {
add("node %s: cpus must be at least 1", n.Name)
}
if n.Memory < MinMemory {
add("node %s: memory must be at least %d MiB", n.Name, MinMemory)
}
if n.Role == RoleSwitch {
if len(n.Segments) > 0 || len(n.Addresses) > 0 {
add("node %s: a switch carries its segments through segments.<name>.switch, not through segments or addresses", n.Name)
}
if hosting[n.Name] == 0 {
add("node %s: switch carries no segment", n.Name)
}
continue
}
if len(n.Segments) == 0 {
add("node %s: must be attached to at least one segment", n.Name)
}
seen := map[string]bool{}
for _, name := range n.Segments {
if seen[name] {
add("node %s: segment %s listed twice", n.Name, name)
continue
}
seen[name] = true
if _, ok := segments[name]; !ok {
add("node %s: segment %s is not declared", n.Name, name)
continue
}
attached[name]++
}
for _, name := range sortedKeys(n.Addresses) {
address := n.Addresses[name]
if !seen[name] {
add("node %s: address given for segment %s it is not attached to", n.Name, name)
continue
}
if _, err := netip.ParseAddr(address); err != nil {
add("node %s: address %q on %s: %v", n.Name, address, name, err)
}
}
}
for _, s := range t.Segments {
if attached[s.Name] == 0 {
add("segment %s: no node is attached", s.Name)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func sortedKeys(m map[string]string) []string {
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}