diff --git a/CHANGELOG.md b/CHANGELOG.md index 289cf7d..01dc024 100644 --- a/CHANGELOG.md +++ b/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** : `` 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` diff --git a/scripts/devis_reseau.py b/scripts/devis_reseau.py index 7937411..43df6c5 100644 --- a/scripts/devis_reseau.py +++ b/scripts/devis_reseau.py @@ -107,6 +107,30 @@ def port_vers(nom_hote: str) -> str: return f"" +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 ", - " 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", ""): + 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 ", - " switchport mode trunk", - f" switchport trunk allowed vlan add {transit['vlan']}", - ] + ([" spanning-tree portfast trunk"] if bord else []) + ] + [ligne + for port in (ports or [""]) + 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 ") - 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", ""): + 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", "")) out += ["!"] + section_rayons(underlay, ",".join(vlans_underlay + vlans_tenants)) out += ["!"] + section_routes(underlay) out += section_stp(underlay, dialecte) diff --git a/scripts/underlay.py b/scripts/underlay.py index 93ad6f4..88beacb 100644 --- a/scripts/underlay.py +++ b/scripts/underlay.py @@ -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: diff --git a/underlay.yml.example b/underlay.yml.example index 0f87027..52547f1 100644 --- a/underlay.yml.example +++ b/underlay.yml.example @@ -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 + # `` 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 }