spanning-tree : RSTP dérivé de la topologie déclarée (étoile)
Le devis ne disait rien du spanning-tree. Sur une fabric convergée à trois
switches, une boucle par brassage accidentel est une tempête de diffusion.
La topologie se déclare (`stp: {mode: rstp, topologie: etoile}`) et le devis
en tire la configuration. Le routeur est désigné pont racine : il est le
centre de l'étoile, tous les chemins passent déjà par lui, donc l'arbre
logique suit le câblage physique plutôt qu'une élection arbitraire. Les
switches d'accès reçoivent une priorité haute — jamais racine.
Ports terminaux déclarés en bord de réseau. BPDU guard délibérément NON
émis : un pont Linux dont le STP serait activé enverrait des BPDU et ferait
tomber le port côté hyperviseur. Le devis dit pourquoi et à quelle condition
l'ajouter.
En étoile aucun lien n'est redondant : RSTP est un filet, pas une nécessité.
Le devis le dit au lieu de laisser croire à une protection indispensable.
Validation : `mode` et `topologie` contrôlés, `stp` sans `routeur` refusé
(aucun pont racine désignable). Sans `stp`, la section signale l'absence de
protection au lieu de disparaître.
Réserve : la forme `binardat` du spanning-tree n'est pas vérifiée sur le
matériel, comme les `ip route`.
Preuves : 24 OK, 0 échec.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
42cd269d29
commit
8f61721cb3
5 changed files with 125 additions and 4 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
|
@ -1,5 +1,32 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-01 (suite 8) — spanning-tree dérivé de la topologie déclarée
|
||||
|
||||
### Ajouté — `underlay.stp` et les sections 6 / B4
|
||||
Le devis ne disait rien du spanning-tree. Sur une fabric convergée à trois switches, une
|
||||
boucle par brassage accidentel n'est pas discrète : c'est une tempête de diffusion.
|
||||
|
||||
La topologie se déclare (`mode: rstp`, `topologie: etoile`) et le devis en tire la
|
||||
configuration. Le **routeur est désigné pont racine** — il est le centre de l'étoile, tous
|
||||
les chemins passent déjà par lui, donc l'arbre logique suit le câblage physique au lieu de
|
||||
sortir d'une élection arbitraire. Les switches d'accès reçoivent une priorité volontairement
|
||||
haute : ils ne doivent jamais devenir racine.
|
||||
|
||||
Les ports terminaux (hyperviseurs, frontière) sont déclarés en bord de réseau. **BPDU guard
|
||||
n'est délibérément pas émis** : un pont Linux dont le STP serait activé enverrait des BPDU et
|
||||
ferait tomber le port côté hyperviseur. Le devis dit pourquoi, et à quelle condition
|
||||
l'ajouter.
|
||||
|
||||
En étoile, aucun lien n'est redondant : RSTP est un filet, pas une nécessité — le devis le
|
||||
dit plutôt que de laisser croire à une protection indispensable.
|
||||
|
||||
`make underlay` valide `mode` et `topologie`, et refuse un `stp` déclaré sans `routeur` :
|
||||
sans lui, aucun pont racine ne peut être désigné. Sans `stp`, la section signale l'absence
|
||||
de protection au lieu de disparaître.
|
||||
|
||||
Réserve consignée : la forme `binardat` des lignes de spanning-tree n'a pas été confrontée au
|
||||
matériel, comme les `ip route`.
|
||||
|
||||
## 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
|
||||
|
|
|
|||
|
|
@ -223,7 +223,10 @@ ansible-vault edit instance/inventories/principal/group_vars/all/vault.yml
|
|||
`underlay` au registre plutôt qu'une exception manuelle dans l'ACL.
|
||||
- **La syntaxe des routes n'est pas dialecte-consciente** — les ACL le sont (masque normal
|
||||
en `binardat`, wildcard en `cisco`), les `ip route` de la section 5 sortent en forme Cisco
|
||||
quel que soit le dialecte. À vérifier sur le matériel réel.
|
||||
quel que soit le dialecte. Même réserve pour les lignes de spanning-tree, dont la forme
|
||||
`binardat` n'a pas été confrontée au matériel. À vérifier sur un `show running-config`.
|
||||
- **Les ports physiques restent à nommer** — `<PORT-VERS-PROXMOX>`, `<PORT-VERS-FRONTIERE>`
|
||||
et `<PORT-TRUNK>` ; rien dans le modèle ne peut les deviner.
|
||||
- **La sortie générale** (mises à jour apt, ACME) n'est pas déclarée dans le registre : les
|
||||
hôtes sortent aujourd'hui parce que la politique `output` des nftables est `accept` et que
|
||||
l'ACL de switch se termine par `permit ip <tenant> any`. À trancher : la déclarer
|
||||
|
|
|
|||
|
|
@ -102,7 +102,58 @@ def section_underlay(underlay: dict | None) -> list[str]:
|
|||
return out
|
||||
|
||||
|
||||
def partie_acces(underlay: dict | None, tenants: list, vlans: str) -> list[str]:
|
||||
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 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
|
||||
|
|
@ -164,7 +215,16 @@ def partie_acces(underlay: dict | None, tenants: list, vlans: str) -> list[str]:
|
|||
"interface <PORT-TRUNK>",
|
||||
" switchport mode trunk",
|
||||
f" switchport trunk allowed vlan add {vlans}",
|
||||
"!", "end", "write memory"]
|
||||
"!"]
|
||||
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"]
|
||||
return out
|
||||
|
||||
|
||||
|
|
@ -350,9 +410,10 @@ def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -
|
|||
out.append(f" switchport trunk allowed vlan add {vlans}")
|
||||
out += ["!"] + section_frontiere(transit)
|
||||
out += ["!"] + section_routes(underlay)
|
||||
out += section_stp(underlay, dialecte, ["<PORT-VERS-PROXMOX>", "<PORT-VERS-FRONTIERE>"])
|
||||
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))
|
||||
out += partie_acces(underlay, tenants, ",".join(vlans_underlay + vlans_tenants), dialecte)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,18 @@ def hotes(underlay: dict | None) -> list[dict]:
|
|||
|
||||
FABRIC_DEFAUT = "principal"
|
||||
|
||||
# Priorites de pont : le routeur est la racine (basse), les switches d'acces ne
|
||||
# doivent jamais l'etre. 4096 laisse 0 disponible pour une reprise d'urgence.
|
||||
STP_PRIORITE_RACINE = 4096
|
||||
STP_PRIORITE_ACCES = 32768
|
||||
STP_MODES = ("rstp", "mstp", "pvst")
|
||||
STP_TOPOLOGIES = ("etoile", "anneau", "maille")
|
||||
|
||||
|
||||
def stp(underlay: dict | None) -> dict:
|
||||
"""Parametres de spanning-tree de la fabric principale. {} si non declares."""
|
||||
return (underlay or {}).get("stp") or {}
|
||||
|
||||
|
||||
def fabric_de(reseau: dict) -> str:
|
||||
"""Fabric physique qui porte ce reseau. `principal` par defaut."""
|
||||
|
|
@ -182,6 +194,16 @@ def valider(underlay: dict | None,
|
|||
erreurs.append(f"reseau '{nom}': passerelle_sortie '{r['passerelle_sortie']}' invalide")
|
||||
vus_sous.append((net, nom))
|
||||
|
||||
s = stp(underlay)
|
||||
if s:
|
||||
if str(s.get("mode", "")) not in STP_MODES:
|
||||
erreurs.append(f"stp.mode '{s.get('mode')}' inconnu (attendu : {', '.join(STP_MODES)})")
|
||||
if str(s.get("topologie", "")) not in STP_TOPOLOGIES:
|
||||
erreurs.append(f"stp.topologie '{s.get('topologie')}' inconnue "
|
||||
f"(attendu : {', '.join(STP_TOPOLOGIES)})")
|
||||
if not routeur(underlay):
|
||||
erreurs.append("stp declare sans `routeur` : impossible de designer le pont racine")
|
||||
|
||||
# Une seule sortie possible : deux transits donneraient deux routes par defaut.
|
||||
transits = [r.get("nom", "?") for r in reseaux(underlay) if r.get("passerelle_sortie")]
|
||||
if len(transits) > 1:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ underlay:
|
|||
# d'adresses qu'il y a de zones. Doit nommer un hote declare dans `hotes:`.
|
||||
routeur: switch-01
|
||||
|
||||
# Spanning-tree de la fabric principale. `topologie` documente le cablage :
|
||||
# `etoile` = rayons depuis le routeur (aucun lien redondant, donc aucune boucle),
|
||||
# `anneau`/`maille` = liens redondants, RSTP devient indispensable. Le routeur est
|
||||
# toujours designe pont racine : l'arbre logique suit alors le cablage physique.
|
||||
stp:
|
||||
mode: rstp # rstp | mstp | pvst
|
||||
topologie: etoile # etoile | anneau | maille
|
||||
|
||||
reseaux:
|
||||
- nom: management
|
||||
description: Switches, mgmt Proxmox, OOB/IPMI
|
||||
|
|
|
|||
Loading…
Reference in a new issue