devis reseau : un segment non etiquete n'appartient a aucune fabric de switch
`segment_physique: true` retire la cle `vlan`. devis_reseau.py ecrivait `r['vlan']` en une vingtaine d'endroits : KeyError, /api/devis-reseau en 500, et LA VUE RESEAU DU PANNEAU RESTAIT VIDE — une panne a deux couches de sa cause. Filtre a la source plutot que colmatage : devis_reseau definit son propre `reseaux_de_fabric` qui ecarte les reseaux sans etiquette, et les dix appels y passent. Colmater les vingt occurrences aurait laisse la vingt-et-unieme. Le bloc de gestion d'un switch echappait au filtre (il lit son reseau directement). Sans etiquette, aucune `interface VlanN` n'existe : l'adresse va sur l'interface de gestion native du boitier. Le devis le DIT au lieu d'inventer une syntaxe. Expose au passage que bifrost-3 et bifrost-4 declarent leur gestion sur un segment qui ne les traverse pas, a des adresses qui n'ont jamais repondu. Les quatre devis du panneau repassent. 42 preuves vertes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a6713ffdcd
commit
fcde343ef0
2 changed files with 69 additions and 11 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
|
@ -1,5 +1,32 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-25 — La vue Réseau du panneau était vide : un `KeyError` deux couches plus bas
|
||||
|
||||
`segment_physique: true` — introduit le matin même pour dire qu'un réseau n'a pas
|
||||
d'étiquette VLAN — retire la clé `vlan`. Or `devis_reseau.py` écrivait `r['vlan']` en une
|
||||
vingtaine d'endroits.
|
||||
|
||||
Le premier segment non étiqueté a levé un `KeyError`, `/api/devis-reseau` a rendu une 500,
|
||||
et **la vue Réseau de l'interface est restée vide**. Une panne qui ne ressemble à rien, à
|
||||
deux couches de sa cause — c'est le genre qu'on cherche partout sauf là où elle est.
|
||||
|
||||
**Filtré à la source, une seule fois.** Colmater les vingt appels aurait laissé le
|
||||
vingt-et-unième. `devis_reseau` définit désormais son propre `reseaux_de_fabric` qui écarte
|
||||
les réseaux sans étiquette, et les dix appels y passent : ce fichier ne configure que des
|
||||
commutateurs, et un commutateur n'a rien à dire d'un segment qui ne l'atteint pas.
|
||||
|
||||
Le bloc de gestion d'un switch échappait au filtre — il lit son réseau directement. Quand
|
||||
ce réseau n'est pas étiqueté, il n'existe aucune `interface VlanN` : l'adresse appartient à
|
||||
l'interface de gestion native du boîtier, dont le nom dépend du modèle. Le devis **le dit**
|
||||
plutôt que d'inventer une syntaxe — un devis qui promet une commande fausse est pire qu'un
|
||||
devis qui se tait.
|
||||
|
||||
*Au passage, ça expose une incohérence de la carte : `bifrost-3` et `bifrost-4` déclarent
|
||||
leur adresse de gestion sur `management`, un segment qui ne les traverse pas. Ces deux
|
||||
adresses sont d'ailleurs celles de l'ancien plan et n'ont jamais répondu.*
|
||||
|
||||
Les quatre devis du panneau — switches, frontière, SDN, pare-feu est-ouest — repassent.
|
||||
|
||||
## 2026-08-25 — Un renommage de rôle laissait son ancien fichier aux commandes
|
||||
|
||||
Mesuré sur `forge-01`, par l'agent invité — donc sans dépendre du réseau. Quatre
|
||||
|
|
|
|||
|
|
@ -87,8 +87,20 @@ def _bloc_gestion(underlay: dict | None, nom_switch: str | None) -> list[str]:
|
|||
if not r or not r.get("sous_reseau"):
|
||||
return []
|
||||
net = ipaddress.ip_network(r["sous_reseau"], strict=False)
|
||||
out = [f"interface Vlan{r['vlan']}", f" description gestion {nom_switch}",
|
||||
f" ip address {h['ip']} {net.netmask}", " no shutdown"]
|
||||
# LE SEGMENT DE GESTION PEUT NE PAS ETRE ETIQUETE (2026-08-25).
|
||||
#
|
||||
# Un switch declare son adresse sur un reseau ; si ce reseau est un SEGMENT PHYSIQUE
|
||||
# (`segment_physique: true`, aucune encapsulation), il n'existe aucune `interface
|
||||
# VlanN` a configurer — l'adresse appartient a l'interface de gestion native du
|
||||
# boitier, dont le nom depend du modele. On le DIT plutot que d'inventer une syntaxe :
|
||||
# un devis qui promet une commande fausse est pire qu'un devis qui se tait.
|
||||
if r.get("vlan") is None:
|
||||
out = [f"! gestion {nom_switch} : le segment '{r['nom']}' n'est pas etiquete —",
|
||||
f"! {h['ip']} {net.netmask} va sur l'interface de gestion native du",
|
||||
"! boitier (nom dependant du modele), pas sur une interface VLAN."]
|
||||
else:
|
||||
out = [f"interface Vlan{r['vlan']}", f" description gestion {nom_switch}",
|
||||
f" ip address {h['ip']} {net.netmask}", " no shutdown"]
|
||||
gw = r.get("passerelle")
|
||||
if gw:
|
||||
porteur = next((x for x in underlay_mod.hotes(underlay)
|
||||
|
|
@ -98,6 +110,25 @@ def _bloc_gestion(underlay: dict | None, nom_switch: str | None) -> list[str]:
|
|||
return out
|
||||
|
||||
|
||||
# UN SEGMENT SANS ETIQUETTE N'APPARTIENT A AUCUNE FABRIC DE COMMUTATEUR (2026-08-25).
|
||||
#
|
||||
# Un reseau peut se declarer `segment_physique: true` : le plan d'administration de
|
||||
# Chezlepro arrive sur un PORT dedie de la frontiere, sans encapsulation, et ne traverse
|
||||
# aucun switch — mesure du 2026-08-24, trois sondes muettes avec temoin positif. Il n'a
|
||||
# donc pas de cle `vlan`.
|
||||
#
|
||||
# Ce devis emettait `r['vlan']` en une vingtaine d'endroits. Le premier segment non
|
||||
# etiquete a leve un `KeyError`, l'API du panneau a rendu une 500, et la vue Reseau de
|
||||
# l'interface est restee VIDE — une panne qui ne ressemble a rien, deux couches plus haut
|
||||
# que sa cause. Colmater les vingt appels aurait laisse le vingt-et-unieme.
|
||||
#
|
||||
# On filtre donc A LA SOURCE, et une seule fois : ce fichier ne configure que des
|
||||
# commutateurs, et un commutateur n'a rien a dire d'un segment qui ne l'atteint pas.
|
||||
def reseaux_de_fabric(underlay: dict | None, fabric: str) -> list[dict]:
|
||||
return [r for r in underlay_mod.reseaux_de_fabric(underlay, fabric)
|
||||
if r.get("vlan") is not None]
|
||||
|
||||
|
||||
def section_underlay(underlay: dict | None) -> list[str]:
|
||||
"""VLANs + SVIs (si passerelle) de la fabric du routeur. Vide si aucun underlay.
|
||||
|
||||
|
|
@ -106,7 +137,7 @@ def section_underlay(underlay: dict | None) -> list[str]:
|
|||
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)
|
||||
reseaux = reseaux_de_fabric(underlay, fabric)
|
||||
if not reseaux:
|
||||
return []
|
||||
out = [f"! ----- 0. Underlay — fabric '{fabric}' (cluster-global, hors tenant) -----"]
|
||||
|
|
@ -152,7 +183,7 @@ def section_underlay(underlay: dict | None) -> list[str]:
|
|||
for autre in underlay_mod.fabriques(underlay):
|
||||
if autre == fabric:
|
||||
continue
|
||||
r_autres = underlay_mod.reseaux_de_fabric(underlay, autre)
|
||||
r_autres = 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 :")
|
||||
|
|
@ -283,7 +314,7 @@ def partie_acces(underlay: dict | None, tenants: list, vlans: str,
|
|||
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)
|
||||
mgmt = next((r for r in reseaux_de_fabric(underlay, fabric)
|
||||
if r.get("passerelle")), None)
|
||||
# SOURCE UNIQUE : `switches_acces()` decide qui est un switch d'acces — reseau de
|
||||
# management ET role `switch`. Une copie locale de ce filtre avait laisse les
|
||||
|
|
@ -306,7 +337,7 @@ def partie_acces(underlay: dict | None, tenants: list, vlans: str,
|
|||
# Le lien de frontiere N'EST PLUS reserve au routeur : depuis que les noeuds de
|
||||
# sortie EVPN y ont une patte, un hyperviseur branche sur un switch d'acces doit
|
||||
# pouvoir l'atteindre. Le sauter ici ferait tomber son trafic sortant en silence.
|
||||
for r in underlay_mod.reseaux_de_fabric(underlay, fabric):
|
||||
for r in reseaux_de_fabric(underlay, fabric):
|
||||
out += [f"vlan {r['vlan']}", f" name {r['nom']}"]
|
||||
if underlay_mod.routage_tenants(underlay) == "sdn":
|
||||
out.append("! Aucun VLAN de tenant : ils vivent dans le SDN, pas sur le fil.")
|
||||
|
|
@ -357,7 +388,7 @@ def reseaux_par_via(underlay: dict | None, role: str) -> dict[str, list[dict]]:
|
|||
"""
|
||||
par_via: dict[str, dict[str, dict]] = {}
|
||||
fab = underlay_mod.fabric_du_routeur(underlay)
|
||||
connus = {r.get("nom"): r for r in underlay_mod.reseaux_de_fabric(underlay, fab)
|
||||
connus = {r.get("nom"): r for r in reseaux_de_fabric(underlay, fab)
|
||||
if r.get("vlan") is not None}
|
||||
for h in underlay_mod.hotes(underlay):
|
||||
if h.get("role") != role or not h.get("reseau"):
|
||||
|
|
@ -382,7 +413,7 @@ def vlans_du_role(underlay: dict | None, role: str,
|
|||
noms = {h.get("reseau") for h in underlay_mod.hotes(underlay)
|
||||
if h.get("role") == role and h.get("reseau")}
|
||||
fab = underlay_mod.fabric_du_routeur(underlay)
|
||||
reseaux = [r for r in underlay_mod.reseaux_de_fabric(underlay, fab)
|
||||
reseaux = [r for r in reseaux_de_fabric(underlay, fab)
|
||||
if r.get("nom") in noms and r.get("vlan") is not None]
|
||||
if not reseaux and repli:
|
||||
reseaux = [repli]
|
||||
|
|
@ -486,7 +517,7 @@ def switch_route(underlay: dict | None) -> bool:
|
|||
cas d'erreur — disparait d'elle-meme.
|
||||
"""
|
||||
r_nom = underlay_mod.routeur(underlay)
|
||||
for r in underlay_mod.reseaux_de_fabric(underlay, underlay_mod.fabric_du_routeur(underlay)):
|
||||
for r in reseaux_de_fabric(underlay, underlay_mod.fabric_du_routeur(underlay)):
|
||||
gw = r.get("passerelle")
|
||||
if not gw:
|
||||
continue
|
||||
|
|
@ -736,7 +767,7 @@ def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -
|
|||
# perdraient leur propre adresse de gestion. Un port terminal porte ce que son hote
|
||||
# declare ; un lien de fabric porte ce qui doit traverser.
|
||||
vlans_fabric = [str(r["vlan"])
|
||||
for r in underlay_mod.reseaux_de_fabric(
|
||||
for r in reseaux_de_fabric(
|
||||
underlay, underlay_mod.fabric_du_routeur(underlay))
|
||||
if r.get("vlan") is not None]
|
||||
# En SDN, les VLAN tenants n'existent pas sur le fil : le trunk ne porte que
|
||||
|
|
@ -753,7 +784,7 @@ def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -
|
|||
# Aucun hyperviseur declare : on ne peut rien deriver de leurs rattachements.
|
||||
# Emettre TOUT l'underlay est le repli sur, et il faut le dire — un devis muet
|
||||
# ferait croire qu'il n'y a pas de port a configurer, ce qui est faux.
|
||||
par_via_hyp = {"": [r for r in underlay_mod.reseaux_de_fabric(
|
||||
par_via_hyp = {"": [r for r in reseaux_de_fabric(
|
||||
underlay, underlay_mod.fabric_du_routeur(underlay)) if r.get("vlan") is not None]}
|
||||
out += ["! Aucun hote `role: hyperviseur` declare dans underlay.yml : ce port porte",
|
||||
"! TOUT l'underlay, faute de mieux. Les declarer rendrait ce bloc precis —",
|
||||
|
|
|
|||
Loading…
Reference in a new issue