two/internal/lab/topology/plan.go
2026-10-04 17:17:02 +02:00

212 lines
4.9 KiB
Go

package topology
import (
"errors"
"fmt"
"net"
"net/netip"
"strconv"
)
const (
SSHBasePort = 2200
CableBasePort = 20000
maxNodes = 1000
maxSegments = 256
)
type Plan struct {
Name string
Images []Image
Segments []SegmentPlan
Nodes []NodePlan
Cables []Cable
}
type SegmentPlan struct {
Name string
Switch string
Bridge string
Network netip.Prefix
Gateway netip.Addr
MTU int
}
type NodePlan struct {
Name string
Role string
Image string
CPUs int
Memory int
SSHPort int
Secondary map[string][]netip.Prefix
Loopback netip.Prefix
FRR string
}
type Cable struct {
Segment string
MTU int
Node string
NodeInterface string
NodeMAC net.HardwareAddr
NodeAddress netip.Prefix
NodePort int
Switch string
SwitchInterface string
SwitchMAC net.HardwareAddr
SwitchPort int
}
func Compute(t *Topology) (*Plan, error) {
if err := t.Validate(); err != nil {
return nil, err
}
p := &Plan{Name: t.Name, Images: append([]Image(nil), t.Images...)}
for i, n := range t.Nodes {
node := NodePlan{
Name: n.Name,
Role: n.Role,
Image: n.Image,
CPUs: n.CPUs,
Memory: n.Memory,
SSHPort: SSHBasePort + i,
FRR: n.FRR,
}
for segment, raws := range n.Secondary {
for _, raw := range raws {
if node.Secondary == nil {
node.Secondary = map[string][]netip.Prefix{}
}
node.Secondary[segment] = append(node.Secondary[segment], netip.MustParsePrefix(raw))
}
}
if n.Loopback != "" {
node.Loopback = netip.MustParsePrefix(n.Loopback)
}
p.Nodes = append(p.Nodes, node)
}
var errs []error
for segIndex, s := range t.Segments {
network := netip.MustParsePrefix(s.CIDR)
gateway := network.Addr().Next()
p.Segments = append(p.Segments, SegmentPlan{
Name: s.Name,
Switch: s.Switch,
Bridge: "br-" + s.Name,
Network: network,
Gateway: gateway,
MTU: s.MTU,
})
addresses, err := allocate(t, s, network, gateway)
if err != nil {
errs = append(errs, err)
continue
}
for nodeIndex, n := range t.Nodes {
address, ok := addresses[n.Name]
if !ok {
continue
}
cable := len(p.Cables)
p.Cables = append(p.Cables, Cable{
Segment: s.Name,
MTU: s.MTU,
Node: n.Name,
NodeInterface: s.Name,
NodeMAC: mac(nodeIndex, segIndex, 0),
NodeAddress: netip.PrefixFrom(address, network.Bits()),
NodePort: CableBasePort + 2*cable,
Switch: s.Switch,
SwitchInterface: "p" + strconv.Itoa(cable),
SwitchMAC: mac(nodeIndex, segIndex, 1),
SwitchPort: CableBasePort + 2*cable + 1,
})
}
}
if last := CableBasePort + 2*len(p.Cables) - 1; last > 65535 {
errs = append(errs, fmt.Errorf("cables: %d cables need udp ports up to %d, beyond 65535", len(p.Cables), last))
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return p, nil
}
func allocate(t *Topology, s Segment, network netip.Prefix, gateway netip.Addr) (map[string]netip.Addr, error) {
broadcast := lastAddr(network)
used := map[netip.Addr]string{gateway: s.Switch + " (gateway)"}
result := map[string]netip.Addr{}
var errs []error
var auto []string
for _, n := range t.Nodes {
if !contains(n.Segments, s.Name) {
continue
}
raw, ok := n.Addresses[s.Name]
if !ok {
auto = append(auto, n.Name)
continue
}
addr := netip.MustParseAddr(raw)
switch {
case !network.Contains(addr):
errs = append(errs, fmt.Errorf("node %s: address %s is outside segment %s (%s)", n.Name, addr, s.Name, network))
case addr == network.Addr() || addr == broadcast:
errs = append(errs, fmt.Errorf("node %s: address %s is the network or broadcast address of segment %s", n.Name, addr, s.Name))
case used[addr] != "":
errs = append(errs, fmt.Errorf("node %s: address %s on segment %s is already taken by %s", n.Name, addr, s.Name, used[addr]))
default:
used[addr] = n.Name
result[n.Name] = addr
}
}
next := gateway.Next()
for _, name := range auto {
for next != broadcast && used[next] != "" {
next = next.Next()
}
if next == broadcast {
errs = append(errs, fmt.Errorf("segment %s: no address left in %s for node %s", s.Name, network, name))
break
}
used[next] = name
result[name] = next
next = next.Next()
}
if len(errs) > 0 {
return nil, errors.Join(errs...)
}
return result, nil
}
func lastAddr(network netip.Prefix) netip.Addr {
a := network.Addr().As4()
host := uint32(1)<<(32-network.Bits()) - 1
v := (uint32(a[0])<<24 | uint32(a[1])<<16 | uint32(a[2])<<8 | uint32(a[3])) | host
return netip.AddrFrom4([4]byte{byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)})
}
func mac(node, segment, side int) net.HardwareAddr {
return net.HardwareAddr{0x02, 0x4c, byte(node >> 8), byte(node), byte(segment), byte(side)}
}
func contains(list []string, value string) bool {
for _, v := range list {
if v == value {
return true
}
}
return false
}