From 6a80e5b55cafd8971c371e151152d6e3e7046736 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 3 Aug 2026 10:21:30 -0400 Subject: [PATCH] =?UTF-8?q?underlay=20:=20un=20r=C3=B4le=20par=20h=C3=B4te?= =?UTF-8?q?,=20et=20les=20hyperviseurs=20au=20mod=C3=A8le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redresser les pairs EVPN vers l'underlay suppose que les hyperviseurs existent dans le modèle. Ils n'y étaient pas. La reconnaissance a montré la cause : vmbr3 porte 10.27.19.{41,43,47} sur les trois nœuds — l'adresse des VTEP est prise dans le supernet de Chezlepro. Le modèle refuse d'exprimer cet état : déclarer 10.27.19.0/24 en underlay ferait échouer P23. La garde détecte la faute avant qu'on ne la documente. Ajouté un `role` sur les hôtes (switch par défaut, hyperviseur, frontiere) : le réseau ne suffit pas à le déduire, et un hyperviseur déclaré recevait une configuration de commutateur en partie B. Corrigé une « source unique » qui n'en était pas une : `switches_acces()` avait été introduite comme LA décision du « qui est un switch d'accès », mais `partie_acces()` gardait sa copie locale du filtre et ne l'appelait jamais. Les deux ont divergé au premier hôte non-commutateur. Écrire « source unique » dans un commentaire ne la crée pas. Preuves : 24 OK, 0 échec. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 34 ++++++++++++++++++++++++++++++++++ docs/sdn-evpn.md | 33 ++++++++++++++++++++++++++++++++- scripts/devis_reseau.py | 9 ++++----- scripts/underlay.py | 24 +++++++++++++++++++++++- underlay.yml.example | 3 +++ 5 files changed, 96 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00d7a2a..724a6c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # CHANGELOG — Set-OPS +## 2026-08-03 (suite 6) — les hyperviseurs entrent au modèle, à leur adresse cible + +Redresser les pairs EVPN vers l'underlay suppose d'abord que les hyperviseurs **existent dans +le modèle**. Ils n'y étaient pas. + +### La reconnaissance a montré la cause +`vmbr3` — la nouvelle interface 2,5G — porte `10.27.19.{41,43,47}` sur les trois nœuds : +l'adresse des VTEP est prise **dans le supernet de Chezlepro**. Le transport du cluster dérive +donc de l'index d'un tenant, et une VM de sa zone *Services-infra* partage son sous-réseau avec +les trois VTEP. + +Le modèle **refuse d'exprimer cet état** : déclarer `10.27.19.0/24` en underlay ferait échouer +**P23**. La garde écrite deux jours plus tôt détecte la faute avant qu'on ne la documente. + +`underlay.yml` déclare donc `asgard`, `gandalf` et `vishnu` à leur adresse **cible** +`10.0.0.{41,43,47}` — dernier octet conservé, comme sur `vmbr0`. + +### Ajouté — un `role` sur les hôtes de l'underlay +`switch` (défaut), `hyperviseur`, `frontiere`. Le réseau ne suffit pas à le déduire : un +hyperviseur partage le réseau de management avec les commutateurs, et **recevait une +configuration de commutateur** en partie B du devis dès qu'on le déclarait. + +### Corrigé — une « source unique » qui n'en était pas une +`switches_acces()` avait été introduite comme *la* décision du « qui est un switch d'accès », +utilisée pour les rayons de l'étoile. Mais `partie_acces()` avait **gardé sa copie locale** du +filtre et ne l'appelait jamais. Les deux ont divergé au premier hôte non-commutateur déclaré. + +Écrire « source unique » dans un commentaire ne la crée pas. + +### Deuxième blocage signalé, non corrigé +`vishnu` : son `vmbr3` n'a **aucun port physique**. Le pont existe, porte une adresse, ne mène +nulle part. Un pair VXLAN pointé sur lui ne fonctionnera jamais — c'est du câblage, pas de la +configuration. + ## 2026-08-03 (suite 5) — l'ICMP entre au registre, parce que l'overlay descend à 1450 Décision : l'**overlay EVPN plafonne à 1450**. Elle a une conséquence qui ne se voit pas — diff --git a/docs/sdn-evpn.md b/docs/sdn-evpn.md index 5f42908..8d4259c 100644 --- a/docs/sdn-evpn.md +++ b/docs/sdn-evpn.md @@ -126,7 +126,38 @@ Un tenant de labo, une zone EVPN, deux VNet, une VM dans chacun — et vérifier Ce n'est qu'après que la question « comment générer cette configuration » se pose. -## 7. Ce qui reste à trancher +## 7. L'état réel du cluster (reconnaissance du 2026-08-03) + +Lecture seule par l'API Proxmox. **Le plan de contrôle existe, le plan de données non.** + +| | | +|---|---| +| Proxmox | 8.4.19 — `asgard`, `gandalf`, `vishnu` | +| Contrôleur | `EVPN0017`, ASN 65000 | +| Zones | `VRF0011`, `VRF0017` — un VRF par tenant, VNI = index, MTU 1450 | +| VNets | **aucun** | +| Nœuds de sortie | **aucun** | + +### Deux blocages à lever avant d'aller plus loin + +**Les VTEP sont adressés dans un tenant.** `vmbr3` porte `10.27.19.{41,43,47}` — le +sous-réseau *Services-infra de Chezlepro*. Le transport du cluster dérive donc de l'index d'un +tenant : un changement d'index le casse, une migration l'emporte. Et une VM de cette zone +partage son sous-réseau avec les trois VTEP, ce qui perce l'isolation à l'endroit même que +l'EVPN devait fermer. + +Le modèle **refuse d'ailleurs d'exprimer cet état** : déclarer `10.27.19.0/24` comme réseau +d'underlay ferait échouer **P23**, qui interdit tout chevauchement avec un supernet tenant. La +garde détecte la faute avant qu'on ne la documente. + +`underlay.yml` déclare donc les trois hyperviseurs à leur adresse **cible** — `10.0.0.{41,43,47}`, +dernier octet conservé comme sur `vmbr0`. Le déplacement réel de l'adresse sur les nœuds reste +à faire : c'est une modification du réseau d'un hyperviseur en service. + +**`vishnu` n'est pas câblé.** Son `vmbr3` n'a **aucun port physique** : le pont existe, porte +une adresse, et ne mène nulle part. Un pair VXLAN pointé sur lui ne fonctionnera jamais. + +## 8. Ce qui reste à trancher - **Où vit la configuration SDN.** Elle est *par cluster*, donc propriété de l'hébergeur — comme `underlay.yml`, et par le même raisonnement. diff --git a/scripts/devis_reseau.py b/scripts/devis_reseau.py index e35d8c2..30f97ba 100644 --- a/scripts/devis_reseau.py +++ b/scripts/devis_reseau.py @@ -237,11 +237,10 @@ def partie_acces(underlay: dict | None, tenants: list, vlans: str, fabric = underlay_mod.fabric_du_routeur(underlay) mgmt = next((r for r in underlay_mod.reseaux_de_fabric(underlay, fabric) if r.get("passerelle")), None) - # UNIQUEMENT les hotes du reseau de management : les autres equipements declares - # (frontieres sur le lien de transit, par exemple) ne sont pas des switches et ne - # doivent recevoir aucune de ces lignes. - autres = [h for h in underlay_mod.hotes(underlay) - if h.get("nom") != r_nom and mgmt and h.get("reseau") == mgmt["nom"]] + # SOURCE UNIQUE : `switches_acces()` decide qui est un switch d'acces — reseau de + # management ET role `switch`. Une copie locale de ce filtre avait laisse les + # hyperviseurs recevoir une configuration de commutateur. + autres = underlay_mod.switches_acces(underlay) if not autres: return [] out = [ diff --git a/scripts/underlay.py b/scripts/underlay.py index ff84ce1..abe2aa5 100644 --- a/scripts/underlay.py +++ b/scripts/underlay.py @@ -187,11 +187,26 @@ def _ports_a_plat(hote: dict) -> list[str]: return plat +ROLES_HOTE = ("switch", "hyperviseur", "frontiere") + + +def role_hote(hote: dict) -> str: + """Nature d'un equipement declare. `switch` par defaut (compatibilite). + + Le reseau ne suffit pas a la deduire : un hyperviseur partage le reseau de + management avec les commutateurs, et recevrait sinon leur configuration. + """ + return str((hote or {}).get("role") or "switch") + + def switches_acces(underlay: dict | None) -> list[dict]: """Les switches d'acces : hotes du reseau de management, hors routeur. Source unique du « qui est un switch d'acces » — utilisee pour leur devis (partie B) ET pour les rayons de l'etoile emis cote routeur. Les deux ne peuvent pas diverger. + + Filtre sur le ROLE autant que sur le reseau : un hyperviseur declare sur le meme + reseau ne doit recevoir aucune ligne de configuration de commutateur. """ r_nom = routeur(underlay) if not r_nom: @@ -201,7 +216,8 @@ def switches_acces(underlay: dict | None) -> list[dict]: if not mgmt: return [] return [h for h in hotes(underlay) - if h.get("nom") != r_nom and h.get("reseau") == mgmt["nom"]] + if h.get("nom") != r_nom and h.get("reseau") == mgmt["nom"] + and role_hote(h) == "switch"] def reseau_transit(underlay: dict | None) -> dict | None: @@ -315,6 +331,12 @@ def valider(underlay: dict | None, erreurs.append(f"reseau '{nom}': passerelle_sortie '{r['passerelle_sortie']}' invalide") vus_sous.append((net, nom)) + for h in hotes(underlay): + r = (h or {}).get("role") + if r is not None and str(r) not in ROLES_HOTE: + erreurs.append(f"hote '{h.get('nom','?')}': role '{r}' inconnu " + f"(attendu : {', '.join(ROLES_HOTE)})") + # Ports physiques : un meme port ne peut pas servir deux fois sur un equipement, # et un rayon ne peut viser qu'un switch d'acces reellement declare. noms_acces = {h.get("nom") for h in switches_acces(underlay)} diff --git a/underlay.yml.example b/underlay.yml.example index 48c27c8..961df4e 100644 --- a/underlay.yml.example +++ b/underlay.yml.example @@ -105,6 +105,9 @@ underlay: # montante : port vers le routeur, cote SWITCH D'ACCES uniquement # `make underlay` refuse un port declare deux fois sur un meme equipement, un rayon # vers un switch inconnu, et une confusion rayons/montante. + # `role` : switch (defaut) | hyperviseur | frontiere. Le reseau ne suffit pas a le + # deduire — un hyperviseur partage le reseau de management avec les commutateurs, et + # recevrait sinon une configuration de commutateur dans la partie B du devis. hotes: # Le switch designe `routeur` porte le SVI de management : son adresse de gestion # EST la passerelle du reseau. `make underlay` refuse les deux valeurs divergentes.