Set-OPS-Public/scripts/devis_reseau.py
Daniel Allaire 76e82bfb96 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

489 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
import os
import re
import sys
from pathlib import Path
import yaml
# 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")
RACINE = Path(__file__).resolve().parent.parent
DOSSIER_INSTANCES = RACINE.parent
# 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
import underlay as underlay_mod # noqa: E402 (fabric physique, cluster-global, hors tenant)
# 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
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)
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}"]
def section_underlay(underlay: dict | None) -> list[str]:
"""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 = [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']}")
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")
# 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}
for h in underlay_mod.hotes(underlay):
if h.get("reseau") in noms_reseaux:
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
def port_vers(nom_hote: str) -> str:
"""Marqueur de port physique vers un equipement nomme."""
return f"<PORT-VERS-{nom_hote.upper()}>"
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}")
out += [" switchport mode trunk", f" switchport trunk allowed vlan add {vlans}"]
if bord:
out.append(" spanning-tree portfast trunk")
return out
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."]
rayons = underlay_mod.ports_de(hote_nomme(underlay, underlay_mod.routeur(underlay))).get("rayons") or {}
for h in acces:
port = str(rayons.get(h["nom"]) or port_vers(h["nom"]))
out += bloc_trunk(port, vlans, f"rayon vers {h['nom']}")
return out
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) -> 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)
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."]
out.append("!")
return out
def partie_acces(underlay: dict | None, tenants: list, vlans: str,
dialecte_b: str = DIALECTE_DEFAUT) -> list[str]:
"""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 []
fabric = underlay_mod.fabric_du_routeur(underlay)
mgmt = next((r for r in underlay_mod.reseaux_de_fabric(underlay, fabric)
if r.get("passerelle")), None)
# UNIQUEMENT les hotes du reseau de management : les autres equipements declares
# (frontieres sur le lien de transit, par exemple) ne sont pas des switches et ne
# doivent recevoir aucune de ces lignes.
autres = [h for h in underlay_mod.hotes(underlay)
if h.get("nom") != r_nom and mgmt and h.get("reseau") == mgmt["nom"]]
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) -----",
]
# 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_de_fabric(underlay, fabric):
if transit and r["vlan"] == transit["vlan"]:
continue
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']}"]
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:
out += [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.")
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}")
out += ["! Ports terminaux vers les hyperviseurs."]
for port in ports_ou_marqueur(h, "hyperviseurs", "<PORT-VERS-PROXMOX>"):
out += bloc_trunk(port, vlans, bord=bord)
s = underlay_mod.stp(underlay)
if s:
mode = str(s.get("mode", "rstp"))
out += ["!", f"! ----- B3. Spanning-tree ({mode.upper()}) — sur les deux -----",
"! 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"]
return out
def section_frontiere(transit: dict | None, bord: bool = False,
ports: list[str] | None = 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']}.",
"! Port terminal : rien derriere lui ne participe au spanning-tree.",
] + [ligne
for port in (ports or ["<PORT-VERS-FRONTIERE>"])
for ligne in bloc_trunk(port, str(transit["vlan"]), bord=bord)]
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 -----",
"! /!\\ 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.",
"!",
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
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 {}
# Un tenant federe = une nomenclature avec un index (l'adressage en decoule).
# `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):
nom = chemin.parent.parent.name
tenants.append((nom, prefixe(nom), n))
tenants.sort(key=lambda t: t[2]["index"])
return tenants
def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -> str:
dialecte = dialecte or DIALECTE_DEFAUT
underlay = underlay_mod.charger()
r_nom = underlay_mod.routeur(underlay)
out = ["configure terminal", f"! dialecte CLI : {dialecte}", "!"]
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).",
"! Underlay (VLAN < 1000) = fabric physique, cluster-global.",
]
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 += [
"! ============================================================",
"!",
]
out += section_underlay(underlay)
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']}")
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']}")
out.append(f" ip address {passerelle_de(n['index'], zone)} {m}")
out.append(f" ip access-group {pfx}{n['index']}-ISOLATION in")
out.append(" no shutdown")
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")
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."]
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_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))
for _, _, n in tenants
for zone in sorted(n["categories"])
]
vlans = ",".join(vlans_underlay + vlans_tenants)
routeur_h = hote_nomme(underlay, underlay_mod.routeur(underlay))
bord = bool(underlay_mod.stp(underlay))
for port in ports_ou_marqueur(routeur_h, "hyperviseurs", "<PORT-VERS-PROXMOX>"):
out += bloc_trunk(port, vlans, bord=bord)
out += ["!"] + section_frontiere(
transit, bord=bool(underlay_mod.stp(underlay)),
ports=ports_ou_marqueur(hote_nomme(underlay, underlay_mod.routeur(underlay)),
"frontiere", "<PORT-VERS-FRONTIERE>"))
out += ["!"] + section_rayons(underlay, ",".join(vlans_underlay + vlans_tenants))
out += ["!"] + section_routes(underlay)
out += section_stp(underlay, dialecte)
out += ["end", "write memory"]
# Les switches d'acces : memes VLANs, aucun SVI de zone, aucune ACL, aucune route.
out += partie_acces(underlay, tenants, ",".join(vlans_underlay + vlans_tenants), dialecte)
return "\n".join(out)
def main() -> None:
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()
tenants = decouvrir()
if not tenants:
print("Aucune instance 'ip-miroir' federee trouvee (../*/plan/nomenclature.yml).",
file=sys.stderr)
sys.exit(1)
print(generer(tenants, args.dialecte))
if __name__ == "__main__":
main()