f-50: lab: déploiement de two sur les hyperviseurs et provisionnement en un script #50

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
GnomeZworc 2026-10-04 17:36:15 +02:00
commit f424d15213
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
16 changed files with 360 additions and 64 deletions

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 != "" {
if len(n.Secondary) > 0 || n.Loopback.IsValid() || n.FRR != "" || n.Release != "" {
extras = append(extras, n)
}
}
if len(extras) > 0 {
fmt.Fprintf(tw, "\nroles\n")
fmt.Fprintf(tw, " name\tloopback\tsecondary\tfrr\n")
fmt.Fprintf(tw, " name\tloopback\tsecondary\tfrr\trelease\n")
for _, n := range extras {
fmt.Fprintf(tw, " %s\t%s\t%s\t%s\n", n.Name, orDash(loopback(n)), orDash(secondary(n)), orDash(filepath.Base(n.FRR)))
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))
}
}

View file

@ -43,6 +43,7 @@ type NodePlan struct {
Secondary map[string][]netip.Prefix
Loopback netip.Prefix
FRR string
Release string
}
type Cable struct {
@ -76,6 +77,7 @@ func Compute(t *Topology) (*Plan, error) {
Memory: n.Memory,
SSHPort: SSHBasePort + i,
FRR: n.FRR,
Release: n.Release,
}
for segment, raws := range n.Secondary {
for _, raw := range raws {

View file

@ -55,8 +55,8 @@ segments:
nodes:
sw1: { role: switch, image: deb, cpus: 2, memory: 1024 }
rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay] }
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] }
hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] }
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 }
hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 }
`
func TestCompute_TwoHypervisorsPlan(t *testing.T) {
@ -165,7 +165,7 @@ segments:
blue: { switch: sw, cidr: 10.2.0.0/24, mtu: 1500 }
nodes:
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [blue, red] }
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [blue, red], release: 0.2.0rc002 }
rr: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red] }
`)
got := make([]string, 0, len(p.Cables))
@ -193,7 +193,7 @@ nodes:
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
n1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [a, b] }
n2: { role: rr, image: deb, cpus: 1, memory: 512, segments: [a, b] }
n3: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [b, a] }
n3: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [b, a], release: 0.2.0rc002 }
`)
macs := map[string]bool{}
ports := map[int]bool{}
@ -314,6 +314,11 @@ nodes
hv1 hypervisor deb 4 16384 MiB 127.0.0.1:2202
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
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
rr1 underlay 10.250.0.2/24 02:4c:00:01:00:00 20000 <-> sw1 p0 02:4c:00:01:00:01 20001

View file

@ -15,7 +15,7 @@ segments:
nodes:
sw1: { role: switch, image: deb, cpus: 2, memory: 1024, secondary: { underlay: [169.254.0.1/28] }, frr: frr/sw1.conf }
rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay], secondary: { underlay: [169.254.0.3/28] }, loopback: 10.255.255.1/32, frr: frr/rr1.conf }
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], frr: /etc/lab/hv1.conf }
hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], frr: /etc/lab/hv1.conf, release: 0.2.0rc002 }
`
func TestCompute_CarriesTheRoleFields(t *testing.T) {
@ -99,7 +99,7 @@ segments:
blue: { switch: sw, cidr: 10.2.0.0/24 }
nodes:
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red, blue] }
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red, blue], release: 0.2.0rc002 }
` + c.node + `
`
requireContains(t, validationError(t, doc), c.want)
@ -115,7 +115,7 @@ segments:
nodes:
sw: { role: switch, image: deb, cpus: 1, memory: 512, secondary: { red: [169.254.0.1/28], blue: [169.254.1.1/28] } }
other: { role: switch, image: deb, cpus: 1, memory: 512 }
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red, blue] }
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red, blue], release: 0.2.0rc002 }
`
msg := validationError(t, doc)
requireContains(t, msg, "node sw: secondary address given for segment blue it is not attached to")
@ -130,7 +130,7 @@ segments:
lo1: { switch: sw, cidr: 10.1.0.0/24 }
nodes:
sw: { role: switch, image: deb, cpus: 1, memory: 512 }
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [lo1] }
hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [lo1], release: 0.2.0rc002 }
`
requireContains(t, validationError(t, doc), "segment lo1: name is reserved for the loopback interface")
}
@ -142,10 +142,10 @@ func TestWrite_ShowsTheRoles(t *testing.T) {
}
want := `
roles
name loopback secondary frr
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
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
`
if !bytes.Contains(buf.Bytes(), []byte(want)) {
t.Errorf("plan:\n%s\ndoes not contain:\n%s", buf.String(), want)
@ -154,10 +154,63 @@ roles
func TestWrite_NoRolesSectionWithoutRoleFields(t *testing.T) {
var buf bytes.Buffer
if err := compute(t, twoHypervisors).Write(&buf); err != nil {
doc := header + `
segments:
underlay: { switch: sw1, cidr: 10.250.0.0/24 }
nodes:
sw1: { role: switch, image: deb, cpus: 1, memory: 512 }
rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [underlay] }
`
if err := compute(t, doc).Write(&buf); err != nil {
t.Fatalf("Write: %v", err)
}
if bytes.Contains(buf.Bytes(), []byte("roles")) {
t.Errorf("plan shows a roles section:\n%s", buf.String())
}
}
func TestValidate_ReleaseRejections(t *testing.T) {
cases := map[string]struct {
node string
want string
}{
"hypervisor without release": {
`hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red] }`,
"node hv: a hypervisor needs the release of two to deploy (release: <tag>)",
},
"release with shell characters": {
`hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red], release: "0.2.0; reboot" }`,
`node hv: release "0.2.0; reboot" must match`,
},
"release on a route reflector": {
`hv: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], release: 0.2.0rc002 }`,
"node hv: release is only for hypervisors",
},
"release on a switch": {
`hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red], release: 0.2.0rc002 }
sw2: { role: switch, image: deb, cpus: 1, memory: 512, release: 0.2.0rc002 }`,
"node sw2: release is only for hypervisors",
},
}
for name, c := range cases {
t.Run(name, func(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 }
` + c.node + `
`
requireContains(t, validationError(t, doc), c.want)
})
}
}
func TestCompute_CarriesTheRelease(t *testing.T) {
if got := nodeOf(t, compute(t, withRoles), "hv1").Release; got != "0.2.0rc002" {
t.Errorf("hv1 release = %q", got)
}
if got := nodeOf(t, compute(t, withRoles), "rr1").Release; got != "" {
t.Errorf("rr1 release = %q", got)
}
}

View file

@ -49,6 +49,7 @@ type Node struct {
Secondary map[string][]string
Loopback string
FRR string
Release string
}
type fileImage struct {
@ -72,6 +73,7 @@ type fileNode struct {
Secondary map[string][]string `yaml:"secondary"`
Loopback string `yaml:"loopback"`
FRR string `yaml:"frr"`
Release string `yaml:"release"`
}
type file struct {
@ -141,6 +143,7 @@ func Parse(data []byte) (*Topology, error) {
Secondary: n.Secondary,
Loopback: n.Loopback,
FRR: n.FRR,
Release: n.Release,
})
}
return t, nil

View file

@ -23,6 +23,7 @@ const (
var (
namePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{0,14}$`)
segmentPattern = regexp.MustCompile(`^[a-z][a-z0-9]{0,11}$`)
releasePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`)
)
func (t *Topology) Validate() error {
@ -206,6 +207,14 @@ func validateExtras(n Node, segments map[string]Segment, add func(string, ...any
}
}
}
switch {
case n.Role == RoleHypervisor && n.Release == "":
add("node %s: a hypervisor needs the release of two to deploy (release: <tag>)", n.Name)
case n.Role == RoleHypervisor && !releasePattern.MatchString(n.Release):
add("node %s: release %q must match %s", n.Name, n.Release, releasePattern)
case n.Role != RoleHypervisor && n.Release != "":
add("node %s: release is only for hypervisors", n.Name)
}
if n.Loopback != "" {
prefix, err := netip.ParsePrefix(n.Loopback)
switch {