f-50: lab: topologie et calcul du plan #50

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
GnomeZworc 2026-10-04 00:49:21 +02:00
commit a706e24b1c
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
11 changed files with 1310 additions and 4 deletions

50
cmd/lab/main.go Normal file
View file

@ -0,0 +1,50 @@
package main
import (
"fmt"
"io"
"os"
"git.g3e.fr/syonad/two/internal/lab/topology"
)
const usage = `usage: lab <command> <topology.yml>
plan validate the topology and print the deterministic plan: addresses, cables, ports
`
func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}
func run(args []string, stdout, stderr io.Writer) int {
if len(args) != 2 {
fmt.Fprint(stderr, usage)
return 2
}
switch args[0] {
case "plan":
return plan(args[1], stdout, stderr)
default:
fmt.Fprint(stderr, usage)
return 2
}
}
func plan(path string, stdout, stderr io.Writer) int {
t, err := topology.Load(path)
if err != nil {
fmt.Fprintf(stderr, "lab: %v\n", err)
return 1
}
p, err := topology.Compute(t)
if err != nil {
fmt.Fprintf(stderr, "lab: %s:\n%v\n", path, err)
return 1
}
if err := p.Write(stdout); err != nil {
fmt.Fprintf(stderr, "lab: %v\n", err)
return 1
}
return 0
}

72
cmd/lab/main_test.go Normal file
View file

@ -0,0 +1,72 @@
package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func runLab(args ...string) (int, string, string) {
var stdout, stderr bytes.Buffer
code := run(args, &stdout, &stderr)
return code, stdout.String(), stderr.String()
}
func TestRun_UsageOnMissingArguments(t *testing.T) {
for _, args := range [][]string{nil, {"plan"}, {"deploy", "x.yml"}, {"plan", "a.yml", "b.yml"}} {
code, _, stderr := runLab(args...)
if code != 2 || !strings.Contains(stderr, "usage: lab") {
t.Errorf("args %v: code %d, stderr %q", args, code, stderr)
}
}
}
func TestRun_PlanOfTheShippedExampleTopology(t *testing.T) {
code, stdout, stderr := runLab("plan", filepath.Join("..", "..", "conf", "lab", "evpn-2hv.yml"))
if code != 0 {
t.Fatalf("code %d, stderr %s", code, stderr)
}
for _, want := range []string{
"lab evpn-2hv: nodes 4, segments 1, cables 3",
"gateway 10.250.0.1",
"hv2 underlay 10.250.0.4/24 02:4c:00:03:00:00 20004 <-> sw1 p2",
} {
if !strings.Contains(stdout, want) {
t.Errorf("output does not contain %q:\n%s", want, stdout)
}
}
}
func TestRun_InvalidTopologyExitsWithErrors(t *testing.T) {
path := filepath.Join(t.TempDir(), "bad.yml")
doc := `name: bad
images:
deb: { url: https://example.invalid/a, sums: https://example.invalid/b }
segments:
under: { switch: sw, cidr: 10.0.0.0/31 }
nodes:
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
rr: { role: router, image: deb, cpus: 1, memory: 512, segments: [under] }
`
if err := os.WriteFile(path, []byte(doc), 0o600); err != nil {
t.Fatal(err)
}
code, stdout, stderr := runLab("plan", path)
if code != 1 || stdout != "" {
t.Fatalf("code %d, stdout %q", code, stdout)
}
for _, want := range []string{path, `role "router"`, "prefix length out of range"} {
if !strings.Contains(stderr, want) {
t.Errorf("stderr does not mention %q:\n%s", want, stderr)
}
}
}
func TestRun_MissingFile(t *testing.T) {
code, _, stderr := runLab("plan", filepath.Join(t.TempDir(), "absent.yml"))
if code != 1 || !strings.Contains(stderr, "absent.yml") {
t.Errorf("code %d, stderr %q", code, stderr)
}
}