diff --git a/CHANGELOG.md b/CHANGELOG.md index 017b443..b1e7795 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # CHANGELOG — Set-OPS +## 2026-08-01 (suite 7) — l'underlay connaît ses fabrics physiques + +Le stockage jumbo (iSCSI, Ceph) est porté par un **réseau indépendant de deux switches +10G**, sans câble commun avec la fabric convergée des `sleipnir`. Le modèle l'ignorait : +le devis déclarait les VLAN 20/30/31 sur les switches convergés et les mettait dans leurs +trunks. C'était faux. + +Chaque réseau de l'underlay porte désormais une `fabric` (`principal` par défaut). Le devis +ne configure que celle du routeur, et **énonce ce qu'il ne couvre pas** plutôt que de le +taire : + +``` +! HORS PERIMETRE — fabric 'stockage' : stockage-iscsi (VLAN 20), ceph-public (VLAN 30), … +! Portee par des switches distincts, sans cable commun avec celle-ci : +! ni VLAN a declarer ici, ni trunk, ni spanning-tree partage. +``` + +Conséquence directe : la question du spanning-tree ne se pose que sur la fabric principale — +trois switches — et pas sur le stockage, dont les deux switches forment un domaine séparé. + +Les `deny` de l'ACL couvrent en revanche **toutes** les fabrics, y compris celles hors +périmètre : la règle porte sur l'adresse de destination, pas sur le câblage. Si un chemin +s'ouvre un jour vers le stockage, il est déjà fermé. + ## 2026-08-01 (suite 6) — nommage : `bifrost` aux frontières, `sleipnir` à la fabric `bifrost-1` et `bifrost-2` sont réservés aux deux frontières OPNsense — Bifröst est le pont diff --git a/scripts/devis_reseau.py b/scripts/devis_reseau.py index f60b1e8..9b7122a 100644 --- a/scripts/devis_reseau.py +++ b/scripts/devis_reseau.py @@ -59,11 +59,17 @@ def remarque(dialecte: str, texte: str) -> list[str]: def section_underlay(underlay: dict | None) -> list[str]: - """VLANs + SVIs (si passerelle) de la fabric physique. Vide si aucun underlay.""" - reseaux = underlay_mod.reseaux(underlay) + """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) if not reseaux: return [] - out = ["! ----- 0. Underlay (fabric physique, cluster-global, hors tenant) -----"] + out = [f"! ----- 0. Underlay — fabric '{fabric}' (cluster-global, hors tenant) -----"] for r in reseaux: out.append(f"vlan {r['vlan']}") out.append(f" name {r['nom']}") @@ -78,6 +84,15 @@ def section_underlay(underlay: dict | None) -> list[str]: out.append(" no shutdown") for h in underlay_mod.hotes(underlay): out.append(f"! {h.get('nom','?')} = {h.get('ip','?')} ({h.get('reseau','?')})") + # 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.") out.append("!") return out @@ -92,7 +107,9 @@ def partie_acces(underlay: dict | None, tenants: list, vlans: str) -> list[str]: r_nom = underlay_mod.routeur(underlay) if not r_nom: return [] - mgmt = next((r for r in underlay_mod.reseaux(underlay) if r.get("passerelle")), None) + fabric = underlay_mod.fabric_du_routeur(underlay) + mgmt = next((r for r in underlay_mod.reseaux_de_fabric(underlay, fabric) + if r.get("passerelle")), None) # UNIQUEMENT les hotes du reseau de management : les autres equipements declares # (frontieres sur le lien de transit, par exemple) ne sont pas des switches et ne # doivent recevoir aucune de ces lignes. @@ -115,7 +132,7 @@ def partie_acces(underlay: dict | None, tenants: list, vlans: str) -> list[str]: # 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) - for r in underlay_mod.reseaux(underlay): + for r in underlay_mod.reseaux_de_fabric(underlay, fabric): if transit and r["vlan"] == transit["vlan"]: continue out += [f"vlan {r['vlan']}", f" name {r['nom']}"] @@ -300,6 +317,9 @@ def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) - # 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)") @@ -310,7 +330,9 @@ def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) - 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. - vlans_underlay = [str(r["vlan"]) for r in underlay_mod.reseaux(underlay) + vlans_underlay = [str(r["vlan"]) + for r in underlay_mod.reseaux_de_fabric( + underlay, underlay_mod.fabric_du_routeur(underlay)) if not (transit and r["vlan"] == transit["vlan"])] vlans_tenants = [ str(vlan_de(n["index"], zone)) diff --git a/scripts/underlay.py b/scripts/underlay.py index dac3105..21af8bb 100644 --- a/scripts/underlay.py +++ b/scripts/underlay.py @@ -58,6 +58,34 @@ def hotes(underlay: dict | None) -> list[dict]: return (underlay or {}).get("hotes", []) or [] +FABRIC_DEFAUT = "principal" + + +def fabric_de(reseau: dict) -> str: + """Fabric physique qui porte ce reseau. `principal` par defaut.""" + return str(reseau.get("fabric") or FABRIC_DEFAUT) + + +def fabriques(underlay: dict | None) -> list[str]: + """Les fabrics declarees, `principal` en tete.""" + vues = {fabric_de(r) for r in reseaux(underlay)} + return [FABRIC_DEFAUT] * (FABRIC_DEFAUT in vues) + sorted(vues - {FABRIC_DEFAUT}) + + +def reseaux_de_fabric(underlay: dict | None, fabric: str) -> list[dict]: + return [r for r in reseaux(underlay) if fabric_de(r) == fabric] + + +def fabric_du_routeur(underlay: dict | None) -> str: + """Fabric du switch routeur : celle du reseau ou il est declare comme hote.""" + r_nom = routeur(underlay) + par_nom = {r.get("nom"): r for r in reseaux(underlay)} + for h in hotes(underlay): + if h.get("nom") == r_nom and h.get("reseau") in par_nom: + return fabric_de(par_nom[h["reseau"]]) + return FABRIC_DEFAUT + + def routeur(underlay: dict | None) -> str | None: """Nom du switch qui porte les SVI, ou None si aucun n'est designe. diff --git a/underlay.yml.example b/underlay.yml.example index 54313ed..a38e9b8 100644 --- a/underlay.yml.example +++ b/underlay.yml.example @@ -15,6 +15,11 @@ # Storage/Ceph restent en general L2 pur (pas de passerelle). --- underlay: + # FABRICS PHYSIQUES. Chaque reseau appartient a une fabric (`principal` par defaut). + # Deux fabrics ne partagent aucun cable : le devis d'une fabric ne declare ni ne + # transporte les VLAN d'une autre, et leurs spanning-tree sont independants. + # Cas typique : le stockage jumbo (iSCSI, Ceph) sur ses propres switches 10G. + # # Switch qui porte le routage : SVI de zone, ACL d'isolation, routes. Sans MLAG, # UN SEUL switch route ; les autres restent en L2 pur et reçoivent la partie B du # devis. Dupliquer les SVI sur plusieurs switches creerait autant de conflits @@ -29,16 +34,19 @@ underlay: passerelle: 10.0.0.1 # SVI (retirer pour du L2 pur) mtu: 1500 - nom: stockage-iscsi + fabric: stockage # switches dedies, hors fabric principale description: iSCSI MPIO vlan: 20 sous_reseau: 10.0.1.0/24 mtu: 9000 # jumbo - nom: ceph-public + fabric: stockage # switches dedies, hors fabric principale description: Clients <-> MON/OSD vlan: 30 sous_reseau: 10.0.2.0/24 mtu: 9000 - nom: ceph-cluster + fabric: stockage # switches dedies, hors fabric principale description: OSD <-> OSD (replication, backfill, recovery) vlan: 31 sous_reseau: 10.0.3.0/24