f-50: lab: scénarios E5, configuration de l'agent par hyperviseur, release 0.2.0rc003 #50

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
GnomeZworc 2026-10-04 20:43:53 +02:00
commit 5728fdd33c
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
25 changed files with 760 additions and 27 deletions

View file

@ -32,8 +32,8 @@ func (e exitErr) Error() string { return "exit status " + strconv.Itoa(int(e)) }
func (e exitErr) ExitCode() int { return int(e) }
type fakeRunner struct {
mu sync.Mutex
calls [][]string
mu sync.Mutex
calls [][]string
fail string
ssh map[string][]error
always map[string]error

View file

@ -76,7 +76,11 @@ func Prepare(ctx context.Context, p *topology.Plan, o Options) ([]render.Node, e
if err != nil {
return nil, err
}
nodes, err := render.Render(p, render.Options{RunDir: o.RunDir, AuthorizedKeys: []string{key}, FRR: frr})
agent, err := ReadAgent(p)
if err != nil {
return nil, err
}
nodes, err := render.Render(p, render.Options{RunDir: o.RunDir, AuthorizedKeys: []string{key}, FRR: frr, Agent: agent})
if err != nil {
return nil, err
}
@ -89,18 +93,26 @@ func Prepare(ctx context.Context, p *topology.Plan, o Options) ([]render.Node, e
}
func ReadFRR(p *topology.Plan) (map[string]string, error) {
configs := map[string]string{}
return readNodeFiles(p, func(n topology.NodePlan) string { return n.FRR })
}
func ReadAgent(p *topology.Plan) (map[string]string, error) {
return readNodeFiles(p, func(n topology.NodePlan) string { return n.Agent })
}
func readNodeFiles(p *topology.Plan, path func(topology.NodePlan) string) (map[string]string, error) {
files := map[string]string{}
for _, n := range p.Nodes {
if n.FRR == "" {
if path(n) == "" {
continue
}
data, err := os.ReadFile(n.FRR)
data, err := os.ReadFile(path(n))
if err != nil {
return nil, fmt.Errorf("node %s: %w", n.Name, err)
}
configs[n.Name] = string(data)
files[n.Name] = string(data)
}
return configs, nil
return files, nil
}
func EnsureKey(ctx context.Context, r Runner, dir string) (string, error) {

View file

@ -385,3 +385,23 @@ func TestPrepare_PutsTheFRRConfigIntoTheSeed(t *testing.T) {
}
t.Errorf("hv1 user-data has no /etc/lab/frr.conf:\n%s", data)
}
func TestReadAgent_ReadsOnlyTheNodesThatDeclareOne(t *testing.T) {
conf := filepath.Join(t.TempDir(), "two.yml")
if err := os.WriteFile(conf, []byte("dhcp:\n backend: two\n"), 0o600); err != nil {
t.Fatal(err)
}
m := newMirror(t)
p := labPlan(t, m)
p.Nodes[2].Agent = conf
got, err := ReadAgent(p)
if err != nil || !reflect.DeepEqual(got, map[string]string{"hv1": "dhcp:\n backend: two\n"}) {
t.Errorf("ReadAgent = %q, %v", got, err)
}
p.Nodes[3].Agent = filepath.Join(t.TempDir(), "absent.yml")
if _, err := ReadAgent(p); err == nil || !strings.Contains(err.Error(), "node hv2: ") {
t.Errorf("error = %v", err)
}
}

View file

@ -31,6 +31,7 @@ type Options struct {
RunDir string
AuthorizedKeys []string
FRR map[string]string
Agent map[string]string
}
type Node struct {
@ -50,6 +51,9 @@ func Render(p *topology.Plan, o Options) ([]Node, error) {
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)
}
if _, ok := o.Agent[n.Name]; n.Agent != "" && !ok {
return nil, fmt.Errorf("node %s: agent configuration %s was not read", n.Name, n.Agent)
}
}
var nodes []Node
for index, n := range p.Nodes {

View file

@ -262,3 +262,37 @@ func TestRoles_SwitchLoopbackIsCreatedByTheSwitchScript(t *testing.T) {
t.Errorf("lab-switch:\n%s\ndoes not end with:\n%s", script, want)
}
}
func TestRoles_HypervisorAgentConfigIsWrittenBeforeTheDeployment(t *testing.T) {
doc := strings.Replace(withRoles, "frr: hv1.conf, release: 0.2.0rc002 }", "frr: hv1.conf, release: 0.2.0rc002, agent: two.yml }", 1)
nodes, err := Render(plan(t, doc), Options{RunDir: "/srv/lab", AuthorizedKeys: []string{labKey}, FRR: frrConfigs, Agent: map[string]string{"hv1": "dhcp:\n backend: two\n"}})
if err != nil {
t.Fatalf("Render: %v", err)
}
var hv1, hv2 Node
for _, n := range nodes {
switch n.Name {
case "hv1":
hv1 = n
case "hv2":
hv2 = n
}
}
f := fileAt(t, user(t, hv1), "/etc/two/agent.yml")
if f.Content != "dhcp:\n backend: two\n" || f.Permissions != "0640" {
t.Errorf("agent.yml (%s) = %q", f.Permissions, f.Content)
}
for _, w := range user(t, hv2).WriteFiles {
if w.Path == "/etc/two/agent.yml" {
t.Error("hv2 receives an agent.yml it does not declare")
}
}
}
func TestRoles_RefusesAnUnreadAgentConfig(t *testing.T) {
doc := strings.Replace(withRoles, "frr: hv1.conf, release: 0.2.0rc002 }", "frr: hv1.conf, release: 0.2.0rc002, agent: two.yml }", 1)
_, err := Render(plan(t, doc), Options{RunDir: "/srv/lab", AuthorizedKeys: []string{labKey}, FRR: frrConfigs})
if err == nil || err.Error() != "node hv1: agent configuration two.yml was not read" {
t.Errorf("error = %v", err)
}
}

View file

@ -31,6 +31,7 @@ const (
DeployScript = "/usr/local/sbin/lab-deploy"
TwoScriptsDir = "/opt/two/scripts"
TwoGitServer = "https://git.g3e.fr/"
AgentConfig = "/etc/two/agent.yml"
)
//go:embed frrouting.gpg
@ -114,6 +115,9 @@ func userData(p *topology.Plan, n topology.NodePlan, o Options) ([]byte, error)
}
steps = append(steps, "systemctl daemon-reload", "systemctl enable --now lab-node.service")
}
if n.Agent != "" {
cfg.WriteFiles = append(cfg.WriteFiles, writeFile{Path: AgentConfig, Permissions: "0640", Content: o.Agent[n.Name]})
}
if n.Role == topology.RoleHypervisor {
cfg.WriteFiles = append(cfg.WriteFiles,
writeFile{Path: TwoScriptsDir + "/deploy.sh", Permissions: "0755", Encoding: "b64", Content: base64.StdEncoding.EncodeToString(scripts.Deploy)},

View file

@ -22,15 +22,15 @@ func (p *Plan) Write(w io.Writer) error {
var extras []NodePlan
for _, n := range p.Nodes {
if len(n.Secondary) > 0 || n.Loopback.IsValid() || n.FRR != "" || n.Release != "" {
if len(n.Secondary) > 0 || n.Loopback.IsValid() || n.FRR != "" || n.Release != "" || n.Agent != "" {
extras = append(extras, n)
}
}
if len(extras) > 0 {
fmt.Fprintf(tw, "\nroles\n")
fmt.Fprintf(tw, " name\tloopback\tsecondary\tfrr\trelease\n")
fmt.Fprintf(tw, " name\tloopback\tsecondary\tfrr\trelease\tagent\n")
for _, n := range extras {
fmt.Fprintf(tw, " %s\t%s\t%s\t%s\t%s\n", n.Name, orDash(loopback(n)), orDash(secondary(n)), orDash(filepath.Base(n.FRR)), orDash(n.Release))
fmt.Fprintf(tw, " %s\t%s\t%s\t%s\t%s\t%s\n", n.Name, orDash(loopback(n)), orDash(secondary(n)), orDash(filepath.Base(n.FRR)), orDash(n.Release), orDash(filepath.Base(n.Agent)))
}
}

View file

@ -44,6 +44,7 @@ type NodePlan struct {
Loopback netip.Prefix
FRR string
Release string
Agent string
}
type Cable struct {
@ -78,6 +79,7 @@ func Compute(t *Topology) (*Plan, error) {
SSHPort: SSHBasePort + i,
FRR: n.FRR,
Release: n.Release,
Agent: n.Agent,
}
for segment, raws := range n.Secondary {
for _, raw := range raws {

View file

@ -315,9 +315,9 @@ nodes
hv2 hypervisor deb 4 16384 MiB 127.0.0.1:2203
roles
name loopback secondary frr release
hv1 - - - 0.2.0rc002
hv2 - - - 0.2.0rc002
name loopback secondary frr release agent
hv1 - - - 0.2.0rc002 -
hv2 - - - 0.2.0rc002 -
segment underlay: 10.250.0.0/24, mtu 9000, switch sw1, bridge br-underlay, gateway 10.250.0.1
node interface address mac udp switch port mac udp

View file

@ -142,10 +142,10 @@ func TestWrite_ShowsTheRoles(t *testing.T) {
}
want := `
roles
name loopback secondary frr release
sw1 - underlay 169.254.0.1/28 sw1.conf -
rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf -
hv1 - - hv1.conf 0.2.0rc002
name loopback secondary frr release agent
sw1 - underlay 169.254.0.1/28 sw1.conf - -
rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf - -
hv1 - - hv1.conf 0.2.0rc002 -
`
if !bytes.Contains(buf.Bytes(), []byte(want)) {
t.Errorf("plan:\n%s\ndoes not contain:\n%s", buf.String(), want)
@ -214,3 +214,52 @@ func TestCompute_CarriesTheRelease(t *testing.T) {
t.Errorf("rr1 release = %q", got)
}
}
func TestValidate_AgentOnlyForHypervisors(t *testing.T) {
doc := header + `
segments:
red: { switch: sw, cidr: 10.1.0.0/24 }
nodes:
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
rr: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], agent: agent.yml }
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red], release: 0.2.0rc003, agent: agent.yml }
`
msg := validationError(t, doc)
requireContains(t, msg, "node rr: agent is only for hypervisors")
if bytes.Contains([]byte(msg), []byte("node hv: agent")) {
t.Errorf("a hypervisor agent was refused:\n%s", msg)
}
}
func TestLoad_ResolvesTheAgentPathAgainstTheTopologyFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "lab.yml")
doc := header + `
segments:
red: { switch: sw, cidr: 10.1.0.0/24 }
nodes:
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
hv1: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red], release: 0.2.0rc003, agent: agent/two.yml }
hv2: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red], release: 0.2.0rc003, agent: /etc/lab/agent.yml }
`
if err := os.WriteFile(path, []byte(doc), 0o600); err != nil {
t.Fatal(err)
}
topo, err := Load(path)
if err != nil {
t.Fatalf("Load: %v", err)
}
want := map[string]string{"sw": "", "hv1": filepath.Join(dir, "agent", "two.yml"), "hv2": "/etc/lab/agent.yml"}
for _, n := range topo.Nodes {
if n.Agent != want[n.Name] {
t.Errorf("%s agent = %q, want %q", n.Name, n.Agent, want[n.Name])
}
}
p, err := Compute(topo)
if err != nil {
t.Fatal(err)
}
if got := nodeOf(t, p, "hv1").Agent; got != filepath.Join(dir, "agent", "two.yml") {
t.Errorf("plan hv1 agent = %q", got)
}
}

View file

@ -50,6 +50,7 @@ type Node struct {
Loopback string
FRR string
Release string
Agent string
}
type fileImage struct {
@ -74,6 +75,7 @@ type fileNode struct {
Loopback string `yaml:"loopback"`
FRR string `yaml:"frr"`
Release string `yaml:"release"`
Agent string `yaml:"agent"`
}
type file struct {
@ -96,6 +98,9 @@ func Load(path string) (*Topology, error) {
if n.FRR != "" && !filepath.IsAbs(n.FRR) {
t.Nodes[i].FRR = filepath.Join(filepath.Dir(path), n.FRR)
}
if n.Agent != "" && !filepath.IsAbs(n.Agent) {
t.Nodes[i].Agent = filepath.Join(filepath.Dir(path), n.Agent)
}
}
return t, nil
}
@ -144,6 +149,7 @@ func Parse(data []byte) (*Topology, error) {
Loopback: n.Loopback,
FRR: n.FRR,
Release: n.Release,
Agent: n.Agent,
})
}
return t, nil

View file

@ -215,6 +215,9 @@ func validateExtras(n Node, segments map[string]Segment, add func(string, ...any
case n.Role != RoleHypervisor && n.Release != "":
add("node %s: release is only for hypervisors", n.Name)
}
if n.Role != RoleHypervisor && n.Agent != "" {
add("node %s: agent is only for hypervisors", n.Name)
}
if n.Loopback != "" {
prefix, err := netip.ParsePrefix(n.Loopback)
switch {