underlay : un rôle par hôte, et les hyperviseurs au modèle

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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-03 10:21:30 -04:00
parent 70fe6de557
commit 6a80e5b55c
5 changed files with 96 additions and 7 deletions

View file

@ -1,5 +1,39 @@
# CHANGELOG — Set-OPS # 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 ## 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 — Décision : l'**overlay EVPN plafonne à 1450**. Elle a une conséquence qui ne se voit pas —

View file

@ -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. 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 — - **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. comme `underlay.yml`, et par le même raisonnement.

View file

@ -237,11 +237,10 @@ def partie_acces(underlay: dict | None, tenants: list, vlans: str,
fabric = underlay_mod.fabric_du_routeur(underlay) fabric = underlay_mod.fabric_du_routeur(underlay)
mgmt = next((r for r in underlay_mod.reseaux_de_fabric(underlay, fabric) mgmt = next((r for r in underlay_mod.reseaux_de_fabric(underlay, fabric)
if r.get("passerelle")), None) if r.get("passerelle")), None)
# UNIQUEMENT les hotes du reseau de management : les autres equipements declares # SOURCE UNIQUE : `switches_acces()` decide qui est un switch d'acces — reseau de
# (frontieres sur le lien de transit, par exemple) ne sont pas des switches et ne # management ET role `switch`. Une copie locale de ce filtre avait laisse les
# doivent recevoir aucune de ces lignes. # hyperviseurs recevoir une configuration de commutateur.
autres = [h for h in underlay_mod.hotes(underlay) autres = underlay_mod.switches_acces(underlay)
if h.get("nom") != r_nom and mgmt and h.get("reseau") == mgmt["nom"]]
if not autres: if not autres:
return [] return []
out = [ out = [

View file

@ -187,11 +187,26 @@ def _ports_a_plat(hote: dict) -> list[str]:
return plat 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]: def switches_acces(underlay: dict | None) -> list[dict]:
"""Les switches d'acces : hotes du reseau de management, hors routeur. """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) 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. 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) r_nom = routeur(underlay)
if not r_nom: if not r_nom:
@ -201,7 +216,8 @@ def switches_acces(underlay: dict | None) -> list[dict]:
if not mgmt: if not mgmt:
return [] return []
return [h for h in hotes(underlay) 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: 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") erreurs.append(f"reseau '{nom}': passerelle_sortie '{r['passerelle_sortie']}' invalide")
vus_sous.append((net, nom)) 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, # 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. # et un rayon ne peut viser qu'un switch d'acces reellement declare.
noms_acces = {h.get("nom") for h in switches_acces(underlay)} noms_acces = {h.get("nom") for h in switches_acces(underlay)}

View file

@ -105,6 +105,9 @@ underlay:
# montante : port vers le routeur, cote SWITCH D'ACCES uniquement # montante : port vers le routeur, cote SWITCH D'ACCES uniquement
# `make underlay` refuse un port declare deux fois sur un meme equipement, un rayon # `make underlay` refuse un port declare deux fois sur un meme equipement, un rayon
# vers un switch inconnu, et une confusion rayons/montante. # 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: hotes:
# Le switch designe `routeur` porte le SVI de management : son adresse de gestion # 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. # EST la passerelle du reseau. `make underlay` refuse les deux valeurs divergentes.