two/internal/lab/render/render.go
2026-10-05 20:15:47 +02:00

138 lines
3.1 KiB
Go

package render
import (
"errors"
"fmt"
"net"
"path/filepath"
"strings"
"git.g3e.fr/syonad/two/internal/lab/topology"
)
const (
QEMUBinary = "qemu-system-x86_64"
AdminInterface = topology.ReservedInterface
AdminAddress = "10.0.2.15/24"
AdminGateway = "10.0.2.2"
AdminDNS = "10.0.2.3"
DiskFile = "disk.qcow2"
SeedFile = "seed.iso"
ConsoleFile = "console.log"
QMPFile = "qmp.sock"
PIDFile = "qemu.pid"
)
var Nameservers = []string{"1.1.1.1", "8.8.8.8"}
type Options struct {
RunDir string
AuthorizedKeys []string
FRR map[string]string
}
type Node struct {
Name string
Dir string
QEMU []string
MetaData []byte
UserData []byte
NetworkConfig []byte
}
func Render(p *topology.Plan, o Options) ([]Node, error) {
if err := o.validate(); err != nil {
return nil, err
}
for _, n := range p.Nodes {
if _, ok := o.FRR[n.Name]; n.FRR != "" && !ok {
return nil, fmt.Errorf("node %s: frr configuration %s was not read", n.Name, n.FRR)
}
}
var nodes []Node
for index, n := range p.Nodes {
dir := filepath.Join(o.RunDir, n.Name)
meta, err := metaData(p, n)
if err != nil {
return nil, err
}
user, err := userData(p, n, o)
if err != nil {
return nil, err
}
network, err := networkConfig(p, n, index)
if err != nil {
return nil, err
}
nodes = append(nodes, Node{
Name: n.Name,
Dir: dir,
QEMU: qemuArgs(p, n, index, dir),
MetaData: meta,
UserData: user,
NetworkConfig: network,
})
}
return nodes, nil
}
func (o Options) validate() error {
var errs []error
if !filepath.IsAbs(o.RunDir) {
errs = append(errs, fmt.Errorf("run dir %q must be an absolute path", o.RunDir))
}
if len(o.AuthorizedKeys) == 0 {
errs = append(errs, errors.New("at least one authorized ssh key is required"))
}
for i, k := range o.AuthorizedKeys {
if strings.ContainsAny(k, "\r\n") {
errs = append(errs, fmt.Errorf("authorized key %d spans several lines", i+1))
continue
}
fields := strings.Fields(k)
if len(fields) < 2 || !validKeyType(fields[0]) {
errs = append(errs, fmt.Errorf("authorized key %d is not an ssh public key", i+1))
}
}
return errors.Join(errs...)
}
func validKeyType(t string) bool {
return strings.HasPrefix(t, "ssh-") || strings.HasPrefix(t, "ecdsa-sha2-") || strings.HasPrefix(t, "sk-")
}
func adminMAC(index int) net.HardwareAddr {
return net.HardwareAddr{0x02, 0x4d, byte(index >> 8), byte(index), 0x00, 0x00}
}
func nodeCables(p *topology.Plan, name string) []topology.Cable {
var out []topology.Cable
for _, c := range p.Cables {
if c.Node == name {
out = append(out, c)
}
}
return out
}
func switchCables(p *topology.Plan, name string) []topology.Cable {
var out []topology.Cable
for _, c := range p.Cables {
if c.Switch == name {
out = append(out, c)
}
}
return out
}
func switchSegments(p *topology.Plan, name string) []topology.SegmentPlan {
var out []topology.SegmentPlan
for _, s := range p.Segments {
if s.Switch == name {
out = append(out, s)
}
}
return out
}