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>
This commit is contained in:
parent
f80b53b592
commit
76e82bfb96
4 changed files with 155 additions and 43 deletions
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -1,5 +1,35 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-01 (suite 11) — les ports physiques entrent dans le modèle
|
||||
|
||||
Les noms de ports n'étaient modélisés **nulle part** : `<PORT-VERS-PROXMOX>` et consorts
|
||||
étaient des marqueurs littéraux dans le générateur. L'opérateur les remplaçait à la main dans
|
||||
la sortie, et recommençait **à chaque régénération**. C'était le seul endroit du devis où le
|
||||
travail était perdu à répétition.
|
||||
|
||||
Ils se déclarent désormais par équipement dans `underlay.yml`, sous quatre clefs qui
|
||||
correspondent aux quatre natures de lien :
|
||||
|
||||
```yaml
|
||||
ports:
|
||||
hyperviseurs: [Te1/0/1, Te1/0/2, Te1/0/3] # ports terminaux (portfast)
|
||||
frontiere: [Gi1/0/23] # vers le pare-feu (portfast)
|
||||
rayons: { sleipnir-02: Te1/0/47, … } # côté ROUTEUR
|
||||
montante: Te1/0/48 # côté SWITCH D'ACCÈS
|
||||
```
|
||||
|
||||
Le devis émet alors les vrais ports, y compris **plusieurs** vers les hyperviseurs — il n'en
|
||||
supposait qu'un seul jusqu'ici, alors que le cluster en compte trois. Non déclarés, les
|
||||
marqueurs reviennent : la dégradation est par équipement, un switch renseigné et un autre non
|
||||
cohabitent sans problème.
|
||||
|
||||
La partie B devient **par switch** : adresse de gestion, montante et ports terminaux
|
||||
différant d'une machine à l'autre, un bloc commun n'avait plus de sens.
|
||||
|
||||
`make underlay` refuse un port déclaré deux fois sur un même équipement, un rayon vers un
|
||||
switch inconnu, des `rayons` sur autre chose que le routeur, une `montante` sur le routeur
|
||||
lui-même. Les quatre cas exercés.
|
||||
|
||||
## 2026-08-01 (suite 10) — une interface, un bloc
|
||||
|
||||
La partie A déclarait ses ports de bord deux fois : l'interface en section 4, son `portfast`
|
||||
|
|
|
|||
|
|
@ -107,6 +107,30 @@ def port_vers(nom_hote: str) -> str:
|
|||
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.
|
||||
|
||||
|
|
@ -121,11 +145,10 @@ def section_rayons(underlay: dict | None, vlans: str) -> list[str]:
|
|||
"! /!\\ 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:
|
||||
out += [f"interface {port_vers(h['nom'])}",
|
||||
f" description rayon vers {h['nom']}",
|
||||
" switchport mode trunk",
|
||||
f" switchport trunk allowed vlan add {vlans}"]
|
||||
port = str(rayons.get(h["nom"]) or port_vers(h["nom"]))
|
||||
out += bloc_trunk(port, vlans, f"rayon vers {h['nom']}")
|
||||
return out
|
||||
|
||||
|
||||
|
|
@ -219,39 +242,32 @@ def partie_acces(underlay: dict | None, tenants: list, vlans: str,
|
|||
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']}",
|
||||
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 += ["!", "! ----- 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>",
|
||||
" switchport mode trunk",
|
||||
f" switchport trunk allowed vlan add {vlans}",
|
||||
" spanning-tree portfast trunk" if underlay_mod.stp(underlay) else "!",
|
||||
"!"]
|
||||
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"! ----- B4. Spanning-tree ({mode.upper()}) -----",
|
||||
f"! Priorite VOLONTAIREMENT haute : ces switches ne doivent jamais devenir",
|
||||
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 += ["!"]
|
||||
|
|
@ -259,7 +275,8 @@ def partie_acces(underlay: dict | None, tenants: list, vlans: str,
|
|||
return out
|
||||
|
||||
|
||||
def section_frontiere(transit: dict | None, bord: bool = False) -> list[str]:
|
||||
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
|
||||
|
|
@ -274,10 +291,9 @@ def section_frontiere(transit: dict | None, bord: bool = False) -> list[str]:
|
|||
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.",
|
||||
"interface <PORT-VERS-FRONTIERE>",
|
||||
" switchport mode trunk",
|
||||
f" switchport trunk allowed vlan add {transit['vlan']}",
|
||||
] + ([" spanning-tree portfast trunk"] if bord else [])
|
||||
] + [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]:
|
||||
|
|
@ -439,12 +455,14 @@ def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -
|
|||
for zone in sorted(n["categories"])
|
||||
]
|
||||
vlans = ",".join(vlans_underlay + vlans_tenants)
|
||||
out.append("interface <PORT-VERS-PROXMOX>")
|
||||
out.append(" switchport mode trunk")
|
||||
out.append(f" switchport trunk allowed vlan add {vlans}")
|
||||
if underlay_mod.stp(underlay):
|
||||
out.append(" spanning-tree portfast trunk")
|
||||
out += ["!"] + section_frontiere(transit, bord=bool(underlay_mod.stp(underlay)))
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -107,6 +107,31 @@ def routeur(underlay: dict | None) -> str | None:
|
|||
return (underlay or {}).get("routeur") or None
|
||||
|
||||
|
||||
def ports_de(hote: dict) -> dict:
|
||||
"""Ports physiques declares d'un equipement. {} si aucun.
|
||||
|
||||
Trois clefs, qui correspondent aux trois natures de lien du devis :
|
||||
`hyperviseurs` (liste) ports terminaux vers Proxmox ;
|
||||
`frontiere` (liste) port(s) vers le pare-feu de bordure ;
|
||||
`rayons` (table) {nom du switch d'acces: port}, cote routeur ;
|
||||
`montante` (str) port vers le routeur, cote switch d'acces.
|
||||
Non declares, le devis retombe sur ses marqueurs : il reste lisible, il n'est
|
||||
simplement pas applicable tel quel.
|
||||
"""
|
||||
return (hote or {}).get("ports") or {}
|
||||
|
||||
|
||||
def _ports_a_plat(hote: dict) -> list[str]:
|
||||
"""Tous les ports d'un equipement, a plat — pour detecter un doublon."""
|
||||
p = ports_de(hote)
|
||||
plat = [str(x) for x in (p.get("hyperviseurs") or [])]
|
||||
plat += [str(x) for x in (p.get("frontiere") or [])]
|
||||
plat += [str(v) for v in (p.get("rayons") or {}).values()]
|
||||
if p.get("montante"):
|
||||
plat.append(str(p["montante"]))
|
||||
return plat
|
||||
|
||||
|
||||
def switches_acces(underlay: dict | None) -> list[dict]:
|
||||
"""Les switches d'acces : hotes du reseau de management, hors routeur.
|
||||
|
||||
|
|
@ -211,6 +236,25 @@ def valider(underlay: dict | None,
|
|||
erreurs.append(f"reseau '{nom}': passerelle_sortie '{r['passerelle_sortie']}' invalide")
|
||||
vus_sous.append((net, nom))
|
||||
|
||||
# Ports physiques : un meme port ne peut pas servir deux fois sur un equipement,
|
||||
# et un rayon ne peut viser qu'un switch d'acces reellement declare.
|
||||
noms_acces = {h.get("nom") for h in switches_acces(underlay)}
|
||||
r_courant = routeur(underlay)
|
||||
for h in hotes(underlay):
|
||||
hn = h.get("nom", "?")
|
||||
plat = _ports_a_plat(h)
|
||||
doublons = sorted({p for p in plat if plat.count(p) > 1})
|
||||
if doublons:
|
||||
erreurs.append(f"hote '{hn}': port(s) declare(s) deux fois : {', '.join(doublons)}")
|
||||
p = ports_de(h)
|
||||
for cible in (p.get("rayons") or {}):
|
||||
if cible not in noms_acces:
|
||||
erreurs.append(f"hote '{hn}': rayon vers '{cible}', qui n'est pas un switch d'acces")
|
||||
if p.get("rayons") and hn != r_courant:
|
||||
erreurs.append(f"hote '{hn}': des `rayons` mais ce n'est pas le routeur")
|
||||
if p.get("montante") and hn == r_courant:
|
||||
erreurs.append(f"hote '{hn}': une `montante` alors qu'il EST le routeur")
|
||||
|
||||
s = stp(underlay)
|
||||
if s:
|
||||
if str(s.get("mode", "")) not in STP_MODES:
|
||||
|
|
|
|||
|
|
@ -75,9 +75,29 @@ underlay:
|
|||
mtu: 1500
|
||||
|
||||
# Hotes fixes documentes (optionnel) : IP hors DHCP, verifiees dans leur reseau.
|
||||
# PORTS PHYSIQUES (optionnel). Sans eux, le devis emet des marqueurs
|
||||
# `<PORT-VERS-...>` a remplacer a la main — et le travail est perdu a chaque
|
||||
# regeneration. Declares ici, le devis sort applicable tel quel.
|
||||
# hyperviseurs : ports terminaux vers Proxmox (portfast)
|
||||
# frontiere : port(s) vers le pare-feu de bordure (portfast)
|
||||
# rayons : {switch d'acces: port}, cote ROUTEUR uniquement
|
||||
# montante : port vers le routeur, cote SWITCH D'ACCES uniquement
|
||||
# `make underlay` refuse un port declare deux fois sur un meme equipement, un rayon
|
||||
# vers un switch inconnu, et une confusion rayons/montante.
|
||||
hotes:
|
||||
# Le switch designe `routeur` porte le SVI de management : son adresse de gestion
|
||||
# EST la passerelle du reseau. `make underlay` refuse les deux valeurs divergentes.
|
||||
- { nom: switch-01, reseau: management, ip: 10.0.0.1 }
|
||||
- { nom: switch-02, reseau: management, ip: 10.0.0.3 }
|
||||
- nom: switch-01
|
||||
reseau: management
|
||||
ip: 10.0.0.1
|
||||
ports:
|
||||
hyperviseurs: [Te1/0/1, Te1/0/2, Te1/0/3]
|
||||
frontiere: [Gi1/0/23]
|
||||
rayons: { switch-02: Te1/0/47, switch-03: Te1/0/48 }
|
||||
- nom: switch-02
|
||||
reseau: management
|
||||
ip: 10.0.0.3
|
||||
ports:
|
||||
montante: Te1/0/48
|
||||
hyperviseurs: [Te1/0/1, Te1/0/2, Te1/0/3]
|
||||
- { nom: switch-03, reseau: management, ip: 10.0.0.4 }
|
||||
|
|
|
|||
Loading…
Reference in a new issue