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.
|
|
|
|
|
|
DIALECTES = ("cisco", "binardat")
|
|
|
|
|
|
DIALECTE_DEFAUT = os.environ.get("SETOPS_DIALECTE", "cisco")
|
|
|
|
|
|
|
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
|
|
|
|
|
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}"]
|
|
|
|
|
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
|
for r in reseaux:
|
|
|
|
|
|
if r.get("passerelle"):
|
|
|
|
|
|
net = ipaddress.ip_network(r["sous_reseau"], strict=False)
|
|
|
|
|
|
out.append(f"interface Vlan{r['vlan']}")
|
|
|
|
|
|
out.append(f" description underlay {r['nom']}")
|
|
|
|
|
|
out.append(f" ip address {r['passerelle']} {net.netmask}")
|
|
|
|
|
|
out.append(" no shutdown")
|
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()}>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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."]
|
|
|
|
|
|
for h in acces:
|
|
|
|
|
|
out += [f"interface {port_vers(h['nom'])}",
|
|
|
|
|
|
f" description rayon vers {h['nom']}",
|
|
|
|
|
|
" switchport mode trunk",
|
|
|
|
|
|
f" switchport trunk allowed vlan add {vlans}"]
|
|
|
|
|
|
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
|
|
|
|
|
|
`rstp` et une priorite globale. NON VERIFIE sur Binardat : a confronter au
|
|
|
|
|
|
`show running-config` du materiel, comme les `ip route`.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if dialecte == "cisco":
|
|
|
|
|
|
return [f"spanning-tree mode {'rapid-pvst' if mode == 'rstp' else mode}",
|
|
|
|
|
|
f"spanning-tree vlan 1-4094 priority {priorite}"]
|
|
|
|
|
|
return [f"spanning-tree mode {mode}", f"spanning-tree priority {priorite}"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def section_stp(underlay: dict | None, dialecte: str, ports_bord: list[str]) -> list[str]:
|
|
|
|
|
|
"""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.",
|
|
|
|
|
|
"! Aucun lien redondant en etoile : RSTP est un filet, pas une necessite."]
|
|
|
|
|
|
out += _stp_lignes(dialecte, mode, underlay_mod.STP_PRIORITE_RACINE)
|
|
|
|
|
|
if ports_bord:
|
|
|
|
|
|
out += ["!",
|
|
|
|
|
|
"! Ports terminaux (hyperviseurs, frontiere) : rien derriere eux ne",
|
|
|
|
|
|
"! participe a l'arbre. Les declarer en bord evite d'attendre la",
|
|
|
|
|
|
"! convergence a chaque montee de lien."]
|
|
|
|
|
|
for p in ports_bord:
|
|
|
|
|
|
out += [f"interface {p}",
|
|
|
|
|
|
" spanning-tree portfast trunk"]
|
|
|
|
|
|
out += ["! 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."]
|
|
|
|
|
|
out.append("!")
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def partie_acces(underlay: dict | None, tenants: list, vlans: str,
|
|
|
|
|
|
dialecte_b: str = DIALECTE_DEFAUT) -> 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-01 20:18:16 -04:00
|
|
|
|
# UNIQUEMENT les hotes du reseau de management : les autres equipements declares
|
|
|
|
|
|
# (frontieres sur le lien de transit, par exemple) ne sont pas des switches et ne
|
|
|
|
|
|
# doivent recevoir aucune de ces lignes.
|
|
|
|
|
|
autres = [h for h in underlay_mod.hotes(underlay)
|
|
|
|
|
|
if h.get("nom") != r_nom and mgmt and h.get("reseau") == mgmt["nom"]]
|
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) -----",
|
|
|
|
|
|
]
|
2026-08-01 20:10:42 -04:00
|
|
|
|
# Le transit ne relie que le routeur a la frontiere : aucune trame ne le traverse
|
|
|
|
|
|
# ici. Le declarer serait un VLAN qui ne verra jamais de trafic.
|
|
|
|
|
|
transit = underlay_mod.reseau_transit(underlay)
|
2026-08-01 20:36:02 -04:00
|
|
|
|
for r in underlay_mod.reseaux_de_fabric(underlay, fabric):
|
2026-08-01 20:10:42 -04:00
|
|
|
|
if transit and r["vlan"] == transit["vlan"]:
|
|
|
|
|
|
continue
|
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']}"]
|
|
|
|
|
|
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']}"]
|
|
|
|
|
|
out += ["!", "! ----- B2. Gestion (seule adresse IP du switch) -----",
|
|
|
|
|
|
"! /!\\ UN SEUL de ces blocs par switch : celui qui porte son nom.",
|
|
|
|
|
|
"! Les coller tous les deux sur la meme machine ecraserait son adresse."]
|
|
|
|
|
|
if mgmt:
|
|
|
|
|
|
net = ipaddress.ip_network(mgmt["sous_reseau"], strict=False)
|
|
|
|
|
|
for h in autres:
|
|
|
|
|
|
out += [f"! --- {h['nom']} ---",
|
|
|
|
|
|
f"interface Vlan{mgmt['vlan']}",
|
|
|
|
|
|
f" description gestion {h['nom']}",
|
|
|
|
|
|
f" ip address {h['ip']} {net.netmask}",
|
|
|
|
|
|
" no shutdown",
|
|
|
|
|
|
f"ip default-gateway {mgmt['passerelle']} ! le routeur {r_nom}"]
|
|
|
|
|
|
else:
|
|
|
|
|
|
out.append("! Aucun reseau underlay avec passerelle : gestion a definir a la main.")
|
2026-08-01 21:31:31 -04:00
|
|
|
|
out += ["!", "! ----- B3a. Montante vers le routeur (rayon de l'etoile) -----",
|
|
|
|
|
|
"! /!\\ PAS un port de bord : il porte les BPDU. Ne jamais y mettre portfast.",
|
|
|
|
|
|
f"interface {port_vers(r_nom)}",
|
|
|
|
|
|
f" description montante vers {r_nom}",
|
|
|
|
|
|
" switchport mode trunk",
|
|
|
|
|
|
f" switchport trunk allowed vlan add {vlans}",
|
|
|
|
|
|
"!",
|
|
|
|
|
|
"! ----- B3b. Trunk vers les hyperviseurs -----",
|
|
|
|
|
|
"! Port terminal : rien derriere lui ne participe au spanning-tree.",
|
|
|
|
|
|
"interface <PORT-VERS-PROXMOX>",
|
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
|
|
|
|
" switchport mode trunk",
|
|
|
|
|
|
f" switchport trunk allowed vlan add {vlans}",
|
2026-08-01 21:31:31 -04:00
|
|
|
|
" spanning-tree portfast trunk" if underlay_mod.stp(underlay) else "!",
|
2026-08-01 20:50:55 -04:00
|
|
|
|
"!"]
|
|
|
|
|
|
s = underlay_mod.stp(underlay)
|
|
|
|
|
|
if s:
|
|
|
|
|
|
mode = str(s.get("mode", "rstp"))
|
|
|
|
|
|
out += [f"! ----- B4. Spanning-tree ({mode.upper()}) -----",
|
|
|
|
|
|
f"! Priorite VOLONTAIREMENT haute : ces switches ne doivent jamais devenir",
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
def section_frontiere(transit: dict | None) -> list[str]:
|
|
|
|
|
|
"""Port du switch vers le pare-feu de bordure. Porte le VLAN de transit, et lui seul.
|
|
|
|
|
|
|
|
|
|
|
|
Distinct du trunk vers Proxmox : les hyperviseurs n'ont aucune interface sur le
|
|
|
|
|
|
transit, et le pare-feu n'a rien a faire des VLAN tenants — il route vers eux, il
|
|
|
|
|
|
ne les etiquette pas.
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not transit:
|
|
|
|
|
|
return ["! ----- 4b. Port vers la frontiere -----",
|
|
|
|
|
|
"! Aucun reseau de transit declare dans underlay.yml (cle 'passerelle_sortie')."]
|
|
|
|
|
|
return [
|
|
|
|
|
|
"! ----- 4b. Port vers la frontiere nord/sud -----",
|
|
|
|
|
|
f"! Porte le seul VLAN {transit['vlan']} ({transit['nom']}). En lien dedie,",
|
|
|
|
|
|
f"! remplacer par : switchport access vlan {transit['vlan']}.",
|
|
|
|
|
|
"interface <PORT-VERS-FRONTIERE>",
|
|
|
|
|
|
" switchport mode trunk",
|
|
|
|
|
|
f" switchport trunk allowed vlan add {transit['vlan']}",
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
def section_routes(underlay: dict | None) -> list[str]:
|
|
|
|
|
|
"""Routes du switch vers la frontiere nord/sud. Vide si aucun transit declare.
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
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.",
|
|
|
|
|
|
f"ip route 0.0.0.0 0.0.0.0 {sortie}"]
|
|
|
|
|
|
admin = _sources_admin_ssh()
|
|
|
|
|
|
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:
|
|
|
|
|
|
net = ipaddress.ip_network(cidr, strict=False)
|
|
|
|
|
|
out.append(f"ip route {net.network_address} {net.netmask} {sortie}"
|
|
|
|
|
|
f" ! administration (intrant nftables_admin_ssh)")
|
|
|
|
|
|
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:
|
|
|
|
|
|
dialecte = dialecte or DIALECTE_DEFAUT
|
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.",
|
|
|
|
|
|
"! VLAN = 1000 + index*10 + zone (unique globalement sur le trunk).",
|
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)
|
|
|
|
|
|
out += ["! ----- 1. VLANs (tenants) -----"]
|
2026-07-07 15:50:18 -04:00
|
|
|
|
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)}")
|
2026-07-23 10:21:34 -04:00
|
|
|
|
out.append(f" name {pfx}{n['index']}-{c['libelle']}")
|
2026-07-07 15:50:18 -04:00
|
|
|
|
out += ["!", "! ----- 2. Interfaces de routage (SVI = passerelle des hotes) -----"]
|
|
|
|
|
|
for nom, pfx, n in tenants:
|
|
|
|
|
|
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']}")
|
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
|
|
|
|
out.append(f" ip address {passerelle_de(n['index'], zone)} {m}")
|
2026-07-23 10:21:34 -04:00
|
|
|
|
out.append(f" ip access-group {pfx}{n['index']}-ISOLATION in")
|
2026-07-07 15:50:18 -04:00
|
|
|
|
out.append(" no shutdown")
|
|
|
|
|
|
out += ["!", "! ----- 3. ACL d'isolation tenant (default-deny inter-tenant) -----"]
|
|
|
|
|
|
for nom, pfx, n in tenants:
|
2026-07-23 17:15:27 -04:00
|
|
|
|
reseau, m = masque_acl(supernet_de(n["index"]), dialecte)
|
2026-07-23 10:21:34 -04:00
|
|
|
|
out.append(f"ip access-list extended {pfx}{n['index']}-ISOLATION")
|
2026-07-23 17:15:27 -04:00
|
|
|
|
out += remarque(dialecte, f"Intra-tenant {nom} : routage local autorise")
|
|
|
|
|
|
out.append(f" permit ip {reseau} {m} {reseau} {m}")
|
2026-07-07 15:50:18 -04:00
|
|
|
|
for autre_nom, _, autre in tenants:
|
|
|
|
|
|
if autre_nom == nom:
|
|
|
|
|
|
continue
|
2026-07-23 17:15:27 -04:00
|
|
|
|
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}")
|
ACL de switch : les tenants n'atteignent plus la fabric physique
L'ACL d'isolation bloquait l'autre tenant, puis se terminait par
`permit ip <tenant> any`. Ce `any` autorisait 10.27.x -> 10.0.0.0/24 : le
management des switches, celui de Proxmox et l'OOB/IPMI, plus iSCSI et Ceph.
Une VM compromise atteignait la console physique des hyperviseurs.
Le commentaire du générateur disait « Reste -> passerelle OPNsense », ce qui
est faux pour l'underlay : ce trafic est routé LOCALEMENT par le switch et ne
passe jamais par la frontière, donc elle ne le filtre jamais.
Un `deny` par sous-réseau underlay est désormais émis avant le `permit`
final, dérivé de underlay.yml, dialecte respecté (masque normal ou wildcard).
Vérifié qu'aucun flux du registre ne vise l'underlay : rien de déclaré ne
casse. Sans underlay déclaré, l'ACL retrouve sa forme d'avant.
Consigné en §6 : le registre n'a pas de mot-clé `underlay` (un besoin
légitime, superviser l'hyperviseur, ne pourrait pas être déclaré), et le
devis émet un jeu unique de SVI pour trois switches sans MLAG.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:59:03 -04:00
|
|
|
|
# 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.
|
2026-08-01 20:36:02 -04:00
|
|
|
|
# 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.
|
ACL de switch : les tenants n'atteignent plus la fabric physique
L'ACL d'isolation bloquait l'autre tenant, puis se terminait par
`permit ip <tenant> any`. Ce `any` autorisait 10.27.x -> 10.0.0.0/24 : le
management des switches, celui de Proxmox et l'OOB/IPMI, plus iSCSI et Ceph.
Une VM compromise atteignait la console physique des hyperviseurs.
Le commentaire du générateur disait « Reste -> passerelle OPNsense », ce qui
est faux pour l'underlay : ce trafic est routé LOCALEMENT par le switch et ne
passe jamais par la frontière, donc elle ne le filtre jamais.
Un `deny` par sous-réseau underlay est désormais émis avant le `permit`
final, dérivé de underlay.yml, dialecte respecté (masque normal ou wildcard).
Vérifié qu'aucun flux du registre ne vise l'underlay : rien de déclaré ne
casse. Sans underlay déclaré, l'ACL retrouve sa forme d'avant.
Consigné en §6 : le registre n'a pas de mot-clé `underlay` (un besoin
légitime, superviser l'hyperviseur, ne pourrait pas être déclaré), et le
devis émet un jeu unique de SVI pour trois switches sans MLAG.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 19:59:03 -04:00
|
|
|
|
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}")
|
2026-07-23 17:15:27 -04:00
|
|
|
|
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)
|
|
|
|
|
|
# Le VLAN de transit ne va PAS vers Proxmox : aucun hyperviseur n'a d'interface
|
|
|
|
|
|
# dessus. Il sort par le port de la frontiere (ci-dessous), et lui seul.
|
2026-08-01 20:36:02 -04:00
|
|
|
|
vlans_underlay = [str(r["vlan"])
|
|
|
|
|
|
for r in underlay_mod.reseaux_de_fabric(
|
|
|
|
|
|
underlay, underlay_mod.fabric_du_routeur(underlay))
|
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 and r["vlan"] == transit["vlan"])]
|
2026-07-24 14:56:38 -04:00
|
|
|
|
vlans_tenants = [
|
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)
|
2026-07-07 15:50:18 -04:00
|
|
|
|
out.append("interface <PORT-VERS-PROXMOX>")
|
|
|
|
|
|
out.append(" switchport mode trunk")
|
|
|
|
|
|
out.append(f" switchport trunk allowed vlan add {vlans}")
|
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
|
|
|
|
out += ["!"] + section_frontiere(transit)
|
2026-08-01 21:31:31 -04:00
|
|
|
|
out += ["!"] + section_rayons(underlay, ",".join(vlans_underlay + vlans_tenants))
|
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 += ["!"] + section_routes(underlay)
|
2026-08-01 20:50:55 -04:00
|
|
|
|
out += section_stp(underlay, dialecte, ["<PORT-VERS-PROXMOX>", "<PORT-VERS-FRONTIERE>"])
|
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.
|
2026-08-01 20:50:55 -04:00
|
|
|
|
out += partie_acces(underlay, tenants, ",".join(vlans_underlay + vlans_tenants), dialecte)
|
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__)
|
|
|
|
|
|
ap.add_argument("--dialecte", choices=DIALECTES, default=DIALECTE_DEFAUT,
|
|
|
|
|
|
help="CLI du commutateur (defaut : SETOPS_DIALECTE ou 'cisco')")
|
|
|
|
|
|
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()
|