Compare commits
2 commits
f9aadc00fc
...
909b4110c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
909b4110c2 |
|||
|
b4660c95d3 |
26 changed files with 800 additions and 33 deletions
|
|
@ -135,7 +135,12 @@ func renderCmd(args []string, stdout, stderr io.Writer) int {
|
||||||
fmt.Fprintf(stderr, "lab: %v\n", err)
|
fmt.Fprintf(stderr, "lab: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
nodes, err := render.Render(p, render.Options{RunDir: dir, AuthorizedKeys: authorized, FRR: frr})
|
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})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "lab: %v\n", err)
|
fmt.Fprintf(stderr, "lab: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
2
conf/lab/agent/two.yml
Normal file
2
conf/lab/agent/two.yml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
dhcp:
|
||||||
|
backend: two
|
||||||
|
|
@ -21,7 +21,8 @@ nodes:
|
||||||
loopback: 10.255.255.1/32, frr: frr/rr1.conf }
|
loopback: 10.255.255.1/32, frr: frr/rr1.conf }
|
||||||
hv1:
|
hv1:
|
||||||
{ role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay],
|
{ role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay],
|
||||||
addresses: { underlay: 192.168.14.11 }, frr: frr/hv1.conf, release: 0.2.0rc002 }
|
addresses: { underlay: 192.168.14.11 }, frr: frr/hv1.conf, release: 0.2.0rc003,
|
||||||
|
agent: agent/two.yml }
|
||||||
hv2:
|
hv2:
|
||||||
{ role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay],
|
{ role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay],
|
||||||
addresses: { underlay: 192.168.14.12 }, frr: frr/hv2.conf, release: 0.2.0rc002 }
|
addresses: { underlay: 192.168.14.12 }, frr: frr/hv2.conf, release: 0.2.0rc003 }
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,21 @@ l'adresse du lien en secondaire et la loopback sur ``lo1`` :
|
||||||
- up des deux côtés
|
- up des deux côtés
|
||||||
* - sessions des deux hyperviseurs vers ``10.255.255.1``
|
* - sessions des deux hyperviseurs vers ``10.255.255.1``
|
||||||
- Established, voisins dynamiques, iBGP AS 64600, famille L2VPN EVPN négociée
|
- Established, voisins dynamiques, iBGP AS 64600, famille L2VPN EVPN négociée
|
||||||
* - routes EVPN échangées
|
* - routes EVPN échangées, VM ↔ VM entre deux hyperviseurs
|
||||||
- aucune : rien à annoncer tant que two n'a pas créé de VXLAN — voir
|
- routes de type 3 échangées, ping et MTU 1500 de bout en bout — à partir de la release
|
||||||
:doc:`premier-hyperviseur`
|
``0.2.0rc003``, qui pose l'adresse VTEP locale des VXLAN
|
||||||
|
(`#51 <https://git.g3e.fr/syonad/two/issues/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é.
|
||||||
|
|
||||||
Le routeur du lab n'est qu'une configuration minimale écrite pour l'essai, pas celle des routeurs
|
Le routeur du lab n'est qu'une configuration minimale écrite pour l'essai, pas celle des routeurs
|
||||||
de cluster (:doc:`routeurs`).
|
de cluster (:doc:`routeurs`).
|
||||||
|
|
@ -109,9 +121,8 @@ 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
|
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` ;
|
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 redondance : une seule VM route reflector, ou deux, et sur quels hyperviseurs ;
|
||||||
* la procédure de reconstruction, et l'état du cluster pendant que le route reflector est
|
* la procédure de reconstruction — l'état du cluster pendant l'absence du route reflector est
|
||||||
absent — les tunnels déjà établis continuent-ils de fonctionner, et pendant combien de
|
mesuré ci-dessus : plus de trafic entre hyperviseurs ;
|
||||||
temps ;
|
|
||||||
* la procédure d'amorçage : ce qui fonctionne, et dans quel ordre, quand on démarre un cluster
|
* 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
|
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`.
|
n'existe pas, cf. :doc:`premier-hyperviseur`.
|
||||||
|
|
|
||||||
|
|
@ -230,8 +230,12 @@ Ce que le fichier déclare :
|
||||||
* ``frr`` — le chemin d'un ``frr.conf``, relatif au fichier de topologie : FRR est installé
|
* ``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`_).
|
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
|
* ``release`` — **obligatoire pour un hyperviseur**, refusé ailleurs : le tag de la release de
|
||||||
two que ``deploy.sh`` installe (``release: 0.2.0rc002``). Sans lui, ``deploy.sh`` prendrait la
|
two que ``deploy.sh`` installe (``release: 0.2.0rc003``). Sans lui, ``deploy.sh`` prendrait la
|
||||||
dernière release, et le lab ne serait plus reproductible.
|
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.
|
``mgmt0`` et ``lo1`` sont réservés : aucun segment ne peut porter ces noms.
|
||||||
|
|
||||||
|
|
@ -278,11 +282,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
|
hv2 hypervisor debian12 4 16384 MiB 127.0.0.1:2203
|
||||||
|
|
||||||
roles
|
roles
|
||||||
name loopback secondary frr release
|
name loopback secondary frr release agent
|
||||||
sw1 - underlay 169.254.0.1/28 sw1.conf -
|
sw1 - underlay 169.254.0.1/28 sw1.conf - -
|
||||||
rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf -
|
rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf - -
|
||||||
hv1 - - hv1.conf 0.2.0rc002
|
hv1 - - hv1.conf 0.2.0rc003 two.yml
|
||||||
hv2 - - hv2.conf 0.2.0rc002
|
hv2 - - hv2.conf 0.2.0rc003 -
|
||||||
|
|
||||||
segment underlay: 192.168.14.0/24, mtu 9000, switch sw1, bridge br-underlay, gateway 192.168.14.1
|
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
|
node interface address mac udp switch port mac udp
|
||||||
|
|
@ -660,6 +664,97 @@ 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
|
* - ``lab down`` puis ``lab up`` d'une autre topologie
|
||||||
- conforme
|
- 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 <nœud> [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
|
Facturation
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|
@ -751,6 +846,7 @@ Tests
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
bash scripts/lab-host_test.sh
|
bash scripts/lab-host_test.sh
|
||||||
|
bash scripts/lab/scenario_test.sh
|
||||||
go test ./internal/lab/... ./cmd/lab/
|
go test ./internal/lab/... ./cmd/lab/
|
||||||
|
|
||||||
Environ une minute et demie, sans réseau : la suite remplace ``curl`` par une fausse API Scaleway
|
Environ une minute et demie, sans réseau : la suite remplace ``curl`` par une fausse API Scaleway
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,11 @@ func Prepare(ctx context.Context, p *topology.Plan, o Options) ([]render.Node, e
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
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 {
|
for _, n := range p.Nodes {
|
||||||
if n.FRR == "" {
|
if path(n) == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
data, err := os.ReadFile(n.FRR)
|
data, err := os.ReadFile(path(n))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("node %s: %w", n.Name, err)
|
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) {
|
func EnsureKey(ctx context.Context, r Runner, dir string) (string, error) {
|
||||||
|
|
|
||||||
|
|
@ -385,3 +385,23 @@ func TestPrepare_PutsTheFRRConfigIntoTheSeed(t *testing.T) {
|
||||||
}
|
}
|
||||||
t.Errorf("hv1 user-data has no /etc/lab/frr.conf:\n%s", data)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ type Options struct {
|
||||||
RunDir string
|
RunDir string
|
||||||
AuthorizedKeys []string
|
AuthorizedKeys []string
|
||||||
FRR map[string]string
|
FRR map[string]string
|
||||||
|
Agent map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Node struct {
|
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 {
|
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)
|
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
|
var nodes []Node
|
||||||
for index, n := range p.Nodes {
|
for index, n := range p.Nodes {
|
||||||
|
|
|
||||||
|
|
@ -262,3 +262,37 @@ func TestRoles_SwitchLoopbackIsCreatedByTheSwitchScript(t *testing.T) {
|
||||||
t.Errorf("lab-switch:\n%s\ndoes not end with:\n%s", script, want)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const (
|
||||||
DeployScript = "/usr/local/sbin/lab-deploy"
|
DeployScript = "/usr/local/sbin/lab-deploy"
|
||||||
TwoScriptsDir = "/opt/two/scripts"
|
TwoScriptsDir = "/opt/two/scripts"
|
||||||
TwoGitServer = "https://git.g3e.fr/"
|
TwoGitServer = "https://git.g3e.fr/"
|
||||||
|
AgentConfig = "/etc/two/agent.yml"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed frrouting.gpg
|
//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")
|
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 {
|
if n.Role == topology.RoleHypervisor {
|
||||||
cfg.WriteFiles = append(cfg.WriteFiles,
|
cfg.WriteFiles = append(cfg.WriteFiles,
|
||||||
writeFile{Path: TwoScriptsDir + "/deploy.sh", Permissions: "0755", Encoding: "b64", Content: base64.StdEncoding.EncodeToString(scripts.Deploy)},
|
writeFile{Path: TwoScriptsDir + "/deploy.sh", Permissions: "0755", Encoding: "b64", Content: base64.StdEncoding.EncodeToString(scripts.Deploy)},
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,15 @@ func (p *Plan) Write(w io.Writer) error {
|
||||||
|
|
||||||
var extras []NodePlan
|
var extras []NodePlan
|
||||||
for _, n := range p.Nodes {
|
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)
|
extras = append(extras, n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(extras) > 0 {
|
if len(extras) > 0 {
|
||||||
fmt.Fprintf(tw, "\nroles\n")
|
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 {
|
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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ type NodePlan struct {
|
||||||
Loopback netip.Prefix
|
Loopback netip.Prefix
|
||||||
FRR string
|
FRR string
|
||||||
Release string
|
Release string
|
||||||
|
Agent string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Cable struct {
|
type Cable struct {
|
||||||
|
|
@ -78,6 +79,7 @@ func Compute(t *Topology) (*Plan, error) {
|
||||||
SSHPort: SSHBasePort + i,
|
SSHPort: SSHBasePort + i,
|
||||||
FRR: n.FRR,
|
FRR: n.FRR,
|
||||||
Release: n.Release,
|
Release: n.Release,
|
||||||
|
Agent: n.Agent,
|
||||||
}
|
}
|
||||||
for segment, raws := range n.Secondary {
|
for segment, raws := range n.Secondary {
|
||||||
for _, raw := range raws {
|
for _, raw := range raws {
|
||||||
|
|
|
||||||
|
|
@ -315,9 +315,9 @@ nodes
|
||||||
hv2 hypervisor deb 4 16384 MiB 127.0.0.1:2203
|
hv2 hypervisor deb 4 16384 MiB 127.0.0.1:2203
|
||||||
|
|
||||||
roles
|
roles
|
||||||
name loopback secondary frr release
|
name loopback secondary frr release agent
|
||||||
hv1 - - - 0.2.0rc002
|
hv1 - - - 0.2.0rc002 -
|
||||||
hv2 - - - 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
|
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
|
node interface address mac udp switch port mac udp
|
||||||
|
|
|
||||||
|
|
@ -142,10 +142,10 @@ func TestWrite_ShowsTheRoles(t *testing.T) {
|
||||||
}
|
}
|
||||||
want := `
|
want := `
|
||||||
roles
|
roles
|
||||||
name loopback secondary frr release
|
name loopback secondary frr release agent
|
||||||
sw1 - underlay 169.254.0.1/28 sw1.conf -
|
sw1 - underlay 169.254.0.1/28 sw1.conf - -
|
||||||
rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf -
|
rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf - -
|
||||||
hv1 - - hv1.conf 0.2.0rc002
|
hv1 - - hv1.conf 0.2.0rc002 -
|
||||||
`
|
`
|
||||||
if !bytes.Contains(buf.Bytes(), []byte(want)) {
|
if !bytes.Contains(buf.Bytes(), []byte(want)) {
|
||||||
t.Errorf("plan:\n%s\ndoes not contain:\n%s", buf.String(), 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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ type Node struct {
|
||||||
Loopback string
|
Loopback string
|
||||||
FRR string
|
FRR string
|
||||||
Release string
|
Release string
|
||||||
|
Agent string
|
||||||
}
|
}
|
||||||
|
|
||||||
type fileImage struct {
|
type fileImage struct {
|
||||||
|
|
@ -74,6 +75,7 @@ type fileNode struct {
|
||||||
Loopback string `yaml:"loopback"`
|
Loopback string `yaml:"loopback"`
|
||||||
FRR string `yaml:"frr"`
|
FRR string `yaml:"frr"`
|
||||||
Release string `yaml:"release"`
|
Release string `yaml:"release"`
|
||||||
|
Agent string `yaml:"agent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type file struct {
|
type file struct {
|
||||||
|
|
@ -96,6 +98,9 @@ func Load(path string) (*Topology, error) {
|
||||||
if n.FRR != "" && !filepath.IsAbs(n.FRR) {
|
if n.FRR != "" && !filepath.IsAbs(n.FRR) {
|
||||||
t.Nodes[i].FRR = filepath.Join(filepath.Dir(path), 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
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +149,7 @@ func Parse(data []byte) (*Topology, error) {
|
||||||
Loopback: n.Loopback,
|
Loopback: n.Loopback,
|
||||||
FRR: n.FRR,
|
FRR: n.FRR,
|
||||||
Release: n.Release,
|
Release: n.Release,
|
||||||
|
Agent: n.Agent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return t, nil
|
return t, nil
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,9 @@ func validateExtras(n Node, segments map[string]Segment, add func(string, ...any
|
||||||
case n.Role != RoleHypervisor && n.Release != "":
|
case n.Role != RoleHypervisor && n.Release != "":
|
||||||
add("node %s: release is only for hypervisors", n.Name)
|
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 != "" {
|
if n.Loopback != "" {
|
||||||
prefix, err := netip.ParsePrefix(n.Loopback)
|
prefix, err := netip.ParsePrefix(n.Loopback)
|
||||||
switch {
|
switch {
|
||||||
|
|
|
||||||
151
scripts/lab/node.sh
Normal file
151
scripts/lab/node.sh
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
67
scripts/lab/scenario.sh
Executable file
67
scripts/lab/scenario.sh
Executable file
|
|
@ -0,0 +1,67 @@
|
||||||
|
#!/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 <<USAGE
|
||||||
|
usage: ${0##*/} <scénario>... | 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
|
||||||
140
scripts/lab/scenario_test.sh
Executable file
140
scripts/lab/scenario_test.sh
Executable file
|
|
@ -0,0 +1,140 @@
|
||||||
|
#!/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 ]]
|
||||||
34
scripts/lab/scenarios/s1-dhcp-two.sh
Normal file
34
scripts/lab/scenarios/s1-dhcp-two.sh
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
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
|
||||||
26
scripts/lab/scenarios/s2-gateway.sh
Normal file
26
scripts/lab/scenarios/s2-gateway.sh
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
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
|
||||||
16
scripts/lab/scenarios/s3-isolation-local.sh
Normal file
16
scripts/lab/scenarios/s3-isolation-local.sh
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
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/tcp/10.231.1.10/22"'
|
||||||
|
check "s3-y ne joint pas s3-x en ICMP" vm_fails vp-s3b 10.231.1.10 'ping -c 2 -W 2 10.230.1.10'
|
||||||
|
NODE
|
||||||
25
scripts/lab/scenarios/s4-evpn.sh
Normal file
25
scripts/lab/scenarios/s4-evpn.sh
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
on hv1 <<'NODE'
|
||||||
|
two_image || { echo "ÉCHOUÉ: image compatible two (hv1)"; exit 0; }
|
||||||
|
KEY=$(vm_key)
|
||||||
|
check "hv1 : VPC vp-s4" vpc_create vp-s4 10.240.0.0/16
|
||||||
|
check "hv1 : subnet sn-s4" subnet_create sn-s4 vp-s4 2401 10.240.1.1 10.240.1.0/24
|
||||||
|
check "hv1 : vxlan-2401 porte l'adresse VTEP locale 192.168.14.11" sh -c "ip -d link show vxlan-2401 | grep -q 'local 192.168.14.11 '"
|
||||||
|
check "hv1 : VM s4-p" vm_create s4-p sn-s4 10.240.1.10 "${KEY}"
|
||||||
|
NODE
|
||||||
|
on hv2 <<'NODE'
|
||||||
|
two_image || { echo "ÉCHOUÉ: image compatible two (hv2)"; exit 0; }
|
||||||
|
check "hv2 : VPC vp-s4" vpc_create vp-s4 10.240.0.0/16
|
||||||
|
check "hv2 : subnet sn-s4" subnet_create sn-s4 vp-s4 2401 10.240.1.1 10.240.1.0/24
|
||||||
|
check "hv2 : vxlan-2401 porte l'adresse VTEP locale 192.168.14.12" sh -c "ip -d link show vxlan-2401 | grep -q 'local 192.168.14.12 '"
|
||||||
|
check "hv2 : VM s4-q" vm_create s4-q sn-s4 10.240.1.11
|
||||||
|
NODE
|
||||||
|
on hv1 <<'NODE'
|
||||||
|
check "s4-p joignable" vm_wait vp-s4 10.240.1.10
|
||||||
|
for i in $(seq 1 30); do vtysh -c "show evpn vni 2401" | grep -q 192.168.14.12 && break; sleep 2; done
|
||||||
|
check "hv1 : VTEP distant 192.168.14.12 appris par EVPN pour la VNI 2401" sh -c "vtysh -c 'show evpn vni 2401' | grep -q '192.168.14.12'"
|
||||||
|
check "hv1 : entrée d'inondation vers 192.168.14.12" sh -c "bridge fdb show dev vxlan-2401 | grep -q '00:00:00:00:00:00 dst 192.168.14.12'"
|
||||||
|
for i in $(seq 1 24); do vm_ssh vp-s4 10.240.1.10 'ping -c 1 -W 2 10.240.1.11' >/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
|
||||||
23
scripts/lab/scenarios/s5-isolation-evpn.sh
Normal file
23
scripts/lab/scenarios/s5-isolation-evpn.sh
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
||||||
36
scripts/lab/scenarios/s6-rr-loss.sh
Normal file
36
scripts/lab/scenarios/s6-rr-loss.sh
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue