2026-07-07 15:50:18 -04:00
|
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
"""Devis de configuration switch (VLANs + SVIs + ACLs) du réseau convergé.
|
|
|
|
|
|
|
|
|
|
|
|
DÉRIVÉ des nomenclatures `ip-miroir` de toutes les instances fédérées (dossiers
|
|
|
|
|
|
frères `../*/plan/nomenclature.yml` ayant `vmid_schema: ip-miroir` + un `index`).
|
|
|
|
|
|
Rien n'est saisi à la main : le devis reste synchrone avec le plan.
|
|
|
|
|
|
|
|
|
|
|
|
VLAN = 1000 + index×10 + zone (4 chiffres, unique globalement sur le trunk convergé).
|
|
|
|
|
|
Sortie : config Cisco IOS-like (à adapter à la plateforme réelle : NX-OS/IOS-XE, HSRP, VRF).
|
|
|
|
|
|
"""
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
import argparse
|
|
|
|
|
|
import ipaddress
|
2026-07-23 17:15:27 -04:00
|
|
|
|
import os
|
2026-07-07 15:50:18 -04:00
|
|
|
|
import re
|
|
|
|
|
|
import sys
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
|
2026-07-23 17:15:27 -04:00
|
|
|
|
# Dialecte de CLI du commutateur. Cisco exige un masque INVERSE (wildcard 0.0.255.255)
|
|
|
|
|
|
# dans les ACL ; Binardat exige un masque NORMAL (255.255.0.0). Le SVI (ip address) prend
|
|
|
|
|
|
# un masque normal sur les deux. Reglable via --dialecte ou SETOPS_DIALECTE.
|
GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui s'éditait à la main dans un YAML,
pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA.
La frontière avait eu sa section ; l'underlay, non.
La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, donc
les ports) restent hors de portée du panneau — elles demandent une vue
dédiée, comme celle des serveurs.
Le dialecte devient un intrant déclaré : il ne vivait que dans
SETOPS_DIALECTE. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.
Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
|
|
|
|
# Liste des dialectes : source unique dans `underlay` (propriete du materiel).
|
|
|
|
|
|
DIALECTE_SECOURS = "cisco"
|
2026-07-23 17:15:27 -04:00
|
|
|
|
|
2026-07-07 15:50:18 -04:00
|
|
|
|
RACINE = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
DOSSIER_INSTANCES = RACINE.parent
|
|
|
|
|
|
|
Adressage derive du seul seed index (rupture, mode compact retire)
Principe : les valeurs de configuration se derivent des intrants, elles ne se
reecrivent pas a la main. La nomenclature dupliquait ce qu'index determine deja
(supernet, sous-reseaux, passerelles, VLAN). Corrige en rupture nette.
- inventory_rules : source unique de derivation — supernet_de, base3_de,
sous_reseau_de, passerelle_de, vlan_de. Modele 6 zones encode une fois
(2e octet = 10+index, 3e octet zone = 15+categorie, VLAN = 1000+index*10+zone).
deriver_nomenclature ne lit plus aucun adressage stocke ; mode compact supprime.
- devis_reseau : importe ces helpers (fin de la duplication) ; decouvre les
tenants sur `index` present (filtre vmid_schema retire).
- GUI : `index` devient un INTRANT (section Reseau). Il vit dans la nomenclature
(plan reseau uniforme, contrairement aux intrants des modeles heterogenes) et
le GUI l'ecrit chirurgicalement (une ligne, sans reformater). Le miroir JS
derive le VLAN du seed (fin de la lecture de c.vlan stocke).
- socle public : nomenclature au format maigre.
Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke
d'adressage — garde-fou permanent, teste en negatif.
Valide : DIFF VIDE sur les 3 instances (la derivation reproduit exactement
l'adressage stocke), 7 modeles valident, devis_reseau genere les memes VLAN
(1011-1016 derives), make verifier rc=0 CONFORME 20/20, node --check du GUI OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:58:15 -04:00
|
|
|
|
# Adressage et VLAN : derives du seul index, source unique dans inventory_rules.
|
|
|
|
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
|
|
from inventory_rules import passerelle_de, supernet_de, vlan_de # noqa: E402
|
2026-07-24 14:56:38 -04:00
|
|
|
|
import underlay as underlay_mod # noqa: E402 (fabric physique, cluster-global, hors tenant)
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
# Reseaux d'administration : SOURCE UNIQUE partagee avec les nftables d'hote et la
|
|
|
|
|
|
# frontiere (intrant `nftables_admin_ssh`). On reutilise le resolveur, pas une copie.
|
|
|
|
|
|
from resoudre_flux import _sources_admin_ssh # noqa: E402
|
Adressage derive du seul seed index (rupture, mode compact retire)
Principe : les valeurs de configuration se derivent des intrants, elles ne se
reecrivent pas a la main. La nomenclature dupliquait ce qu'index determine deja
(supernet, sous-reseaux, passerelles, VLAN). Corrige en rupture nette.
- inventory_rules : source unique de derivation — supernet_de, base3_de,
sous_reseau_de, passerelle_de, vlan_de. Modele 6 zones encode une fois
(2e octet = 10+index, 3e octet zone = 15+categorie, VLAN = 1000+index*10+zone).
deriver_nomenclature ne lit plus aucun adressage stocke ; mode compact supprime.
- devis_reseau : importe ces helpers (fin de la duplication) ; decouvre les
tenants sur `index` present (filtre vmid_schema retire).
- GUI : `index` devient un INTRANT (section Reseau). Il vit dans la nomenclature
(plan reseau uniforme, contrairement aux intrants des modeles heterogenes) et
le GUI l'ecrit chirurgicalement (une ligne, sans reformater). Le miroir JS
derive le VLAN du seed (fin de la lecture de c.vlan stocke).
- socle public : nomenclature au format maigre.
Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke
d'adressage — garde-fou permanent, teste en negatif.
Valide : DIFF VIDE sur les 3 instances (la derivation reproduit exactement
l'adressage stocke), 7 modeles valident, devis_reseau genere les memes VLAN
(1011-1016 derives), make verifier rc=0 CONFORME 20/20, node --check du GUI OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:58:15 -04:00
|
|
|
|
|
GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui s'éditait à la main dans un YAML,
pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA.
La frontière avait eu sa section ; l'underlay, non.
La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, donc
les ports) restent hors de portée du panneau — elles demandent une vue
dédiée, comme celle des serveurs.
Le dialecte devient un intrant déclaré : il ne vivait que dans
SETOPS_DIALECTE. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.
Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
|
|
|
|
DIALECTES = underlay_mod.DIALECTES
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def dialecte_effectif(explicite: str | None = None) -> str:
|
|
|
|
|
|
"""Drapeau > variable d'environnement > intrant de fabric > secours.
|
|
|
|
|
|
|
|
|
|
|
|
L'intrant `underlay.dialecte` est la valeur DECLAREE (reglable depuis le panneau) ;
|
|
|
|
|
|
l'environnement reste un surcharge ponctuelle pour un essai.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return (explicite or os.environ.get("SETOPS_DIALECTE")
|
|
|
|
|
|
or underlay_mod.dialecte(underlay_mod.charger()) or DIALECTE_SECOURS)
|
|
|
|
|
|
|
2026-07-07 15:50:18 -04:00
|
|
|
|
|
|
|
|
|
|
def masque(cidr: int) -> str:
|
|
|
|
|
|
return str(ipaddress.IPv4Network(f"0.0.0.0/{cidr}").netmask)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def reseau_wildcard(supernet: str) -> tuple[str, str]:
|
|
|
|
|
|
reseau = ipaddress.IPv4Network(supernet, strict=False)
|
|
|
|
|
|
return str(reseau.network_address), str(reseau.hostmask)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 17:15:27 -04:00
|
|
|
|
def masque_acl(supernet: str, dialecte: str) -> tuple[str, str]:
|
|
|
|
|
|
"""Reseau + masque au format ACL du dialecte : wildcard (cisco) ou normal (binardat)."""
|
|
|
|
|
|
reseau = ipaddress.IPv4Network(supernet, strict=False)
|
|
|
|
|
|
if dialecte == "binardat":
|
|
|
|
|
|
return str(reseau.network_address), str(reseau.netmask)
|
|
|
|
|
|
return str(reseau.network_address), str(reseau.hostmask)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def remarque(dialecte: str, texte: str) -> list[str]:
|
|
|
|
|
|
"""Commentaire d'ACL selon le dialecte. Binardat n'a pas de 'remark' : on l'omet."""
|
|
|
|
|
|
return [] if dialecte == "binardat" else [f" remark {texte}"]
|
|
|
|
|
|
|
|
|
|
|
|
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
def _bloc_gestion(underlay: dict | None, nom_switch: str | None) -> list[str]:
|
|
|
|
|
|
"""Interface de gestion d'un switch L2 : son adresse, et sa sortie par defaut.
|
|
|
|
|
|
|
|
|
|
|
|
La sortie est la `passerelle` du reseau de management — qui appartient desormais a
|
|
|
|
|
|
la frontiere, pas a un switch. Le commentaire NOMME son porteur : ecrire « le
|
|
|
|
|
|
routeur » serait faux depuis qu'aucun switch ne route.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not nom_switch:
|
|
|
|
|
|
return []
|
|
|
|
|
|
h = next((x for x in underlay_mod.hotes(underlay)
|
|
|
|
|
|
if x.get("nom") == nom_switch and x.get("role") == "switch"), None)
|
|
|
|
|
|
if not h:
|
|
|
|
|
|
return []
|
|
|
|
|
|
r = next((x for x in underlay_mod.reseaux(underlay) if x.get("nom") == h.get("reseau")), None)
|
|
|
|
|
|
if not r or not r.get("sous_reseau"):
|
|
|
|
|
|
return []
|
|
|
|
|
|
net = ipaddress.ip_network(r["sous_reseau"], strict=False)
|
|
|
|
|
|
out = [f"interface Vlan{r['vlan']}", f" description gestion {nom_switch}",
|
|
|
|
|
|
f" ip address {h['ip']} {net.netmask}", " no shutdown"]
|
|
|
|
|
|
gw = r.get("passerelle")
|
|
|
|
|
|
if gw:
|
|
|
|
|
|
porteur = next((x for x in underlay_mod.hotes(underlay)
|
|
|
|
|
|
if x.get("reseau") == r.get("nom") and str(x.get("ip")) == str(gw)), None)
|
|
|
|
|
|
qui = f"{porteur['nom']} ({porteur.get('role')})" if porteur else "porteur inconnu"
|
|
|
|
|
|
out.append(f"ip default-gateway {gw} ! {qui}")
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-24 14:56:38 -04:00
|
|
|
|
def section_underlay(underlay: dict | None) -> list[str]:
|
2026-08-01 20:36:02 -04:00
|
|
|
|
"""VLANs + SVIs (si passerelle) de la fabric du routeur. Vide si aucun underlay.
|
|
|
|
|
|
|
|
|
|
|
|
Seule la fabric du routeur est configuree ici : les autres (stockage jumbo sur ses
|
|
|
|
|
|
propres switches, par exemple) ne partagent aucun cable avec celle-ci. Declarer
|
|
|
|
|
|
leurs VLAN sur ces switches serait faux, et les mettre dans leurs trunks aussi.
|
|
|
|
|
|
"""
|
|
|
|
|
|
fabric = underlay_mod.fabric_du_routeur(underlay)
|
|
|
|
|
|
reseaux = underlay_mod.reseaux_de_fabric(underlay, fabric)
|
2026-07-24 14:56:38 -04:00
|
|
|
|
if not reseaux:
|
|
|
|
|
|
return []
|
2026-08-01 20:36:02 -04:00
|
|
|
|
out = [f"! ----- 0. Underlay — fabric '{fabric}' (cluster-global, hors tenant) -----"]
|
2026-07-24 14:56:38 -04:00
|
|
|
|
for r in reseaux:
|
|
|
|
|
|
out.append(f"vlan {r['vlan']}")
|
|
|
|
|
|
out.append(f" name {r['nom']}")
|
|
|
|
|
|
if r.get("mtu") and int(r["mtu"]) > 1500:
|
|
|
|
|
|
out.append(f" ! jumbo MTU {r['mtu']} — a appliquer sur les ports + bridges Proxmox")
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
# Un SVI n'est emis que si la passerelle du reseau appartient a un SWITCH. Depuis
|
|
|
|
|
|
# que la frontiere est le seul equipement L3 (2026-08-04), la passerelle est
|
|
|
|
|
|
# ailleurs : emettre un SVI creerait une seconde adresse sur le meme sous-reseau,
|
|
|
|
|
|
# et un second routeur la ou on a decide qu'il n'y en aurait qu'un.
|
|
|
|
|
|
porteurs = {(h.get("reseau"), str(h.get("ip"))): h for h in underlay_mod.hotes(underlay)}
|
|
|
|
|
|
svi = 0
|
2026-07-24 14:56:38 -04:00
|
|
|
|
for r in reseaux:
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
gw = r.get("passerelle")
|
|
|
|
|
|
if not gw:
|
|
|
|
|
|
continue
|
|
|
|
|
|
h = porteurs.get((r.get("nom"), str(gw)))
|
|
|
|
|
|
if not h or h.get("role") != "switch":
|
|
|
|
|
|
out.append(f"! VLAN {r['vlan']} : passerelle {gw} portee par "
|
|
|
|
|
|
f"{h.get('nom') if h else '?'} ({h.get('role') if h else 'inconnu'}) "
|
|
|
|
|
|
f"— aucun SVI ici, ce switch ne route pas.")
|
|
|
|
|
|
continue
|
|
|
|
|
|
net = ipaddress.ip_network(r["sous_reseau"], strict=False)
|
|
|
|
|
|
out += [f"interface Vlan{r['vlan']}", f" description underlay {r['nom']}",
|
|
|
|
|
|
f" ip address {gw} {net.netmask}", " no shutdown"]
|
|
|
|
|
|
svi += 1
|
|
|
|
|
|
if not svi:
|
|
|
|
|
|
# Ce switch ne route pas — mais il faut quand meme pouvoir l'administrer.
|
|
|
|
|
|
# Tant qu'il portait le SVI, son adresse de gestion ETAIT la passerelle et
|
|
|
|
|
|
# sortait plus haut. Sans SVI, elle n'etait plus emise nulle part : le devis
|
|
|
|
|
|
# aurait laisse le switch de tete sans adresse, visible seulement en commentaire.
|
|
|
|
|
|
out += _bloc_gestion(underlay, underlay_mod.routeur(underlay))
|
2026-08-01 20:41:49 -04:00
|
|
|
|
# Roster : seuls les hotes de CETTE fabric. Un equipement d'une autre fabric
|
|
|
|
|
|
# (switches de stockage, par exemple) n'a rien a faire dans ce devis, meme en
|
|
|
|
|
|
# commentaire — c'est une configuration qu'on applique, pas un inventaire.
|
|
|
|
|
|
noms_reseaux = {r["nom"] for r in reseaux}
|
2026-07-24 14:56:38 -04:00
|
|
|
|
for h in underlay_mod.hotes(underlay):
|
2026-08-01 20:41:49 -04:00
|
|
|
|
if h.get("reseau") in noms_reseaux:
|
|
|
|
|
|
out.append(f"! {h.get('nom','?')} = {h.get('ip','?')} ({h.get('reseau','?')})")
|
2026-08-01 20:36:02 -04:00
|
|
|
|
# Ce que ce devis NE couvre PAS : les fabrics portees par d'autres switches.
|
|
|
|
|
|
for autre in underlay_mod.fabriques(underlay):
|
|
|
|
|
|
if autre == fabric:
|
|
|
|
|
|
continue
|
|
|
|
|
|
r_autres = underlay_mod.reseaux_de_fabric(underlay, autre)
|
|
|
|
|
|
out.append(f"! HORS PERIMETRE — fabric '{autre}' : "
|
|
|
|
|
|
+ ", ".join(f"{r['nom']} (VLAN {r['vlan']})" for r in r_autres))
|
|
|
|
|
|
out.append("! Portee par des switches distincts, sans cable commun avec celle-ci :")
|
|
|
|
|
|
out.append("! ni VLAN a declarer ici, ni trunk, ni spanning-tree partage.")
|
2026-07-24 14:56:38 -04:00
|
|
|
|
out.append("!")
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 21:31:31 -04:00
|
|
|
|
def port_vers(nom_hote: str) -> str:
|
|
|
|
|
|
"""Marqueur de port physique vers un equipement nomme."""
|
|
|
|
|
|
return f"<PORT-VERS-{nom_hote.upper()}>"
|
|
|
|
|
|
|
|
|
|
|
|
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
def hote_nomme(underlay: dict | None, nom: str | None) -> dict:
|
|
|
|
|
|
"""L'hote underlay portant ce nom, ou {}."""
|
|
|
|
|
|
return next((h for h in underlay_mod.hotes(underlay) if h.get("nom") == nom), {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ports_ou_marqueur(hote: dict, clef: str, marqueur: str) -> list[str]:
|
|
|
|
|
|
"""Ports declares pour cette nature de lien, sinon le marqueur a completer."""
|
|
|
|
|
|
valeurs = underlay_mod.ports_de(hote).get(clef) or []
|
|
|
|
|
|
if isinstance(valeurs, str):
|
|
|
|
|
|
valeurs = [valeurs]
|
|
|
|
|
|
return [str(v) for v in valeurs] or [marqueur]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bloc_trunk(port: str, vlans: str, description: str = "", bord: bool = False) -> list[str]:
|
|
|
|
|
|
"""Un bloc d'interface trunk complet — l'interface se lit d'un seul tenant."""
|
|
|
|
|
|
out = [f"interface {port}"]
|
|
|
|
|
|
if description:
|
|
|
|
|
|
out.append(f" description {description}")
|
2026-08-02 18:15:22 -04:00
|
|
|
|
# `allowed vlan <liste>` DEFINIT la liste ; `allowed vlan add <liste>` l'AJOUTE a
|
|
|
|
|
|
# l'existante. Un port trunk neuf autorise tous les VLAN : `add` n'y retrancherait
|
|
|
|
|
|
# rien et le devis donnerait l'illusion de restreindre. La forme sans mot-cle est
|
|
|
|
|
|
# aussi atomique — `none` puis `add` couperait le trunk entre les deux commandes,
|
|
|
|
|
|
# ce qui suffit a perdre la session si on l'applique sur le port de gestion.
|
|
|
|
|
|
# Formes confirmees par `switchport trunk allowed vlan ?` (Binardat, 2026-08-02).
|
|
|
|
|
|
out += [" switchport mode trunk", f" switchport trunk allowed vlan {vlans}"]
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
if bord:
|
|
|
|
|
|
out.append(" spanning-tree portfast trunk")
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 21:31:31 -04:00
|
|
|
|
def section_rayons(underlay: dict | None, vlans: str) -> list[str]:
|
|
|
|
|
|
"""Les rayons de l'etoile : un trunk du routeur vers chaque switch d'acces.
|
|
|
|
|
|
|
|
|
|
|
|
Ils sont emis A PART du trunk vers les hyperviseurs parce qu'ils n'ont pas le meme
|
|
|
|
|
|
statut vis-a-vis du spanning-tree : un rayon porte les BPDU de la fabric et ne doit
|
|
|
|
|
|
JAMAIS etre declare en bord de reseau, sinon la protection tombe la ou elle sert.
|
|
|
|
|
|
"""
|
|
|
|
|
|
acces = underlay_mod.switches_acces(underlay)
|
|
|
|
|
|
if not acces:
|
|
|
|
|
|
return []
|
|
|
|
|
|
out = ["! ----- 4c. Rayons de l'etoile (vers les switches d'acces) -----",
|
|
|
|
|
|
"! /!\\ CES PORTS NE SONT PAS DES PORTS DE BORD. Ils portent les BPDU de la",
|
|
|
|
|
|
"! fabric : y appliquer `portfast` desactiverait la protection anti-boucle",
|
|
|
|
|
|
"! exactement la ou elle sert. La section 6 ne les declare volontairement pas."]
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
rayons = underlay_mod.ports_de(hote_nomme(underlay, underlay_mod.routeur(underlay))).get("rayons") or {}
|
2026-08-01 21:31:31 -04:00
|
|
|
|
for h in acces:
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
port = str(rayons.get(h["nom"]) or port_vers(h["nom"]))
|
|
|
|
|
|
out += bloc_trunk(port, vlans, f"rayon vers {h['nom']}")
|
2026-08-01 21:31:31 -04:00
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 20:50:55 -04:00
|
|
|
|
def _stp_lignes(dialecte: str, mode: str, priorite: int) -> list[str]:
|
|
|
|
|
|
"""Mode + priorite de pont, dans la forme du dialecte.
|
|
|
|
|
|
|
|
|
|
|
|
Cisco IOS parle `rapid-pvst` (RSTP par VLAN) ; les plateformes generiques parlent
|
2026-08-02 18:52:37 -04:00
|
|
|
|
`rstp`/`mstp` et une priorite globale.
|
|
|
|
|
|
|
devis switch : spanning-tree vérifié, les six familles de syntaxe sont closes
`spanning-tree ?` en mode configuration tranche la dernière inconnue, en
faveur de la forme émise : `mode` et `priority` s'acceptent au niveau global.
La priorité n'a pas besoin d'être portée par une instance, même en MSTP — la
réserve inverse, notée la veille, était infondée et le devis ne la porte
plus. Une mise en garde fausse nuit autant qu'une syntaxe fausse.
Ajouté : `spanning-tree` seul, qui garantit l'état actif. Sans lui, `mode` et
`priority` sur un boîtier où le protocole aurait été désactivé
configureraient un arbre qui ne tourne pas. Sans effet s'il est déjà actif —
même logique déclarative que pour les trunks.
Six familles vérifiées contre le matériel : VLAN, SVI, trunks, routes, ACL,
spanning-tree. Deux ont révélé un défaut réel plutôt que de confirmer
l'existant — les routes (CIDR) et les trunks, dont `add` ne retranchait rien.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 19:28:22 -04:00
|
|
|
|
Formes VERIFIEES sur Binardat (`spanning-tree ?` en mode configuration, 2026-08-02) :
|
|
|
|
|
|
`spanning-tree` seul active le protocole, `mode` et `priority` s'acceptent au niveau
|
|
|
|
|
|
GLOBAL — la priorite n'a pas besoin d'etre portee par une instance, meme en MSTP.
|
2026-08-01 20:50:55 -04:00
|
|
|
|
"""
|
|
|
|
|
|
if dialecte == "cisco":
|
|
|
|
|
|
return [f"spanning-tree mode {'rapid-pvst' if mode == 'rstp' else mode}",
|
|
|
|
|
|
f"spanning-tree vlan 1-4094 priority {priorite}"]
|
devis switch : spanning-tree vérifié, les six familles de syntaxe sont closes
`spanning-tree ?` en mode configuration tranche la dernière inconnue, en
faveur de la forme émise : `mode` et `priority` s'acceptent au niveau global.
La priorité n'a pas besoin d'être portée par une instance, même en MSTP — la
réserve inverse, notée la veille, était infondée et le devis ne la porte
plus. Une mise en garde fausse nuit autant qu'une syntaxe fausse.
Ajouté : `spanning-tree` seul, qui garantit l'état actif. Sans lui, `mode` et
`priority` sur un boîtier où le protocole aurait été désactivé
configureraient un arbre qui ne tourne pas. Sans effet s'il est déjà actif —
même logique déclarative que pour les trunks.
Six familles vérifiées contre le matériel : VLAN, SVI, trunks, routes, ACL,
spanning-tree. Deux ont révélé un défaut réel plutôt que de confirmer
l'existant — les routes (CIDR) et les trunks, dont `add` ne retranchait rien.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 19:28:22 -04:00
|
|
|
|
# `spanning-tree` seul garantit l'etat actif : sur un boitier ou il aurait ete
|
|
|
|
|
|
# desactive, les deux lignes suivantes ne suffiraient pas. Sans effet s'il l'est deja.
|
|
|
|
|
|
return ["spanning-tree",
|
|
|
|
|
|
f"spanning-tree mode {mode}",
|
|
|
|
|
|
f"spanning-tree priority {priorite}"]
|
2026-08-01 20:50:55 -04:00
|
|
|
|
|
|
|
|
|
|
|
2026-08-01 21:35:32 -04:00
|
|
|
|
def section_stp(underlay: dict | None, dialecte: str) -> list[str]:
|
2026-08-01 20:50:55 -04:00
|
|
|
|
"""Spanning-tree du switch routeur : il est la racine, par construction.
|
|
|
|
|
|
|
|
|
|
|
|
En etoile, tous les chemins passent deja par le centre : en faire le pont racine
|
|
|
|
|
|
aligne l'arbre sur la topologie physique au lieu de laisser une election arbitraire
|
|
|
|
|
|
le decider. Aucun lien redondant n'existe, donc aucune boucle — RSTP reste actif
|
|
|
|
|
|
comme filet contre un brassage accidentel, qui provoquerait sinon une tempete de
|
|
|
|
|
|
diffusion sur la fabric convergee.
|
|
|
|
|
|
"""
|
|
|
|
|
|
s = underlay_mod.stp(underlay)
|
|
|
|
|
|
if not s:
|
|
|
|
|
|
return ["! ----- 6. Spanning-tree -----",
|
|
|
|
|
|
"! Non declare (cle `stp` dans underlay.yml). Sur une fabric a plusieurs",
|
|
|
|
|
|
"! switches, un brassage accidentel peut alors boucler sans protection.", "!"]
|
|
|
|
|
|
mode, topo = str(s.get("mode", "rstp")), str(s.get("topologie", ""))
|
|
|
|
|
|
r_nom = underlay_mod.routeur(underlay)
|
|
|
|
|
|
out = [f"! ----- 6. Spanning-tree ({mode.upper()}, topologie {topo}) -----",
|
|
|
|
|
|
f"! {r_nom} est le CENTRE de l'etoile : tous les chemins passent par lui.",
|
|
|
|
|
|
"! On en fait le pont racine plutot que de laisser une election arbitraire",
|
|
|
|
|
|
"! le decider — l'arbre logique suit alors le cablage physique.",
|
2026-08-02 18:52:37 -04:00
|
|
|
|
f"! Aucun lien redondant en etoile : {mode.upper()} est un filet, pas une necessite."]
|
2026-08-01 20:50:55 -04:00
|
|
|
|
out += _stp_lignes(dialecte, mode, underlay_mod.STP_PRIORITE_RACINE)
|
2026-08-01 21:35:32 -04:00
|
|
|
|
out += ["!",
|
|
|
|
|
|
"! Les ports de bord sont declares AVEC leur interface (sections 4 et 4b),",
|
|
|
|
|
|
"! pas ici : chaque interface se lit d'un seul tenant. Les rayons de la",
|
|
|
|
|
|
"! section 4c n'en sont volontairement pas.",
|
|
|
|
|
|
"! BPDU guard non emis volontairement : un pont Linux dont le STP serait",
|
|
|
|
|
|
"! active enverrait des BPDU et ferait tomber le port cote hyperviseur.",
|
|
|
|
|
|
"! Ne l'ajouter qu'apres avoir verifie que ces ponts n'en emettent pas."]
|
2026-08-01 20:50:55 -04:00
|
|
|
|
out.append("!")
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def partie_acces(underlay: dict | None, tenants: list, vlans: str,
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
dialecte_b: str = DIALECTE_SECOURS,
|
|
|
|
|
|
vlans_proxmox: str | None = None) -> list[str]:
|
devis switch : un seul routeur, les autres en L2 pur
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`).
Le devis émettait un jeu unique de SVI sans dire à quel switch il
s'adressait : appliqué sur les trois, il aurait créé autant de conflits
d'adresses qu'il y a de zones.
Il se scinde désormais en deux :
- partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ;
- partie B (switches d'accès, L2 pur) : mêmes VLANs pour commuter les trames
étiquetées, une adresse de gestion par switch tirée de `underlay.hotes`
avec `ip default-gateway` vers le routeur, et les trunks. Aucun SVI de
zone, aucune ACL, aucune route.
Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine.
Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de
laisser croire que le devis est applicable partout.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:04:38 -04:00
|
|
|
|
"""Devis des switches d'ACCES : L2 pur, aucun SVI de zone, aucune ACL, aucune route.
|
|
|
|
|
|
|
|
|
|
|
|
Sans MLAG, un seul switch route (cf. `underlay.routeur`). Les autres commutent les
|
|
|
|
|
|
trames etiquetees et n'ont d'adresse IP que pour leur propre gestion — ils joignent
|
|
|
|
|
|
le reste par la passerelle de management, pas par un SVI a eux.
|
|
|
|
|
|
"""
|
|
|
|
|
|
r_nom = underlay_mod.routeur(underlay)
|
|
|
|
|
|
if not r_nom:
|
|
|
|
|
|
return []
|
2026-08-01 20:36:02 -04:00
|
|
|
|
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)
|
2026-08-03 10:21:30 -04:00
|
|
|
|
# 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)
|
devis switch : un seul routeur, les autres en L2 pur
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`).
Le devis émettait un jeu unique de SVI sans dire à quel switch il
s'adressait : appliqué sur les trois, il aurait créé autant de conflits
d'adresses qu'il y a de zones.
Il se scinde désormais en deux :
- partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ;
- partie B (switches d'accès, L2 pur) : mêmes VLANs pour commuter les trames
étiquetées, une adresse de gestion par switch tirée de `underlay.hotes`
avec `ip default-gateway` vers le routeur, et les trunks. Aucun SVI de
zone, aucune ACL, aucune route.
Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine.
Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de
laisser croire que le devis est applicable partout.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:04:38 -04:00
|
|
|
|
if not autres:
|
|
|
|
|
|
return []
|
|
|
|
|
|
out = [
|
|
|
|
|
|
"",
|
|
|
|
|
|
"! ============================================================",
|
|
|
|
|
|
f"! PARTIE B — SWITCHES D'ACCES (L2 pur) : {', '.join(h['nom'] for h in autres)}",
|
|
|
|
|
|
f"! Le routage est porte par {r_nom} SEUL (underlay.routeur). Ces switches",
|
|
|
|
|
|
"! commutent les trames etiquetees et ne portent AUCUN SVI de zone, AUCUNE",
|
|
|
|
|
|
"! ACL, AUCUNE route : dupliquer les SVI creerait autant de conflits d'IP.",
|
|
|
|
|
|
"! ============================================================",
|
|
|
|
|
|
"configure terminal",
|
|
|
|
|
|
"!",
|
|
|
|
|
|
"! ----- B1. Memes VLANs (pour commuter les trames etiquetees) -----",
|
|
|
|
|
|
]
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
# Le lien de frontiere N'EST PLUS reserve au routeur : depuis que les noeuds de
|
|
|
|
|
|
# sortie EVPN y ont une patte, un hyperviseur branche sur un switch d'acces doit
|
|
|
|
|
|
# pouvoir l'atteindre. Le sauter ici ferait tomber son trafic sortant en silence.
|
2026-08-01 20:36:02 -04:00
|
|
|
|
for r in underlay_mod.reseaux_de_fabric(underlay, fabric):
|
devis switch : un seul routeur, les autres en L2 pur
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`).
Le devis émettait un jeu unique de SVI sans dire à quel switch il
s'adressait : appliqué sur les trois, il aurait créé autant de conflits
d'adresses qu'il y a de zones.
Il se scinde désormais en deux :
- partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ;
- partie B (switches d'accès, L2 pur) : mêmes VLANs pour commuter les trames
étiquetées, une adresse de gestion par switch tirée de `underlay.hotes`
avec `ip default-gateway` vers le routeur, et les trunks. Aucun SVI de
zone, aucune ACL, aucune route.
Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine.
Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de
laisser croire que le devis est applicable partout.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:04:38 -04:00
|
|
|
|
out += [f"vlan {r['vlan']}", f" name {r['nom']}"]
|
2026-08-03 08:35:44 -04:00
|
|
|
|
if underlay_mod.routage_tenants(underlay) == "sdn":
|
|
|
|
|
|
out.append("! Aucun VLAN de tenant : ils vivent dans le SDN, pas sur le fil.")
|
|
|
|
|
|
else:
|
|
|
|
|
|
for nom, pfx, n in tenants:
|
|
|
|
|
|
for zone in sorted(n["categories"]):
|
|
|
|
|
|
out += [f"vlan {vlan_de(n['index'], zone)}",
|
|
|
|
|
|
f" name {pfx}{n['index']}-{n['categories'][zone]['libelle']}"]
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
bord = bool(underlay_mod.stp(underlay))
|
|
|
|
|
|
out += ["!", "! ----- B2. Configuration par switch -----",
|
|
|
|
|
|
"! /!\\ UN SEUL de ces blocs par machine : celui qui porte son nom. Adresse de",
|
|
|
|
|
|
"! gestion ET ports different d'un switch a l'autre — tout coller ecraserait."]
|
|
|
|
|
|
net = ipaddress.ip_network(mgmt["sous_reseau"], strict=False) if mgmt else None
|
|
|
|
|
|
for h in autres:
|
|
|
|
|
|
out += ["!", f"! --- {h['nom']} " + "-" * max(0, 48 - len(h["nom"])), "!"]
|
|
|
|
|
|
if net:
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
out += _bloc_gestion(underlay, h["nom"])
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
else:
|
|
|
|
|
|
out.append("! Aucun reseau underlay avec passerelle : gestion a definir a la main.")
|
|
|
|
|
|
out += ["! Montante vers le routeur — PAS un port de bord : elle porte les BPDU."]
|
|
|
|
|
|
montante = str(underlay_mod.ports_de(h).get("montante") or port_vers(r_nom))
|
|
|
|
|
|
out += bloc_trunk(montante, vlans, f"montante vers {r_nom}")
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
out += ["! Ports terminaux vers les hyperviseurs : ce que leurs hotes declarent,",
|
|
|
|
|
|
"! pas tout l'underlay — la gestion des equipements n'a rien a y faire."]
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
for port in ports_ou_marqueur(h, "hyperviseurs", "<PORT-VERS-PROXMOX>"):
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
out += bloc_trunk(port, vlans_proxmox or vlans, bord=bord)
|
2026-08-01 20:50:55 -04:00
|
|
|
|
s = underlay_mod.stp(underlay)
|
|
|
|
|
|
if s:
|
|
|
|
|
|
mode = str(s.get("mode", "rstp"))
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
out += ["!", f"! ----- B3. Spanning-tree ({mode.upper()}) — sur les deux -----",
|
|
|
|
|
|
"! Priorite VOLONTAIREMENT haute : ces switches ne doivent jamais devenir",
|
2026-08-01 20:50:55 -04:00
|
|
|
|
f"! racine. Le centre de l'etoile, {r_nom}, la garde en toutes circonstances."]
|
|
|
|
|
|
out += _stp_lignes(dialecte_b, mode, underlay_mod.STP_PRIORITE_ACCES)
|
|
|
|
|
|
out += ["!"]
|
|
|
|
|
|
out += ["end", "write memory"]
|
devis switch : un seul routeur, les autres en L2 pur
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`).
Le devis émettait un jeu unique de SVI sans dire à quel switch il
s'adressait : appliqué sur les trois, il aurait créé autant de conflits
d'adresses qu'il y a de zones.
Il se scinde désormais en deux :
- partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ;
- partie B (switches d'accès, L2 pur) : mêmes VLANs pour commuter les trames
étiquetées, une adresse de gestion par switch tirée de `underlay.hotes`
avec `ip default-gateway` vers le routeur, et les trunks. Aucun SVI de
zone, aucune ACL, aucune route.
Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine.
Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de
laisser croire que le devis est applicable partout.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:04:38 -04:00
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59)
Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont
faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN
une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur
infra-pki-01, et les treize suivantes auraient suivi.
deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive
proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un
second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm :
SETOPS_PONT='t11appl', SETOPS_VLAN=''.
Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la
même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur
proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que
si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire
est tombé, à raison — il couvre maintenant cette distinction.
D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et
abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un
tenant est à deux valeurs de la portabilité complète (noeud, stockage).
D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas
de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint
l'administration que depuis son propre domaine de diffusion. Ça tranche la
question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une
interface dédiée, ce qui lève l'objection qui la bloquait.
D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le
devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès
VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du
transport, soit le domaine qu'on venait d'en sortir.
D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont.
Régression créée puis corrigée : le modèle public, qui ne déclare aucun
hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout
l'underlay en disant que c'est un repli.
30 preuves OK, 4 tests unitaires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:20:56 -04:00
|
|
|
|
def reseaux_par_via(underlay: dict | None, role: str) -> dict[str, list[dict]]:
|
|
|
|
|
|
"""{interface de l'hote: [reseaux]} — un port de commutateur par interface.
|
|
|
|
|
|
|
|
|
|
|
|
Un hyperviseur n'a pas une seule patte : son administration arrive par `vmbr0`
|
|
|
|
|
|
(carte dediee), son transport et sa sortie par `bond3`. Les grouper toutes sur un
|
|
|
|
|
|
seul port remettrait le VLAN de gestion sur le trunk du transport — precisement le
|
|
|
|
|
|
domaine de diffusion qu'on vient d'en sortir.
|
|
|
|
|
|
|
|
|
|
|
|
Les hotes sans `via` tombent dans un groupe unique : rien ne change pour eux.
|
|
|
|
|
|
"""
|
|
|
|
|
|
par_via: dict[str, dict[str, dict]] = {}
|
|
|
|
|
|
fab = underlay_mod.fabric_du_routeur(underlay)
|
|
|
|
|
|
connus = {r.get("nom"): r for r in underlay_mod.reseaux_de_fabric(underlay, fab)
|
|
|
|
|
|
if r.get("vlan") is not None}
|
|
|
|
|
|
for h in underlay_mod.hotes(underlay):
|
|
|
|
|
|
if h.get("role") != role or not h.get("reseau"):
|
|
|
|
|
|
continue
|
|
|
|
|
|
r = connus.get(h["reseau"])
|
|
|
|
|
|
if r:
|
|
|
|
|
|
par_via.setdefault(str(h.get("via") or ""), {})[r["nom"]] = r
|
|
|
|
|
|
return {via: sorted(d.values(), key=lambda r: int(r["vlan"]))
|
|
|
|
|
|
for via, d in sorted(par_via.items())}
|
|
|
|
|
|
|
|
|
|
|
|
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
def vlans_du_role(underlay: dict | None, role: str,
|
|
|
|
|
|
repli: dict | None = None) -> list[dict]:
|
|
|
|
|
|
"""Reseaux qu'un port doit porter — DERIVES des rattachements declares du role.
|
|
|
|
|
|
|
|
|
|
|
|
Deux fois de suite une liste figee a produit un port muet : d'abord celui de la
|
|
|
|
|
|
frontiere quand les bifrost ont recu une patte sur la sortie tenant, puis celui
|
|
|
|
|
|
des hyperviseurs quand les noeuds de sortie ont rejoint le lien de frontiere. Le
|
|
|
|
|
|
devis avait l'air juste et le trafic ne serait jamais arrive. On derive donc, au
|
|
|
|
|
|
lieu d'enumerer : le port porte ce que ses hotes declarent, rien de plus.
|
|
|
|
|
|
"""
|
|
|
|
|
|
noms = {h.get("reseau") for h in underlay_mod.hotes(underlay)
|
|
|
|
|
|
if h.get("role") == role and h.get("reseau")}
|
|
|
|
|
|
fab = underlay_mod.fabric_du_routeur(underlay)
|
|
|
|
|
|
reseaux = [r for r in underlay_mod.reseaux_de_fabric(underlay, fab)
|
|
|
|
|
|
if r.get("nom") in noms and r.get("vlan") is not None]
|
|
|
|
|
|
if not reseaux and repli:
|
|
|
|
|
|
reseaux = [repli]
|
|
|
|
|
|
return sorted(reseaux, key=lambda r: int(r["vlan"]))
|
|
|
|
|
|
|
|
|
|
|
|
|
séparation des plans réseau + où vont les services de l'hébergeur (D-46 à D-48)
Le port du commutateur vers la frontière était figé sur le seul VLAN de transit.
Depuis que les bifrost ont une patte sur le VLAN de sortie tenant, ce port serait
resté muet : le devis aurait eu l'air juste et le trafic ne serait jamais arrivé.
Il dérive maintenant des rattachements déclarés des hôtes `role: frontiere`.
Contexte, côté underlay (dépôt de l'hébergeur) : le VTEP vivait sur le VLAN 10,
donc le transport tenant partageait son domaine de diffusion avec l'administration
des équipements. Plus grave, un nœud de sortie décapsule le trafic tenant et le
remet dans sa table principale — dont la route par défaut sort par vmbr0,
l'interface de gestion des nœuds. D'où deux VLAN dédiés, 11 (transport VXLAN) et
41 (trafic décapsulé). Le second n'a volontairement pas de passerelle : le
commutateur le transporte sans le router, et le devis n'émet donc pas
d'interface Vlan41.
Services de l'hébergeur — décision consignée, rien n'est construit :
Aucun équipement de l'hébergeur n'est dans un inventaire Ansible, et rien ne
sauvegarde leurs configurations. Ni hyperviseurs, ni commutateurs, ni frontière.
D-46 : un hébergeur porte trois catégories — son tenant (un client comme les
autres), ses opérations (supervision de la fabric, journaux, sauvegarde des
configs, DNS d'underlay), et le plan de contrôle (déjà dehors).
D-47 : les opérations vivent dans le dépôt de l'hébergeur, et leurs VM se
rattachent à un pont VLAN, jamais un VNet. Un service qui observe la fabric ne
peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison
de la panne.
D-48 : les hyperviseurs sont gérables par Ansible ; « hors flotte » ne vaut que
pour les commutateurs (aucun agent) et la frontière (API seulement).
Question laissée ouverte : l'index 0 réservé au tenant propre de l'hébergeur. Il
produit les VNI 1001-1006, que le parc hérité utilise déjà (1001 TechnoLibre
historique, 1003 KBR), et P21 déclencherait une fausse collision entre deux
dépôts d'hébergeurs.
Construction parallèle vérifiée : aucune collision entre les 38 VM héritées et
les VNI/VLAN projetés.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:02:59 -04:00
|
|
|
|
def vlans_de_la_frontiere(underlay: dict | None, transit: dict | None) -> list[dict]:
|
|
|
|
|
|
"""Reseaux que le port de la frontiere doit porter — DERIVES de ses rattachements.
|
|
|
|
|
|
|
|
|
|
|
|
Le transit ne suffit plus. Depuis la separation des plans (2026-08-04), la
|
|
|
|
|
|
frontiere a aussi une patte sur le VLAN de sortie tenant : c'est par la que le
|
|
|
|
|
|
trafic DECAPSULE lui parvient, sans traverser ni la gestion des noeuds ni celle
|
|
|
|
|
|
des equipements. Une liste figee sur le seul transit aurait produit un port muet
|
|
|
|
|
|
sur ce VLAN — le devis aurait eu l'air juste et le trafic ne serait jamais arrive.
|
|
|
|
|
|
"""
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
return vlans_du_role(underlay, "frontiere", transit)
|
séparation des plans réseau + où vont les services de l'hébergeur (D-46 à D-48)
Le port du commutateur vers la frontière était figé sur le seul VLAN de transit.
Depuis que les bifrost ont une patte sur le VLAN de sortie tenant, ce port serait
resté muet : le devis aurait eu l'air juste et le trafic ne serait jamais arrivé.
Il dérive maintenant des rattachements déclarés des hôtes `role: frontiere`.
Contexte, côté underlay (dépôt de l'hébergeur) : le VTEP vivait sur le VLAN 10,
donc le transport tenant partageait son domaine de diffusion avec l'administration
des équipements. Plus grave, un nœud de sortie décapsule le trafic tenant et le
remet dans sa table principale — dont la route par défaut sort par vmbr0,
l'interface de gestion des nœuds. D'où deux VLAN dédiés, 11 (transport VXLAN) et
41 (trafic décapsulé). Le second n'a volontairement pas de passerelle : le
commutateur le transporte sans le router, et le devis n'émet donc pas
d'interface Vlan41.
Services de l'hébergeur — décision consignée, rien n'est construit :
Aucun équipement de l'hébergeur n'est dans un inventaire Ansible, et rien ne
sauvegarde leurs configurations. Ni hyperviseurs, ni commutateurs, ni frontière.
D-46 : un hébergeur porte trois catégories — son tenant (un client comme les
autres), ses opérations (supervision de la fabric, journaux, sauvegarde des
configs, DNS d'underlay), et le plan de contrôle (déjà dehors).
D-47 : les opérations vivent dans le dépôt de l'hébergeur, et leurs VM se
rattachent à un pont VLAN, jamais un VNet. Un service qui observe la fabric ne
peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison
de la panne.
D-48 : les hyperviseurs sont gérables par Ansible ; « hors flotte » ne vaut que
pour les commutateurs (aucun agent) et la frontière (API seulement).
Question laissée ouverte : l'index 0 réservé au tenant propre de l'hébergeur. Il
produit les VNI 1001-1006, que le parc hérité utilise déjà (1001 TechnoLibre
historique, 1003 KBR), et P21 déclencherait une fausse collision entre deux
dépôts d'hébergeurs.
Construction parallèle vérifiée : aucune collision entre les 38 VM héritées et
les VNI/VLAN projetés.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:02:59 -04:00
|
|
|
|
|
|
|
|
|
|
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
def section_frontiere(transit: dict | None, bord: bool = False,
|
séparation des plans réseau + où vont les services de l'hébergeur (D-46 à D-48)
Le port du commutateur vers la frontière était figé sur le seul VLAN de transit.
Depuis que les bifrost ont une patte sur le VLAN de sortie tenant, ce port serait
resté muet : le devis aurait eu l'air juste et le trafic ne serait jamais arrivé.
Il dérive maintenant des rattachements déclarés des hôtes `role: frontiere`.
Contexte, côté underlay (dépôt de l'hébergeur) : le VTEP vivait sur le VLAN 10,
donc le transport tenant partageait son domaine de diffusion avec l'administration
des équipements. Plus grave, un nœud de sortie décapsule le trafic tenant et le
remet dans sa table principale — dont la route par défaut sort par vmbr0,
l'interface de gestion des nœuds. D'où deux VLAN dédiés, 11 (transport VXLAN) et
41 (trafic décapsulé). Le second n'a volontairement pas de passerelle : le
commutateur le transporte sans le router, et le devis n'émet donc pas
d'interface Vlan41.
Services de l'hébergeur — décision consignée, rien n'est construit :
Aucun équipement de l'hébergeur n'est dans un inventaire Ansible, et rien ne
sauvegarde leurs configurations. Ni hyperviseurs, ni commutateurs, ni frontière.
D-46 : un hébergeur porte trois catégories — son tenant (un client comme les
autres), ses opérations (supervision de la fabric, journaux, sauvegarde des
configs, DNS d'underlay), et le plan de contrôle (déjà dehors).
D-47 : les opérations vivent dans le dépôt de l'hébergeur, et leurs VM se
rattachent à un pont VLAN, jamais un VNet. Un service qui observe la fabric ne
peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison
de la panne.
D-48 : les hyperviseurs sont gérables par Ansible ; « hors flotte » ne vaut que
pour les commutateurs (aucun agent) et la frontière (API seulement).
Question laissée ouverte : l'index 0 réservé au tenant propre de l'hébergeur. Il
produit les VNI 1001-1006, que le parc hérité utilise déjà (1001 TechnoLibre
historique, 1003 KBR), et P21 déclencherait une fausse collision entre deux
dépôts d'hébergeurs.
Construction parallèle vérifiée : aucune collision entre les 38 VM héritées et
les VNI/VLAN projetés.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:02:59 -04:00
|
|
|
|
ports: list[str] | None = None,
|
|
|
|
|
|
underlay: dict | None = None) -> list[str]:
|
|
|
|
|
|
"""Port du switch vers le pare-feu de bordure.
|
devis switch : le port vers la frontière, et l'ordre d'application
Deux omissions que le devis faisait porter à l'opérateur.
Le VLAN de transit était étiqueté sur le trunk `<PORT-VERS-PROXMOX>`, et
aucune interface vers le pare-feu n'était émise. Les hyperviseurs n'ont pas
d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN
tenants — il route vers eux, il ne les étiquette pas. Le transit sort du
trunk Proxmox et prend son propre port (section 4b), dérivé de l'underlay.
Les routes de la section 5 déplacent la sortie du switch, y compris celle de
ses propres réponses. Tant que la frontière ne répond pas, elles coupent
l'accès d'administration AU SWITCH LUI-MÊME — le mécanisme qui a rendu une
VM muette le 2026-07-29, appliqué à l'équipement depuis lequel on travaille.
Le devis les crachait à la suite des autres comme si elles étaient
équivalentes ; il énonce maintenant les préalables et l'ordre.
Sans transit déclaré, les deux sections s'affichent en clair comme
manquantes plutôt que de disparaître.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:52:50 -04:00
|
|
|
|
|
séparation des plans réseau + où vont les services de l'hébergeur (D-46 à D-48)
Le port du commutateur vers la frontière était figé sur le seul VLAN de transit.
Depuis que les bifrost ont une patte sur le VLAN de sortie tenant, ce port serait
resté muet : le devis aurait eu l'air juste et le trafic ne serait jamais arrivé.
Il dérive maintenant des rattachements déclarés des hôtes `role: frontiere`.
Contexte, côté underlay (dépôt de l'hébergeur) : le VTEP vivait sur le VLAN 10,
donc le transport tenant partageait son domaine de diffusion avec l'administration
des équipements. Plus grave, un nœud de sortie décapsule le trafic tenant et le
remet dans sa table principale — dont la route par défaut sort par vmbr0,
l'interface de gestion des nœuds. D'où deux VLAN dédiés, 11 (transport VXLAN) et
41 (trafic décapsulé). Le second n'a volontairement pas de passerelle : le
commutateur le transporte sans le router, et le devis n'émet donc pas
d'interface Vlan41.
Services de l'hébergeur — décision consignée, rien n'est construit :
Aucun équipement de l'hébergeur n'est dans un inventaire Ansible, et rien ne
sauvegarde leurs configurations. Ni hyperviseurs, ni commutateurs, ni frontière.
D-46 : un hébergeur porte trois catégories — son tenant (un client comme les
autres), ses opérations (supervision de la fabric, journaux, sauvegarde des
configs, DNS d'underlay), et le plan de contrôle (déjà dehors).
D-47 : les opérations vivent dans le dépôt de l'hébergeur, et leurs VM se
rattachent à un pont VLAN, jamais un VNet. Un service qui observe la fabric ne
peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison
de la panne.
D-48 : les hyperviseurs sont gérables par Ansible ; « hors flotte » ne vaut que
pour les commutateurs (aucun agent) et la frontière (API seulement).
Question laissée ouverte : l'index 0 réservé au tenant propre de l'hébergeur. Il
produit les VNI 1001-1006, que le parc hérité utilise déjà (1001 TechnoLibre
historique, 1003 KBR), et P21 déclencherait une fausse collision entre deux
dépôts d'hébergeurs.
Construction parallèle vérifiée : aucune collision entre les 38 VM héritées et
les VNI/VLAN projetés.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:02:59 -04:00
|
|
|
|
Distinct du trunk vers Proxmox : le pare-feu n'a rien a faire des VLAN tenants —
|
|
|
|
|
|
il route vers eux, il ne les etiquette pas.
|
devis switch : le port vers la frontière, et l'ordre d'application
Deux omissions que le devis faisait porter à l'opérateur.
Le VLAN de transit était étiqueté sur le trunk `<PORT-VERS-PROXMOX>`, et
aucune interface vers le pare-feu n'était émise. Les hyperviseurs n'ont pas
d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN
tenants — il route vers eux, il ne les étiquette pas. Le transit sort du
trunk Proxmox et prend son propre port (section 4b), dérivé de l'underlay.
Les routes de la section 5 déplacent la sortie du switch, y compris celle de
ses propres réponses. Tant que la frontière ne répond pas, elles coupent
l'accès d'administration AU SWITCH LUI-MÊME — le mécanisme qui a rendu une
VM muette le 2026-07-29, appliqué à l'équipement depuis lequel on travaille.
Le devis les crachait à la suite des autres comme si elles étaient
équivalentes ; il énonce maintenant les préalables et l'ordre.
Sans transit déclaré, les deux sections s'affichent en clair comme
manquantes plutôt que de disparaître.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:52:50 -04:00
|
|
|
|
"""
|
|
|
|
|
|
if not transit:
|
|
|
|
|
|
return ["! ----- 4b. Port vers la frontiere -----",
|
|
|
|
|
|
"! Aucun reseau de transit declare dans underlay.yml (cle 'passerelle_sortie')."]
|
séparation des plans réseau + où vont les services de l'hébergeur (D-46 à D-48)
Le port du commutateur vers la frontière était figé sur le seul VLAN de transit.
Depuis que les bifrost ont une patte sur le VLAN de sortie tenant, ce port serait
resté muet : le devis aurait eu l'air juste et le trafic ne serait jamais arrivé.
Il dérive maintenant des rattachements déclarés des hôtes `role: frontiere`.
Contexte, côté underlay (dépôt de l'hébergeur) : le VTEP vivait sur le VLAN 10,
donc le transport tenant partageait son domaine de diffusion avec l'administration
des équipements. Plus grave, un nœud de sortie décapsule le trafic tenant et le
remet dans sa table principale — dont la route par défaut sort par vmbr0,
l'interface de gestion des nœuds. D'où deux VLAN dédiés, 11 (transport VXLAN) et
41 (trafic décapsulé). Le second n'a volontairement pas de passerelle : le
commutateur le transporte sans le router, et le devis n'émet donc pas
d'interface Vlan41.
Services de l'hébergeur — décision consignée, rien n'est construit :
Aucun équipement de l'hébergeur n'est dans un inventaire Ansible, et rien ne
sauvegarde leurs configurations. Ni hyperviseurs, ni commutateurs, ni frontière.
D-46 : un hébergeur porte trois catégories — son tenant (un client comme les
autres), ses opérations (supervision de la fabric, journaux, sauvegarde des
configs, DNS d'underlay), et le plan de contrôle (déjà dehors).
D-47 : les opérations vivent dans le dépôt de l'hébergeur, et leurs VM se
rattachent à un pont VLAN, jamais un VNet. Un service qui observe la fabric ne
peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison
de la panne.
D-48 : les hyperviseurs sont gérables par Ansible ; « hors flotte » ne vaut que
pour les commutateurs (aucun agent) et la frontière (API seulement).
Question laissée ouverte : l'index 0 réservé au tenant propre de l'hébergeur. Il
produit les VNI 1001-1006, que le parc hérité utilise déjà (1001 TechnoLibre
historique, 1003 KBR), et P21 déclencherait une fausse collision entre deux
dépôts d'hébergeurs.
Construction parallèle vérifiée : aucune collision entre les 38 VM héritées et
les VNI/VLAN projetés.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:02:59 -04:00
|
|
|
|
reseaux = vlans_de_la_frontiere(underlay, transit)
|
|
|
|
|
|
liste = ",".join(str(r["vlan"]) for r in reseaux)
|
|
|
|
|
|
detail = ", ".join(f"{r['vlan']} ({r['nom']})" for r in reseaux)
|
devis switch : le port vers la frontière, et l'ordre d'application
Deux omissions que le devis faisait porter à l'opérateur.
Le VLAN de transit était étiqueté sur le trunk `<PORT-VERS-PROXMOX>`, et
aucune interface vers le pare-feu n'était émise. Les hyperviseurs n'ont pas
d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN
tenants — il route vers eux, il ne les étiquette pas. Le transit sort du
trunk Proxmox et prend son propre port (section 4b), dérivé de l'underlay.
Les routes de la section 5 déplacent la sortie du switch, y compris celle de
ses propres réponses. Tant que la frontière ne répond pas, elles coupent
l'accès d'administration AU SWITCH LUI-MÊME — le mécanisme qui a rendu une
VM muette le 2026-07-29, appliqué à l'équipement depuis lequel on travaille.
Le devis les crachait à la suite des autres comme si elles étaient
équivalentes ; il énonce maintenant les préalables et l'ordre.
Sans transit déclaré, les deux sections s'affichent en clair comme
manquantes plutôt que de disparaître.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:52:50 -04:00
|
|
|
|
return [
|
|
|
|
|
|
"! ----- 4b. Port vers la frontiere nord/sud -----",
|
séparation des plans réseau + où vont les services de l'hébergeur (D-46 à D-48)
Le port du commutateur vers la frontière était figé sur le seul VLAN de transit.
Depuis que les bifrost ont une patte sur le VLAN de sortie tenant, ce port serait
resté muet : le devis aurait eu l'air juste et le trafic ne serait jamais arrivé.
Il dérive maintenant des rattachements déclarés des hôtes `role: frontiere`.
Contexte, côté underlay (dépôt de l'hébergeur) : le VTEP vivait sur le VLAN 10,
donc le transport tenant partageait son domaine de diffusion avec l'administration
des équipements. Plus grave, un nœud de sortie décapsule le trafic tenant et le
remet dans sa table principale — dont la route par défaut sort par vmbr0,
l'interface de gestion des nœuds. D'où deux VLAN dédiés, 11 (transport VXLAN) et
41 (trafic décapsulé). Le second n'a volontairement pas de passerelle : le
commutateur le transporte sans le router, et le devis n'émet donc pas
d'interface Vlan41.
Services de l'hébergeur — décision consignée, rien n'est construit :
Aucun équipement de l'hébergeur n'est dans un inventaire Ansible, et rien ne
sauvegarde leurs configurations. Ni hyperviseurs, ni commutateurs, ni frontière.
D-46 : un hébergeur porte trois catégories — son tenant (un client comme les
autres), ses opérations (supervision de la fabric, journaux, sauvegarde des
configs, DNS d'underlay), et le plan de contrôle (déjà dehors).
D-47 : les opérations vivent dans le dépôt de l'hébergeur, et leurs VM se
rattachent à un pont VLAN, jamais un VNet. Un service qui observe la fabric ne
peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison
de la panne.
D-48 : les hyperviseurs sont gérables par Ansible ; « hors flotte » ne vaut que
pour les commutateurs (aucun agent) et la frontière (API seulement).
Question laissée ouverte : l'index 0 réservé au tenant propre de l'hébergeur. Il
produit les VNI 1001-1006, que le parc hérité utilise déjà (1001 TechnoLibre
historique, 1003 KBR), et P21 déclencherait une fausse collision entre deux
dépôts d'hébergeurs.
Construction parallèle vérifiée : aucune collision entre les 38 VM héritées et
les VNI/VLAN projetés.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:02:59 -04:00
|
|
|
|
f"! Porte {detail}.",
|
|
|
|
|
|
"! Derive des rattachements declares des hotes `role: frontiere`.",
|
2026-08-01 21:35:32 -04:00
|
|
|
|
"! Port terminal : rien derriere lui ne participe au spanning-tree.",
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
] + [ligne
|
|
|
|
|
|
for port in (ports or ["<PORT-VERS-FRONTIERE>"])
|
séparation des plans réseau + où vont les services de l'hébergeur (D-46 à D-48)
Le port du commutateur vers la frontière était figé sur le seul VLAN de transit.
Depuis que les bifrost ont une patte sur le VLAN de sortie tenant, ce port serait
resté muet : le devis aurait eu l'air juste et le trafic ne serait jamais arrivé.
Il dérive maintenant des rattachements déclarés des hôtes `role: frontiere`.
Contexte, côté underlay (dépôt de l'hébergeur) : le VTEP vivait sur le VLAN 10,
donc le transport tenant partageait son domaine de diffusion avec l'administration
des équipements. Plus grave, un nœud de sortie décapsule le trafic tenant et le
remet dans sa table principale — dont la route par défaut sort par vmbr0,
l'interface de gestion des nœuds. D'où deux VLAN dédiés, 11 (transport VXLAN) et
41 (trafic décapsulé). Le second n'a volontairement pas de passerelle : le
commutateur le transporte sans le router, et le devis n'émet donc pas
d'interface Vlan41.
Services de l'hébergeur — décision consignée, rien n'est construit :
Aucun équipement de l'hébergeur n'est dans un inventaire Ansible, et rien ne
sauvegarde leurs configurations. Ni hyperviseurs, ni commutateurs, ni frontière.
D-46 : un hébergeur porte trois catégories — son tenant (un client comme les
autres), ses opérations (supervision de la fabric, journaux, sauvegarde des
configs, DNS d'underlay), et le plan de contrôle (déjà dehors).
D-47 : les opérations vivent dans le dépôt de l'hébergeur, et leurs VM se
rattachent à un pont VLAN, jamais un VNet. Un service qui observe la fabric ne
peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison
de la panne.
D-48 : les hyperviseurs sont gérables par Ansible ; « hors flotte » ne vaut que
pour les commutateurs (aucun agent) et la frontière (API seulement).
Question laissée ouverte : l'index 0 réservé au tenant propre de l'hébergeur. Il
produit les VNI 1001-1006, que le parc hérité utilise déjà (1001 TechnoLibre
historique, 1003 KBR), et P21 déclencherait une fausse collision entre deux
dépôts d'hébergeurs.
Construction parallèle vérifiée : aucune collision entre les 38 VM héritées et
les VNI/VLAN projetés.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:02:59 -04:00
|
|
|
|
for ligne in bloc_trunk(port, liste, bord=bord)]
|
devis switch : le port vers la frontière, et l'ordre d'application
Deux omissions que le devis faisait porter à l'opérateur.
Le VLAN de transit était étiqueté sur le trunk `<PORT-VERS-PROXMOX>`, et
aucune interface vers le pare-feu n'était émise. Les hyperviseurs n'ont pas
d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN
tenants — il route vers eux, il ne les étiquette pas. Le transit sort du
trunk Proxmox et prend son propre port (section 4b), dérivé de l'underlay.
Les routes de la section 5 déplacent la sortie du switch, y compris celle de
ses propres réponses. Tant que la frontière ne répond pas, elles coupent
l'accès d'administration AU SWITCH LUI-MÊME — le mécanisme qui a rendu une
VM muette le 2026-07-29, appliqué à l'équipement depuis lequel on travaille.
Le devis les crachait à la suite des autres comme si elles étaient
équivalentes ; il énonce maintenant les préalables et l'ordre.
Sans transit déclaré, les deux sections s'affichent en clair comme
manquantes plutôt que de disparaître.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:52:50 -04:00
|
|
|
|
|
|
|
|
|
|
|
frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.
`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.
Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.
Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
|
|
|
|
def inventaire_de(nom_instance: str) -> Path | None:
|
|
|
|
|
|
"""hosts.yml d'une instance FEDEREE, active ou non. None si elle n'en a pas."""
|
|
|
|
|
|
base = DOSSIER_INSTANCES / nom_instance / "inventories"
|
|
|
|
|
|
for env in ("principal", "production", "lab"):
|
|
|
|
|
|
p = base / env / "hosts.yml"
|
|
|
|
|
|
if p.is_file():
|
|
|
|
|
|
return p
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def admin_de(nom_instance: str) -> list[str]:
|
|
|
|
|
|
"""Reseaux d'administration declares par une instance (intrant nftables_admin_ssh)."""
|
|
|
|
|
|
inv = inventaire_de(nom_instance)
|
|
|
|
|
|
if not inv:
|
|
|
|
|
|
return []
|
|
|
|
|
|
for fichier in sorted((inv.parent / "group_vars" / "all").glob("*.yml")):
|
|
|
|
|
|
if "vault" in fichier.name:
|
|
|
|
|
|
continue
|
|
|
|
|
|
data = yaml.safe_load(fichier.read_text(encoding="utf-8")) or {}
|
|
|
|
|
|
if isinstance(data, dict) and data.get("nftables_admin_ssh"):
|
|
|
|
|
|
src = data["nftables_admin_ssh"]
|
|
|
|
|
|
return [str(s) for s in src] if isinstance(src, list) else [str(src)]
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def admin_tous_tenants() -> list[str]:
|
|
|
|
|
|
"""Union des reseaux d'administration de TOUS les tenants federes.
|
|
|
|
|
|
|
|
|
|
|
|
Le switch routeur est partage : il doit savoir revenir vers chaque plan de gestion,
|
|
|
|
|
|
pas seulement celui de l'instance active. Router n'est pas autoriser — le
|
|
|
|
|
|
cloisonnement se fait a la frontiere, par un alias distinct par tenant.
|
|
|
|
|
|
SOURCE UNIQUE : `devis_opnsense` importe cette fonction plutot que d'en refaire une.
|
|
|
|
|
|
"""
|
|
|
|
|
|
return sorted({c for nom, _pfx, _n in decouvrir() for c in admin_de(nom)})
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 18:03:37 -04:00
|
|
|
|
def route_statique(reseau: str, saut: str, dialecte: str) -> str:
|
|
|
|
|
|
"""Une route statique, dans la forme du dialecte.
|
|
|
|
|
|
|
|
|
|
|
|
VERIFIE sur un `show running-config` Binardat (2026-08-02) : la plateforme ecrit
|
|
|
|
|
|
`ip route 0.0.0.0/0 192.168.10.254` — notation CIDR, pas de masque separe. Cisco IOS
|
|
|
|
|
|
veut `ip route <reseau> <masque> <saut>`.
|
|
|
|
|
|
"""
|
|
|
|
|
|
net = ipaddress.ip_network(reseau, strict=False)
|
|
|
|
|
|
if dialecte == "binardat":
|
|
|
|
|
|
return f"ip route {net.with_prefixlen} {saut}"
|
|
|
|
|
|
return f"ip route {net.network_address} {net.netmask} {saut}"
|
|
|
|
|
|
|
|
|
|
|
|
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
def switch_route(underlay: dict | None) -> bool:
|
|
|
|
|
|
"""Le switch routeur porte-t-il au moins un SVI ? Sinon il ne route rien.
|
|
|
|
|
|
|
|
|
|
|
|
Depuis que la frontiere est le seul equipement L3, plus aucune passerelle de
|
|
|
|
|
|
l'underlay n'appartient a un switch : ni SVI, ni routes statiques. La section 5 du
|
|
|
|
|
|
devis — la plus risquee, celle qui coupait l'acces d'administration au switch en
|
|
|
|
|
|
cas d'erreur — disparait d'elle-meme.
|
|
|
|
|
|
"""
|
|
|
|
|
|
r_nom = underlay_mod.routeur(underlay)
|
|
|
|
|
|
for r in underlay_mod.reseaux_de_fabric(underlay, underlay_mod.fabric_du_routeur(underlay)):
|
|
|
|
|
|
gw = r.get("passerelle")
|
|
|
|
|
|
if not gw:
|
|
|
|
|
|
continue
|
|
|
|
|
|
for h in underlay_mod.hotes(underlay):
|
|
|
|
|
|
if (h.get("reseau") == r.get("nom") and str(h.get("ip")) == str(gw)
|
|
|
|
|
|
and h.get("nom") == r_nom):
|
|
|
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-08-02 18:03:37 -04:00
|
|
|
|
def section_routes(underlay: dict | None, dialecte: str = DIALECTE_SECOURS) -> list[str]:
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
"""Routes du switch vers la frontiere nord/sud. Vide si le switch ne route pas.
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
|
|
|
|
|
|
Deux routes, et il en faut IMPERATIVEMENT deux :
|
|
|
|
|
|
- l'ALLER : defaut vers la frontiere, sinon les hotes n'ont aucune sortie ;
|
|
|
|
|
|
- le RETOUR : vers les reseaux d'administration, sinon la reponse d'une VM part
|
|
|
|
|
|
par la passerelle de management et revient au pare-feu par une autre interface
|
|
|
|
|
|
que celle ou l'etat a ete cree — elle est jetee en silence. Symptome : le SVI
|
|
|
|
|
|
de zone repond au ping, mais aucun hote derriere lui n'est joignable.
|
|
|
|
|
|
|
|
|
|
|
|
Les reseaux d'administration viennent de l'intrant `nftables_admin_ssh` : la meme
|
|
|
|
|
|
source unique que la garde anti-lockout des nftables et l'alias SETOPS_ADMIN de la
|
|
|
|
|
|
frontiere. Aucune adresse n'est ecrite ici.
|
|
|
|
|
|
"""
|
|
|
|
|
|
transit = underlay_mod.reseau_transit(underlay)
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
if not switch_route(underlay):
|
|
|
|
|
|
return ["! ----- 5. Routes -----",
|
|
|
|
|
|
"! AUCUNE. Ce switch ne porte aucun SVI : il commute, il ne route pas.",
|
|
|
|
|
|
"! La frontiere est le seul equipement L3 — c'est elle qui route vers les",
|
|
|
|
|
|
"! tenants et vers l'exterieur. Les switches n'ont qu'une sortie par defaut,",
|
|
|
|
|
|
"! emise plus haut avec leur adresse de gestion.",
|
|
|
|
|
|
"!"]
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
if not transit:
|
|
|
|
|
|
return ["! ----- 5. Routes vers la frontiere -----",
|
|
|
|
|
|
"! Aucun reseau de transit declare dans underlay.yml (cle 'passerelle_sortie').",
|
|
|
|
|
|
"! Sans lui : pas de route par defaut, et pas de retour vers l'administration.",
|
|
|
|
|
|
"!"]
|
|
|
|
|
|
sortie = transit["passerelle_sortie"]
|
|
|
|
|
|
out = ["! ----- 5. Routes vers la frontiere nord/sud -----",
|
devis switch : le port vers la frontière, et l'ordre d'application
Deux omissions que le devis faisait porter à l'opérateur.
Le VLAN de transit était étiqueté sur le trunk `<PORT-VERS-PROXMOX>`, et
aucune interface vers le pare-feu n'était émise. Les hyperviseurs n'ont pas
d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN
tenants — il route vers eux, il ne les étiquette pas. Le transit sort du
trunk Proxmox et prend son propre port (section 4b), dérivé de l'underlay.
Les routes de la section 5 déplacent la sortie du switch, y compris celle de
ses propres réponses. Tant que la frontière ne répond pas, elles coupent
l'accès d'administration AU SWITCH LUI-MÊME — le mécanisme qui a rendu une
VM muette le 2026-07-29, appliqué à l'équipement depuis lequel on travaille.
Le devis les crachait à la suite des autres comme si elles étaient
équivalentes ; il énonce maintenant les préalables et l'ordre.
Sans transit déclaré, les deux sections s'affichent en clair comme
manquantes plutôt que de disparaître.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:52:50 -04:00
|
|
|
|
"! /!\\ A APPLIQUER EN DERNIER, ET SEULEMENT UNE FOIS LA FRONTIERE VIVANTE.",
|
|
|
|
|
|
f"! Ces routes deplacent la sortie du switch — y compris celle de ses propres",
|
|
|
|
|
|
f"! reponses — vers {sortie}. Tant que cette adresse ne repond pas, elles",
|
|
|
|
|
|
"! coupent l'acces d'administration AU SWITCH LUI-MEME.",
|
|
|
|
|
|
"! Prealables : boitier cable, adresse sur le lien de transit, et joignable",
|
|
|
|
|
|
f"! depuis le switch (ping {sortie} depuis Vlan{transit['vlan']}).",
|
|
|
|
|
|
"! Garder une session console ouverte pendant l'operation.",
|
|
|
|
|
|
"!",
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
f"! Transit '{transit['nom']}' (VLAN {transit['vlan']}) : SVI {transit['passerelle']}"
|
|
|
|
|
|
f" <-> frontiere {sortie}",
|
|
|
|
|
|
"! Aller : sortie generale de la flotte.",
|
2026-08-02 18:03:37 -04:00
|
|
|
|
route_statique("0.0.0.0/0", sortie, dialecte)]
|
frontière : les routes de retour couvrent tous les tenants, et la case WAN
Le devis frontière annonçait trois routes de retour « déjà émises par
devis-reseau ». Le devis switch n'en émettait qu'une : il lisait
`nftables_admin_ssh` de la seule instance active, alors que la frontière
était passée multi-tenant. Les réseaux d'administration de Technolibre
n'étaient routés nulle part — et une affirmation fausse est pire qu'un
silence, elle désamorce la vérification.
`admin_tous_tenants()` vit dans devis_reseau et devis_opnsense l'importe au
lieu d'en refaire une copie : routes de retour et règles lisent les mêmes
tenants par construction. Vérifié identiques.
Ajouté : l'avertissement « Block private networks ». Le SSH d'administration
a une source RFC1918 arrivant sur une interface WAN, où ce filtre est actif
par défaut et s'applique AVANT les règles — coché, il jette le paquet sans
qu'aucune règle ne soit consultée. Un réglage d'interface est invisible dans
les règles, il fallait l'écrire à part.
Prédicat exactement RFC1918, périmètre de cette case ; `is_private` aurait
été trop large (documentation, CGNAT) et l'avertissement se serait déclenché
à tort. Trois cas exercés : RFC1918 averti, 8.8.8.8 muet, 203.0.113.7 muet.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 16:17:25 -04:00
|
|
|
|
admin = admin_tous_tenants() or _sources_admin_ssh()
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
if admin:
|
|
|
|
|
|
out.append("! Retour : sans ces routes, les reponses partent par une autre interface")
|
|
|
|
|
|
out.append("! que celle ou l'etat a ete cree, et le pare-feu les jette en silence.")
|
|
|
|
|
|
for cidr in admin:
|
2026-08-02 18:03:37 -04:00
|
|
|
|
out.append(route_statique(cidr, sortie, dialecte)
|
|
|
|
|
|
+ " ! administration (intrant nftables_admin_ssh)")
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
else:
|
|
|
|
|
|
out.append("! ATTENTION : intrant `nftables_admin_ssh` vide — aucune route de retour")
|
|
|
|
|
|
out.append("! vers l'administration ne peut etre derivee. Cf. preuve P24.")
|
|
|
|
|
|
out.append("!")
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-07 15:50:18 -04:00
|
|
|
|
def prefixe(nom_dossier: str) -> str:
|
|
|
|
|
|
base = re.sub(r"^OPS-", "", nom_dossier)
|
|
|
|
|
|
base = re.sub(r"-lab$", "", base)
|
|
|
|
|
|
return (re.sub(r"[^A-Za-z0-9]", "", base).upper() or "T")[:4]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def decouvrir() -> list[tuple[str, str, dict]]:
|
|
|
|
|
|
tenants = []
|
|
|
|
|
|
for chemin in sorted(DOSSIER_INSTANCES.glob("*/plan/nomenclature.yml")):
|
|
|
|
|
|
n = yaml.safe_load(chemin.read_text(encoding="utf-8")) or {}
|
Adressage derive du seul seed index (rupture, mode compact retire)
Principe : les valeurs de configuration se derivent des intrants, elles ne se
reecrivent pas a la main. La nomenclature dupliquait ce qu'index determine deja
(supernet, sous-reseaux, passerelles, VLAN). Corrige en rupture nette.
- inventory_rules : source unique de derivation — supernet_de, base3_de,
sous_reseau_de, passerelle_de, vlan_de. Modele 6 zones encode une fois
(2e octet = 10+index, 3e octet zone = 15+categorie, VLAN = 1000+index*10+zone).
deriver_nomenclature ne lit plus aucun adressage stocke ; mode compact supprime.
- devis_reseau : importe ces helpers (fin de la duplication) ; decouvre les
tenants sur `index` present (filtre vmid_schema retire).
- GUI : `index` devient un INTRANT (section Reseau). Il vit dans la nomenclature
(plan reseau uniforme, contrairement aux intrants des modeles heterogenes) et
le GUI l'ecrit chirurgicalement (une ligne, sans reformater). Le miroir JS
derive le VLAN du seed (fin de la lecture de c.vlan stocke).
- socle public : nomenclature au format maigre.
Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke
d'adressage — garde-fou permanent, teste en negatif.
Valide : DIFF VIDE sur les 3 instances (la derivation reproduit exactement
l'adressage stocke), 7 modeles valident, devis_reseau genere les memes VLAN
(1011-1016 derives), make verifier rc=0 CONFORME 20/20, node --check du GUI OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:58:15 -04:00
|
|
|
|
# Un tenant federe = une nomenclature avec un index (l'adressage en decoule).
|
2026-07-23 10:21:34 -04:00
|
|
|
|
# `federe: false` exclut un bac a sable local (labo) du reseau converge :
|
|
|
|
|
|
# il ne partage pas la fabric de production, on ne provisionne pas ses VLAN.
|
|
|
|
|
|
if n.get("index") is not None and n.get("categories") and n.get("federe", True):
|
2026-07-07 15:50:18 -04:00
|
|
|
|
nom = chemin.parent.parent.name
|
|
|
|
|
|
tenants.append((nom, prefixe(nom), n))
|
|
|
|
|
|
tenants.sort(key=lambda t: t[2]["index"])
|
|
|
|
|
|
return tenants
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 17:15:27 -04:00
|
|
|
|
def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -> str:
|
GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui s'éditait à la main dans un YAML,
pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA.
La frontière avait eu sa section ; l'underlay, non.
La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, donc
les ports) restent hors de portée du panneau — elles demandent une vue
dédiée, comme celle des serveurs.
Le dialecte devient un intrant déclaré : il ne vivait que dans
SETOPS_DIALECTE. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.
Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
|
|
|
|
dialecte = dialecte_effectif(dialecte)
|
2026-07-24 14:56:38 -04:00
|
|
|
|
underlay = underlay_mod.charger()
|
devis switch : un seul routeur, les autres en L2 pur
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`).
Le devis émettait un jeu unique de SVI sans dire à quel switch il
s'adressait : appliqué sur les trois, il aurait créé autant de conflits
d'adresses qu'il y a de zones.
Il se scinde désormais en deux :
- partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ;
- partie B (switches d'accès, L2 pur) : mêmes VLANs pour commuter les trames
étiquetées, une adresse de gestion par switch tirée de `underlay.hotes`
avec `ip default-gateway` vers le routeur, et les trunks. Aucun SVI de
zone, aucune ACL, aucune route.
Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine.
Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de
laisser croire que le devis est applicable partout.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:04:38 -04:00
|
|
|
|
r_nom = underlay_mod.routeur(underlay)
|
2026-07-23 17:15:27 -04:00
|
|
|
|
out = ["configure terminal", f"! dialecte CLI : {dialecte}", "!"]
|
2026-07-07 15:50:18 -04:00
|
|
|
|
out += [
|
|
|
|
|
|
"! ============================================================",
|
|
|
|
|
|
"! DEVIS SWITCH — reseau converge multi-tenant (Set-OPS)",
|
|
|
|
|
|
"! Genere par scripts/devis_reseau.py depuis les nomenclatures.",
|
2026-08-03 08:35:44 -04:00
|
|
|
|
"! VLAN = 1000 + index*10 + zone (unique globalement dans la federation).",
|
2026-07-24 14:56:38 -04:00
|
|
|
|
"! Underlay (VLAN < 1000) = fabric physique, cluster-global.",
|
devis switch : un seul routeur, les autres en L2 pur
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`).
Le devis émettait un jeu unique de SVI sans dire à quel switch il
s'adressait : appliqué sur les trois, il aurait créé autant de conflits
d'adresses qu'il y a de zones.
Il se scinde désormais en deux :
- partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ;
- partie B (switches d'accès, L2 pur) : mêmes VLANs pour commuter les trames
étiquetées, une adresse de gestion par switch tirée de `underlay.hotes`
avec `ip default-gateway` vers le routeur, et les trunks. Aucun SVI de
zone, aucune ACL, aucune route.
Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine.
Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de
laisser croire que le devis est applicable partout.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:04:38 -04:00
|
|
|
|
]
|
|
|
|
|
|
if r_nom:
|
|
|
|
|
|
out += [
|
|
|
|
|
|
"!",
|
|
|
|
|
|
f"! PARTIE A — SWITCH ROUTEUR : {r_nom} (underlay.routeur)",
|
|
|
|
|
|
"! Sans MLAG, UN SEUL switch route. Cette partie ne va QUE sur lui.",
|
|
|
|
|
|
"! Les autres recoivent la partie B (L2 pur), plus bas.",
|
|
|
|
|
|
]
|
|
|
|
|
|
else:
|
|
|
|
|
|
out += [
|
|
|
|
|
|
"!",
|
|
|
|
|
|
"! /!\\ AUCUN ROUTEUR DESIGNE (cle `routeur` dans underlay.yml).",
|
|
|
|
|
|
"! Applique tel quel sur plusieurs switches, ce devis duplique les SVI",
|
|
|
|
|
|
"! et cree autant de conflits d'adresses. Designer le switch routeur.",
|
|
|
|
|
|
]
|
|
|
|
|
|
out += [
|
2026-07-07 15:50:18 -04:00
|
|
|
|
"! ============================================================",
|
|
|
|
|
|
"!",
|
|
|
|
|
|
]
|
2026-07-24 14:56:38 -04:00
|
|
|
|
out += section_underlay(underlay)
|
devis switch : le SDN prend le routage tenant, le devis se vide
Trois décisions appliquées : une zone EVPN par tenant, routage ET filtrage
inter-zone à ce niveau, inter-tenant obligatoirement par l'OPNsense.
`underlay.routage_tenants` (`switch` par défaut, `sdn`) : en SDN le devis
cesse d'émettre VLAN tenants, SVI et ACL, et les retire des trunks. Chez
Chezlepro les trunks passent de quinze VLAN à deux — seul du VXLAN circule,
que le commutateur transporte sans le lire.
Le MTU devient une garde : `make underlay` refuse un transport sous 1550 en
mode SDN, en disant pourquoi — sous ce seuil le ping passe et les transferts
échouent.
Le partage des responsabilités est écrit dans docs/sdn-evpn.md : qui route,
qui filtre, pour chaque nature de trafic. Deux conséquences nommées —
l'inter-tenant ne peut plus être oublié (il traverse une bordure en block par
défaut), et le commutateur ne voit plus rien du trafic tenant.
Point ouvert : le registre des flux n'a aucun mot-clé pour un flux
inter-tenant. Défaut sûr, mais on ne peut pas déclarer d'exception légitime.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:26:21 -04:00
|
|
|
|
if underlay_mod.routage_tenants(underlay) == "sdn":
|
|
|
|
|
|
out += ["! ----- 1 a 3. VLANs, SVI et ACL des tenants — PORTES PAR LE SDN -----",
|
|
|
|
|
|
"! `underlay.routage_tenants: sdn` : une zone EVPN par tenant porte son VRF.",
|
|
|
|
|
|
"! Le routage ET le filtrage entre les zones d'un meme tenant vivent sur les",
|
|
|
|
|
|
"! hyperviseurs. AUCUN VLAN DE TENANT NE CIRCULE SUR CE FIL — seulement du",
|
|
|
|
|
|
"! VXLAN encapsule dans de l'IP, que ce commutateur transporte sans le lire.",
|
2026-08-03 00:45:56 -04:00
|
|
|
|
"!",
|
devis switch : le SDN prend le routage tenant, le devis se vide
Trois décisions appliquées : une zone EVPN par tenant, routage ET filtrage
inter-zone à ce niveau, inter-tenant obligatoirement par l'OPNsense.
`underlay.routage_tenants` (`switch` par défaut, `sdn`) : en SDN le devis
cesse d'émettre VLAN tenants, SVI et ACL, et les retire des trunks. Chez
Chezlepro les trunks passent de quinze VLAN à deux — seul du VXLAN circule,
que le commutateur transporte sans le lire.
Le MTU devient une garde : `make underlay` refuse un transport sous 1550 en
mode SDN, en disant pourquoi — sous ce seuil le ping passe et les transferts
échouent.
Le partage des responsabilités est écrit dans docs/sdn-evpn.md : qui route,
qui filtre, pour chaque nature de trafic. Deux conséquences nommées —
l'inter-tenant ne peut plus être oublié (il traverse une bordure en block par
défaut), et le commutateur ne voit plus rien du trafic tenant.
Point ouvert : le registre des flux n'a aucun mot-clé pour un flux
inter-tenant. Défaut sûr, mais on ne peut pas déclarer d'exception légitime.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:26:21 -04:00
|
|
|
|
"! Ce commutateur ne porte donc ni VLAN tenant, ni SVI de zone, ni ACL. Les",
|
|
|
|
|
|
"! passerelles `.1` n'ont pas change d'adresse, elles ont change de porteur :",
|
|
|
|
|
|
"! passerelle anycast du VNet, presente sur chaque hyperviseur.",
|
2026-08-03 00:45:56 -04:00
|
|
|
|
"!",
|
devis switch : le SDN prend le routage tenant, le devis se vide
Trois décisions appliquées : une zone EVPN par tenant, routage ET filtrage
inter-zone à ce niveau, inter-tenant obligatoirement par l'OPNsense.
`underlay.routage_tenants` (`switch` par défaut, `sdn`) : en SDN le devis
cesse d'émettre VLAN tenants, SVI et ACL, et les retire des trunks. Chez
Chezlepro les trunks passent de quinze VLAN à deux — seul du VXLAN circule,
que le commutateur transporte sans le lire.
Le MTU devient une garde : `make underlay` refuse un transport sous 1550 en
mode SDN, en disant pourquoi — sous ce seuil le ping passe et les transferts
échouent.
Le partage des responsabilités est écrit dans docs/sdn-evpn.md : qui route,
qui filtre, pour chaque nature de trafic. Deux conséquences nommées —
l'inter-tenant ne peut plus être oublié (il traverse une bordure en block par
défaut), et le commutateur ne voit plus rien du trafic tenant.
Point ouvert : le registre des flux n'a aucun mot-clé pour un flux
inter-tenant. Défaut sûr, mais on ne peut pas déclarer d'exception légitime.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:26:21 -04:00
|
|
|
|
"! L'inter-tenant sort du VRF et passe par la FRONTIERE, qui le police",
|
|
|
|
|
|
"! (`make devis-opnsense`). Il n'est pas filtre ici : il ne passe pas ici.",
|
2026-08-03 00:45:56 -04:00
|
|
|
|
"!"]
|
|
|
|
|
|
else:
|
devis switch : le SDN prend le routage tenant, le devis se vide
Trois décisions appliquées : une zone EVPN par tenant, routage ET filtrage
inter-zone à ce niveau, inter-tenant obligatoirement par l'OPNsense.
`underlay.routage_tenants` (`switch` par défaut, `sdn`) : en SDN le devis
cesse d'émettre VLAN tenants, SVI et ACL, et les retire des trunks. Chez
Chezlepro les trunks passent de quinze VLAN à deux — seul du VXLAN circule,
que le commutateur transporte sans le lire.
Le MTU devient une garde : `make underlay` refuse un transport sous 1550 en
mode SDN, en disant pourquoi — sous ce seuil le ping passe et les transferts
échouent.
Le partage des responsabilités est écrit dans docs/sdn-evpn.md : qui route,
qui filtre, pour chaque nature de trafic. Deux conséquences nommées —
l'inter-tenant ne peut plus être oublié (il traverse une bordure en block par
défaut), et le commutateur ne voit plus rien du trafic tenant.
Point ouvert : le registre des flux n'a aucun mot-clé pour un flux
inter-tenant. Défaut sûr, mais on ne peut pas déclarer d'exception légitime.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:26:21 -04:00
|
|
|
|
out += ["! ----- 1. VLANs (tenants) -----"]
|
|
|
|
|
|
for nom, pfx, n in tenants:
|
|
|
|
|
|
out.append(f"! {nom} (index {n['index']})")
|
|
|
|
|
|
for zone in sorted(n["categories"]):
|
|
|
|
|
|
c = n["categories"][zone]
|
|
|
|
|
|
out.append(f"vlan {vlan_de(n['index'], zone)}")
|
|
|
|
|
|
out.append(f" name {pfx}{n['index']}-{c['libelle']}")
|
|
|
|
|
|
acl = underlay_mod.acl_inter_tenant(underlay)
|
|
|
|
|
|
out += ["!", "! ----- 2. Interfaces de routage (SVI = passerelle des hotes) -----"]
|
2026-08-03 00:45:56 -04:00
|
|
|
|
for nom, pfx, n in tenants:
|
devis switch : le SDN prend le routage tenant, le devis se vide
Trois décisions appliquées : une zone EVPN par tenant, routage ET filtrage
inter-zone à ce niveau, inter-tenant obligatoirement par l'OPNsense.
`underlay.routage_tenants` (`switch` par défaut, `sdn`) : en SDN le devis
cesse d'émettre VLAN tenants, SVI et ACL, et les retire des trunks. Chez
Chezlepro les trunks passent de quinze VLAN à deux — seul du VXLAN circule,
que le commutateur transporte sans le lire.
Le MTU devient une garde : `make underlay` refuse un transport sous 1550 en
mode SDN, en disant pourquoi — sous ce seuil le ping passe et les transferts
échouent.
Le partage des responsabilités est écrit dans docs/sdn-evpn.md : qui route,
qui filtre, pour chaque nature de trafic. Deux conséquences nommées —
l'inter-tenant ne peut plus être oublié (il traverse une bordure en block par
défaut), et le commutateur ne voit plus rien du trafic tenant.
Point ouvert : le registre des flux n'a aucun mot-clé pour un flux
inter-tenant. Défaut sûr, mais on ne peut pas déclarer d'exception légitime.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:26:21 -04:00
|
|
|
|
m = masque(int(n.get("cidr_hote", 24)))
|
|
|
|
|
|
for zone in sorted(n["categories"]):
|
|
|
|
|
|
c = n["categories"][zone]
|
|
|
|
|
|
out.append(f"interface Vlan{vlan_de(n['index'], zone)}")
|
|
|
|
|
|
out.append(f" description {nom}-{c['libelle']}")
|
|
|
|
|
|
out.append(f" ip address {passerelle_de(n['index'], zone)} {m}")
|
|
|
|
|
|
if acl:
|
|
|
|
|
|
out.append(f" ip access-group {pfx}{n['index']}-ISOLATION in")
|
|
|
|
|
|
out.append(" no shutdown")
|
|
|
|
|
|
if not acl:
|
|
|
|
|
|
out += ["!", "! ----- 3. Isolation inter-tenant — PAS D'ACL SUR CETTE FABRIC -----",
|
|
|
|
|
|
"! `underlay.acl_inter_tenant: false` : le materiel ne sait pas lier une ACL",
|
|
|
|
|
|
"! a une interface de routage. Emettre des ACL qu'on ne peut pas lier serait",
|
|
|
|
|
|
"! pire que rien — elles auraient l'air d'isoler sans jamais filtrer.",
|
|
|
|
|
|
"!",
|
|
|
|
|
|
"! L'isolation repose donc ENTIEREMENT sur les nftables de chaque hote",
|
|
|
|
|
|
"! (`make flux`), en `policy drop`, au moindre privilege par IP source.",
|
|
|
|
|
|
"!",
|
|
|
|
|
|
"! /!\\ CE QUI N'EST PLUS PROTEGE AU NIVEAU RESEAU : le plan de gestion de la",
|
|
|
|
|
|
"! fabric. Une VM qui emet vers l'underlay voit son paquet ROUTE localement par",
|
|
|
|
|
|
"! ce commutateur — mgmt des switches, mgmt Proxmox, OOB/IPMI. Les nftables des",
|
|
|
|
|
|
"! VM n'y peuvent rien (politique `output` permissive), et l'IPMI n'est pas un",
|
|
|
|
|
|
"! hote gere. Seule parade structurelle : sortir le management de la fabric",
|
|
|
|
|
|
"! routee des tenants, comme l'est deja le stockage.",
|
|
|
|
|
|
"!"]
|
|
|
|
|
|
else:
|
|
|
|
|
|
out += ["!", "! ----- 3. ACL d'isolation tenant (default-deny inter-tenant) -----"]
|
|
|
|
|
|
for nom, pfx, n in tenants:
|
|
|
|
|
|
reseau, m = masque_acl(supernet_de(n["index"]), dialecte)
|
|
|
|
|
|
out.append(f"ip access-list extended {pfx}{n['index']}-ISOLATION")
|
|
|
|
|
|
out += remarque(dialecte, f"Intra-tenant {nom} : routage local autorise")
|
|
|
|
|
|
out.append(f" permit ip {reseau} {m} {reseau} {m}")
|
|
|
|
|
|
for autre_nom, _, autre in tenants:
|
|
|
|
|
|
if autre_nom == nom:
|
|
|
|
|
|
continue
|
|
|
|
|
|
a_reseau, a_m = masque_acl(supernet_de(autre["index"]), dialecte)
|
|
|
|
|
|
out += remarque(dialecte, f"Bloquer le tenant {autre_nom}")
|
|
|
|
|
|
out.append(f" deny ip {reseau} {m} {a_reseau} {a_m}")
|
|
|
|
|
|
# L'underlay est la fabric physique : mgmt des switches, mgmt Proxmox, OOB/IPMI,
|
|
|
|
|
|
# iSCSI, Ceph. Le trafic d'un tenant vers ces reseaux est route LOCALEMENT par le
|
|
|
|
|
|
# switch : il ne passe jamais par la frontiere, donc il n'est jamais filtre. Sans
|
|
|
|
|
|
# ce deny, le `permit any` final l'autorise — une VM atteindrait la console
|
|
|
|
|
|
# physique des hyperviseurs. Aucun flux du registre ne vise l'underlay.
|
|
|
|
|
|
# TOUTES les fabrics y passent, meme celles portees par d'autres switches : la
|
|
|
|
|
|
# regle porte sur l'adresse de DESTINATION, pas sur le cablage. Si un jour un
|
|
|
|
|
|
# chemin s'ouvre vers le stockage, il est deja ferme.
|
|
|
|
|
|
for r in underlay_mod.reseaux(underlay):
|
|
|
|
|
|
u_reseau, u_m = masque_acl(r["sous_reseau"], dialecte)
|
|
|
|
|
|
out += remarque(dialecte, f"Bloquer l'underlay {r['nom']} (fabric physique)")
|
|
|
|
|
|
out.append(f" deny ip {reseau} {m} {u_reseau} {u_m}")
|
|
|
|
|
|
out += remarque(dialecte, "Reste (Internet / inter-tenant controle) -> passerelle OPNsense")
|
|
|
|
|
|
out.append(f" permit ip {reseau} {m} any")
|
2026-08-01 21:31:31 -04:00
|
|
|
|
out += ["!", "! ----- 4. Trunk vers les hyperviseurs -----",
|
|
|
|
|
|
"! Ports TERMINAUX : rien derriere eux ne participe au spanning-tree.",
|
|
|
|
|
|
"! Les liens vers les autres switches sont en section 4c, pas ici."]
|
devis switch : le port vers la frontière, et l'ordre d'application
Deux omissions que le devis faisait porter à l'opérateur.
Le VLAN de transit était étiqueté sur le trunk `<PORT-VERS-PROXMOX>`, et
aucune interface vers le pare-feu n'était émise. Les hyperviseurs n'ont pas
d'interface sur le transit, et le pare-feu n'a rien à faire des VLAN
tenants — il route vers eux, il ne les étiquette pas. Le transit sort du
trunk Proxmox et prend son propre port (section 4b), dérivé de l'underlay.
Les routes de la section 5 déplacent la sortie du switch, y compris celle de
ses propres réponses. Tant que la frontière ne répond pas, elles coupent
l'accès d'administration AU SWITCH LUI-MÊME — le mécanisme qui a rendu une
VM muette le 2026-07-29, appliqué à l'équipement depuis lequel on travaille.
Le devis les crachait à la suite des autres comme si elles étaient
équivalentes ; il énonce maintenant les préalables et l'ordre.
Sans transit déclaré, les deux sections s'affichent en clair comme
manquantes plutôt que de disparaître.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:52:50 -04:00
|
|
|
|
transit = underlay_mod.reseau_transit(underlay)
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
# DERIVE des rattachements des hyperviseurs, pas d'une liste figee. Depuis que les
|
|
|
|
|
|
# noeuds de sortie EVPN ont une patte sur le lien de frontiere, exclure le transit
|
|
|
|
|
|
# « parce qu'aucun hyperviseur n'y est » serait faux — et le trunk laisserait
|
|
|
|
|
|
# tomber le trafic tenant sortant sans rien signaler.
|
réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59)
Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont
faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN
une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur
infra-pki-01, et les treize suivantes auraient suivi.
deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive
proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un
second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm :
SETOPS_PONT='t11appl', SETOPS_VLAN=''.
Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la
même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur
proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que
si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire
est tombé, à raison — il couvre maintenant cette distinction.
D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et
abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un
tenant est à deux valeurs de la portabilité complète (noeud, stockage).
D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas
de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint
l'administration que depuis son propre domaine de diffusion. Ça tranche la
question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une
interface dédiée, ce qui lève l'objection qui la bloquait.
D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le
devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès
VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du
transport, soit le domaine qu'on venait d'en sortir.
D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont.
Régression créée puis corrigée : le modèle public, qui ne déclare aucun
hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout
l'underlay en disant que c'est un repli.
30 preuves OK, 4 tests unitaires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:20:56 -04:00
|
|
|
|
# Un bloc de port par interface d'hyperviseur : `vmbr0` (administration, un seul
|
|
|
|
|
|
# VLAN -> port d'ACCES) et `bond3` (transport + sortie, plusieurs -> trunk).
|
|
|
|
|
|
par_via_hyp = reseaux_par_via(underlay, "hyperviseur")
|
2026-08-01 20:36:02 -04:00
|
|
|
|
vlans_underlay = [str(r["vlan"])
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
for r in vlans_du_role(underlay, "hyperviseur")]
|
|
|
|
|
|
# Les liens INTER-SWITCH portent tout l'underlay de la fabric, pas seulement ce
|
|
|
|
|
|
# dont les hyperviseurs ont besoin : sans le VLAN de management, sleipnir-02 et -03
|
|
|
|
|
|
# perdraient leur propre adresse de gestion. Un port terminal porte ce que son hote
|
|
|
|
|
|
# declare ; un lien de fabric porte ce qui doit traverser.
|
|
|
|
|
|
vlans_fabric = [str(r["vlan"])
|
|
|
|
|
|
for r in underlay_mod.reseaux_de_fabric(
|
|
|
|
|
|
underlay, underlay_mod.fabric_du_routeur(underlay))
|
|
|
|
|
|
if r.get("vlan") is not None]
|
devis switch : le SDN prend le routage tenant, le devis se vide
Trois décisions appliquées : une zone EVPN par tenant, routage ET filtrage
inter-zone à ce niveau, inter-tenant obligatoirement par l'OPNsense.
`underlay.routage_tenants` (`switch` par défaut, `sdn`) : en SDN le devis
cesse d'émettre VLAN tenants, SVI et ACL, et les retire des trunks. Chez
Chezlepro les trunks passent de quinze VLAN à deux — seul du VXLAN circule,
que le commutateur transporte sans le lire.
Le MTU devient une garde : `make underlay` refuse un transport sous 1550 en
mode SDN, en disant pourquoi — sous ce seuil le ping passe et les transferts
échouent.
Le partage des responsabilités est écrit dans docs/sdn-evpn.md : qui route,
qui filtre, pour chaque nature de trafic. Deux conséquences nommées —
l'inter-tenant ne peut plus être oublié (il traverse une bordure en block par
défaut), et le commutateur ne voit plus rien du trafic tenant.
Point ouvert : le registre des flux n'a aucun mot-clé pour un flux
inter-tenant. Défaut sûr, mais on ne peut pas déclarer d'exception légitime.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 08:26:21 -04:00
|
|
|
|
# En SDN, les VLAN tenants n'existent pas sur le fil : le trunk ne porte que
|
|
|
|
|
|
# l'underlay, qui transporte le VXLAN.
|
|
|
|
|
|
vlans_tenants = [] if underlay_mod.routage_tenants(underlay) == "sdn" else [
|
2026-07-07 15:50:18 -04:00
|
|
|
|
str(vlan_de(n["index"], zone))
|
|
|
|
|
|
for _, _, n in tenants
|
|
|
|
|
|
for zone in sorted(n["categories"])
|
2026-07-24 14:56:38 -04:00
|
|
|
|
]
|
|
|
|
|
|
vlans = ",".join(vlans_underlay + vlans_tenants)
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
routeur_h = hote_nomme(underlay, underlay_mod.routeur(underlay))
|
|
|
|
|
|
bord = bool(underlay_mod.stp(underlay))
|
réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59)
Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont
faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN
une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur
infra-pki-01, et les treize suivantes auraient suivi.
deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive
proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un
second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm :
SETOPS_PONT='t11appl', SETOPS_VLAN=''.
Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la
même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur
proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que
si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire
est tombé, à raison — il couvre maintenant cette distinction.
D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et
abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un
tenant est à deux valeurs de la portabilité complète (noeud, stockage).
D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas
de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint
l'administration que depuis son propre domaine de diffusion. Ça tranche la
question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une
interface dédiée, ce qui lève l'objection qui la bloquait.
D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le
devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès
VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du
transport, soit le domaine qu'on venait d'en sortir.
D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont.
Régression créée puis corrigée : le modèle public, qui ne déclare aucun
hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout
l'underlay en disant que c'est un repli.
30 preuves OK, 4 tests unitaires.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:20:56 -04:00
|
|
|
|
if not par_via_hyp:
|
|
|
|
|
|
# Aucun hyperviseur declare : on ne peut rien deriver de leurs rattachements.
|
|
|
|
|
|
# Emettre TOUT l'underlay est le repli sur, et il faut le dire — un devis muet
|
|
|
|
|
|
# ferait croire qu'il n'y a pas de port a configurer, ce qui est faux.
|
|
|
|
|
|
par_via_hyp = {"": [r for r in underlay_mod.reseaux_de_fabric(
|
|
|
|
|
|
underlay, underlay_mod.fabric_du_routeur(underlay)) if r.get("vlan") is not None]}
|
|
|
|
|
|
out += ["! Aucun hote `role: hyperviseur` declare dans underlay.yml : ce port porte",
|
|
|
|
|
|
"! TOUT l'underlay, faute de mieux. Les declarer rendrait ce bloc precis —",
|
|
|
|
|
|
"! et separerait l'administration du transport, comme il se doit."]
|
|
|
|
|
|
for via, reseaux_via in par_via_hyp.items():
|
|
|
|
|
|
etiq = f"-{via.upper()}" if via else ""
|
|
|
|
|
|
liste = ",".join(str(r["vlan"]) for r in reseaux_via)
|
|
|
|
|
|
detail = ", ".join(f"{r['vlan']} ({r['nom']})" for r in reseaux_via)
|
|
|
|
|
|
out += [f"! Interface {via or '(non precisee)'} des hyperviseurs — porte {detail}."]
|
|
|
|
|
|
for port in ports_ou_marqueur(routeur_h, "hyperviseurs",
|
|
|
|
|
|
f"<PORT-VERS-PROXMOX{etiq}>"):
|
|
|
|
|
|
if len(reseaux_via) == 1:
|
|
|
|
|
|
# Un seul VLAN sur cette interface : port d'ACCES, trame non etiquetee.
|
|
|
|
|
|
# C'est le cas de `vmbr0`, qui n'est pas VLAN-aware et porte l'adresse
|
|
|
|
|
|
# d'administration en clair sur le VLAN natif.
|
|
|
|
|
|
out += [f"interface {port}",
|
|
|
|
|
|
f" switchport mode access",
|
|
|
|
|
|
f" switchport access vlan {reseaux_via[0]['vlan']}"]
|
|
|
|
|
|
if bord:
|
|
|
|
|
|
out.append(" spanning-tree portfast")
|
|
|
|
|
|
else:
|
|
|
|
|
|
out += bloc_trunk(port, liste, bord=bord)
|
|
|
|
|
|
out += ["!"]
|
underlay : les ports physiques entrent dans le modèle
Les noms de ports n'étaient modélisés nulle part — des marqueurs littéraux
dans le générateur, remplacés à la main dans la sortie et perdus à chaque
régénération. Seul endroit du devis où le travail était refait à répétition.
Ils se déclarent par équipement sous quatre clefs correspondant aux quatre
natures de lien : `hyperviseurs` et `frontiere` (terminaux, portfast),
`rayons` (côté routeur), `montante` (côté switch d'accès).
Le devis émet les vrais ports, y compris PLUSIEURS vers les hyperviseurs —
il n'en supposait qu'un, alors que le cluster en compte trois. Non déclarés,
les marqueurs reviennent, par équipement : un switch renseigné et un autre
non cohabitent.
La partie B devient par switch : gestion, montante et ports terminaux
diffèrent d'une machine à l'autre, un bloc commun n'avait plus de sens.
Gardes exercées : port déclaré deux fois, rayon vers un switch inconnu,
`rayons` sur autre chose que le routeur, `montante` sur le routeur.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:47:15 -04:00
|
|
|
|
out += ["!"] + section_frontiere(
|
|
|
|
|
|
transit, bord=bool(underlay_mod.stp(underlay)),
|
|
|
|
|
|
ports=ports_ou_marqueur(hote_nomme(underlay, underlay_mod.routeur(underlay)),
|
séparation des plans réseau + où vont les services de l'hébergeur (D-46 à D-48)
Le port du commutateur vers la frontière était figé sur le seul VLAN de transit.
Depuis que les bifrost ont une patte sur le VLAN de sortie tenant, ce port serait
resté muet : le devis aurait eu l'air juste et le trafic ne serait jamais arrivé.
Il dérive maintenant des rattachements déclarés des hôtes `role: frontiere`.
Contexte, côté underlay (dépôt de l'hébergeur) : le VTEP vivait sur le VLAN 10,
donc le transport tenant partageait son domaine de diffusion avec l'administration
des équipements. Plus grave, un nœud de sortie décapsule le trafic tenant et le
remet dans sa table principale — dont la route par défaut sort par vmbr0,
l'interface de gestion des nœuds. D'où deux VLAN dédiés, 11 (transport VXLAN) et
41 (trafic décapsulé). Le second n'a volontairement pas de passerelle : le
commutateur le transporte sans le router, et le devis n'émet donc pas
d'interface Vlan41.
Services de l'hébergeur — décision consignée, rien n'est construit :
Aucun équipement de l'hébergeur n'est dans un inventaire Ansible, et rien ne
sauvegarde leurs configurations. Ni hyperviseurs, ni commutateurs, ni frontière.
D-46 : un hébergeur porte trois catégories — son tenant (un client comme les
autres), ses opérations (supervision de la fabric, journaux, sauvegarde des
configs, DNS d'underlay), et le plan de contrôle (déjà dehors).
D-47 : les opérations vivent dans le dépôt de l'hébergeur, et leurs VM se
rattachent à un pont VLAN, jamais un VNet. Un service qui observe la fabric ne
peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison
de la panne.
D-48 : les hyperviseurs sont gérables par Ansible ; « hors flotte » ne vaut que
pour les commutateurs (aucun agent) et la frontière (API seulement).
Question laissée ouverte : l'index 0 réservé au tenant propre de l'hébergeur. Il
produit les VNI 1001-1006, que le parc hérité utilise déjà (1001 TechnoLibre
historique, 1003 KBR), et P21 déclencherait une fausse collision entre deux
dépôts d'hébergeurs.
Construction parallèle vérifiée : aucune collision entre les 38 VM héritées et
les VNI/VLAN projetés.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 13:02:59 -04:00
|
|
|
|
"frontiere", "<PORT-VERS-FRONTIERE>"),
|
|
|
|
|
|
underlay=underlay)
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
out += ["!"] + section_rayons(underlay, ",".join(vlans_fabric + vlans_tenants))
|
2026-08-02 18:03:37 -04:00
|
|
|
|
out += ["!"] + section_routes(underlay, dialecte)
|
2026-08-01 21:35:32 -04:00
|
|
|
|
out += section_stp(underlay, dialecte)
|
frontière nord/sud : devis dérivé, lien de transit et les deux routes
La bordure devient un artefact dérivé, comme le devis switch — et le chemin
qui y mène est enfin déclaré.
`make devis-opnsense` (+ preuve P24) dérive la politique de bordure du
registre des flux : les flux `pair: externe`, que `resoudre_flux.py` saute
volontairement parce qu'ils relèvent de la frontière et non du pare-feu
d'hôte. Aucun port, aucune adresse, aucun nom d'hôte dans le générateur.
Le lien manquait dans tous les fichiers : le devis switch ne contenait pas
une seule `ip route`. Un réseau underlay portant `passerelle_sortie` le
déclare — il vit dans l'underlay et non dans un tenant parce que la
frontière route vers TOUS les supernets tenants par le même saut, donc il
ne peut dériver d'aucun `index`. `devis-reseau` en tire deux routes :
l'aller (sortie générale) et le retour vers l'administration, dont l'absence
a coûté la passe de déploiement du 2026-07-29 — la réponse revient au
pare-feu par une autre interface que celle où l'état a été créé, et se fait
jeter en silence.
Les réseaux d'administration viennent de l'intrant `nftables_admin_ssh` :
même source unique que la garde anti-lockout des nftables et l'alias
SETOPS_ADMIN. Les trois pare-feux et les routes ne peuvent plus diverger.
La frontière est réglable depuis la console (section « Frontière » du
panneau Intrants) ; les identifiants d'API restent interdits d'écriture par
le GUI et vivent dans la voûte.
Correctifs de la même passe :
- le panneau refusait d'enregistrer les intrants de la frontière : le
garde-fou confondait une référence de voûte `{{ vault_* }}` préservée
avec un secret soumis. Il regarde désormais la valeur, pas le nom.
- `supprimer_vm_debian.yml` ne chargeait que `proxmox.vault.yml` pour ses
secrets ; retirer ce reliquat aurait cassé `make detruire`. Aligné sur le
playbook de clonage, voûte unique en dernier.
- documentation : la voûte est unique, `proxmox.vault.yml` n'est qu'un
reliquat de compatibilité.
Preuves : 24 OK, 0 échec. Cas de rejet du validateur d'underlay exercés un
par un ; résolution du jeton Proxmox vérifiée en exécution réelle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:32:04 -04:00
|
|
|
|
out += ["end", "write memory"]
|
devis switch : un seul routeur, les autres en L2 pur
Sans MLAG, le routage est porté par un unique switch (`underlay.routeur`).
Le devis émettait un jeu unique de SVI sans dire à quel switch il
s'adressait : appliqué sur les trois, il aurait créé autant de conflits
d'adresses qu'il y a de zones.
Il se scinde désormais en deux :
- partie A (switch routeur) : VLANs, SVI, ACL, trunks, routes ;
- partie B (switches d'accès, L2 pur) : mêmes VLANs pour commuter les trames
étiquetées, une adresse de gestion par switch tirée de `underlay.hotes`
avec `ip default-gateway` vers le routeur, et les trunks. Aucun SVI de
zone, aucune ACL, aucune route.
Gardes : `make underlay` refuse un `routeur` inconnu des hôtes déclarés ; la
partie B avertit qu'un seul de ses blocs de gestion va sur chaque machine.
Sans routeur désigné, l'en-tête signale le risque de duplication au lieu de
laisser croire que le devis est applicable partout.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 20:04:38 -04:00
|
|
|
|
# Les switches d'acces : memes VLANs, aucun SVI de zone, aucune ACL, aucune route.
|
routage : plus aucun commutateur ne route, la frontière est le seul L3 (D-49 à D-51)
Aucun des trois SVI de sleipnir-01 n'avait de consommateur : les VTEP sont dans
le même sous-réseau, les nœuds de sortie sont adjacents à la frontière, et les
commutateurs peuvent sortir par l'OPNsense qui a déjà une patte sur le VLAN 10.
Deux décisions séparées avaient vidé ce rôle sans qu'on regarde leur effet
cumulé : le passage à l'EVPN a retiré les VLAN tenants du fil, puis la fusion du
lien de sortie dans le VLAN 40 a rendu les nœuds de sortie adjacents.
sleipnir-01 disparaît, pas seulement son rôle : en étoile, le centre est sur tous
les chemins, donc un point de panne unique du plan de données — ce qui vidait
aussi de son sens l'ajout d'une seconde carte à bond3. Deux switches L2 reliés,
bond3 répartis. D-51 ; D-05 renversée.
Le devis perd trois SVI, quatre routes, et surtout sa section 5 — celle qui
coupait l'accès d'administration au switch en cas d'erreur.
D-50 : `passerelle` signifiait « adresse du SVI du switch », une hypothèse
déguisée en donnée. Elle signifie maintenant « la passerelle de ce sous-réseau,
où qu'elle vive », et le devis dérive s'il doit émettre une interface routée —
uniquement si le porteur déclaré a le rôle switch. Le même moteur sert les deux
postures : le modèle public démontre celle où le switch route.
Deux gardes remplacées, pas affaiblies. À la place de « passerelle_sortie exige
passerelle » et « routeur.ip == passerelle », une règle plus forte : une
passerelle doit être l'adresse d'un hôte déclaré sur ce réseau. Elle attrape en
plus les passerelles fantômes. Éprouvée par trois sabotages, tous attrapés — et
elle a trouvé une sous-déclaration dans le modèle public.
Quatre trous corrigés, tous de la même famille (une liste figée finit par
mentir) : port de frontière figé sur le transit, trunk Proxmox excluant le
transit, switches d'accès sautant sa déclaration, et le switch de tête privé
d'adresse de gestion par la suppression du SVI.
D-03 renversée : le /29 élargi en /24 fait tomber l'exemption d'invariant, le .1
revient à la passerelle.
30 preuves OK.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 14:23:57 -04:00
|
|
|
|
out += partie_acces(underlay, tenants, ",".join(vlans_fabric + vlans_tenants), dialecte,
|
|
|
|
|
|
vlans_proxmox=",".join(vlans_underlay + vlans_tenants))
|
2026-07-07 15:50:18 -04:00
|
|
|
|
return "\n".join(out)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
2026-07-23 17:15:27 -04:00
|
|
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
GUI : section « Fabric » — l'underlay se règle depuis la console
Tout le modèle d'underlay bâti aujourd'hui s'éditait à la main dans un YAML,
pendant que la doctrine dit qu'un sysadmin doit exploiter l'outil sans IA.
La frontière avait eu sa section ; l'underlay, non.
La section couvre les valeurs plates : switch routeur, dialecte de CLI, mode
et topologie de spanning-tree. Les listes de tables (`reseaux`, `hotes`, donc
les ports) restent hors de portée du panneau — elles demandent une vue
dédiée, comme celle des serveurs.
Le dialecte devient un intrant déclaré : il ne vivait que dans
SETOPS_DIALECTE. C'est une propriété du matériel, donc de la fabric.
Précédence : `--dialecte` > environnement > intrant déclaré > cisco.
Écriture chirurgicale plutôt que safe_dump : underlay.yml porte 23 lignes de
commentaires qui expliquent des décisions d'architecture, et un dump les
aurait effacées comme c'est arrivé à plan/applications.yml. Vérifié : trois
valeurs modifiées, 73 lignes et 23 commentaires avant comme après. Une clef
absente du fichier est refusée plutôt qu'inventée à un endroit arbitraire.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 22:01:00 -04:00
|
|
|
|
ap.add_argument("--dialecte", choices=DIALECTES, default=None,
|
|
|
|
|
|
help="CLI du commutateur (defaut : SETOPS_DIALECTE, sinon underlay.dialecte)")
|
2026-07-23 17:15:27 -04:00
|
|
|
|
args = ap.parse_args()
|
2026-07-07 15:50:18 -04:00
|
|
|
|
tenants = decouvrir()
|
|
|
|
|
|
if not tenants:
|
|
|
|
|
|
print("Aucune instance 'ip-miroir' federee trouvee (../*/plan/nomenclature.yml).",
|
|
|
|
|
|
file=sys.stderr)
|
|
|
|
|
|
sys.exit(1)
|
2026-07-23 17:15:27 -04:00
|
|
|
|
print(generer(tenants, args.dialecte))
|
2026-07-07 15:50:18 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
main()
|