diff --git a/cmd/lab/main.go b/cmd/lab/main.go index 3ff3cf2..df86e39 100644 --- a/cmd/lab/main.go +++ b/cmd/lab/main.go @@ -135,12 +135,7 @@ func renderCmd(args []string, stdout, stderr io.Writer) int { fmt.Fprintf(stderr, "lab: %v\n", err) return 1 } - agent, err := provision.ReadAgent(p) - if err != nil { - fmt.Fprintf(stderr, "lab: %v\n", err) - return 1 - } - nodes, err := render.Render(p, render.Options{RunDir: dir, AuthorizedKeys: authorized, FRR: frr, Agent: agent}) + nodes, err := render.Render(p, render.Options{RunDir: dir, AuthorizedKeys: authorized, FRR: frr}) if err != nil { fmt.Fprintf(stderr, "lab: %v\n", err) return 1 diff --git a/conf/lab/agent/two.yml b/conf/lab/agent/two.yml deleted file mode 100644 index 6334d32..0000000 --- a/conf/lab/agent/two.yml +++ /dev/null @@ -1,2 +0,0 @@ -dhcp: - backend: two diff --git a/conf/lab/evpn-2hv.yml b/conf/lab/evpn-2hv.yml index 0176ef1..a3ebfa5 100644 --- a/conf/lab/evpn-2hv.yml +++ b/conf/lab/evpn-2hv.yml @@ -21,8 +21,7 @@ nodes: loopback: 10.255.255.1/32, frr: frr/rr1.conf } hv1: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay], - addresses: { underlay: 192.168.14.11 }, frr: frr/hv1.conf, release: 0.2.0rc003, - agent: agent/two.yml } + addresses: { underlay: 192.168.14.11 }, frr: frr/hv1.conf, release: 0.2.0rc002 } hv2: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay], - addresses: { underlay: 192.168.14.12 }, frr: frr/hv2.conf, release: 0.2.0rc003 } + addresses: { underlay: 192.168.14.12 }, frr: frr/hv2.conf, release: 0.2.0rc002 } diff --git a/docs/deploiement/route-reflector.rst b/docs/deploiement/route-reflector.rst index 5f71659..ab57b32 100644 --- a/docs/deploiement/route-reflector.rst +++ b/docs/deploiement/route-reflector.rst @@ -94,21 +94,9 @@ l'adresse du lien en secondaire et la loopback sur ``lo1`` : - up des deux côtés * - sessions des deux hyperviseurs vers ``10.255.255.1`` - Established, voisins dynamiques, iBGP AS 64600, famille L2VPN EVPN négociée - * - routes EVPN échangées, VM ↔ VM entre deux hyperviseurs - - routes de type 3 échangées, ping et MTU 1500 de bout en bout — à partir de la release - ``0.2.0rc003``, qui pose l'adresse VTEP locale des VXLAN - (`#51 `_) - -.. warning:: - - **Les tunnels ne survivent pas à la perte du route reflector.** Mesuré dans le lab - (2026-10-04, FRR 10.7.1, un seul route reflector, sans ``graceful-restart``) : à l'arrêt de - FRR sur le route reflector, la session EVPN des hyperviseurs tombe aussitôt, FRR retire les - routes apprises et, avec elles, le VTEP distant et l'entrée d'inondation du VXLAN — **plus - aucun paquet ne passe** entre hyperviseurs, à 30 s comme à 90 s. Au redémarrage de FRR sur le - route reflector, VTEP distant et trafic reviennent **31 s** plus tard. Le trafic entre VM d'un - même hyperviseur n'est pas concerné. La redondance (deux route reflectors) ou - ``graceful-restart`` sont les deux leviers ; ni l'un ni l'autre n'est encore qualifié. + * - routes EVPN échangées + - aucune : rien à annoncer tant que two n'a pas créé de VXLAN — voir + :doc:`premier-hyperviseur` Le routeur du lab n'est qu'une configuration minimale écrite pour l'essai, pas celle des routeurs de cluster (:doc:`routeurs`). @@ -121,8 +109,9 @@ de cluster (:doc:`routeurs`). lancer sur n'importe quel hyperviseur), et s'il s'agit d'une VM créée par l'agent comme les autres ou d'un cas particulier — l'image, elle, est l'image golden de :doc:`image-qcow2` ; * la redondance : une seule VM route reflector, ou deux, et sur quels hyperviseurs ; - * la procédure de reconstruction — l'état du cluster pendant l'absence du route reflector est - mesuré ci-dessus : plus de trafic entre hyperviseurs ; + * la procédure de reconstruction, et l'état du cluster pendant que le route reflector est + absent — les tunnels déjà établis continuent-ils de fonctionner, et pendant combien de + temps ; * la procédure d'amorçage : ce qui fonctionne, et dans quel ordre, quand on démarre un cluster entier depuis zéro — le premier hyperviseur n'a pas de session FRR établie tant que cette VM n'existe pas, cf. :doc:`premier-hyperviseur`. diff --git a/docs/developpement/lab.rst b/docs/developpement/lab.rst index 0ed0d2f..2504bc0 100644 --- a/docs/developpement/lab.rst +++ b/docs/developpement/lab.rst @@ -230,12 +230,8 @@ Ce que le fichier déclare : * ``frr`` — le chemin d'un ``frr.conf``, relatif au fichier de topologie : FRR est installé au démarrage et la configuration déposée **telle quelle** (voir `Rôles`_). * ``release`` — **obligatoire pour un hyperviseur**, refusé ailleurs : le tag de la release de - two que ``deploy.sh`` installe (``release: 0.2.0rc003``). Sans lui, ``deploy.sh`` prendrait la + two que ``deploy.sh`` installe (``release: 0.2.0rc002``). Sans lui, ``deploy.sh`` prendrait la dernière release, et le lab ne serait plus reproductible. - * ``agent`` — pour un hyperviseur seulement : le chemin d'un ``agent.yml``, relatif au fichier - de topologie, déposé dans ``/etc/two/agent.yml`` avant ``deploy.sh``. Sans lui, l'agent tourne - avec sa configuration par défaut. L'exemple met hv1 sur le serveur DHCP intégré - (``conf/lab/agent/two.yml`` : ``dhcp.backend: two``) et laisse hv2 sur dnsmasq. ``mgmt0`` et ``lo1`` sont réservés : aucun segment ne peut porter ces noms. @@ -282,11 +278,11 @@ Limites : 1000 nœuds, 256 segments, et autant de câbles que la plage UDP le pe hv2 hypervisor debian12 4 16384 MiB 127.0.0.1:2203 roles - 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.0rc003 two.yml - hv2 - - hv2.conf 0.2.0rc003 - + 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 + hv2 - - hv2.conf 0.2.0rc002 segment underlay: 192.168.14.0/24, mtu 9000, switch sw1, bridge br-underlay, gateway 192.168.14.1 node interface address mac udp switch port mac udp @@ -664,97 +660,6 @@ Le 2026-10-04, sur un Xeon E5-2640 v3, Debian 12 et QEMU 7.2 sur le serveur, top * - ``lab down`` puis ``lab up`` d'une autre topologie - conforme -Scénarios ---------- - -Les scénarios se lancent **depuis le Mac**, sur un lab démarré (``up``, ``push``, ``./lab up``) : - -.. code-block:: text - - scripts/lab/scenario.sh s1 # un scénario - scripts/lab/scenario.sh s1 s3 # plusieurs - scripts/lab/scenario.sh all # tous, dans l'ordre - -Chacun affiche une ligne ``RÉUSSI`` ou ``ÉCHOUÉ`` par vérification — un échec porte la dernière -ligne de la commande en cause —, des lignes ``INFO`` pour les mesures, puis son bilan. Le code de -sortie vaut 1 si une vérification échoue **ou si aucune n'a été faite**. - -.. list-table:: - :header-rows: 1 - :widths: 22 58 20 - - * - Scénario - - Ce qui doit être vrai - - Hyperviseurs - * - ``s1-dhcp-two`` - - backend DHCP ``two``, une VPC et deux subnets : chaque VM reçoit l'adresse de **son** - subnet, démarrée seule ou en même temps qu'une autre ; le bail est tenu par - systemd-networkd ; route par défaut, route vers la VPC et ``/32`` vers les métadonnées - via ``interface_ip`` ; l'état de chaque serveur DHCP ne connaît que les MAC de son subnet - (#46) - - hv1 - * - ``s2-gateway`` - - route par défaut via ``interface_ip``, ou via ``gateway`` avec ``default_route`` ; route - vers la VPC toujours via ``interface_ip`` ; même résultat après recréation du subnet (#31) - - hv1 - * - ``s3-isolation-local`` - - deux VPC sur le même hyperviseur ne se joignent pas, en ICMP comme en TCP ; chaque VM - joint sa passerelle (témoin) - - hv1 - * - ``s4-evpn`` - - adresse VTEP locale sur les VXLAN (#51), VTEP distant appris par EVPN, ping VM ↔ VM - entre hyperviseurs, trame de 1472 octets en ``-M do``, 1473 refusés - - hv1, hv2 - * - ``s5-isolation-evpn`` - - deux VPC de même plage, VNI différentes, sur deux hyperviseurs : la VM joint celle de sa - VPC sur l'autre hyperviseur (témoin) et pas celle de l'autre VPC — aucune résolution ARP - - hv1, hv2 - * - ``s6-rr-loss`` - - FRR arrêté sur le route reflector, ping continu entre les VM de ``s4`` : la session tombe, - le trafic restant est **mesuré** (``INFO``) à 30 et 90 s ; au retour du route reflector, le - VTEP distant est réappris et le trafic repasse — délai mesuré - - hv1, hv2, rr1 - -``s6`` réutilise les VM de ``s4`` : le lancer après. - -**Comment c'est fait.** ``scripts/lab/scenario.sh`` exécute chaque scénario sur le Mac ; un -scénario envoie des blocs de shell aux nœuds par ``on [VAR=valeur…] <<'NODE'``, précédés de -``scripts/lab/node.sh`` — appels à l'API de l'agent, attente des états, image Debian compatible two -(préparée une fois par hyperviseur, ``seedfrom`` avec barre oblique finale), clé SSH des VM, -``check`` et ``vm_fails``. Une vérification négative (« ne joint pas ») passe par ``vm_fails`` : -elle n'est réussie que si le SSH vers la VM a fonctionné **et** que la commande y a échoué — un -SSH en panne ne passe jamais pour une isolation. - -Résultats du 2026-10-04 sur le serveur de lab, release ``0.2.0rc003``, hv1 sur le DHCP intégré et -hv2 sur dnsmasq — toute la série en 8 minutes : - -.. code-block:: text - - $ scripts/lab/scenario.sh all | grep -E '^(=== s[0-9].* : |INFO)' - INFO: MAC de sn-s1a : 00:22:33:00:00:0a 00:22:33:00:00:0b ; de sn-s1b : 00:22:33:00:00:0a 00:22:33:00:00:0b - === s1-dhcp-two : 37 réussi(s), 0 échoué(s) - === s2-gateway : 22 réussi(s), 0 échoué(s) - === s3-isolation-local : 12 réussi(s), 0 échoué(s) - === s4-evpn : 14 réussi(s), 0 échoué(s) - === s5-isolation-evpn : 13 réussi(s), 0 échoué(s) - INFO: 30 s après l'arrêt du route reflector : 0 réponses dans les 10 dernières secondes (50 si le trafic passe intégralement) - INFO: VTEP distant encore connu : 0 ; entrée d'inondation : 0 - INFO: 90 s après l'arrêt : 0 réponses dans les 10 dernières secondes - INFO: retour du VTEP distant et du trafic 31 s après le redémarrage de FRR - === s6-rr-loss : 8 réussi(s), 0 échoué(s) - -Dans ce run, le contrôle « uniquement les MAC de son subnet » de ``s1`` ne prouvait rien : two -dérive la MAC du rang de l'IP, et les VM ``.10``/``.11`` des deux subnets avaient les mêmes MAC. -Vérifié à la main sur le lab, chaque serveur DHCP ne connaissait que les couples MAC/IP de son -subnet ; le scénario compare désormais ces couples. - -Les VM sont accessibles depuis le netns de leur VPC, sur l'hyperviseur, avec l'utilisateur -``syonad`` créé par les métadonnées de two : - -.. code-block:: text - - scripts/lab-host.sh ssh "./lab ssh hv1 'sudo ip netns exec vp-s4 ssh -i /root/.ssh/lab-vm syonad@10.240.1.10'" - Facturation ----------- @@ -846,7 +751,6 @@ Tests .. code-block:: bash bash scripts/lab-host_test.sh - bash scripts/lab/scenario_test.sh go test ./internal/lab/... ./cmd/lab/ Environ une minute et demie, sans réseau : la suite remplace ``curl`` par une fausse API Scaleway diff --git a/internal/lab/machine/machine_test.go b/internal/lab/machine/machine_test.go index 78e2ba4..d3bc49a 100644 --- a/internal/lab/machine/machine_test.go +++ b/internal/lab/machine/machine_test.go @@ -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 diff --git a/internal/lab/provision/provision.go b/internal/lab/provision/provision.go index b83751a..b1ee4d0 100644 --- a/internal/lab/provision/provision.go +++ b/internal/lab/provision/provision.go @@ -76,11 +76,7 @@ func Prepare(ctx context.Context, p *topology.Plan, o Options) ([]render.Node, e if err != nil { return nil, err } - 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}) + nodes, err := render.Render(p, render.Options{RunDir: o.RunDir, AuthorizedKeys: []string{key}, FRR: frr}) if err != nil { return nil, err } @@ -93,26 +89,18 @@ func Prepare(ctx context.Context, p *topology.Plan, o Options) ([]render.Node, e } func ReadFRR(p *topology.Plan) (map[string]string, error) { - 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{} + configs := map[string]string{} for _, n := range p.Nodes { - if path(n) == "" { + if n.FRR == "" { continue } - data, err := os.ReadFile(path(n)) + data, err := os.ReadFile(n.FRR) if err != nil { return nil, fmt.Errorf("node %s: %w", n.Name, err) } - files[n.Name] = string(data) + configs[n.Name] = string(data) } - return files, nil + return configs, nil } func EnsureKey(ctx context.Context, r Runner, dir string) (string, error) { diff --git a/internal/lab/provision/provision_test.go b/internal/lab/provision/provision_test.go index 403d3cc..668a0f8 100644 --- a/internal/lab/provision/provision_test.go +++ b/internal/lab/provision/provision_test.go @@ -385,23 +385,3 @@ 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) - } -} diff --git a/internal/lab/render/render.go b/internal/lab/render/render.go index be95453..6a11f0d 100644 --- a/internal/lab/render/render.go +++ b/internal/lab/render/render.go @@ -31,7 +31,6 @@ type Options struct { RunDir string AuthorizedKeys []string FRR map[string]string - Agent map[string]string } type Node struct { @@ -51,9 +50,6 @@ 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 { diff --git a/internal/lab/render/roles_test.go b/internal/lab/render/roles_test.go index fd0e0de..001677b 100644 --- a/internal/lab/render/roles_test.go +++ b/internal/lab/render/roles_test.go @@ -262,37 +262,3 @@ 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) - } -} diff --git a/internal/lab/render/seed.go b/internal/lab/render/seed.go index 0255140..804ed45 100644 --- a/internal/lab/render/seed.go +++ b/internal/lab/render/seed.go @@ -31,7 +31,6 @@ 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 @@ -115,9 +114,6 @@ 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)}, diff --git a/internal/lab/topology/format.go b/internal/lab/topology/format.go index 1ba73fd..567933a 100644 --- a/internal/lab/topology/format.go +++ b/internal/lab/topology/format.go @@ -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 != "" || n.Agent != "" { + 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\trelease\tagent\n") + fmt.Fprintf(tw, " name\tloopback\tsecondary\tfrr\trelease\n") for _, n := range extras { - 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))) + 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)) } } diff --git a/internal/lab/topology/plan.go b/internal/lab/topology/plan.go index e5ba293..83487ba 100644 --- a/internal/lab/topology/plan.go +++ b/internal/lab/topology/plan.go @@ -44,7 +44,6 @@ type NodePlan struct { Loopback netip.Prefix FRR string Release string - Agent string } type Cable struct { @@ -79,7 +78,6 @@ 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 { diff --git a/internal/lab/topology/plan_test.go b/internal/lab/topology/plan_test.go index c8d4e05..4f23fef 100644 --- a/internal/lab/topology/plan_test.go +++ b/internal/lab/topology/plan_test.go @@ -315,9 +315,9 @@ nodes hv2 hypervisor deb 4 16384 MiB 127.0.0.1:2203 roles - name loopback secondary frr release agent - hv1 - - - 0.2.0rc002 - - hv2 - - - 0.2.0rc002 - + 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 diff --git a/internal/lab/topology/roles_test.go b/internal/lab/topology/roles_test.go index d3e209c..97ee53c 100644 --- a/internal/lab/topology/roles_test.go +++ b/internal/lab/topology/roles_test.go @@ -142,10 +142,10 @@ func TestWrite_ShowsTheRoles(t *testing.T) { } want := ` roles - 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 - + 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) @@ -214,52 +214,3 @@ 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) - } -} diff --git a/internal/lab/topology/topology.go b/internal/lab/topology/topology.go index 66337d8..72c532a 100644 --- a/internal/lab/topology/topology.go +++ b/internal/lab/topology/topology.go @@ -50,7 +50,6 @@ type Node struct { Loopback string FRR string Release string - Agent string } type fileImage struct { @@ -75,7 +74,6 @@ type fileNode struct { Loopback string `yaml:"loopback"` FRR string `yaml:"frr"` Release string `yaml:"release"` - Agent string `yaml:"agent"` } type file struct { @@ -98,9 +96,6 @@ 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 } @@ -149,7 +144,6 @@ func Parse(data []byte) (*Topology, error) { Loopback: n.Loopback, FRR: n.FRR, Release: n.Release, - Agent: n.Agent, }) } return t, nil diff --git a/internal/lab/topology/validate.go b/internal/lab/topology/validate.go index 432fedf..2128c68 100644 --- a/internal/lab/topology/validate.go +++ b/internal/lab/topology/validate.go @@ -215,9 +215,6 @@ 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 { diff --git a/scripts/lab/node.sh b/scripts/lab/node.sh deleted file mode 100644 index 0a7111c..0000000 --- a/scripts/lab/node.sh +++ /dev/null @@ -1,151 +0,0 @@ -set -u -API=http://127.0.0.1:8080 -IMAGE_URL=https://cloud.debian.org/images/cloud/bookworm/latest -IMAGE_FILE=debian-12-genericcloud-amd64.qcow2 -TWO_IMAGE=/var/tmp/deb-two.qcow2 -VM_KEY=/root/.ssh/lab-vm -VOLUMES=/var/lib/two/volumes - -check () { - local desc="${1}" out - shift - if out=$("$@" 2>&1); then - echo "RÉUSSI: ${desc}" - else - echo "ÉCHOUÉ: ${desc}${out:+ — $(printf '%s' "${out}" | tail -n 1)}" - fi -} - -info () { echo "INFO: ${1}"; } - -api_post () { - curl -fsS -o /dev/null -X POST -H 'Content-Type: application/json' -d "${2}" "${API}/${1}" -} - -api_delete () { - curl -fsS -o /dev/null -X DELETE "${API}/${1}" -} - -api_state () { - curl -fsS "${API}/${1}" 2>/dev/null | jq -r .state -} - -wait_state () { - local path="${1}" want="${2}" i s - for i in $(seq 1 90); do - s=$(api_state "${path}") - [ "${s}" = "${want}" ] && return 0 - [ "${want}" = deleted ] && [ -z "${s}" ] && return 0 - [ "${s}" = error ] && { echo "${path}: error" >&2; return 1; } - sleep 2 - done - echo "${path}: ${s:-absent}, ${want} attendu" >&2 - return 1 -} - -vpc_create () { - api_state "vpcs/${1}" | grep -qx running && return 0 - api_post vpcs "{\"name\":\"${1}\",\"cidr\":\"${2}\"}" && wait_state "vpcs/${1}" running -} - -subnet_create () { - local name="${1}" vpc="${2}" vni="${3}" gw="${4}" cidr="${5}" extra="${6:-}" - api_state "subnets/${name}" | grep -qx running && return 0 - api_post subnets "{\"name\":\"${name}\",\"vpc\":\"${vpc}\",\"mode\":\"vxlan\",\"vxlan_id\":${vni},\"iface_type\":\"vms\",\"interface_ip\":\"${gw}\",\"cidr\":\"${cidr}\"${extra}}" \ - && wait_state "subnets/${name}" running -} - -two_image () { - [ -f "${TWO_IMAGE}" ] && return 0 - local work - work=$(mktemp -d) - curl -fsSL -o "${work}/${IMAGE_FILE}" "${IMAGE_URL}/${IMAGE_FILE}" || return 1 - curl -fsSL "${IMAGE_URL}/SHA512SUMS" | grep " ${IMAGE_FILE}\$" > "${work}/sums" || return 1 - (cd "${work}" && sha512sum -c sums >/dev/null) || { echo "image: somme SHA-512 incorrecte" >&2; return 1; } - command -v qemu-nbd >/dev/null || DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qemu-utils >/dev/null - modprobe nbd max_part=8 - qemu-nbd -c /dev/nbd0 "${work}/${IMAGE_FILE}" || return 1 - for i in $(seq 1 20); do [ -b /dev/nbd0p1 ] && break; sleep 0.5; done - mkdir -p /mnt/two-image - mount /dev/nbd0p1 /mnt/two-image || { qemu-nbd -d /dev/nbd0 >/dev/null; return 1; } - printf '%s\n' 'datasource_list: [ NoCloud ]' 'datasource:' ' NoCloud:' " seedfrom: 'http://169.254.169.254:80/'" ' timeout: 5' ' max_wait: 10' \ - > /mnt/two-image/etc/cloud/cloud.cfg.d/99_metadata.cfg - umount /mnt/two-image - qemu-nbd -d /dev/nbd0 >/dev/null - mv "${work}/${IMAGE_FILE}" "${TWO_IMAGE}" - rm -rf "${work}" -} - -vm_key () { - [ -f "${VM_KEY}" ] || ssh-keygen -q -t ed25519 -N '' -C lab-vm -f "${VM_KEY}" - cat "${VM_KEY}.pub" -} - -vm_create () { - local name="${1}" subnet="${2}" ip="${3}" key="${4:-}" meta="{}" - [ -n "${key}" ] && meta="{\"sshkey\":\"${key}\"}" - mkdir -p "${VOLUMES}" - cp "${TWO_IMAGE}" "${VOLUMES}/${name}.qcow2" || return 1 - api_post vms "{\"name\":\"${name}\",\"memory\":1024,\"cpus\":1,\"metadata\":${meta},\"interfaces\":[{\"subnet\":\"${subnet}\",\"ip\":\"${ip}\",\"primary\":true}],\"storage\":[{\"path\":\"${VOLUMES}/${name}.qcow2\",\"dev\":\"vda\"}]}" \ - && wait_state "vms/${name}" running -} - -vm_delete () { - api_delete "vms/${1}" && wait_state "vms/${1}" deleted -} - -vm_ssh () { - local vpc="${1}" ip="${2}" - shift 2 - ip netns exec "${vpc}" ssh -i "${VM_KEY}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ - -o LogLevel=ERROR -o ConnectTimeout=3 -o BatchMode=yes -n "syonad@${ip}" "$@" -} - -vm_wait () { - local i - for i in $(seq 1 40); do - vm_ssh "${1}" "${2}" true 2>/dev/null && return 0 - sleep 5 - done - echo "${2}: SSH injoignable" >&2 - return 1 -} - -vm_fails () { - local out - out=$(vm_ssh "${1}" "${2}" "${3} >/dev/null 2>&1; echo rc=\$?") || { echo "SSH vers ${2} en échec"; return 1; } - case "${out}" in - rc=0) echo "la commande a réussi dans ${2}"; return 1 ;; - rc=*) return 0 ;; - *) echo "sortie inattendue : ${out}"; return 1 ;; - esac -} - -vm_grep () { - vm_ssh "${1}" "${2}" "${3} 2>&1" | grep -q -- "${4}" -} - -has_addr () { - vm_ssh "${1}" "${2}" 'ip -4 -o addr show dev ens3' | grep -q " ${2}/" -} - -route_via () { - vm_ssh "${1}" "${2}" "ip -4 route show ${3}" | grep -q "via ${4} " -} - -dhcp_hosts () { - python3 - "/run/two/dhcp/${1}.sock" <<'PY' -import json, socket, sys -s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) -s.connect(sys.argv[1]) -s.sendall(b'{"verb":"get-state"}\n') -buf = b"" -while not buf.endswith(b"\n"): - c = s.recv(65536) - if not c: - break - buf += c -state = json.loads(buf).get("state") or {} -print(" ".join(sorted(h["mac"].lower() + "=" + h["ip"] for h in state.get("hosts") or []))) -PY -} diff --git a/scripts/lab/scenario.sh b/scripts/lab/scenario.sh deleted file mode 100755 index 58a6d24..0000000 --- a/scripts/lab/scenario.sh +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env bash - -LAB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -LAB_HOST="${LAB_HOST:-${LAB_DIR}/../lab-host.sh}" -SCENARIO_DIR="${SCENARIO_DIR:-${LAB_DIR}/scenarios}" -SCENARIO_LOG="" - -usage () { - cat >&2 <... | all - - scénarios : $(cd "${SCENARIO_DIR}" && ls -- *.sh | sed 's/\.sh$//' | tr '\n' ' ') - -Chaque scénario s'exécute depuis le Mac sur le lab en cours (lab-host.sh up, push, ./lab up). -Une ligne RÉUSSI ou ÉCHOUÉ par vérification ; code de sortie 1 si une vérification échoue ou -si aucune n'a été faite. -USAGE - exit 2 -} - -on () { - local node="${1}" assignment - shift - { - for assignment in "$@"; do - printf 'export %q=%q\n' "${assignment%%=*}" "${assignment#*=}" - done - cat "${LAB_DIR}/node.sh" - cat - } | "${LAB_HOST}" ssh "./lab ssh ${node} 'sudo bash -s'" 2>&1 | tee -a "${SCENARIO_LOG}" -} - -run_scenario () { - local file="${1}" name passed failed - name="$(basename "${file}" .sh)" - SCENARIO_LOG="$(mktemp)" - echo "=== ${name}" - ( . "${file}" ) - passed=$(grep -c '^RÉUSSI: ' "${SCENARIO_LOG}") - failed=$(grep -c '^ÉCHOUÉ: ' "${SCENARIO_LOG}") - rm -f "${SCENARIO_LOG}" - echo "=== ${name} : ${passed} réussi(s), ${failed} échoué(s)" - [[ "${failed}" -eq 0 && "${passed}" -gt 0 ]] -} - -main () { - local -a files=() - local arg file rc=0 - [[ $# -gt 0 ]] || usage - for arg in "$@"; do - if [[ "${arg}" == all ]]; then - files+=("${SCENARIO_DIR}"/*.sh) - continue - fi - file=$(ls "${SCENARIO_DIR}/${arg}"*.sh 2>/dev/null | head -n 1) - [[ -n "${file}" ]] || { echo "scénario inconnu : ${arg}" >&2; usage; } - files+=("${file}") - done - for file in "${files[@]}"; do - run_scenario "${file}" || rc=1 - done - return "${rc}" -} - -if [[ "${BASH_SOURCE[0]}" == "${0}" || -z "${BASH_SOURCE[0]}" ]]; then - main "$@" -fi diff --git a/scripts/lab/scenario_test.sh b/scripts/lab/scenario_test.sh deleted file mode 100755 index 0ca394c..0000000 --- a/scripts/lab/scenario_test.sh +++ /dev/null @@ -1,140 +0,0 @@ -#!/usr/bin/env bash - -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PASS=0 -FAIL=0 - -fail () { echo "FAIL: ${CURRENT}: ${1}"; FAIL=$(( FAIL + 1 )); CASE_OK=0; } - -setup () { - CURRENT="${1}" - CASE_OK=1 - WORK=$(mktemp -d "${TMPDIR:-/tmp}/scenario.XXXXXX") - mkdir -p "${WORK}/scenarios" - cat > "${WORK}/lab-host" <<'FAKE' -#!/usr/bin/env bash -N=$(ls "${FAKE_DIR}" | grep -c '^payload\.') -cat > "${FAKE_DIR}/payload.$(( N + 1 ))" -printf '%s\n' "$*" >> "${FAKE_DIR}/calls.log" -if ! bash -n "${FAKE_DIR}/payload.$(( N + 1 ))" 2>"${FAKE_DIR}/syntax.$(( N + 1 ))"; then - echo "ÉCHOUÉ: syntaxe du bloc $(( N + 1 ))" - exit 0 -fi -[ -n "${FAKE_OUT:-}" ] && printf '%b\n' "${FAKE_OUT}" -exit 0 -FAKE - chmod +x "${WORK}/lab-host" - : > "${WORK}/calls.log" -} - -run () { - env LAB_HOST="${WORK}/lab-host" FAKE_DIR="${WORK}" SCENARIO_DIR="${SCENARIO_DIR:-${WORK}/scenarios}" \ - FAKE_OUT="${FAKE_OUT:-}" bash "${DIR}/scenario.sh" "$@" > "${WORK}/out.log" 2>&1 -} - -teardown () { - [[ "${CASE_OK}" -eq 1 ]] && PASS=$(( PASS + 1 )) && echo "ok: ${CURRENT}" - [[ "${CASE_OK}" -eq 1 ]] || { echo " sortie :"; sed 's/^/ | /' "${WORK}/out.log"; } - rm -rf "${WORK}" -} - -node () { - bash -c ". '${DIR}/node.sh'; ${1}" 2>&1 -} - -test_on_sends_assignments_library_and_block_to_the_node () { - setup "on : variables, bibliothèque et bloc envoyés au bon nœud" - printf '%s\n' "on hv2 KEY='ssh-ed25519 AAAA x' NAME=vm <<'NODE'" 'echo "${NAME}"' 'NODE' > "${WORK}/scenarios/t1.sh" - FAKE_OUT='RÉUSSI: un' run t1 || fail "code de sortie $?" - [[ $(cat "${WORK}/calls.log") == "ssh ./lab ssh hv2 'sudo bash -s'" ]] || fail "appel : $(cat "${WORK}/calls.log")" - grep -q '^check () {' "${WORK}/payload.1" || fail "node.sh absent du bloc" - [[ $(tail -n 1 "${WORK}/payload.1") == 'echo "${NAME}"' ]] || fail "bloc du scénario absent en fin d'envoi" - [[ $(bash -c "$(grep '^export ' "${WORK}/payload.1"); printf '%s|%s' \"\${KEY}\" \"\${NAME}\"") == "ssh-ed25519 AAAA x|vm" ]] \ - || fail "variables mal transmises : $(grep '^export ' "${WORK}/payload.1")" - teardown -} - -test_scenario_result_counts_and_exit_codes () { - local CASE - for CASE in ok fail none; do - setup "résultat d'un scénario (${CASE})" - printf '%s\n' "on hv1 <<'NODE'" 'true' 'NODE' > "${WORK}/scenarios/t2.sh" - case "${CASE}" in - ok) FAKE_OUT='RÉUSSI: a\nRÉUSSI: b' run t2; [[ $? -eq 0 ]] || fail "code non nul" - grep -q '=== t2 : 2 réussi(s), 0 échoué(s)' "${WORK}/out.log" || fail "bilan absent" ;; - fail) FAKE_OUT='RÉUSSI: a\nÉCHOUÉ: b — détail' run t2; [[ $? -eq 1 ]] || fail "code différent de 1" - grep -q '=== t2 : 1 réussi(s), 1 échoué(s)' "${WORK}/out.log" || fail "bilan absent" ;; - none) FAKE_OUT='INFO: rien' run t2; [[ $? -eq 1 ]] || fail "un scénario sans vérification a réussi" ;; - esac - teardown - done -} - -test_unknown_scenario_and_no_argument () { - setup "scénario inconnu ou absent : usage, code 2" - run nope; [[ $? -eq 2 ]] || fail "scénario inconnu : code $?" - run; [[ $? -eq 2 ]] || fail "sans argument : code $?" - [[ -s "${WORK}/calls.log" ]] && fail "un nœud a été appelé" - teardown -} - -test_all_runs_every_scenario_and_fails_if_one_fails () { - setup "all : chaque scénario, échec si l'un échoue" - printf '%s\n' "on hv1 <<'NODE'" 'true' 'NODE' > "${WORK}/scenarios/a.sh" - printf '%s\n' "on hv1 <<'NODE'" 'true' 'NODE' > "${WORK}/scenarios/b.sh" - FAKE_OUT='ÉCHOUÉ: x' run all; [[ $? -eq 1 ]] || fail "code différent de 1" - [[ $(grep -c '^=== [ab] :' "${WORK}/out.log") -eq 2 ]] || fail "les deux scénarios n'ont pas tourné" - teardown -} - -test_every_shipped_block_is_valid_bash () { - local f - setup "chaque bloc envoyé par les scénarios livrés est du bash valide" - SCENARIO_DIR="${DIR}/scenarios" FAKE_OUT='RÉUSSI: x' run all - for f in "${WORK}"/syntax.*; do - [[ -s "${f}" ]] && fail "${f##*/} : $(cat "${f}")" - done - [[ $(ls "${WORK}" | grep -c '^payload\.') -ge 10 ]] || fail "trop peu de blocs envoyés : $(ls "${WORK}" | grep -c '^payload\.')" - grep -q 'ÉCHOUÉ: syntaxe' "${WORK}/out.log" && fail "bloc invalide" - teardown -} - -test_check_reports_success_and_failure_with_reason () { - setup "check : RÉUSSI, ou ÉCHOUÉ avec la dernière ligne de sortie" - [[ $(node 'check "a" true') == "RÉUSSI: a" ]] || fail "succès : $(node 'check "a" true')" - [[ $(node 'check "b" sh -c "echo un; echo deux >&2; exit 3"') == "ÉCHOUÉ: b — deux" ]] || fail "échec : $(node 'check "b" sh -c "echo un; echo deux >&2; exit 3"')" - [[ $(node 'check "c" false') == "ÉCHOUÉ: c" ]] || fail "échec muet : $(node 'check "c" false')" - teardown -} - -test_vm_fails_only_when_ssh_worked_and_command_failed () { - setup "vm_fails : réussit seulement si SSH marche et la commande échoue" - [[ $(node 'vm_ssh () { echo rc=1; }; vm_fails v 1.2.3.4 x && echo OUI') == "OUI" ]] || fail "commande échouée non reconnue" - [[ $(node 'vm_ssh () { echo rc=0; }; vm_fails v 1.2.3.4 x || echo NON') == *NON ]] || fail "commande réussie prise pour un échec" - [[ $(node 'vm_ssh () { return 255; }; vm_fails v 1.2.3.4 x || echo NON') == *NON ]] || fail "SSH en panne pris pour une isolation" - [[ $(node 'vm_ssh () { echo bizarre; }; vm_fails v 1.2.3.4 x || echo NON') == *NON ]] || fail "sortie inattendue acceptée" - teardown -} - -test_route_via_matches_the_next_hop_exactly () { - setup "route_via : le next-hop exact, pas un préfixe" - [[ $(node 'vm_ssh () { echo "default via 10.220.1.1 dev ens3 proto dhcp"; }; route_via v ip default 10.220.1.1 && echo OUI') == OUI ]] || fail "next-hop exact refusé" - [[ $(node 'vm_ssh () { echo "default via 10.220.1.10 dev ens3"; }; route_via v ip default 10.220.1.1 || echo NON') == NON ]] || fail "10.220.1.10 pris pour 10.220.1.1" - teardown -} - -test_wait_state () { - setup "wait_state : état atteint, erreur, suppression" - [[ $(node 'api_state () { echo running; }; wait_state vms/x running && echo OUI') == OUI ]] || fail "running" - local start=${SECONDS} - [[ $(node 'api_state () { echo error; }; wait_state vms/x running 2>&1 || echo NON') == *NON ]] || fail "error accepté" - (( SECONDS - start < 5 )) || fail "une ressource en error n'arrête pas l'attente immédiatement ($(( SECONDS - start )) s)" - [[ $(node 'api_state () { echo; }; wait_state vms/x deleted && echo OUI') == OUI ]] || fail "absence = supprimé" - teardown -} - -for T in $(declare -F | awk '{print $3}' | grep '^test_'); do - "${T}" -done -echo "${PASS} réussi(s), ${FAIL} échec(s)" -[[ "${FAIL}" -eq 0 ]] diff --git a/scripts/lab/scenarios/s1-dhcp-two.sh b/scripts/lab/scenarios/s1-dhcp-two.sh deleted file mode 100644 index bbf727c..0000000 --- a/scripts/lab/scenarios/s1-dhcp-two.sh +++ /dev/null @@ -1,34 +0,0 @@ -on hv1 <<'NODE' -two_image || { echo "ÉCHOUÉ: image compatible two"; exit 0; } -KEY=$(vm_key) -check "VPC vp-s1" vpc_create vp-s1 10.210.0.0/16 -check "subnet sn-s1a" subnet_create sn-s1a vp-s1 2101 10.210.1.1 10.210.1.0/24 -check "subnet sn-s1b" subnet_create sn-s1b vp-s1 2102 10.210.2.1 10.210.2.0/24 -check "backend two : socket de contrôle de sn-s1a" test -S /run/two/dhcp/vp-s1_br-s1a.sock -check "backend two : socket de contrôle de sn-s1b" test -S /run/two/dhcp/vp-s1_br-s1b.sock -check "VM s1-a1, démarrée seule" vm_create s1-a1 sn-s1a 10.210.1.10 "${KEY}" -check "VM s1-a1 joignable" vm_wait vp-s1 10.210.1.10 -check "VM s1-b1, démarrée seule" vm_create s1-b1 sn-s1b 10.210.2.10 "${KEY}" -check "VM s1-b1 joignable" vm_wait vp-s1 10.210.2.10 -vm_create s1-a2 sn-s1a 10.210.1.11 "${KEY}" > /tmp/s1-a2.log 2>&1 & A=$! -vm_create s1-b2 sn-s1b 10.210.2.11 "${KEY}" > /tmp/s1-b2.log 2>&1 & B=$! -wait "${A}"; RA=$? -wait "${B}"; RB=$? -check "VM s1-a2, démarrée en même temps que s1-b2" test "${RA}" -eq 0 -check "VM s1-b2, démarrée en même temps que s1-a2" test "${RB}" -eq 0 -for vm in 10.210.1.10:10.210.1.1 10.210.1.11:10.210.1.1 10.210.2.10:10.210.2.1 10.210.2.11:10.210.2.1; do - ip=${vm%%:*}; gw=${vm#*:} - check "${ip} joignable" vm_wait vp-s1 "${ip}" - check "${ip} : adresse de son propre subnet" has_addr vp-s1 "${ip}" - check "${ip} : bail DHCP tenu par systemd-networkd" vm_ssh vp-s1 "${ip}" 'ls /run/systemd/netif/leases/ | grep -q .' - check "${ip} : route par défaut via ${gw}" route_via vp-s1 "${ip}" default "${gw}" - check "${ip} : route vers la VPC via ${gw}" route_via vp-s1 "${ip}" 10.210.0.0/16 "${gw}" - check "${ip} : route /32 vers les métadonnées via ${gw}" route_via vp-s1 "${ip}" 169.254.169.254 "${gw}" -done -vm_host () { echo "$(vm_ssh vp-s1 "${1}" 'cat /sys/class/net/ens3/address')=${1}"; } -A_HOSTS=$(printf '%s\n' "$(vm_host 10.210.1.10)" "$(vm_host 10.210.1.11)" | sort | tr '\n' ' ' | sed 's/ $//') -B_HOSTS=$(printf '%s\n' "$(vm_host 10.210.2.10)" "$(vm_host 10.210.2.11)" | sort | tr '\n' ' ' | sed 's/ $//') -info "sn-s1a : ${A_HOSTS} ; sn-s1b : ${B_HOSTS}" -check "probe sn-s1a : uniquement les couples MAC/IP de sn-s1a" test "$(dhcp_hosts vp-s1_br-s1a)" = "${A_HOSTS}" -check "probe sn-s1b : uniquement les couples MAC/IP de sn-s1b" test "$(dhcp_hosts vp-s1_br-s1b)" = "${B_HOSTS}" -NODE diff --git a/scripts/lab/scenarios/s2-gateway.sh b/scripts/lab/scenarios/s2-gateway.sh deleted file mode 100644 index 4785731..0000000 --- a/scripts/lab/scenarios/s2-gateway.sh +++ /dev/null @@ -1,26 +0,0 @@ -on hv1 <<'NODE' -two_image || { echo "ÉCHOUÉ: image compatible two"; exit 0; } -KEY=$(vm_key) -check "VPC vp-s2" vpc_create vp-s2 10.220.0.0/16 -check "subnet sn-s2a, default_route absent" subnet_create sn-s2a vp-s2 2201 10.220.1.1 10.220.1.0/24 -check "subnet sn-s2b, default_route et gateway 10.220.2.254" subnet_create sn-s2b vp-s2 2202 10.220.2.1 10.220.2.0/24 ',"default_route":true,"gateway":"10.220.2.254"' -check "VM s2-a" vm_create s2-a sn-s2a 10.220.1.10 "${KEY}" -check "VM s2-b" vm_create s2-b sn-s2b 10.220.2.10 "${KEY}" -check "s2-a joignable" vm_wait vp-s2 10.220.1.10 -check "s2-b joignable" vm_wait vp-s2 10.220.2.10 -check "sn-s2a : route par défaut via interface_ip 10.220.1.1" route_via vp-s2 10.220.1.10 default 10.220.1.1 -check "sn-s2a : route vers la VPC via interface_ip" route_via vp-s2 10.220.1.10 10.220.0.0/16 10.220.1.1 -check "sn-s2a : route /32 vers les métadonnées via interface_ip" route_via vp-s2 10.220.1.10 169.254.169.254 10.220.1.1 -check "sn-s2b : route par défaut via la gateway 10.220.2.254" route_via vp-s2 10.220.2.10 default 10.220.2.254 -check "sn-s2b : route vers la VPC via interface_ip, pas via la gateway" route_via vp-s2 10.220.2.10 10.220.0.0/16 10.220.2.1 -check "sn-s2b : route /32 vers les métadonnées via interface_ip" route_via vp-s2 10.220.2.10 169.254.169.254 10.220.2.1 -check "suppression de s2-a" vm_delete s2-a -check "suppression de sn-s2a" api_delete subnets/sn-s2a -check "sn-s2a supprimé" wait_state subnets/sn-s2a deleted -check "sn-s2a recréé" subnet_create sn-s2a vp-s2 2201 10.220.1.1 10.220.1.0/24 -check "VM s2-a2 sur le subnet recréé" vm_create s2-a2 sn-s2a 10.220.1.11 "${KEY}" -check "s2-a2 joignable" vm_wait vp-s2 10.220.1.11 -check "après recréation : route par défaut via interface_ip 10.220.1.1" route_via vp-s2 10.220.1.11 default 10.220.1.1 -check "après recréation : route vers la VPC via interface_ip" route_via vp-s2 10.220.1.11 10.220.0.0/16 10.220.1.1 -check "après recréation : route /32 vers les métadonnées" route_via vp-s2 10.220.1.11 169.254.169.254 10.220.1.1 -NODE diff --git a/scripts/lab/scenarios/s3-isolation-local.sh b/scripts/lab/scenarios/s3-isolation-local.sh deleted file mode 100644 index dc822f5..0000000 --- a/scripts/lab/scenarios/s3-isolation-local.sh +++ /dev/null @@ -1,16 +0,0 @@ -on hv1 <<'NODE' -two_image || { echo "ÉCHOUÉ: image compatible two"; exit 0; } -KEY=$(vm_key) -check "VPC vp-s3a" vpc_create vp-s3a 10.230.0.0/16 -check "VPC vp-s3b" vpc_create vp-s3b 10.231.0.0/16 -check "subnet sn-s3a" subnet_create sn-s3a vp-s3a 2301 10.230.1.1 10.230.1.0/24 -check "subnet sn-s3b" subnet_create sn-s3b vp-s3b 2302 10.231.1.1 10.231.1.0/24 -check "VM s3-x dans vp-s3a" vm_create s3-x sn-s3a 10.230.1.10 "${KEY}" -check "VM s3-y dans vp-s3b" vm_create s3-y sn-s3b 10.231.1.10 "${KEY}" -check "s3-x joignable depuis sa VPC" vm_wait vp-s3a 10.230.1.10 -check "s3-y joignable depuis sa VPC (témoin : la cible est vivante)" vm_wait vp-s3b 10.231.1.10 -check "s3-x joint sa passerelle (témoin)" vm_ssh vp-s3a 10.230.1.10 'ping -c 2 -W 2 10.230.1.1' -check "s3-x ne joint pas s3-y en ICMP" vm_fails vp-s3a 10.230.1.10 'ping -c 2 -W 2 10.231.1.10' -check "s3-x n'ouvre pas de connexion TCP vers s3-y:22" vm_fails vp-s3a 10.230.1.10 'timeout 5 bash -c "/dev/null 2>&1 && break; sleep 5; done -check "s4-p (hv1) joint s4-q (hv2)" vm_ssh vp-s4 10.240.1.10 'ping -c 3 -W 2 10.240.1.11' -check "trame pleine taille : 1472 octets en -M do" vm_ssh vp-s4 10.240.1.10 'ping -M do -s 1472 -c 2 -W 2 10.240.1.11' -check "1473 octets refusés localement (MTU 1500)" vm_grep vp-s4 10.240.1.10 'ping -M do -s 1473 -c 1 -W 2 10.240.1.11' 'message too long' -NODE diff --git a/scripts/lab/scenarios/s5-isolation-evpn.sh b/scripts/lab/scenarios/s5-isolation-evpn.sh deleted file mode 100644 index a761056..0000000 --- a/scripts/lab/scenarios/s5-isolation-evpn.sh +++ /dev/null @@ -1,23 +0,0 @@ -on hv1 <<'NODE' -two_image || { echo "ÉCHOUÉ: image compatible two (hv1)"; exit 0; } -KEY=$(vm_key) -check "hv1 : VPC vp-s5a" vpc_create vp-s5a 10.245.0.0/16 -check "hv1 : subnet sn-s5a (VNI 2501)" subnet_create sn-s5a vp-s5a 2501 10.245.1.1 10.245.1.0/24 -check "hv1 : VM s5-r dans vp-s5a" vm_create s5-r sn-s5a 10.245.1.10 "${KEY}" -NODE -on hv2 <<'NODE' -two_image || { echo "ÉCHOUÉ: image compatible two (hv2)"; exit 0; } -check "hv2 : VPC vp-s5a" vpc_create vp-s5a 10.245.0.0/16 -check "hv2 : subnet sn-s5a (VNI 2501)" subnet_create sn-s5a vp-s5a 2501 10.245.1.1 10.245.1.0/24 -check "hv2 : VM s5-t dans vp-s5a (témoin)" vm_create s5-t sn-s5a 10.245.1.12 -check "hv2 : VPC vp-s5b, même plage" vpc_create vp-s5b 10.245.0.0/16 -check "hv2 : subnet sn-s5b (VNI 2502), même plage" subnet_create sn-s5b vp-s5b 2502 10.245.1.1 10.245.1.0/24 -check "hv2 : VM s5-s dans vp-s5b" vm_create s5-s sn-s5b 10.245.1.11 -NODE -on hv1 <<'NODE' -check "s5-r joignable" vm_wait vp-s5a 10.245.1.10 -for i in $(seq 1 24); do vm_ssh vp-s5a 10.245.1.10 'ping -c 1 -W 2 10.245.1.12' >/dev/null 2>&1 && break; sleep 5; done -check "s5-r joint s5-t, même VPC sur hv2 (témoin)" vm_ssh vp-s5a 10.245.1.10 'ping -c 3 -W 2 10.245.1.12' -check "s5-r ne joint pas s5-s, autre VPC sur hv2, même plage" vm_fails vp-s5a 10.245.1.10 'ping -c 3 -W 2 10.245.1.11' -check "aucune entrée ARP résolue pour 10.245.1.11 dans s5-r" vm_fails vp-s5a 10.245.1.10 'ip neigh show 10.245.1.11 | grep -q lladdr' -NODE diff --git a/scripts/lab/scenarios/s6-rr-loss.sh b/scripts/lab/scenarios/s6-rr-loss.sh deleted file mode 100644 index e4ea28e..0000000 --- a/scripts/lab/scenarios/s6-rr-loss.sh +++ /dev/null @@ -1,36 +0,0 @@ -on hv1 <<'NODE' -check "prérequis : VM s4-p de S4 en marche" test "$(api_state vms/s4-p)" = running -check "session EVPN de hv1 établie avant l'arrêt" sh -c "vtysh -c 'show bgp neighbors 10.255.255.1' | grep -q 'BGP state = Established'" -check "prérequis : s4-p joint s4-q" vm_ssh vp-s4 10.240.1.10 'ping -c 2 -W 2 10.240.1.11' -vm_ssh vp-s4 10.240.1.10 'rm -f /tmp/s6.log; nohup ping -D -i 0.2 10.240.1.11 > /tmp/s6.log 2>&1 &' -sleep 5 -NODE -on rr1 <<'NODE' -date +%s > /tmp/s6.stop -check "arrêt de FRR sur le route reflector" systemctl stop frr -NODE -sleep 30 -on hv1 <<'NODE' -recent () { vm_ssh vp-s4 10.240.1.10 "now=\$(date +%s); awk -F'[][]' -v now=\$now '/bytes from/ && \$2 > now - ${1}' /tmp/s6.log | wc -l"; } -check "session EVPN de hv1 tombée" sh -c "! vtysh -c 'show bgp neighbors 10.255.255.1' | grep -q 'BGP state = Established'" -info "30 s après l'arrêt du route reflector : $(recent 10) réponses dans les 10 dernières secondes (50 si le trafic passe intégralement)" -info "VTEP distant encore connu : $(vtysh -c 'show evpn vni 2401' | grep -c 192.168.14.12) ; entrée d'inondation : $(bridge fdb show dev vxlan-2401 | grep -c '00:00:00:00:00:00 dst')" -sleep 60 -info "90 s après l'arrêt : $(recent 10) réponses dans les 10 dernières secondes" -NODE -on rr1 <<'NODE' -check "redémarrage de FRR sur le route reflector" systemctl start frr -date +%s > /tmp/s6.start -NODE -on hv1 <<'NODE' -recent () { vm_ssh vp-s4 10.240.1.10 "now=\$(date +%s); awk -F'[][]' -v now=\$now '/bytes from/ && \$2 > now - ${1}' /tmp/s6.log | wc -l"; } -start=$(date +%s) -for i in $(seq 1 90); do - vtysh -c "show evpn vni 2401" | grep -q 192.168.14.12 && [ "$(recent 3)" -gt 0 ] && break - sleep 2 -done -info "retour du VTEP distant et du trafic $(( $(date +%s) - start )) s après le redémarrage de FRR" -check "après le retour du route reflector : VTEP distant réappris" sh -c "vtysh -c 'show evpn vni 2401' | grep -q 192.168.14.12" -check "après le retour du route reflector : le trafic passe" test "$(recent 5)" -gt 0 -vm_ssh vp-s4 10.240.1.10 'pkill -x ping' || true -NODE