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
|
# 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
|
## 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
|
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"):
|
if not r or not r.get("sous_reseau"):
|
||||||
return []
|
return []
|
||||||
net = ipaddress.ip_network(r["sous_reseau"], strict=False)
|
net = ipaddress.ip_network(r["sous_reseau"], strict=False)
|
||||||
out = [f"interface Vlan{r['vlan']}", f" description gestion {nom_switch}",
|
# LE SEGMENT DE GESTION PEUT NE PAS ETRE ETIQUETE (2026-08-25).
|
||||||
f" ip address {h['ip']} {net.netmask}", " no shutdown"]
|
#
|
||||||
|
# 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")
|
gw = r.get("passerelle")
|
||||||
if gw:
|
if gw:
|
||||||
porteur = next((x for x in underlay_mod.hotes(underlay)
|
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
|
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]:
|
def section_underlay(underlay: dict | None) -> list[str]:
|
||||||
"""VLANs + SVIs (si passerelle) de la fabric du routeur. Vide si aucun underlay.
|
"""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.
|
leurs VLAN sur ces switches serait faux, et les mettre dans leurs trunks aussi.
|
||||||
"""
|
"""
|
||||||
fabric = underlay_mod.fabric_du_routeur(underlay)
|
fabric = underlay_mod.fabric_du_routeur(underlay)
|
||||||
reseaux = underlay_mod.reseaux_de_fabric(underlay, fabric)
|
reseaux = reseaux_de_fabric(underlay, fabric)
|
||||||
if not reseaux:
|
if not reseaux:
|
||||||
return []
|
return []
|
||||||
out = [f"! ----- 0. Underlay — fabric '{fabric}' (cluster-global, hors tenant) -----"]
|
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):
|
for autre in underlay_mod.fabriques(underlay):
|
||||||
if autre == fabric:
|
if autre == fabric:
|
||||||
continue
|
continue
|
||||||
r_autres = underlay_mod.reseaux_de_fabric(underlay, autre)
|
r_autres = reseaux_de_fabric(underlay, autre)
|
||||||
out.append(f"! HORS PERIMETRE — fabric '{autre}' : "
|
out.append(f"! HORS PERIMETRE — fabric '{autre}' : "
|
||||||
+ ", ".join(f"{r['nom']} (VLAN {r['vlan']})" for r in r_autres))
|
+ ", ".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("! 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:
|
if not r_nom:
|
||||||
return []
|
return []
|
||||||
fabric = underlay_mod.fabric_du_routeur(underlay)
|
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)
|
if r.get("passerelle")), None)
|
||||||
# SOURCE UNIQUE : `switches_acces()` decide qui est un switch d'acces — reseau de
|
# 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
|
# 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
|
# 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
|
# 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.
|
# 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']}"]
|
out += [f"vlan {r['vlan']}", f" name {r['nom']}"]
|
||||||
if underlay_mod.routage_tenants(underlay) == "sdn":
|
if underlay_mod.routage_tenants(underlay) == "sdn":
|
||||||
out.append("! Aucun VLAN de tenant : ils vivent dans le SDN, pas sur le fil.")
|
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]] = {}
|
par_via: dict[str, dict[str, dict]] = {}
|
||||||
fab = underlay_mod.fabric_du_routeur(underlay)
|
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}
|
if r.get("vlan") is not None}
|
||||||
for h in underlay_mod.hotes(underlay):
|
for h in underlay_mod.hotes(underlay):
|
||||||
if h.get("role") != role or not h.get("reseau"):
|
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)
|
noms = {h.get("reseau") for h in underlay_mod.hotes(underlay)
|
||||||
if h.get("role") == role and h.get("reseau")}
|
if h.get("role") == role and h.get("reseau")}
|
||||||
fab = underlay_mod.fabric_du_routeur(underlay)
|
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 r.get("nom") in noms and r.get("vlan") is not None]
|
||||||
if not reseaux and repli:
|
if not reseaux and repli:
|
||||||
reseaux = [repli]
|
reseaux = [repli]
|
||||||
|
|
@ -486,7 +517,7 @@ def switch_route(underlay: dict | None) -> bool:
|
||||||
cas d'erreur — disparait d'elle-meme.
|
cas d'erreur — disparait d'elle-meme.
|
||||||
"""
|
"""
|
||||||
r_nom = underlay_mod.routeur(underlay)
|
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")
|
gw = r.get("passerelle")
|
||||||
if not gw:
|
if not gw:
|
||||||
continue
|
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
|
# perdraient leur propre adresse de gestion. Un port terminal porte ce que son hote
|
||||||
# declare ; un lien de fabric porte ce qui doit traverser.
|
# declare ; un lien de fabric porte ce qui doit traverser.
|
||||||
vlans_fabric = [str(r["vlan"])
|
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))
|
underlay, underlay_mod.fabric_du_routeur(underlay))
|
||||||
if r.get("vlan") is not None]
|
if r.get("vlan") is not None]
|
||||||
# En SDN, les VLAN tenants n'existent pas sur le fil : le trunk ne porte que
|
# 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.
|
# 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
|
# 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.
|
# 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]}
|
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",
|
out += ["! Aucun hote `role: hyperviseur` declare dans underlay.yml : ce port porte",
|
||||||
"! TOUT l'underlay, faute de mieux. Les declarer rendrait ce bloc precis —",
|
"! TOUT l'underlay, faute de mieux. Les declarer rendrait ce bloc precis —",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue