diff --git a/cmd/lab/main.go b/cmd/lab/main.go index df86e39..3dc6425 100644 --- a/cmd/lab/main.go +++ b/cmd/lab/main.go @@ -130,12 +130,7 @@ func renderCmd(args []string, stdout, stderr io.Writer) int { fmt.Fprintf(stderr, "lab: %v\n", err) return 1 } - frr, err := provision.ReadFRR(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}) + nodes, err := render.Render(p, render.Options{RunDir: dir, AuthorizedKeys: authorized}) if err != nil { fmt.Fprintf(stderr, "lab: %v\n", err) return 1 diff --git a/cmd/lab/main_test.go b/cmd/lab/main_test.go index fe17d78..e8972b8 100644 --- a/cmd/lab/main_test.go +++ b/cmd/lab/main_test.go @@ -33,9 +33,8 @@ func TestRun_PlanOfTheShippedExampleTopology(t *testing.T) { } for _, want := range []string{ "lab evpn-2hv: nodes 4, segments 1, cables 3", - "gateway 192.168.14.1", - "hv2 underlay 192.168.14.12/24 02:4c:00:03:00:00 20004 <-> sw1 p2", - "rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf", + "gateway 10.250.0.1", + "hv2 underlay 10.250.0.4/24 02:4c:00:03:00:00 20004 <-> sw1 p2", } { if !strings.Contains(stdout, want) { t.Errorf("output does not contain %q:\n%s", want, stdout) @@ -300,55 +299,3 @@ func TestRun_UpRefusesToReplaceARunningLab(t *testing.T) { t.Error("the topology of a running lab was replaced") } } - -func TestRun_RenderShipsTheExampleFRRConfigs(t *testing.T) { - dir := t.TempDir() - key := filepath.Join(dir, "lab.pub") - if err := os.WriteFile(key, []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFG/JMmjfko96WkJV8DiL6rip/H/q/R++y8s27Z+Cj6O two-lab-automation\n"), 0o600); err != nil { - t.Fatal(err) - } - out := filepath.Join(dir, "run") - code, _, stderr := runLab("render", "-key", key, filepath.Join("..", "..", "conf", "lab", "evpn-2hv.yml"), out) - if code != 0 { - t.Fatalf("code %d, stderr %s", code, stderr) - } - for node, want := range map[string]string{ - "sw1": "router bgp 65100", - "rr1": "bgp listen range 192.168.14.0/24 peer-group fabric", - "hv1": "bgp router-id 192.168.14.11", - "hv2": "bgp router-id 192.168.14.12", - } { - data, err := os.ReadFile(filepath.Join(out, node, "user-data")) - if err != nil { - t.Fatal(err) - } - if !strings.Contains(string(data), want) { - t.Errorf("%s user-data does not carry %q", node, want) - } - } -} - -func TestRun_RenderReportsAMissingFRRConfig(t *testing.T) { - dir := t.TempDir() - key := filepath.Join(dir, "lab.pub") - if err := os.WriteFile(key, []byte("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFG/JMmjfko96WkJV8DiL6rip/H/q/R++y8s27Z+Cj6O x\n"), 0o600); err != nil { - t.Fatal(err) - } - topo := filepath.Join(dir, "lab.yml") - doc := `name: x -images: - deb: { url: https://example.invalid/deb.qcow2, sums: https://example.invalid/SHA512SUMS } -segments: - underlay: { switch: sw1, cidr: 10.1.0.0/24 } -nodes: - sw1: { role: switch, image: deb, cpus: 1, memory: 512, frr: frr/absent.conf } - hv1: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [underlay], release: 0.2.0rc002 } -` - if err := os.WriteFile(topo, []byte(doc), 0o600); err != nil { - t.Fatal(err) - } - code, _, stderr := runLab("render", "-key", key, topo, filepath.Join(dir, "run")) - if code != 1 || !strings.Contains(stderr, "node sw1: ") || !strings.Contains(stderr, filepath.Join(dir, "frr", "absent.conf")) { - t.Errorf("code %d, stderr %q", code, stderr) - } -} diff --git a/conf/lab/evpn-2hv.yml b/conf/lab/evpn-2hv.yml index a3ebfa5..3d389f6 100644 --- a/conf/lab/evpn-2hv.yml +++ b/conf/lab/evpn-2hv.yml @@ -8,20 +8,11 @@ images: segments: underlay: switch: sw1 - cidr: 192.168.14.0/24 + cidr: 10.250.0.0/24 mtu: 9000 nodes: - sw1: - { role: switch, image: debian12, cpus: 2, memory: 1024, - secondary: { underlay: [169.254.0.1/28] }, frr: frr/sw1.conf } - rr1: - { role: rr, image: debian12, cpus: 1, memory: 1024, segments: [underlay], - addresses: { underlay: 192.168.14.2 }, secondary: { underlay: [169.254.0.3/28] }, - 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.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.0rc002 } + sw1: { role: switch, image: debian12, cpus: 2, memory: 1024 } + rr1: { role: rr, image: debian12, cpus: 1, memory: 1024, segments: [underlay] } + hv1: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay] } + hv2: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay] } diff --git a/conf/lab/frr/hv1.conf b/conf/lab/frr/hv1.conf deleted file mode 100644 index ecf9e12..0000000 --- a/conf/lab/frr/hv1.conf +++ /dev/null @@ -1,19 +0,0 @@ -frr defaults traditional -hostname hv1 -log syslog informational -! -router bgp 64600 - bgp router-id 192.168.14.11 - no bgp default ipv4-unicast - neighbor fabric peer-group - neighbor fabric remote-as 64600 - neighbor fabric capability extended-nexthop - neighbor 10.255.255.1 peer-group fabric - ! - address-family l2vpn evpn - neighbor fabric activate - advertise-all-vni - exit-address-family - ! -exit -! diff --git a/conf/lab/frr/hv2.conf b/conf/lab/frr/hv2.conf deleted file mode 100644 index 5d0e0d9..0000000 --- a/conf/lab/frr/hv2.conf +++ /dev/null @@ -1,19 +0,0 @@ -frr defaults traditional -hostname hv2 -log syslog informational -! -router bgp 64600 - bgp router-id 192.168.14.12 - no bgp default ipv4-unicast - neighbor fabric peer-group - neighbor fabric remote-as 64600 - neighbor fabric capability extended-nexthop - neighbor 10.255.255.1 peer-group fabric - ! - address-family l2vpn evpn - neighbor fabric activate - advertise-all-vni - exit-address-family - ! -exit -! diff --git a/conf/lab/frr/rr1.conf b/conf/lab/frr/rr1.conf deleted file mode 100644 index 44081b1..0000000 --- a/conf/lab/frr/rr1.conf +++ /dev/null @@ -1,43 +0,0 @@ -frr defaults traditional -hostname rr1 -log syslog informational -! -ip prefix-list RR-LOOPBACK-OUT seq 10 permit 10.255.255.1/32 -! -route-map NO-IN deny 999 - description deny -exit -! -router bgp 65000 - no bgp default ipv4-unicast - bgp router-id 10.255.255.1 - bgp cluster-id 10.255.255.1 - neighbor CLUSTER peer-group - neighbor CLUSTER remote-as 65100 - neighbor CLUSTER bfd - neighbor 169.254.0.1 peer-group CLUSTER - neighbor 169.254.0.1 description router-1 - neighbor 169.254.0.2 peer-group CLUSTER - neighbor 169.254.0.2 description router-2 - neighbor fabric peer-group - neighbor fabric remote-as 64600 - neighbor fabric local-as 64600 no-prepend replace-as - neighbor fabric capability extended-nexthop - neighbor fabric update-source 10.255.255.1 - bgp listen range 192.168.14.0/24 peer-group fabric - bgp listen limit 200 - ! - address-family ipv4 unicast - network 10.255.255.1/32 - neighbor CLUSTER activate - neighbor CLUSTER prefix-list RR-LOOPBACK-OUT out - neighbor CLUSTER route-map NO-IN in - exit-address-family - ! - address-family l2vpn evpn - neighbor fabric activate - neighbor fabric route-reflector-client - exit-address-family - ! -exit -! diff --git a/conf/lab/frr/sw1.conf b/conf/lab/frr/sw1.conf deleted file mode 100644 index 55df925..0000000 --- a/conf/lab/frr/sw1.conf +++ /dev/null @@ -1,28 +0,0 @@ -frr defaults traditional -hostname sw1 -log syslog informational -! -ip prefix-list RR-LOOPBACK-IN seq 10 permit 10.255.255.1/32 -! -route-map RR-IN permit 10 - match ip address prefix-list RR-LOOPBACK-IN -exit -! -route-map NO-OUT deny 999 -exit -! -router bgp 65100 - no bgp default ipv4-unicast - bgp router-id 169.254.0.1 - neighbor 169.254.0.3 remote-as 65000 - neighbor 169.254.0.3 description rr1 - neighbor 169.254.0.3 bfd - ! - address-family ipv4 unicast - neighbor 169.254.0.3 activate - neighbor 169.254.0.3 route-map RR-IN in - neighbor 169.254.0.3 route-map NO-OUT out - exit-address-family - ! -exit -! diff --git a/docs/deploiement/architecture-cluster.rst b/docs/deploiement/architecture-cluster.rst index 3956bab..d1eb9bf 100644 --- a/docs/deploiement/architecture-cluster.rst +++ b/docs/deploiement/architecture-cluster.rst @@ -4,23 +4,21 @@ Architecture du cluster Topologie --------- -.. figure:: /schemas/architecture-cluster.svg - :alt: Architecture du cluster : routeurs, route reflector, hyperviseurs, plans de données et de contrôle +.. figure:: /schemas/topologie-cluster.svg + :alt: Topologie du cluster : routeurs, route reflector et hyperviseurs :align: center :width: 100% :class: only-light - Architecture cible. Trait bleu : plan de données (VXLAN). Tirets violets : plan de contrôle - (sessions BGP). Pointillé gris : interfaces créées par l'agent. + Topologie cible. Trait plein : plan de données. Trait pointillé : plan de contrôle. -.. figure:: /schemas/architecture-cluster-dark.svg - :alt: Architecture du cluster : routeurs, route reflector, hyperviseurs, plans de données et de contrôle +.. figure:: /schemas/topologie-cluster-dark.svg + :alt: Topologie du cluster : routeurs, route reflector et hyperviseurs :align: center :width: 100% :class: only-dark - Architecture cible. Trait bleu : plan de données (VXLAN). Tirets violets : plan de contrôle - (sessions BGP). Pointillé gris : interfaces créées par l'agent. + Topologie cible. Trait plein : plan de données. Trait pointillé : plan de contrôle. Deux plans distincts, à ne pas confondre au moment du diagnostic : diff --git a/docs/deploiement/image-qcow2.rst b/docs/deploiement/image-qcow2.rst index cba2e27..1283f52 100644 --- a/docs/deploiement/image-qcow2.rst +++ b/docs/deploiement/image-qcow2.rst @@ -250,7 +250,7 @@ champ ``password`` de l'API de l'agent ne sert donc **pas** à ouvrir une sessio datasource_list: [ NoCloud ] datasource: NoCloud: - seedfrom: 'http://169.254.169.254:80/' + seedfrom: 'http://169.254.169.254:80' timeout: 5 max_wait: 10 ENDFILE @@ -315,20 +315,12 @@ Points de vigilance aucun espace à l'host : les qcow2 ne se rétractent pas. L'activation reste utile pour le jour où l'option sera ajoutée côté agent, mais ne pas compter dessus pour la place disque. -.. warning:: +.. note:: - **La barre oblique finale de** ``seedfrom`` **est indispensable avant cloud-init 23.1.** - cloud-init construit l'URL des documents en concaténant ``seedfrom`` avec ``meta-data``, - ``user-data`` et ``vendor-data`` (``util.read_seeded``). Jusqu'à la 22.4 — celle de Debian 12, - 22.4.2 —, sans barre oblique finale il demande ``http://169.254.169.254:80meta-data`` : la - source échoue, la VM démarre en ``DataSourceNone``, sans nom d'hôte ni user-data. À partir de - 23.1, un drapeau actif par défaut (``NOCLOUD_SEED_URL_APPEND_FORWARD_SLASH``) ajoute la barre - oblique manquante, ce qui explique qu'une image récente fonctionne sans elle. - - Vérifié le 2026-10-04 dans le lab (#50), sur une VM Debian 12 lancée par l'agent : sans barre - oblique, ``Datasource DataSourceNone`` ; avec, ``DataSourceNoCloudNet - [seed=…http://169.254.169.254…]`` et le nom d'hôte de la VM. Avec la barre oblique, la - configuration fonctionne quelle que soit la version de cloud-init. + **À vérifier** — ``seedfrom`` sans barre oblique finale. cloud-init construit l'URL des + documents en concaténant ``seedfrom`` avec ``meta-data`` et ``user-data``. Confirmer sur une + VM réelle que les deux documents sont bien récupérés, et corriger en + ``http://169.254.169.254:80/`` si ce n'est pas le cas. .. note:: diff --git a/docs/deploiement/route-reflector.rst b/docs/deploiement/route-reflector.rst index ab57b32..e0095d2 100644 --- a/docs/deploiement/route-reflector.rst +++ b/docs/deploiement/route-reflector.rst @@ -8,106 +8,15 @@ reflector, qui redistribue. Il tourne lui-même en machine virtuelle, ce qui crée une dépendance circulaire à traiter explicitement : la VM qui porte le plan de contrôle du cluster est hébergée par le cluster. -Adressage ---------- - -Le route reflector est **sur le même segment L2 que les hyperviseurs**, avec trois adresses : - -.. list-table:: - :header-rows: 1 - :widths: 30 25 45 - - * - Adresse - - Interface - - Rôle - * - une adresse de ``192.168.14.0/24`` - - interface principale - - celle de n'importe quelle machine du segment ; les réponses aux hyperviseurs en partent - * - ``169.254.0.3/28`` - - interface principale, **adresse secondaire** - - le lien avec les routeurs de cluster (``169.254.0.1`` et ``.2``), sur le même L2 - * - ``10.255.255.1/32`` - - ``lo1``, interface ``dummy`` - - ``router-id``, ``cluster-id`` et source des sessions EVPN ; seule route annoncée aux routeurs - -Les hyperviseurs ne connaissent que la loopback : ils ouvrent leur session vers ``10.255.255.1`` -par leur passerelle (``192.168.14.1``), les routeurs l'ayant apprise du route reflector en eBGP. -Aucune adresse d'hyperviseur n'est déclarée côté route reflector : il accepte toute session venant -du segment (``bgp listen range``). - -L'adresse du lien est une **adresse secondaire** de l'interface principale, pas une interface à -part : même L2, même MAC sur le fil, une interface de moins qu'avec un ``ipvlan``. Elle doit -survivre aux renouvellements DHCP de l'adresse principale : la déclarer dans la configuration -réseau du système, pas la poser à la main. - .. note:: - **Non vérifié sur l'image de production.** Avec NetworkManager, la forme attendue est : + **À rédiger.** À documenter : - .. code-block:: bash - - nmcli connection modify +ipv4.addresses 169.254.0.3/28 - nmcli connection add type dummy ifname lo1 con-name lo1 \ - ipv4.method manual ipv4.addresses 10.255.255.1/32 ipv6.method disabled - - Le lab, en Debian, pose ces adresses par cloud-init et un script rejoué au démarrage ; ces - commandes restent à valider sur l'image golden. - -Configuration de FRR --------------------- - -``bgpd`` et ``bfdd`` activés dans ``/etc/frr/daemons``. La configuration ci-dessous est **celle du -lab** (``conf/lab/frr/rr1.conf``) : ASN, loopback et plages sont ceux de la production, seul le nom -d'hôte diffère. Le lab qualifie donc exactement ce fichier. - -.. literalinclude:: ../../conf/lab/frr/rr1.conf - :language: text - -Ce qu'elle établit : - -* **Routeurs de cluster** (groupe ``CLUSTER``) : eBGP vers l'AS 65100, avec BFD, en IPv4 - unicast. Le route reflector n'annonce que sa loopback (``RR-LOOPBACK-OUT``) et **n'accepte - rien** (``NO-IN``) : il ne reçoit aucune route des routeurs. -* **Hyperviseurs** (groupe ``fabric``) : voisins dynamiques, toute session venant de - ``192.168.14.0/24`` étant acceptée, jusqu'à 200. ``local-as 64600 no-prepend replace-as`` fait - que, du point de vue des hyperviseurs, la session est en iBGP dans l'AS 64600 — celui de leur - configuration — alors que le route reflector est en AS 65000 face aux routeurs. -* **EVPN** : seule famille activée vers les hyperviseurs, qui sont ses clients - (``route-reflector-client``) : il réfléchit les routes EVPN de chacun vers tous les autres. - -Vérifié dans le lab -~~~~~~~~~~~~~~~~~~~ - -Le 2026-10-04, FRR 10.7.1, route reflector en Debian 12 sur le segment des hyperviseurs, avec -l'adresse du lien en secondaire et la loopback sur ``lo1`` : - -.. list-table:: - :header-rows: 1 - :widths: 55 45 - - * - Vérification - - Résultat - * - session avec le routeur, IPv4 unicast - - Established ; le routeur reçoit **un seul** préfixe, la loopback, et l'installe via - ``169.254.0.3`` - * - BFD avec le routeur - - 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 - - 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`). - -.. note:: - - **À rédiger.** Reste à documenter : - - * la création de la VM : ressources, subnet et mode utilisés (``bridge``, pour pouvoir la - 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 création de la VM : ressources, subnet et mode utilisés, 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` ; + * son adressage, et comment les hyperviseurs le connaissent ; + * la configuration du démon de routage qu'elle héberge ; * 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 absent — les tunnels déjà établis continuent-ils de fonctionner, et pendant combien de diff --git a/docs/developpement/lab.rst b/docs/developpement/lab.rst index 2504bc0..8f56789 100644 --- a/docs/developpement/lab.rst +++ b/docs/developpement/lab.rst @@ -116,9 +116,8 @@ Commandes que si l'entrée standard en est un prepare installe sur le serveur ce dont lab a besoin (qemu, genisoimage), vérifie /dev/kvm et la virtualisation imbriquée ; lancé aussi par up - push compile cmd/lab pour linux/amd64 et dépose sur le serveur ~/lab et le - répertoire de la topologie dans ~/topology/ (avec les fichiers qu'elle - référence) ; ensuite : ssh './lab up topology/' + push compile cmd/lab pour linux/amd64 et dépose sur le serveur ~/lab et + ~/ ; ensuite : ssh './lab up ' down supprime tous les serveurs de lab du projet et attend leur disparition session [cmd] up, puis la commande distante (ou un shell), puis down quoi qu'il arrive @@ -179,15 +178,13 @@ Une campagne sur le lab enchaîne ces commandes depuis le Mac ; ``lab`` s'exécu scripts/lab-host.sh up scripts/lab-host.sh push conf/lab/evpn-2hv.yml - scripts/lab-host.sh ssh './lab up topology/evpn-2hv.yml' + scripts/lab-host.sh ssh './lab up evpn-2hv.yml' scripts/lab-host.sh ssh './lab ssh hv1' # shell interactif sur hv1 scripts/lab-host.sh ssh './lab ssh hv1 ip -br a' # commande, code de retour propagé scripts/lab-host.sh down -``push`` transfère par la connexion SSH du script — mêmes options, même clé, même -``known_hosts`` que ``ssh`` : le binaire par ``cat``, le répertoire de la topologie par ``tar`` -(sans les métadonnées macOS), chacun renommé une fois complet. Tout le répertoire part, pour que -les fichiers que la topologie référence (``frr/*.conf``) arrivent avec elle. +``push`` transfère par la connexion SSH du script (``cat`` côté serveur, fichier renommé une fois +complet) : mêmes options, même clé, même ``known_hosts`` que ``ssh``. Topologie --------- @@ -215,26 +212,9 @@ Ce que le fichier déclare : ``nodes`` ``role`` (``switch``, ``rr`` ou ``hypervisor``), ``image``, ``cpus``, ``memory`` en Mio (256 au moins), ``segments`` auxquels le nœud est relié, et ``addresses`` pour fixer - l'adresse d'un nœud sur un segment (``addresses: {underlay: 192.168.14.50}``). Un switch ne + l'adresse d'un nœud sur un segment (``addresses: {underlay: 10.250.0.50}``). Un switch ne déclare ni ``segments`` ni ``addresses`` : il porte ceux dont il est le ``switch``. - Champs de rôle, facultatifs : - - * ``secondary`` — des adresses supplémentaires par segment, avec leur longueur de préfixe - (``secondary: {underlay: [169.254.0.3/28]}``), posées sur la même interface que l'adresse - principale : même L2, même MAC. Elles doivent être **hors** du CIDR du segment, pour ne - jamais croiser l'attribution automatique. Sur un switch, elles vont sur le bridge du - segment ; - * ``loopback`` — une adresse sur une interface ``dummy`` nommée ``lo1`` - (``loopback: 10.255.255.1/32``) ; - * ``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.0rc002``). Sans lui, ``deploy.sh`` prendrait la - dernière release, et le lab ne serait plus reproductible. - - ``mgmt0`` et ``lo1`` sont réservés : aucun segment ne peut porter ces noms. - Ce que l'outil en déduit, de façon déterministe — même fichier, même plan : .. list-table:: @@ -277,18 +257,11 @@ Limites : 1000 nœuds, 256 segments, et autant de câbles que la plage UDP le pe hv1 hypervisor debian12 4 16384 MiB 127.0.0.1:2202 hv2 hypervisor debian12 4 16384 MiB 127.0.0.1:2203 - roles - name loopback secondary frr release - sw1 - underlay 169.254.0.1/28 sw1.conf - - rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf - - hv1 - - hv1.conf 0.2.0rc002 - 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 - rr1 underlay 192.168.14.2/24 02:4c:00:01:00:00 20000 <-> sw1 p0 02:4c:00:01:00:01 20001 - hv1 underlay 192.168.14.11/24 02:4c:00:02:00:00 20002 <-> sw1 p1 02:4c:00:02:00:01 20003 - hv2 underlay 192.168.14.12/24 02:4c:00:03:00:00 20004 <-> sw1 p2 02:4c:00:03:00:01 20005 + segment underlay: 10.250.0.0/24, mtu 9000, switch sw1, bridge br-underlay, gateway 10.250.0.1 + node interface address mac udp switch port mac udp + rr1 underlay 10.250.0.2/24 02:4c:00:01:00:00 20000 <-> sw1 p0 02:4c:00:01:00:01 20001 + hv1 underlay 10.250.0.3/24 02:4c:00:02:00:00 20002 <-> sw1 p1 02:4c:00:02:00:01 20003 + hv2 underlay 10.250.0.4/24 02:4c:00:03:00:00 20004 <-> sw1 p2 02:4c:00:03:00:01 20005 Un fichier invalide est refusé avec **toutes** ses erreurs à la fois, et un code de sortie 1. Les champs inconnus et les clés en double sont refusés aussi : @@ -301,122 +274,8 @@ champs inconnus et les clés en double sont refusés aussi : segment underlay: cidr 10.250.0.0/31 prefix length out of range [/8, /30] node sw1: switch carries no segment -Les ASN, la loopback du route reflector, le lien ``169.254.0.0/28`` et le subnet des hyperviseurs -de l'exemple sont **ceux de la production** (décision du 2026-10-04, #50) : les fichiers de -``conf/lab/`` restent ainsi au plus près de ce qui tourne réellement. Toutes les adresses y sont -**fixées** par ``addresses`` — le route reflector en ``.2``, les hyperviseurs à partir de ``.11`` — -pour que le modèle se lise sans le plan et ne dépende pas de l'ordre de déclaration : le -``frr.conf`` d'un hyperviseur, écrit à la main, porte son adresse en ``router-id``. Seul le switch -n'en déclare pas : il porte toujours la passerelle, la première adresse du segment. - -Rôles -~~~~~ - -Les configurations FRR du lab vivent dans ``conf/lab/frr/``, une par nœud, **écrites à la main** : -ce sont les mêmes fichiers que la documentation de déploiement inclut, pour que le lab qualifie -exactement ce qu'elle prescrit. Celle du route reflector : - -.. literalinclude:: ../../conf/lab/frr/rr1.conf - :language: text - -Au premier démarrage, cloud-init installe FRR (``frr-stable`` de ``deb.frrouting.org``, sans les -paquets recommandés), active ``bgpd`` — et ``bfdd`` sur le switch et le route reflector —, puis -dépose le ``frr.conf`` du nœud et redémarre FRR. La mise à jour des index de paquets est réessayée -pendant cinq minutes : un nœud peut démarrer avant que le switch, par lequel il sort, n'ait posé -son NAT. - -La **clé du dépôt FRR** n'est pas téléchargée au démarrage : elle est enregistrée dans ``lab`` -(``internal/lab/render/frrouting.gpg``) et déposée par cloud-init. Elle a été récupérée le -2026-10-04 sur ``deb.frrouting.org`` ; les empreintes de ses clés primaires sont publiées sous la -même valeur sur ``keys.openpgp.org`` et ``keyserver.ubuntu.com`` : - -.. code-block:: text - - 3D99 68AC 9AE7 BE11 6928 8DDB 1FD5 8398 95F5 7FDA David Lamparter - 4A56 C773 8BB3 F815 95A8 05D2 A832 7699 08F1 3ED1 FRRouting Debian Repository - A90F C36D 9429 4097 98E9 C2D8 74DE ED43 AB19 4DBF Jafar Al-Gharaibeh - -Une clé renouvelée par FRR fera échouer l'installation (signature inconnue) : remplacer le fichier -après avoir vérifié les nouvelles empreintes. - -**Hyperviseurs.** Ils se déploient comme en production, par ``deploy.sh`` — celui **du dépôt**, -embarqué dans ``lab`` avec ``bootstrap_kvm.sh`` (paquet ``scripts``) et déposé dans -``/opt/two/scripts/``, où ``deploy.sh`` cherche d'abord ``bootstrap_kvm.sh`` : - -.. code-block:: text - - deploy.sh --noup_script -i -u -t - -``--noup_script`` empêche l'auto-mise à jour de remplacer le script par celui de ``main`` : le lab -teste les scripts de sa branche. L'uplink ``-u`` est l'interface qui porte la route par défaut — -celle du premier segment de l'hyperviseur dans l'ordre de déclaration des segments — parce que -``deploy.sh`` y lit l'adresse et la passerelle qu'il déplace sur ``br-000000``. ``deploy.sh`` -télécharge la release sur ``git.g3e.fr`` sans réessayer : le lancement attend d'abord que le serveur -réponde, à travers le switch. FRR est installé **après** : il démarre sur le réseau final. - -**Un seul script de provisionnement par nœud.** cloud-init exécute ``runcmd`` comme un script -``sh`` sans ``set -e`` : seule la dernière commande compte, et un ``deploy.sh`` en échec suivi d'un -FRR installé avec succès passerait pour un démarrage réussi. Chaque nœud reçoit donc -``/usr/local/sbin/lab-provision``, en ``set -eu``, qui enchaîne ses étapes ; ``runcmd`` n'appelle -que lui, et la première étape en échec met cloud-init en erreur. - -**Ce que vérifie** ``lab up``, une fois cloud-init terminé sans erreur : ``agent.service`` actif -sur chaque hyperviseur, ``frr`` actif sur chaque nœud qui en a un. Ce contrôle couvre ce que -cloud-init ne voit pas — si la migration réseau échoue, ``deploy.sh`` arme un redémarrage de -secours, et la VM redémarrée ne rejoue pas ``runcmd``. - -.. warning:: - - **Un hyperviseur du lab ne survit pas à un redémarrage.** En production, la racine est en - tmpfs et ``deploy.sh --bootstrap`` est rejoué à chaque démarrage ; dans le lab, ``-i`` n'est - exécuté qu'au premier, et la migration réseau, qui n'est pas persistée, est perdue. Recréer le - lab : ``lab down`` puis ``lab up``. - -Vérifié le 2026-10-04 sur le serveur de lab, topologie ``evpn-2hv``, release ``0.2.0rc002`` : - -.. code-block:: text - - $ scripts/lab-host.sh ssh './lab up -timeout 25m topology/evpn-2hv.yml' - sw1: started - rr1: started - hv1: started - hv2: started - sw1: ready - rr1: ready - hv1: ready - hv2: ready - -.. list-table:: - :header-rows: 1 - :widths: 55 45 - - * - Vérification - - Résultat - * - ``lab up`` complet : FRR, ``deploy.sh`` et vérification des services de chaque rôle - - 6 min 15 - * - session switch ↔ route reflector, IPv4 unicast, BFD - - Established, BFD up ; le switch reçoit la seule loopback du route reflector - * - sessions EVPN des deux hyperviseurs vers la loopback du route reflector - - Established, voisins dynamiques, stables - * - réseau d'un hyperviseur après ``deploy.sh`` - - adresse sur ``br-000000``, MTU 9000, API de l'agent qui répond - * - VPC, subnet ``vxlan`` et VM Debian ``genericcloud`` créés par l'API de hv1 - - ``login:`` en 20 s, en KVM imbriqué - * - DHCP et routes (option 121) servis par two à la VM - - conformes, route ``/32`` vers ``169.254.169.254`` comprise - * - métadonnées, image configurée selon :doc:`/deploiement/image-qcow2` - - ``DataSourceNoCloudNet``, nom d'hôte appliqué — avec la barre oblique finale de - ``seedfrom`` (voir cette page) - * - VM ↔ VM entre les deux hyperviseurs, même subnet ``vxlan`` - - **échec** : les VXLAN de two n'ont pas d'adresse VTEP locale, rien n'est annoncé en EVPN — - `#51 `_ ; avec l'adresse posée, ping et MTU 1500 - passent - -.. note:: - - La configuration du switch (``conf/lab/frr/sw1.conf``) **n'est pas celle des routeurs** : écrite - pour l'essai du 2026-10-04, elle se contente d'établir la session avec le route reflector et de - n'accepter que sa loopback. Elle sera remplacée par la configuration réelle des routeurs. +Les plages d'adresses de l'exemple sont des valeurs de travail : le plan d'adressage du lab reste à +définir (#50). Rendu des VM ------------ @@ -737,10 +596,6 @@ Sécurité * **Clés d'hôte des VM non vérifiées** par ``lab ssh`` (``known_hosts`` jetable) : elles changent à chaque ``up``. Acceptable uniquement parce que la connexion reste sur la boucle locale d'un serveur auquel on s'est authentifié. -* **Code exécuté sans épinglage par** ``deploy.sh`` : la bibliothèque ``shflags`` est récupérée - par ``curl`` sur la branche ``main`` d'un autre dépôt (``H6N/tools``) et exécutée par ``eval``, - sans vérification d'intégrité — dans le lab comme en production. Les artefacts de la release - sont, eux, vérifiés contre ``SHA256SUMS``. * **Image** : ``SHA512SUMS`` vient de la même origine que l'image, en HTTPS. La vérification protège contre la corruption, pas contre une origine compromise ; la signature GPG de Debian (``SHA512SUMS.sign``) n'est pas encore vérifiée. diff --git a/docs/schemas/architecture-cluster-dark.svg b/docs/schemas/architecture-cluster-dark.svg deleted file mode 100644 index 483d0f0..0000000 --- a/docs/schemas/architecture-cluster-dark.svg +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - - - - - ROUTEURS DE CLUSTER · AS 65100 - - routeur 1 - 169.254.0.1/28 - - routeur 2 - 169.254.0.2/28 - passerelle 192.168.14.1/24 - - - VM ROUTE REFLECTOR · AS 65000 - lo1 · 10.255.255.1/32 - router-id, cluster-id, source des sessions EVPN - 192.168.14.2/24 · 169.254.0.3/28 - principale, et secondaire pour le lien avec les routeurs - hébergée par le cluster lui-même - - - eBGP · BFD (×2) - annonce lo1 seule - n'accepte rien - - - - - - - - segment underlay · L2 - 192.168.14.0/24 · 169.254.0.0/28 - - - HYPERVISEUR 1 - - - br-000000 - uplink · 192.168.14.11 - - vxlan-<vni> - dans br-<subnet>, sans learning - - netns du VPC - passerelle, DHCP - - VM - QEMU / KVM - - VTEP - - veth - - tap - - - FRR - bgpd · AS 64600 - advertise-all-vni - - agent - API REST · Badger - - - FDB - - crée - - - HYPERVISEUR 2 - - - br-000000 - uplink · 192.168.14.12 - - vxlan-<vni> - dans br-<subnet>, sans learning - - netns du VPC - passerelle, DHCP - - VM - QEMU / KVM - - VTEP - - veth - - tap - - - FRR - bgpd · AS 64600 - advertise-all-vni - - agent - API REST · Badger - - - FDB - - crée - - - - - - - - iBGP l2vpn evpn · AS 64600 - iBGP - - - VXLAN - UDP 4789 - - - lien L2 / interface - - plan de données : tunnels VXLAN - - plan de contrôle : sessions BGP - - créé par l'agent - - Les hyperviseurs joignent 10.255.255.1 par leur passerelle 192.168.14.1 : les routeurs l'ont apprise du route reflector en eBGP, sans rien lui renvoyer. - Le route reflector accepte toute session venant de 192.168.14.0/24 (bgp listen range) et réfléchit les routes EVPN entre hyperviseurs. - Chaque FRR en peuple la FDB des VXLAN locaux — intégration de FRR avec les interfaces créées par two : à qualifier (#50, E5). - MTU du segment : 1550 au moins (VXLAN figé à 1500 par l'agent, + 50 octets d'encapsulation) ; 9000 dans le lab. - ASN, loopback et plages : valeurs de production (#50). Adresses en 192.168.14.0/24 : celles du lab. Placement de la VM route reflector : à rédiger. - diff --git a/docs/schemas/architecture-cluster.svg b/docs/schemas/architecture-cluster.svg deleted file mode 100644 index 25028d6..0000000 --- a/docs/schemas/architecture-cluster.svg +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - - - - - ROUTEURS DE CLUSTER · AS 65100 - - routeur 1 - 169.254.0.1/28 - - routeur 2 - 169.254.0.2/28 - passerelle 192.168.14.1/24 - - - VM ROUTE REFLECTOR · AS 65000 - lo1 · 10.255.255.1/32 - router-id, cluster-id, source des sessions EVPN - 192.168.14.2/24 · 169.254.0.3/28 - principale, et secondaire pour le lien avec les routeurs - hébergée par le cluster lui-même - - - eBGP · BFD (×2) - annonce lo1 seule - n'accepte rien - - - - - - - - segment underlay · L2 - 192.168.14.0/24 · 169.254.0.0/28 - - - HYPERVISEUR 1 - - - br-000000 - uplink · 192.168.14.11 - - vxlan-<vni> - dans br-<subnet>, sans learning - - netns du VPC - passerelle, DHCP - - VM - QEMU / KVM - - VTEP - - veth - - tap - - - FRR - bgpd · AS 64600 - advertise-all-vni - - agent - API REST · Badger - - - FDB - - crée - - - HYPERVISEUR 2 - - - br-000000 - uplink · 192.168.14.12 - - vxlan-<vni> - dans br-<subnet>, sans learning - - netns du VPC - passerelle, DHCP - - VM - QEMU / KVM - - VTEP - - veth - - tap - - - FRR - bgpd · AS 64600 - advertise-all-vni - - agent - API REST · Badger - - - FDB - - crée - - - - - - - - iBGP l2vpn evpn · AS 64600 - iBGP - - - VXLAN - UDP 4789 - - - lien L2 / interface - - plan de données : tunnels VXLAN - - plan de contrôle : sessions BGP - - créé par l'agent - - Les hyperviseurs joignent 10.255.255.1 par leur passerelle 192.168.14.1 : les routeurs l'ont apprise du route reflector en eBGP, sans rien lui renvoyer. - Le route reflector accepte toute session venant de 192.168.14.0/24 (bgp listen range) et réfléchit les routes EVPN entre hyperviseurs. - Chaque FRR en peuple la FDB des VXLAN locaux — intégration de FRR avec les interfaces créées par two : à qualifier (#50, E5). - MTU du segment : 1550 au moins (VXLAN figé à 1500 par l'agent, + 50 octets d'encapsulation) ; 9000 dans le lab. - ASN, loopback et plages : valeurs de production (#50). Adresses en 192.168.14.0/24 : celles du lab. Placement de la VM route reflector : à rédiger. - diff --git a/docs/schemas/topologie-cluster-dark.svg b/docs/schemas/topologie-cluster-dark.svg new file mode 100644 index 0000000..48a3916 --- /dev/null +++ b/docs/schemas/topologie-cluster-dark.svg @@ -0,0 +1,52 @@ + + + + + ROUTEURS DE CLUSTER + + routeur A + + routeur B + + + VM route reflector + plan de contrôle + + + HYPERVISEUR 1 + + br-000000 + + FRR + + agent + + + + + HYPERVISEUR 2 + + br-000000 + + FRR + + agent + + + + + + uplink + uplink + + + + diff --git a/docs/schemas/topologie-cluster.svg b/docs/schemas/topologie-cluster.svg new file mode 100644 index 0000000..858d18a --- /dev/null +++ b/docs/schemas/topologie-cluster.svg @@ -0,0 +1,52 @@ + + + + + ROUTEURS DE CLUSTER + + routeur A + + routeur B + + + VM route reflector + plan de contrôle + + + HYPERVISEUR 1 + + br-000000 + + FRR + + agent + + + + + HYPERVISEUR 2 + + br-000000 + + FRR + + agent + + + + + + uplink + uplink + + + + diff --git a/internal/lab/machine/machine.go b/internal/lab/machine/machine.go index 29e1312..1630ed7 100644 --- a/internal/lab/machine/machine.go +++ b/internal/lab/machine/machine.go @@ -125,10 +125,6 @@ func (l Lab) Up(ctx context.Context, fetcher provision.Fetcher, timeout time.Dur errs = append(errs, fmt.Errorf("node %s: %w", n.Name, err)) continue } - if err := l.checkServices(ctx, n); err != nil { - errs = append(errs, fmt.Errorf("node %s: %w", n.Name, err)) - continue - } fmt.Fprintf(l.Out, "%s: ready\n", n.Name) } return errors.Join(errs...) @@ -167,26 +163,6 @@ func (l Lab) waitReady(ctx context.Context, n topology.NodePlan) error { } } -func services(n topology.NodePlan) []string { - var units []string - if n.Role == topology.RoleHypervisor { - units = append(units, "agent.service") - } - if n.FRR != "" { - units = append(units, "frr.service") - } - return units -} - -func (l Lab) checkServices(ctx context.Context, n topology.NodePlan) error { - for _, unit := range services(n) { - if err := l.Runner.Run(ctx, "ssh", l.sshArgs(n, true, []string{"systemctl", "is-active", "--quiet", unit})...); err != nil { - return fmt.Errorf("%s is not active: %w", unit, err) - } - } - return nil -} - func exitCode(err error) int { var coded interface{ ExitCode() int } if errors.As(err, &coded) { diff --git a/internal/lab/machine/machine_test.go b/internal/lab/machine/machine_test.go index d3bc49a..22635d6 100644 --- a/internal/lab/machine/machine_test.go +++ b/internal/lab/machine/machine_test.go @@ -34,10 +34,9 @@ func (e exitErr) ExitCode() int { return int(e) } type fakeRunner struct { mu sync.Mutex calls [][]string - fail string - ssh map[string][]error - always map[string]error - inactive string + fail string + ssh map[string][]error + always map[string]error } func (f *fakeRunner) Run(_ context.Context, name string, args ...string) error { @@ -56,9 +55,6 @@ func (f *fakeRunner) Run(_ context.Context, name string, args ...string) error { return os.WriteFile(private+".pub", []byte(labKey+"\n"), 0o644) case "ssh": port := args[indexOf(args, "-p")+1] - if args[len(args)-1] == f.inactive { - return exitErr(3) - } answers := f.ssh[port] if len(answers) == 0 { return f.always[port] @@ -128,7 +124,7 @@ nodes: } const switchLast = ` rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay] } - hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 } + hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] } sw1: { role: switch, image: deb, cpus: 2, memory: 1024 } ` @@ -413,8 +409,8 @@ func TestUp_StartsSwitchesFirstAndWaitsForEveryNode(t *testing.T) { } ssh := f.runner.commands("ssh") - if len(ssh) != 4 { - t.Fatalf("%d ssh calls, want 4", len(ssh)) + if len(ssh) != 3 { + t.Fatalf("%d ssh calls, want 3", len(ssh)) } want := []string{"ssh", "-i", filepath.Join(f.lab.RunDir, "lab_ed25519"), @@ -444,8 +440,8 @@ func TestUp_RetriesWhileSSHIsUnreachable(t *testing.T) { if err := f.lab.Up(context.Background(), f.fetcher, time.Second); err != nil { t.Fatalf("Up: %v", err) } - if n := len(f.runner.commands("ssh")); n != 6 { - t.Errorf("%d ssh calls, want 6", n) + if n := len(f.runner.commands("ssh")); n != 5 { + t.Errorf("%d ssh calls, want 5", n) } } @@ -545,44 +541,3 @@ func TestStop_RefusesPidsThatTargetAGroup(t *testing.T) { } } } - -func TestUp_ChecksTheServicesOfEachRole(t *testing.T) { - f := newFixture(t, switchLast) - conf := filepath.Join(t.TempDir(), "sw1.conf") - if err := os.WriteFile(conf, []byte("hostname sw1\n"), 0o600); err != nil { - t.Fatal(err) - } - f.lab.Plan.Nodes[2].FRR = conf - - if err := f.lab.Up(context.Background(), f.fetcher, time.Second); err != nil { - t.Fatalf("Up: %v", err) - } - - var checks []string - for _, c := range f.runner.commands("ssh") { - if c[len(c)-4] == "systemctl" { - checks = append(checks, c[indexOf(c, "-p")+1]+" "+strings.Join(c[len(c)-4:], " ")) - } - } - want := []string{ - "2201 systemctl is-active --quiet agent.service", - "2202 systemctl is-active --quiet frr.service", - } - if !reflect.DeepEqual(checks, want) { - t.Errorf("checks = %q, want %q", checks, want) - } -} - -func TestUp_ReportsAnInactiveServiceAndWaitsForTheOthers(t *testing.T) { - f := newFixture(t, switchLast) - f.runner.inactive = "agent.service" - - err := f.lab.Up(context.Background(), f.fetcher, time.Second) - - if err == nil || err.Error() != "node hv1: agent.service is not active: exit status 3" { - t.Errorf("error = %v", err) - } - if !strings.Contains(f.out.String(), "rr1: ready\n") || !strings.Contains(f.out.String(), "sw1: ready\n") || strings.Contains(f.out.String(), "hv1: ready") { - t.Errorf("output = %q", f.out.String()) - } -} diff --git a/internal/lab/provision/provision.go b/internal/lab/provision/provision.go index b1ee4d0..98c1b2f 100644 --- a/internal/lab/provision/provision.go +++ b/internal/lab/provision/provision.go @@ -72,11 +72,7 @@ func Prepare(ctx context.Context, p *topology.Plan, o Options) ([]render.Node, e if err != nil { return nil, err } - frr, err := ReadFRR(p) - if err != nil { - return nil, err - } - nodes, err := render.Render(p, render.Options{RunDir: o.RunDir, AuthorizedKeys: []string{key}, FRR: frr}) + nodes, err := render.Render(p, render.Options{RunDir: o.RunDir, AuthorizedKeys: []string{key}}) if err != nil { return nil, err } @@ -88,21 +84,6 @@ func Prepare(ctx context.Context, p *topology.Plan, o Options) ([]render.Node, e return nodes, nil } -func ReadFRR(p *topology.Plan) (map[string]string, error) { - configs := map[string]string{} - for _, n := range p.Nodes { - if n.FRR == "" { - continue - } - data, err := os.ReadFile(n.FRR) - if err != nil { - return nil, fmt.Errorf("node %s: %w", n.Name, err) - } - configs[n.Name] = string(data) - } - return configs, nil -} - func EnsureKey(ctx context.Context, r Runner, dir string) (string, error) { private := filepath.Join(dir, KeyFile) if _, err := os.Stat(private); errors.Is(err, os.ErrNotExist) { diff --git a/internal/lab/provision/provision_test.go b/internal/lab/provision/provision_test.go index 668a0f8..b05d693 100644 --- a/internal/lab/provision/provision_test.go +++ b/internal/lab/provision/provision_test.go @@ -60,8 +60,8 @@ segments: nodes: sw1: { role: switch, image: debian12, cpus: 2, memory: 1024 } rr1: { role: rr, image: debian12, cpus: 1, memory: 1024, segments: [underlay] } - hv1: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 } - hv2: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 } + hv1: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay] } + hv2: { role: hypervisor, image: debian12, cpus: 4, memory: 16384, segments: [underlay] } ` topo, err := topology.Parse([]byte(doc)) if err != nil { @@ -315,73 +315,3 @@ func TestEnsureKey_WithTheRealSSHKeygen(t *testing.T) { t.Errorf("private key mode = %v, %v", info.Mode().Perm(), err) } } - -func TestReadFRR_ReadsOnlyTheNodesThatDeclareOne(t *testing.T) { - dir := t.TempDir() - conf := filepath.Join(dir, "rr1.conf") - if err := os.WriteFile(conf, []byte("hostname rr1\n"), 0o600); err != nil { - t.Fatal(err) - } - m := newMirror(t) - p := labPlan(t, m) - p.Nodes[1].FRR = conf - - got, err := ReadFRR(p) - - if err != nil || !reflect.DeepEqual(got, map[string]string{"rr1": "hostname rr1\n"}) { - t.Errorf("ReadFRR = %q, %v", got, err) - } -} - -func TestReadFRR_NamesTheNodeOfAMissingFile(t *testing.T) { - m := newMirror(t) - p := labPlan(t, m) - p.Nodes[2].FRR = filepath.Join(t.TempDir(), "absent.conf") - - if _, err := ReadFRR(p); err == nil || !strings.Contains(err.Error(), "node hv1: ") || !strings.Contains(err.Error(), "absent.conf") { - t.Errorf("error = %v", err) - } -} - -func TestPrepare_PutsTheFRRConfigIntoTheSeed(t *testing.T) { - m := newMirror(t) - publish(m, []byte("qcow2 image")) - root := t.TempDir() - conf := filepath.Join(root, "hv1.conf") - if err := os.WriteFile(conf, []byte("hostname hv1\n"), 0o600); err != nil { - t.Fatal(err) - } - p := labPlan(t, m) - p.Nodes[2].FRR = conf - - nodes, err := Prepare(context.Background(), p, Options{ - RunDir: filepath.Join(root, "run"), - Fetcher: Fetcher{Client: m.server.Client(), CacheDir: filepath.Join(root, "cache")}, - Runner: &fakeRunner{}, - }) - if err != nil { - t.Fatalf("Prepare: %v", err) - } - data, err := os.ReadFile(filepath.Join(nodes[2].Dir, "user-data")) - if err != nil { - t.Fatal(err) - } - var cfg struct { - Files []struct { - Path string `yaml:"path"` - Content string `yaml:"content"` - } `yaml:"write_files"` - } - if err := yaml.Unmarshal(data, &cfg); err != nil { - t.Fatal(err) - } - for _, f := range cfg.Files { - if f.Path == "/etc/lab/frr.conf" { - if f.Content != "hostname hv1\n" { - t.Errorf("frr.conf = %q", f.Content) - } - return - } - } - t.Errorf("hv1 user-data has no /etc/lab/frr.conf:\n%s", data) -} diff --git a/internal/lab/render/frrouting.gpg b/internal/lab/render/frrouting.gpg deleted file mode 100644 index c38f82b..0000000 Binary files a/internal/lab/render/frrouting.gpg and /dev/null differ diff --git a/internal/lab/render/render.go b/internal/lab/render/render.go index 6a11f0d..49b3089 100644 --- a/internal/lab/render/render.go +++ b/internal/lab/render/render.go @@ -30,7 +30,6 @@ var Nameservers = []string{"1.1.1.1", "8.8.8.8"} type Options struct { RunDir string AuthorizedKeys []string - FRR map[string]string } type Node struct { @@ -46,11 +45,6 @@ func Render(p *topology.Plan, o Options) ([]Node, error) { if err := o.validate(); err != nil { return nil, err } - for _, n := range p.Nodes { - 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) - } - } var nodes []Node for index, n := range p.Nodes { dir := filepath.Join(o.RunDir, n.Name) @@ -58,7 +52,7 @@ func Render(p *topology.Plan, o Options) ([]Node, error) { if err != nil { return nil, err } - user, err := userData(p, n, o) + user, err := userData(p, n, o.AuthorizedKeys) if err != nil { return nil, err } diff --git a/internal/lab/render/render_test.go b/internal/lab/render/render_test.go index 78bb186..f270d13 100644 --- a/internal/lab/render/render_test.go +++ b/internal/lab/render/render_test.go @@ -22,8 +22,8 @@ segments: nodes: sw1: { role: switch, image: deb, cpus: 2, memory: 1024 } rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay] } - hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 } - hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 } + hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] } + hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] } ` const twoSegments = `name: two-seg @@ -36,7 +36,7 @@ segments: blue: { switch: sw, cidr: 10.2.0.0/24, mtu: 1500 } nodes: sw: { role: switch, image: deb, cpus: 1, memory: 512 } - hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [blue, red], release: 0.2.0rc002 } + hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [blue, red] } ` func plan(t *testing.T, doc string) *topology.Plan { @@ -317,7 +317,7 @@ func TestNetworkConfig_DefaultRouteOnlyOnFirstSegment(t *testing.T) { func TestUserData_Hypervisor(t *testing.T) { cfg := user(t, nodeNamed(t, renderAll(t, twoHypervisors), "hv1")) - if cfg.Hostname != "hv1" || !cfg.DisableRoot || len(cfg.Packages) != 0 || !reflect.DeepEqual(cfg.Runcmd, [][]string{{"/usr/local/sbin/lab-provision"}}) { + if cfg.Hostname != "hv1" || !cfg.DisableRoot || len(cfg.WriteFiles) != 0 || len(cfg.Runcmd) != 0 || len(cfg.Packages) != 0 { t.Errorf("hv1 user-data = %+v", cfg) } if !reflect.DeepEqual(cfg.SSHAuthorizedKeys, []string{labKey}) { @@ -376,12 +376,9 @@ nft -f /etc/lab-switch.nft t.Errorf("unit misses %q", want) } } - if !reflect.DeepEqual(cfg.Runcmd, [][]string{{"/usr/local/sbin/lab-provision"}}) { + if !reflect.DeepEqual(cfg.Runcmd, [][]string{{"systemctl", "daemon-reload"}, {"systemctl", "enable", "--now", "lab-switch.service"}}) { t.Errorf("runcmd = %v", cfg.Runcmd) } - if got := fileAt(t, cfg, "/usr/local/sbin/lab-provision").Content; got != "#!/bin/sh\nset -eu\nsystemctl daemon-reload\nsystemctl enable --now lab-switch.service\n" { - t.Errorf("lab-provision:\n%s", got) - } } func TestUserData_SwitchWithTwoSegments(t *testing.T) { diff --git a/internal/lab/render/roles_test.go b/internal/lab/render/roles_test.go deleted file mode 100644 index 001677b..0000000 --- a/internal/lab/render/roles_test.go +++ /dev/null @@ -1,264 +0,0 @@ -package render - -import ( - "bytes" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "os" - "reflect" - "strings" - "testing" -) - -const withRoles = `name: evpn-2hv -images: - deb: - url: https://example.invalid/deb.qcow2 - sums: https://example.invalid/SHA512SUMS -segments: - underlay: { switch: sw1, cidr: 192.168.14.0/24 } -nodes: - sw1: { role: switch, image: deb, cpus: 2, memory: 1024, secondary: { underlay: [169.254.0.1/28] }, frr: sw1.conf } - rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay], secondary: { underlay: [169.254.0.3/28] }, loopback: 10.255.255.1/32, frr: rr1.conf } - hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], frr: hv1.conf, release: 0.2.0rc002 } - hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 } -` - -var frrConfigs = map[string]string{ - "sw1": "hostname sw1\nrouter bgp 65100\n", - "rr1": "hostname rr1\nrouter bgp 65000\n", - "hv1": "hostname hv1\nrouter bgp 64600\n", -} - -func renderRoles(t *testing.T) map[string]Node { - t.Helper() - nodes, err := Render(plan(t, withRoles), Options{RunDir: "/srv/lab", AuthorizedKeys: []string{labKey}, FRR: frrConfigs}) - if err != nil { - t.Fatalf("Render: %v", err) - } - out := map[string]Node{} - for _, n := range nodes { - out[n.Name] = n - } - return out -} - -func runcmd(t *testing.T, n Node) []string { - t.Helper() - var out []string - for _, c := range user(t, n).Runcmd { - out = append(out, strings.Join(c, " ")) - } - return out -} - -const frrScriptHead = `#!/bin/sh -set -eu -export DEBIAN_FRONTEND=noninteractive -. /etc/os-release -echo "deb [signed-by=/usr/share/keyrings/frrouting.gpg] https://deb.frrouting.org/frr ${VERSION_CODENAME} frr-stable" > /etc/apt/sources.list.d/frr.list -n=0 -until apt-get update -qq --error-on=any; do - n=$((n + 1)) - [ "$n" -lt 30 ] || exit 1 - sleep 10 -done -apt-get install -y -qq --no-install-recommends frr frr-pythontools -sed -i 's/^bgpd=no/bgpd=yes/' /etc/frr/daemons -` - -func TestRoles_SecondaryAddressFollowsThePrimaryOnTheNode(t *testing.T) { - doc := network(t, nodeNamed(t, renderRoles(t), "rr1")) - got := iface(t, doc, "underlay").Addresses - if !reflect.DeepEqual(got, []string{"192.168.14.2/24", "169.254.0.3/28"}) { - t.Errorf("rr1 underlay addresses = %v", got) - } -} - -func TestRoles_SwitchCarriesItsSecondaryOnTheBridge(t *testing.T) { - script := fileAt(t, user(t, nodeNamed(t, renderRoles(t), "sw1")), "/usr/local/sbin/lab-switch").Content - want := "ip addr replace 192.168.14.1/24 dev br-underlay\nip addr replace 169.254.0.1/28 dev br-underlay\nip link set dev br-underlay up\n" - if !strings.Contains(script, want) { - t.Errorf("lab-switch:\n%s\ndoes not contain:\n%s", script, want) - } -} - -func TestRoles_LoopbackOnADummyInterfaceReplayedAtBoot(t *testing.T) { - rr1 := nodeNamed(t, renderRoles(t), "rr1") - cfg := user(t, rr1) - - script := fileAt(t, cfg, "/usr/local/sbin/lab-node") - want := "#!/bin/sh\nset -eu\nip link add lo1 type dummy 2>/dev/null || true\nip addr replace 10.255.255.1/32 dev lo1\nip link set dev lo1 up\n" - if script.Content != want || script.Permissions != "0755" { - t.Errorf("lab-node (%s):\n%s", script.Permissions, script.Content) - } - unit := fileAt(t, cfg, "/etc/systemd/system/lab-node.service").Content - if !strings.Contains(unit, "ExecStart=/usr/local/sbin/lab-node\n") || !strings.Contains(unit, "WantedBy=multi-user.target\n") { - t.Errorf("lab-node.service:\n%s", unit) - } - want = "#!/bin/sh\nset -eu\nsystemctl daemon-reload\nsystemctl enable --now lab-node.service\n/usr/local/sbin/lab-frr\n" - if got := fileAt(t, cfg, "/usr/local/sbin/lab-provision").Content; got != want { - t.Errorf("lab-provision:\n%s\nwant:\n%s", got, want) - } -} - -func TestRoles_FRRConfigIsWrittenVerbatimAndPrivately(t *testing.T) { - for _, name := range []string{"sw1", "rr1", "hv1"} { - f := fileAt(t, user(t, nodeNamed(t, renderRoles(t), name)), "/etc/lab/frr.conf") - if f.Content != frrConfigs[name] || f.Permissions != "0640" { - t.Errorf("%s frr.conf (%s) = %q", name, f.Permissions, f.Content) - } - } -} - -func TestRoles_FRRDaemonsDependOnTheRole(t *testing.T) { - tail := "install -o frr -g frr -m 0640 /etc/lab/frr.conf /etc/frr/frr.conf\nsystemctl restart frr\n" - bfd := "sed -i 's/^bfdd=no/bfdd=yes/' /etc/frr/daemons\n" - nodes := renderRoles(t) - for name, want := range map[string]string{ - "sw1": frrScriptHead + bfd + tail, - "rr1": frrScriptHead + bfd + tail, - "hv1": frrScriptHead + tail, - } { - f := fileAt(t, user(t, nodeNamed(t, nodes, name)), "/usr/local/sbin/lab-frr") - if f.Content != want || f.Permissions != "0755" { - t.Errorf("%s lab-frr (%s):\n%s\nwant:\n%s", name, f.Permissions, f.Content, want) - } - } -} - -func TestRoles_FRRKeyIsThePinnedRepositoryKey(t *testing.T) { - f := fileAt(t, user(t, nodeNamed(t, renderRoles(t), "hv1")), "/usr/share/keyrings/frrouting.gpg") - if f.Encoding != "b64" || f.Permissions != "0644" { - t.Errorf("key file: encoding %q, permissions %q", f.Encoding, f.Permissions) - } - key, err := base64.StdEncoding.DecodeString(f.Content) - if err != nil { - t.Fatal(err) - } - sum := sha256.Sum256(key) - if got := hex.EncodeToString(sum[:]); got != "bf10935b9296e2ce7c5d9855fa29ef30c35810b0fc4b1f53005494a04a33554d" { - t.Errorf("key sha256 = %s", got) - } -} - -func TestRoles_ProvisioningIsOneScriptThatStopsAtTheFirstFailure(t *testing.T) { - nodes := renderRoles(t) - for name, steps := range map[string]string{ - "sw1": "systemctl daemon-reload\nsystemctl enable --now lab-switch.service\n/usr/local/sbin/lab-frr\n", - "hv1": "/usr/local/sbin/lab-deploy\n/usr/local/sbin/lab-frr\n", - "hv2": "/usr/local/sbin/lab-deploy\n", - } { - cfg := user(t, nodeNamed(t, nodes, name)) - if !reflect.DeepEqual(cfg.Runcmd, [][]string{{"/usr/local/sbin/lab-provision"}}) { - t.Errorf("%s runcmd = %q", name, cfg.Runcmd) - } - f := fileAt(t, cfg, "/usr/local/sbin/lab-provision") - if f.Content != "#!/bin/sh\nset -eu\n"+steps || f.Permissions != "0755" { - t.Errorf("%s lab-provision (%s):\n%s", name, f.Permissions, f.Content) - } - } -} - -func TestRoles_HypervisorDeploysTheReleaseThroughTheRepositoryScripts(t *testing.T) { - cfg := user(t, nodeNamed(t, renderRoles(t), "hv1")) - - f := fileAt(t, cfg, "/usr/local/sbin/lab-deploy") - want := `#!/bin/sh -set -eu -n=0 -until curl -fsS -o /dev/null https://git.g3e.fr/; do - n=$((n + 1)) - [ "$n" -lt 30 ] || exit 1 - sleep 10 -done -cd /opt/two/scripts -bash ./deploy.sh --noup_script -i -u underlay -t 0.2.0rc002 -` - if f.Content != want || f.Permissions != "0755" { - t.Errorf("lab-deploy (%s):\n%s\nwant:\n%s", f.Permissions, f.Content, want) - } - - for path, source := range map[string]string{ - "/opt/two/scripts/deploy.sh": "../../../scripts/deploy.sh", - "/opt/two/scripts/bootstrap_kvm.sh": "../../../scripts/bootstrap_kvm.sh", - } { - f := fileAt(t, cfg, path) - got, err := base64.StdEncoding.DecodeString(f.Content) - if err != nil { - t.Fatal(err) - } - repo, err := os.ReadFile(source) - if err != nil { - t.Fatal(err) - } - if f.Encoding != "b64" || f.Permissions != "0755" || !bytes.Equal(got, repo) { - t.Errorf("%s: encoding %q, permissions %q, identical to %s: %v", path, f.Encoding, f.Permissions, source, bytes.Equal(got, repo)) - } - } -} - -func TestRoles_UplinkIsTheInterfaceCarryingTheDefaultRoute(t *testing.T) { - hv := nodeNamed(t, renderAll(t, twoSegments), "hv") - var withDefault []string - for name, e := range network(t, hv).Ethernets { - for _, r := range e.Routes { - if r.To == "0.0.0.0/0" { - withDefault = append(withDefault, name) - } - } - } - if !reflect.DeepEqual(withDefault, []string{"red"}) { - t.Fatalf("default route on %v, want [red]", withDefault) - } - if got := fileAt(t, user(t, hv), "/usr/local/sbin/lab-deploy").Content; !strings.HasSuffix(got, "bash ./deploy.sh --noup_script -i -u red -t 0.2.0rc002\n") { - t.Errorf("lab-deploy:\n%s", got) - } -} - -func TestRoles_OnlyHypervisorsDeployTwo(t *testing.T) { - nodes := renderRoles(t) - for _, name := range []string{"sw1", "rr1"} { - for _, f := range user(t, nodeNamed(t, nodes, name)).WriteFiles { - if strings.HasPrefix(f.Path, "/opt/two/") || f.Path == "/usr/local/sbin/lab-deploy" { - t.Errorf("%s receives %s", name, f.Path) - } - } - } -} - -func TestRoles_NodeWithoutRoleFieldsGetsNoFRR(t *testing.T) { - for _, f := range user(t, nodeNamed(t, renderRoles(t), "hv2")).WriteFiles { - if strings.Contains(f.Path, "frr") { - t.Errorf("hv2 receives %s", f.Path) - } - } -} - -func TestRoles_RefusesAnUnreadFRRConfig(t *testing.T) { - configs := map[string]string{"sw1": "x", "rr1": "y"} - _, err := Render(plan(t, withRoles), Options{RunDir: "/srv/lab", AuthorizedKeys: []string{labKey}, FRR: configs}) - if err == nil || err.Error() != "node hv1: frr configuration hv1.conf was not read" { - t.Errorf("error = %v", err) - } -} - -func TestRoles_SwitchLoopbackIsCreatedByTheSwitchScript(t *testing.T) { - doc := strings.Replace(withRoles, "secondary: { underlay: [169.254.0.1/28] }, frr: sw1.conf", "secondary: { underlay: [169.254.0.1/28] }, loopback: 10.255.254.1/32, frr: sw1.conf", 1) - nodes, err := Render(plan(t, doc), Options{RunDir: "/srv/lab", AuthorizedKeys: []string{labKey}, FRR: frrConfigs}) - if err != nil { - t.Fatalf("Render: %v", err) - } - var sw1 Node - for _, n := range nodes { - if n.Name == "sw1" { - sw1 = n - } - } - script := fileAt(t, user(t, sw1), "/usr/local/sbin/lab-switch").Content - want := "ip link set dev br-underlay up\nip link add lo1 type dummy 2>/dev/null || true\nip addr replace 10.255.254.1/32 dev lo1\nip link set dev lo1 up\nnft -f /etc/lab-switch.nft\n" - if !strings.HasSuffix(script, want) { - t.Errorf("lab-switch:\n%s\ndoes not end with:\n%s", script, want) - } -} diff --git a/internal/lab/render/seed.go b/internal/lab/render/seed.go index 804ed45..c959d3c 100644 --- a/internal/lab/render/seed.go +++ b/internal/lab/render/seed.go @@ -1,41 +1,20 @@ package render import ( - _ "embed" - "encoding/base64" "fmt" "strings" "go.yaml.in/yaml/v3" "git.g3e.fr/syonad/two/internal/lab/topology" - "git.g3e.fr/syonad/two/scripts" ) const ( SwitchScript = "/usr/local/sbin/lab-switch" SwitchNFT = "/etc/lab-switch.nft" SwitchUnit = "/etc/systemd/system/lab-switch.service" - - NodeScript = "/usr/local/sbin/lab-node" - NodeUnit = "/etc/systemd/system/lab-node.service" - - FRRKey = "/usr/share/keyrings/frrouting.gpg" - FRRConfig = "/etc/lab/frr.conf" - FRRScript = "/usr/local/sbin/lab-frr" - FRRSuite = "frr-stable" - FRRRepo = "https://deb.frrouting.org/frr" - FRRPackages = "frr frr-pythontools" - - ProvisionScript = "/usr/local/sbin/lab-provision" - DeployScript = "/usr/local/sbin/lab-deploy" - TwoScriptsDir = "/opt/two/scripts" - TwoGitServer = "https://git.g3e.fr/" ) -//go:embed frrouting.gpg -var frrKey []byte - type metaDoc struct { InstanceID string `yaml:"instance-id"` LocalHostname string `yaml:"local-hostname"` @@ -44,7 +23,6 @@ type metaDoc struct { type writeFile struct { Path string `yaml:"path"` Permissions string `yaml:"permissions"` - Encoding string `yaml:"encoding,omitempty"` Content string `yaml:"content"` } @@ -90,51 +68,24 @@ func metaData(p *topology.Plan, n topology.NodePlan) ([]byte, error) { return yaml.Marshal(metaDoc{InstanceID: p.Name + "-" + n.Name, LocalHostname: n.Name}) } -func userData(p *topology.Plan, n topology.NodePlan, o Options) ([]byte, error) { +func userData(p *topology.Plan, n topology.NodePlan, keys []string) ([]byte, error) { cfg := cloudConfig{ Hostname: n.Name, SSHPwauth: false, DisableRoot: true, - SSHAuthorizedKeys: o.AuthorizedKeys, + SSHAuthorizedKeys: keys, } - var steps []string - switch { - case n.Role == topology.RoleSwitch: + if n.Role == topology.RoleSwitch { cfg.Packages = []string{"nftables"} cfg.WriteFiles = []writeFile{ - {Path: SwitchScript, Permissions: "0755", Content: switchScript(p, n)}, + {Path: SwitchScript, Permissions: "0755", Content: switchScript(p, n.Name)}, {Path: SwitchNFT, Permissions: "0644", Content: switchNFT(p, n.Name)}, - {Path: SwitchUnit, Permissions: "0644", Content: unit("Lab switch: bridges, gateways and NAT", SwitchScript)}, + {Path: SwitchUnit, Permissions: "0644", Content: switchUnit()}, } - steps = append(steps, "systemctl daemon-reload", "systemctl enable --now lab-switch.service") - case n.Loopback.IsValid(): - cfg.WriteFiles = []writeFile{ - {Path: NodeScript, Permissions: "0755", Content: "#!/bin/sh\nset -eu\n" + loopbackLines(n)}, - {Path: NodeUnit, Permissions: "0644", Content: unit("Lab node: loopback", NodeScript)}, + cfg.Runcmd = [][]string{ + {"systemctl", "daemon-reload"}, + {"systemctl", "enable", "--now", "lab-switch.service"}, } - steps = append(steps, "systemctl daemon-reload", "systemctl enable --now lab-node.service") - } - 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)}, - writeFile{Path: TwoScriptsDir + "/bootstrap_kvm.sh", Permissions: "0755", Encoding: "b64", Content: base64.StdEncoding.EncodeToString(scripts.BootstrapKVM)}, - writeFile{Path: DeployScript, Permissions: "0755", Content: deployScript(p, n)}, - ) - steps = append(steps, DeployScript) - } - if n.FRR != "" { - cfg.WriteFiles = append(cfg.WriteFiles, - writeFile{Path: FRRKey, Permissions: "0644", Encoding: "b64", Content: base64.StdEncoding.EncodeToString(frrKey)}, - writeFile{Path: FRRConfig, Permissions: "0640", Content: o.FRR[n.Name]}, - writeFile{Path: FRRScript, Permissions: "0755", Content: frrScript(n)}, - ) - steps = append(steps, FRRScript) - } - if len(steps) > 0 { - cfg.WriteFiles = append(cfg.WriteFiles, - writeFile{Path: ProvisionScript, Permissions: "0755", Content: "#!/bin/sh\nset -eu\n" + strings.Join(steps, "\n") + "\n"}, - ) - cfg.Runcmd = [][]string{{ProvisionScript}} } body, err := yaml.Marshal(cfg) if err != nil { @@ -143,47 +94,6 @@ func userData(p *topology.Plan, n topology.NodePlan, o Options) ([]byte, error) return append([]byte("#cloud-config\n"), body...), nil } -func retry(command string) string { - return "n=0\nuntil " + command + "; do\n n=$((n + 1))\n [ \"$n\" -lt 30 ] || exit 1\n sleep 10\ndone\n" -} - -func deployScript(p *topology.Plan, n topology.NodePlan) string { - uplink := nodeCables(p, n.Name)[0].NodeInterface - return "#!/bin/sh\nset -eu\n" + - retry("curl -fsS -o /dev/null "+TwoGitServer) + - "cd " + TwoScriptsDir + "\n" + - fmt.Sprintf("bash ./deploy.sh --noup_script -i -u %s -t %s\n", uplink, n.Release) -} - -func frrDaemons(n topology.NodePlan) []string { - if n.Role == topology.RoleHypervisor { - return []string{"bgpd"} - } - return []string{"bgpd", "bfdd"} -} - -func frrScript(n topology.NodePlan) string { - var b strings.Builder - b.WriteString("#!/bin/sh\nset -eu\nexport DEBIAN_FRONTEND=noninteractive\n. /etc/os-release\n") - fmt.Fprintf(&b, "echo \"deb [signed-by=%s] %s ${VERSION_CODENAME} %s\" > /etc/apt/sources.list.d/frr.list\n", FRRKey, FRRRepo, FRRSuite) - b.WriteString(retry("apt-get update -qq --error-on=any")) - fmt.Fprintf(&b, "apt-get install -y -qq --no-install-recommends %s\n", FRRPackages) - for _, d := range frrDaemons(n) { - fmt.Fprintf(&b, "sed -i 's/^%s=no/%s=yes/' /etc/frr/daemons\n", d, d) - } - fmt.Fprintf(&b, "install -o frr -g frr -m 0640 %s /etc/frr/frr.conf\n", FRRConfig) - b.WriteString("systemctl restart frr\n") - return b.String() -} - -func loopbackLines(n topology.NodePlan) string { - if !n.Loopback.IsValid() { - return "" - } - return fmt.Sprintf("ip link add %s type dummy 2>/dev/null || true\nip addr replace %s dev %s\nip link set dev %s up\n", - topology.LoopbackInterface, n.Loopback, topology.LoopbackInterface, topology.LoopbackInterface) -} - func networkConfig(p *topology.Plan, n topology.NodePlan, index int) ([]byte, error) { doc := networkDoc{Version: 2, Ethernets: map[string]ethernet{}} admin := ethernet{ @@ -211,9 +121,6 @@ func networkConfig(p *topology.Plan, n topology.NodePlan, index int) ([]byte, er MTU: c.MTU, Addresses: []string{c.NodeAddress.String()}, } - for _, prefix := range n.Secondary[c.Segment] { - e.Addresses = append(e.Addresses, prefix.String()) - } if i == 0 { e.Routes = []route{{To: "0.0.0.0/0", Via: gatewayOf(p, c.Segment)}} e.Nameservers = &nameservers{Addresses: Nameservers} @@ -232,8 +139,7 @@ func gatewayOf(p *topology.Plan, segment string) string { return "" } -func switchScript(p *topology.Plan, n topology.NodePlan) string { - name := n.Name +func switchScript(p *topology.Plan, name string) string { var b strings.Builder b.WriteString("#!/bin/sh\nset -eu\nsysctl -qw net.ipv4.ip_forward=1\n") for _, s := range switchSegments(p, name) { @@ -247,12 +153,8 @@ func switchScript(p *topology.Plan, n topology.NodePlan) string { } fmt.Fprintf(&b, "ip link set dev %s mtu %d\n", s.Bridge, s.MTU) fmt.Fprintf(&b, "ip addr replace %s/%d dev %s\n", s.Gateway, s.Network.Bits(), s.Bridge) - for _, prefix := range n.Secondary[s.Name] { - fmt.Fprintf(&b, "ip addr replace %s dev %s\n", prefix, s.Bridge) - } fmt.Fprintf(&b, "ip link set dev %s up\n", s.Bridge) } - b.WriteString(loopbackLines(n)) fmt.Fprintf(&b, "nft -f %s\n", SwitchNFT) return b.String() } @@ -273,9 +175,9 @@ table ip lab_nat { `, strings.Join(networks, ", "), AdminInterface) } -func unit(description, script string) string { +func switchUnit() string { return fmt.Sprintf(`[Unit] -Description=%s +Description=Lab switch: bridges, gateways and NAT Wants=network-online.target After=network-online.target @@ -286,5 +188,5 @@ ExecStart=%s [Install] WantedBy=multi-user.target -`, description, script) +`, SwitchScript) } diff --git a/internal/lab/topology/format.go b/internal/lab/topology/format.go index 567933a..e501bff 100644 --- a/internal/lab/topology/format.go +++ b/internal/lab/topology/format.go @@ -3,9 +3,6 @@ package topology import ( "fmt" "io" - "path/filepath" - "sort" - "strings" "text/tabwriter" ) @@ -20,20 +17,6 @@ func (p *Plan) Write(w io.Writer) error { fmt.Fprintf(tw, " %s\t%s\t%s\t%d\t%d MiB\t127.0.0.1:%d\n", n.Name, n.Role, n.Image, n.CPUs, n.Memory, n.SSHPort) } - var extras []NodePlan - for _, n := range p.Nodes { - 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\n") - for _, n := range extras { - fmt.Fprintf(tw, " %s\t%s\t%s\t%s\t%s\n", n.Name, orDash(loopback(n)), orDash(secondary(n)), orDash(filepath.Base(n.FRR)), orDash(n.Release)) - } - } - for _, s := range p.Segments { fmt.Fprintf(tw, "\nsegment %s: %s, mtu %d, switch %s, bridge %s, gateway %s\n", s.Name, s.Network, s.MTU, s.Switch, s.Bridge, s.Gateway) @@ -49,32 +32,3 @@ func (p *Plan) Write(w io.Writer) error { } return tw.Flush() } - -func loopback(n NodePlan) string { - if !n.Loopback.IsValid() { - return "" - } - return LoopbackInterface + " " + n.Loopback.String() -} - -func secondary(n NodePlan) string { - segments := make([]string, 0, len(n.Secondary)) - for s := range n.Secondary { - segments = append(segments, s) - } - sort.Strings(segments) - var parts []string - for _, s := range segments { - for _, prefix := range n.Secondary[s] { - parts = append(parts, s+" "+prefix.String()) - } - } - return strings.Join(parts, ", ") -} - -func orDash(s string) string { - if s == "" || s == "." { - return "-" - } - return s -} diff --git a/internal/lab/topology/plan.go b/internal/lab/topology/plan.go index 83487ba..0fd0ee6 100644 --- a/internal/lab/topology/plan.go +++ b/internal/lab/topology/plan.go @@ -34,16 +34,12 @@ type SegmentPlan struct { } type NodePlan struct { - Name string - Role string - Image string - CPUs int - Memory int - SSHPort int - Secondary map[string][]netip.Prefix - Loopback netip.Prefix - FRR string - Release string + Name string + Role string + Image string + CPUs int + Memory int + SSHPort int } type Cable struct { @@ -69,28 +65,14 @@ func Compute(t *Topology) (*Plan, error) { p := &Plan{Name: t.Name, Images: append([]Image(nil), t.Images...)} for i, n := range t.Nodes { - node := NodePlan{ + p.Nodes = append(p.Nodes, NodePlan{ Name: n.Name, Role: n.Role, Image: n.Image, CPUs: n.CPUs, Memory: n.Memory, SSHPort: SSHBasePort + i, - FRR: n.FRR, - Release: n.Release, - } - for segment, raws := range n.Secondary { - for _, raw := range raws { - if node.Secondary == nil { - node.Secondary = map[string][]netip.Prefix{} - } - node.Secondary[segment] = append(node.Secondary[segment], netip.MustParsePrefix(raw)) - } - } - if n.Loopback != "" { - node.Loopback = netip.MustParsePrefix(n.Loopback) - } - p.Nodes = append(p.Nodes, node) + }) } var errs []error diff --git a/internal/lab/topology/plan_test.go b/internal/lab/topology/plan_test.go index 4f23fef..1621621 100644 --- a/internal/lab/topology/plan_test.go +++ b/internal/lab/topology/plan_test.go @@ -55,8 +55,8 @@ segments: nodes: sw1: { role: switch, image: deb, cpus: 2, memory: 1024 } rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay] } - hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 } - hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], release: 0.2.0rc002 } + hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] } + hv2: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay] } ` func TestCompute_TwoHypervisorsPlan(t *testing.T) { @@ -165,7 +165,7 @@ segments: blue: { switch: sw, cidr: 10.2.0.0/24, mtu: 1500 } nodes: sw: { role: switch, image: deb, cpus: 1, memory: 512 } - hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [blue, red], release: 0.2.0rc002 } + hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [blue, red] } rr: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red] } `) got := make([]string, 0, len(p.Cables)) @@ -193,7 +193,7 @@ nodes: sw: { role: switch, image: deb, cpus: 1, memory: 512 } n1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [a, b] } n2: { role: rr, image: deb, cpus: 1, memory: 512, segments: [a, b] } - n3: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [b, a], release: 0.2.0rc002 } + n3: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [b, a] } `) macs := map[string]bool{} ports := map[int]bool{} @@ -314,11 +314,6 @@ nodes hv1 hypervisor deb 4 16384 MiB 127.0.0.1:2202 hv2 hypervisor deb 4 16384 MiB 127.0.0.1:2203 -roles - name loopback secondary frr release - hv1 - - - 0.2.0rc002 - hv2 - - - 0.2.0rc002 - segment underlay: 10.250.0.0/24, mtu 9000, switch sw1, bridge br-underlay, gateway 10.250.0.1 node interface address mac udp switch port mac udp rr1 underlay 10.250.0.2/24 02:4c:00:01:00:00 20000 <-> sw1 p0 02:4c:00:01:00:01 20001 diff --git a/internal/lab/topology/roles_test.go b/internal/lab/topology/roles_test.go deleted file mode 100644 index 97ee53c..0000000 --- a/internal/lab/topology/roles_test.go +++ /dev/null @@ -1,216 +0,0 @@ -package topology - -import ( - "bytes" - "net/netip" - "os" - "path/filepath" - "reflect" - "testing" -) - -const withRoles = header + ` -segments: - underlay: { switch: sw1, cidr: 192.168.14.0/24 } -nodes: - sw1: { role: switch, image: deb, cpus: 2, memory: 1024, secondary: { underlay: [169.254.0.1/28] }, frr: frr/sw1.conf } - rr1: { role: rr, image: deb, cpus: 1, memory: 1024, segments: [underlay], secondary: { underlay: [169.254.0.3/28] }, loopback: 10.255.255.1/32, frr: frr/rr1.conf } - hv1: { role: hypervisor, image: deb, cpus: 4, memory: 16384, segments: [underlay], frr: /etc/lab/hv1.conf, release: 0.2.0rc002 } -` - -func TestCompute_CarriesTheRoleFields(t *testing.T) { - p := compute(t, withRoles) - - rr1 := nodeOf(t, p, "rr1") - if !reflect.DeepEqual(rr1.Secondary, map[string][]netip.Prefix{"underlay": {netip.MustParsePrefix("169.254.0.3/28")}}) { - t.Errorf("rr1 secondary = %v", rr1.Secondary) - } - if rr1.Loopback != netip.MustParsePrefix("10.255.255.1/32") { - t.Errorf("rr1 loopback = %v", rr1.Loopback) - } - if rr1.FRR != "frr/rr1.conf" { - t.Errorf("rr1 frr = %q", rr1.FRR) - } - hv1 := nodeOf(t, p, "hv1") - if hv1.Secondary != nil || hv1.Loopback.IsValid() { - t.Errorf("hv1 = %+v, want no secondary and no loopback", hv1) - } -} - -func TestLoad_ResolvesFRRPathsAgainstTheTopologyFile(t *testing.T) { - dir := t.TempDir() - path := filepath.Join(dir, "lab.yml") - if err := os.WriteFile(path, []byte(withRoles), 0o600); err != nil { - t.Fatal(err) - } - - topo, err := Load(path) - if err != nil { - t.Fatalf("Load: %v", err) - } - - want := map[string]string{ - "sw1": filepath.Join(dir, "frr", "sw1.conf"), - "rr1": filepath.Join(dir, "frr", "rr1.conf"), - "hv1": "/etc/lab/hv1.conf", - } - for _, n := range topo.Nodes { - if n.FRR != want[n.Name] { - t.Errorf("%s frr = %q, want %q", n.Name, n.FRR, want[n.Name]) - } - } -} - -func TestValidate_RoleFieldRejections(t *testing.T) { - cases := map[string]struct { - node string - want string - }{ - "secondary on a segment not attached": { - `rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], secondary: { blue: [169.254.0.3/28] } }`, - "node rr1: secondary address given for segment blue it is not attached to", - }, - "secondary without prefix length": { - `rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], secondary: { red: [169.254.0.3] } }`, - `node rr1: secondary address "169.254.0.3" on red`, - }, - "secondary in IPv6": { - `rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], secondary: { red: ["fd00::3/64"] } }`, - "node rr1: secondary address fd00::3/64 on red is not IPv4", - }, - "secondary inside the segment": { - `rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], secondary: { red: [10.1.0.9/24] } }`, - "node rr1: secondary address 10.1.0.9/24 is inside segment red (10.1.0.0/24), use addresses instead", - }, - "loopback without prefix length": { - `rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], loopback: 10.255.255.1 }`, - `node rr1: loopback "10.255.255.1"`, - }, - "loopback in IPv6": { - `rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], loopback: "fd00::1/128" }`, - "node rr1: loopback fd00::1/128 is not IPv4", - }, - } - for name, c := range cases { - t.Run(name, func(t *testing.T) { - doc := header + ` -segments: - red: { switch: sw, cidr: 10.1.0.0/24 } - blue: { switch: sw, cidr: 10.2.0.0/24 } -nodes: - sw: { role: switch, image: deb, cpus: 1, memory: 512 } - hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red, blue], release: 0.2.0rc002 } - ` + c.node + ` -` - requireContains(t, validationError(t, doc), c.want) - }) - } -} - -func TestValidate_SwitchSecondaryOnlyOnItsOwnSegments(t *testing.T) { - doc := header + ` -segments: - red: { switch: sw, cidr: 10.1.0.0/24 } - blue: { switch: other, cidr: 10.2.0.0/24 } -nodes: - sw: { role: switch, image: deb, cpus: 1, memory: 512, secondary: { red: [169.254.0.1/28], blue: [169.254.1.1/28] } } - other: { role: switch, image: deb, cpus: 1, memory: 512 } - hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red, blue], release: 0.2.0rc002 } -` - msg := validationError(t, doc) - requireContains(t, msg, "node sw: secondary address given for segment blue it is not attached to") - if bytes.Contains([]byte(msg), []byte("segment red")) { - t.Errorf("the switch's own segment was refused:\n%s", msg) - } -} - -func TestValidate_LoopbackInterfaceNameIsReserved(t *testing.T) { - doc := header + ` -segments: - lo1: { switch: sw, cidr: 10.1.0.0/24 } -nodes: - sw: { role: switch, image: deb, cpus: 1, memory: 512 } - hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [lo1], release: 0.2.0rc002 } -` - requireContains(t, validationError(t, doc), "segment lo1: name is reserved for the loopback interface") -} - -func TestWrite_ShowsTheRoles(t *testing.T) { - var buf bytes.Buffer - if err := compute(t, withRoles).Write(&buf); err != nil { - t.Fatalf("Write: %v", err) - } - want := ` -roles - name loopback secondary frr release - sw1 - underlay 169.254.0.1/28 sw1.conf - - rr1 lo1 10.255.255.1/32 underlay 169.254.0.3/28 rr1.conf - - hv1 - - hv1.conf 0.2.0rc002 -` - if !bytes.Contains(buf.Bytes(), []byte(want)) { - t.Errorf("plan:\n%s\ndoes not contain:\n%s", buf.String(), want) - } -} - -func TestWrite_NoRolesSectionWithoutRoleFields(t *testing.T) { - var buf bytes.Buffer - doc := header + ` -segments: - underlay: { switch: sw1, cidr: 10.250.0.0/24 } -nodes: - sw1: { role: switch, image: deb, cpus: 1, memory: 512 } - rr1: { role: rr, image: deb, cpus: 1, memory: 512, segments: [underlay] } -` - if err := compute(t, doc).Write(&buf); err != nil { - t.Fatalf("Write: %v", err) - } - if bytes.Contains(buf.Bytes(), []byte("roles")) { - t.Errorf("plan shows a roles section:\n%s", buf.String()) - } -} - -func TestValidate_ReleaseRejections(t *testing.T) { - cases := map[string]struct { - node string - want string - }{ - "hypervisor without release": { - `hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red] }`, - "node hv: a hypervisor needs the release of two to deploy (release: )", - }, - "release with shell characters": { - `hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red], release: "0.2.0; reboot" }`, - `node hv: release "0.2.0; reboot" must match`, - }, - "release on a route reflector": { - `hv: { role: rr, image: deb, cpus: 1, memory: 512, segments: [red], release: 0.2.0rc002 }`, - "node hv: release is only for hypervisors", - }, - "release on a switch": { - `hv: { role: hypervisor, image: deb, cpus: 1, memory: 512, segments: [red], release: 0.2.0rc002 } - sw2: { role: switch, image: deb, cpus: 1, memory: 512, release: 0.2.0rc002 }`, - "node sw2: release is only for hypervisors", - }, - } - for name, c := range cases { - t.Run(name, func(t *testing.T) { - doc := header + ` -segments: - red: { switch: sw, cidr: 10.1.0.0/24 } -nodes: - sw: { role: switch, image: deb, cpus: 1, memory: 512 } - ` + c.node + ` -` - requireContains(t, validationError(t, doc), c.want) - }) - } -} - -func TestCompute_CarriesTheRelease(t *testing.T) { - if got := nodeOf(t, compute(t, withRoles), "hv1").Release; got != "0.2.0rc002" { - t.Errorf("hv1 release = %q", got) - } - if got := nodeOf(t, compute(t, withRoles), "rr1").Release; got != "" { - t.Errorf("rr1 release = %q", got) - } -} diff --git a/internal/lab/topology/topology.go b/internal/lab/topology/topology.go index 72c532a..bd184d7 100644 --- a/internal/lab/topology/topology.go +++ b/internal/lab/topology/topology.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "path/filepath" "go.yaml.in/yaml/v3" ) @@ -46,10 +45,6 @@ type Node struct { Memory int Segments []string Addresses map[string]string - Secondary map[string][]string - Loopback string - FRR string - Release string } type fileImage struct { @@ -64,16 +59,12 @@ type fileSegment struct { } type fileNode struct { - Role string `yaml:"role"` - Image string `yaml:"image"` - CPUs int `yaml:"cpus"` - Memory int `yaml:"memory"` - Segments []string `yaml:"segments"` - Addresses map[string]string `yaml:"addresses"` - Secondary map[string][]string `yaml:"secondary"` - Loopback string `yaml:"loopback"` - FRR string `yaml:"frr"` - Release string `yaml:"release"` + Role string `yaml:"role"` + Image string `yaml:"image"` + CPUs int `yaml:"cpus"` + Memory int `yaml:"memory"` + Segments []string `yaml:"segments"` + Addresses map[string]string `yaml:"addresses"` } type file struct { @@ -92,11 +83,6 @@ func Load(path string) (*Topology, error) { if err != nil { return nil, fmt.Errorf("%s: %w", path, err) } - for i, n := range t.Nodes { - if n.FRR != "" && !filepath.IsAbs(n.FRR) { - t.Nodes[i].FRR = filepath.Join(filepath.Dir(path), n.FRR) - } - } return t, nil } @@ -140,10 +126,6 @@ func Parse(data []byte) (*Topology, error) { Memory: n.Memory, Segments: n.Segments, Addresses: n.Addresses, - Secondary: n.Secondary, - Loopback: n.Loopback, - FRR: n.FRR, - Release: n.Release, }) } return t, nil diff --git a/internal/lab/topology/validate.go b/internal/lab/topology/validate.go index 2128c68..6a7d53e 100644 --- a/internal/lab/topology/validate.go +++ b/internal/lab/topology/validate.go @@ -17,13 +17,11 @@ const ( MinPrefix = 8 ReservedInterface = "mgmt0" - LoopbackInterface = "lo1" ) var ( namePattern = regexp.MustCompile(`^[a-z][a-z0-9-]{0,14}$`) segmentPattern = regexp.MustCompile(`^[a-z][a-z0-9]{0,11}$`) - releasePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$`) ) func (t *Topology) Validate() error { @@ -70,9 +68,6 @@ func (t *Topology) Validate() error { if s.Name == ReservedInterface { add("segment %s: name is reserved for the administration interface", s.Name) } - if s.Name == LoopbackInterface { - add("segment %s: name is reserved for the loopback interface", s.Name) - } sw, ok := nodes[s.Switch] switch { case s.Switch == "": @@ -121,7 +116,6 @@ func (t *Topology) Validate() error { if n.Memory < MinMemory { add("node %s: memory must be at least %d MiB", n.Name, MinMemory) } - validateExtras(n, segments, add) if n.Role == RoleSwitch { if len(n.Segments) > 0 || len(n.Addresses) > 0 { add("node %s: a switch carries its segments through segments..switch, not through segments or addresses", n.Name) @@ -171,61 +165,6 @@ func (t *Topology) Validate() error { return nil } -func validateExtras(n Node, segments map[string]Segment, add func(string, ...any)) { - carried := map[string]bool{} - if n.Role == RoleSwitch { - for name, s := range segments { - if s.Switch == n.Name { - carried[name] = true - } - } - } else { - for _, name := range n.Segments { - carried[name] = true - } - } - names := make([]string, 0, len(n.Secondary)) - for name := range n.Secondary { - names = append(names, name) - } - sort.Strings(names) - for _, name := range names { - if !carried[name] { - add("node %s: secondary address given for segment %s it is not attached to", n.Name, name) - continue - } - network, _ := netip.ParsePrefix(segments[name].CIDR) - for _, raw := range n.Secondary[name] { - prefix, err := netip.ParsePrefix(raw) - switch { - case err != nil: - add("node %s: secondary address %q on %s: %v", n.Name, raw, name, err) - case !prefix.Addr().Is4(): - add("node %s: secondary address %s on %s is not IPv4", n.Name, raw, name) - case network.IsValid() && network.Contains(prefix.Addr()): - add("node %s: secondary address %s is inside segment %s (%s), use addresses instead", n.Name, raw, name, network) - } - } - } - switch { - case n.Role == RoleHypervisor && n.Release == "": - add("node %s: a hypervisor needs the release of two to deploy (release: )", n.Name) - case n.Role == RoleHypervisor && !releasePattern.MatchString(n.Release): - add("node %s: release %q must match %s", n.Name, n.Release, releasePattern) - case n.Role != RoleHypervisor && n.Release != "": - add("node %s: release is only for hypervisors", n.Name) - } - if n.Loopback != "" { - prefix, err := netip.ParsePrefix(n.Loopback) - switch { - case err != nil: - add("node %s: loopback %q: %v", n.Name, n.Loopback, err) - case !prefix.Addr().Is4(): - add("node %s: loopback %s is not IPv4", n.Name, n.Loopback) - } - } -} - func sortedKeys(m map[string]string) []string { keys := make([]string, 0, len(m)) for k := range m { diff --git a/scripts/embed.go b/scripts/embed.go deleted file mode 100644 index 481e8e5..0000000 --- a/scripts/embed.go +++ /dev/null @@ -1,9 +0,0 @@ -package scripts - -import _ "embed" - -//go:embed deploy.sh -var Deploy []byte - -//go:embed bootstrap_kvm.sh -var BootstrapKVM []byte diff --git a/scripts/lab-host.sh b/scripts/lab-host.sh index d06c1d7..412fd6a 100755 --- a/scripts/lab-host.sh +++ b/scripts/lab-host.sh @@ -28,16 +28,14 @@ usage: ${0##*/} [arguments] plan résout l'offre horaire, l'OS et les clés SSH, affiche la requête de création et le prix ; ne crée rien - up crée le serveur de lab, attend la fin de son installation et son SSH, - puis le prépare (voir prepare) + up crée le serveur de lab, attend la fin de son installation et son SSH status liste les serveurs de lab du projet ssh [commande] se connecte au serveur de lab ; avec une commande, un terminal n'est demandé que si l'entrée standard en est un prepare installe sur le serveur ce dont lab a besoin (qemu, genisoimage), vérifie /dev/kvm et la virtualisation imbriquée ; lancé aussi par up - push compile cmd/lab pour linux/amd64 et dépose sur le serveur ~/lab et le - répertoire de la topologie dans ~/topology/ (avec les fichiers qu'elle - référence) ; ensuite : ssh './lab up topology/' + push compile cmd/lab pour linux/amd64 et dépose sur le serveur ~/lab et + ~/ ; ensuite : ssh './lab up ' down supprime tous les serveurs de lab du projet et attend leur disparition session [cmd] up, puis la commande distante (ou un shell), puis down quoi qu'il arrive @@ -334,12 +332,6 @@ push_file () { ssh_run -- "cat > '${TARGET}.part' && chmod ${MODE} '${TARGET}.part' && mv '${TARGET}.part' '${TARGET}'" < "${SOURCE}" } -push_dir () { - local SOURCE="${1}" - COPYFILE_DISABLE=1 tar --no-xattrs -C "${SOURCE}" -cf - . \ - | ssh_run -- "rm -rf topology.part && mkdir topology.part && tar -C topology.part --no-same-owner -xf - && rm -rf topology && mv topology.part topology" -} - cmd_push () { local TOPOLOGY="${1:-}" local NAME="${TOPOLOGY##*/}" @@ -352,8 +344,8 @@ cmd_push () { (cd "${REPO_DIR}" && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "${BINARY}" ./cmd/lab) \ || die "compilation de lab échouée" push_file "${BINARY}" lab 755 || die "envoi de lab échoué" - push_dir "$(dirname "${TOPOLOGY}")" || die "envoi du répertoire de ${NAME} échoué" - info "déposés sur le serveur : ~/lab, ~/topology/ — ensuite : ${0##*/} ssh './lab up topology/${NAME}'" + push_file "${TOPOLOGY}" "${NAME}" 644 || die "envoi de ${NAME} échoué" + info "déposés sur le serveur : ~/lab, ~/${NAME} — ensuite : ${0##*/} ssh './lab up ${NAME}'" } delete_server () { diff --git a/scripts/lab-host_test.sh b/scripts/lab-host_test.sh index 9953579..9db9776 100755 --- a/scripts/lab-host_test.sh +++ b/scripts/lab-host_test.sh @@ -147,7 +147,7 @@ case "$*" in *"bash -s"*) cat > "${FAKE_DIR}/prepare.sh" exit "${FAKE_PREPARE_RC:-0}" ;; - *"cat > "*|*"-xf -"*) + *"cat > "*) N=$(ls "${FAKE_DIR}" | grep -c '^pushed\.') cat > "${FAKE_DIR}/pushed.$(( N + 1 ))" exit "${FAKE_PUSH_RC:-0}" ;; @@ -659,27 +659,18 @@ test_prepare_without_known_server_is_refused () { } test_push_builds_for_linux_and_sends_binary_and_topology () { - setup "push : compile lab pour linux/amd64, envoie le binaire et le répertoire de la topologie" + setup "push : compile lab pour linux/amd64, envoie le binaire et la topologie" known_server - mkdir -p "${WORK}/conf/frr" - printf 'name: evpn-2hv\n' > "${WORK}/conf/evpn-2hv.yml" - printf 'hostname rr1\n' > "${WORK}/conf/frr/rr1.conf" - xattr -w com.apple.test lab "${WORK}/conf/evpn-2hv.yml" 2>/dev/null || true - run_lab push "${WORK}/conf/evpn-2hv.yml" || fail "code de sortie $?" - local REPO LISTING + printf 'name: evpn-2hv\n' > "${WORK}/evpn-2hv.yml" + run_lab push "${WORK}/evpn-2hv.yml" || fail "code de sortie $?" + local REPO REPO="$(cd "$(dirname "${SCRIPT}")/.." && pwd)" [[ $(cat "${WORK}/go.log") == "${REPO}|build -o ${WORK}/.cache/two-lab/lab ./cmd/lab|CGO_ENABLED=0 GOOS=linux GOARCH=amd64" ]] \ || fail "compilation : $(cat "${WORK}/go.log")" grep -q "debian@203.0.113.7 cat > 'lab.part' && chmod 755 'lab.part' && mv 'lab.part' 'lab'" "${WORK}/ssh.log" || fail "envoi de lab absent" - grep -q "debian@203.0.113.7 rm -rf topology.part && mkdir topology.part && tar -C topology.part --no-same-owner -xf - && rm -rf topology && mv topology.part topology" "${WORK}/ssh.log" \ - || fail "envoi du répertoire absent" + grep -q "debian@203.0.113.7 cat > 'evpn-2hv.yml.part' && chmod 644 'evpn-2hv.yml.part' && mv 'evpn-2hv.yml.part' 'evpn-2hv.yml'" "${WORK}/ssh.log" || fail "envoi de la topologie absent" [[ $(cat "${WORK}/pushed.1") == "binaire-lab" ]] || fail "contenu de lab : $(cat "${WORK}/pushed.1")" - LISTING=$(tar -tf "${WORK}/pushed.2" | sed 's|^\./||' | grep -v '/$' | grep -v '^$' | sort | tr '\n' ' ') - [[ "${LISTING}" == "evpn-2hv.yml frr/rr1.conf " ]] || fail "contenu de l'archive : ${LISTING}" - grep -aq 'LIBARCHIVE.xattr' "${WORK}/pushed.2" && fail "attributs étendus macOS dans l'archive" - mkdir "${WORK}/x" && tar -C "${WORK}/x" -xf "${WORK}/pushed.2" - [[ $(cat "${WORK}/x/frr/rr1.conf") == "hostname rr1" ]] || fail "frr/rr1.conf altéré" - grep -q "ssh './lab up topology/evpn-2hv.yml'" "${WORK}/out.log" || fail "consigne finale absente" + [[ $(cat "${WORK}/pushed.2") == "name: evpn-2hv" ]] || fail "contenu de la topologie : $(cat "${WORK}/pushed.2")" teardown }