portee : les trois devis d'un site partagent enfin la meme regle
Le commit du 2026-08-14 nommait lui-meme ce qui restait : « meme hypothese ailleurs, non corrigee — devis_sdn et devis_reseau partent du meme decouvrir(). A traiter quand ils serviront sur un second site. » C'est fait AVANT, pas pendant la visite. Les trois devis equipent le MATERIEL d'un site : la frontiere (regles, routes), le commutateur (VLAN, SVI, routes) et le SDN de l'hyperviseur (zones, VNets). Un tenant d'ailleurs y ajoutait des objets que le materiel accepte, qui ne correspondent jamais a rien, et que rien ne signale. UNE SEULE FONCTION AU LIEU D'UN FILTRE RECOPIE TROIS FOIS : `devis_reseau.decouvrir_du_site()` = decouvrir() restreint par `underlay.tenants`, la doctrine ecrite une fois. Le filtre inline de devis_opnsense est retire au profit d'elle. `admin_tous_tenants()` la suit : le routeur d'un site n'a pas a savoir revenir vers le plan de gestion d'un tenant qu'il ne porte pas. EPROUVE dans les trois situations : underlay sans la cle -> les deux tenants, comme avant ; underlay du second site -> OPS-Technolibre seul ; nom declare qu'aucun dossier ne fournit -> ATTENTION et le reste est retenu ; filtre qui ne retient rien -> refus, code 1. SANS EFFET SUR LE SITE ACTUEL : l'underlay de Chezlepro ne declare pas `tenants`, et cle absente = toute la federation (verifie : decouvrir() et decouvrir_du_site() rendent la meme liste ici). ET LA CLE EST ENFIN DOCUMENTEE — c'etait le vrai trou. `underlay.tenants` existait depuis le 14 sans figurer ni dans underlay.yml.example ni dans l'annexe du runbook d'implantation : indecouvrable pour qui monte un second site. prouver 37 OK, 0 echec, 0 saute ; make test inchange. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
577c78e659
commit
3445ccb836
6 changed files with 124 additions and 24 deletions
52
CHANGELOG.md
52
CHANGELOG.md
|
|
@ -1,5 +1,57 @@
|
||||||
# CHANGELOG — Set-OPS
|
# CHANGELOG — Set-OPS
|
||||||
|
|
||||||
|
## 2026-08-18 — Les trois devis d'un site partagent enfin la même portée
|
||||||
|
|
||||||
|
Le 14 août, la frontière a appris qu'elle ne police que les tenants de **son** site. Le
|
||||||
|
commit le disait lui-même : *« même hypothèse ailleurs, non corrigée — `devis_sdn` et
|
||||||
|
`devis_reseau` partent du même `decouvrir()`. À traiter quand ils serviront sur un second
|
||||||
|
site. »* C'est fait avant, pas pendant.
|
||||||
|
|
||||||
|
Les trois devis équipent le **matériel d'un site** :
|
||||||
|
|
||||||
|
| devis | ce qu'il pose | ce qu'un tenant d'ailleurs y ajoutait |
|
||||||
|
|---|---|---|
|
||||||
|
| `devis_opnsense` | règles et routes de la frontière | des routes vers des sous-réseaux inexistants |
|
||||||
|
| `devis_reseau` | VLAN, SVI, routes du commutateur | des VLAN qu'aucune VM ne peuplera |
|
||||||
|
| `devis_sdn` | zones et VNets EVPN de l'hyperviseur | des zones sans machine |
|
||||||
|
|
||||||
|
Aucun de ces objets ne fait de mal visible : le matériel les accepte, ils ne
|
||||||
|
correspondent jamais à rien, et rien ne les signale. C'est la définition même du chèque
|
||||||
|
vert sur un périmètre vide — sauf qu'ici, il faut le lire à l'envers : une politique qui
|
||||||
|
a l'air complète et ne protège rien.
|
||||||
|
|
||||||
|
### Une seule fonction, au lieu d'un filtre recopié trois fois
|
||||||
|
|
||||||
|
`devis_reseau.decouvrir_du_site()` — `decouvrir()` restreint par `underlay.tenants`, avec
|
||||||
|
la doctrine écrite une fois pour les trois. Le filtre inline de `devis_opnsense` est
|
||||||
|
retiré au profit d'elle. `admin_tous_tenants()` la suit : le routeur d'un site n'a aucune
|
||||||
|
raison de savoir revenir vers le plan de gestion d'un tenant qu'il ne porte pas.
|
||||||
|
|
||||||
|
Éprouvé dans les trois situations qui comptent :
|
||||||
|
|
||||||
|
```
|
||||||
|
underlay sans la clé → ['OPS-Chezlepro', 'OPS-Technolibre'] (identique à avant)
|
||||||
|
underlay du second site → ['OPS-Technolibre']
|
||||||
|
un nom qu'aucun dossier ne fournit → ATTENTION, et le reste est retenu
|
||||||
|
le filtre ne retient rien → refus, code 1 (jamais un devis vide)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Sans effet sur le site actuel** : l'underlay de Chezlepro ne déclare pas `tenants`, et
|
||||||
|
clé absente = toute la fédération. `prouver` 37/37, `make test` inchangé.
|
||||||
|
|
||||||
|
### Et la clé est enfin documentée
|
||||||
|
|
||||||
|
C'était le vrai trou : `underlay.tenants` existait depuis le 14 et n'apparaissait **ni**
|
||||||
|
dans `underlay.yml.example` **ni** dans l'annexe du runbook d'implantation. Un exploitant
|
||||||
|
montant un second site ne pouvait pas la découvrir — il aurait posé la politique du
|
||||||
|
premier tenant chez le second, et le seul symptôme aurait été un silence.
|
||||||
|
|
||||||
|
> **Traiter la deuxième occurrence quand on nomme la première.** Le défaut de portée
|
||||||
|
> était écrit noir sur blanc dans le commit du 14, avec la liste des endroits où il
|
||||||
|
> restait. Quatre jours plus tard, le coût de le finir est d'une heure ; sur place, il
|
||||||
|
> aurait coûté une visite.
|
||||||
|
|
||||||
|
|
||||||
## 2026-08-18 — Le panneau d'intrants effaçait la mémoire écrite du dépôt
|
## 2026-08-18 — Le panneau d'intrants effaçait la mémoire écrite du dépôt
|
||||||
|
|
||||||
Un enregistrement du panneau « Intrants de base », à 13:48, a emporté **94 lignes de
|
Un enregistrement du panneau « Intrants de base », à 13:48, a emporté **94 lignes de
|
||||||
|
|
|
||||||
|
|
@ -208,6 +208,12 @@ underlay:
|
||||||
# la bande basse lui appartient. Sans elle, `make underlay` refuse le réseau de
|
# la bande basse lui appartient. Sans elle, `make underlay` refuse le réseau de
|
||||||
# gestion en le prenant pour celui d'un AUTRE site — message déroutant, cause triviale.
|
# gestion en le prenant pour celui d'un AUTRE site — message déroutant, cause triviale.
|
||||||
index: <index>
|
index: <index>
|
||||||
|
# LES TENANTS QUE CE SITE PORTE — noms de dossier, pas de fantaisie. Les trois devis
|
||||||
|
# d'équipement (frontière, commutateur, SDN) découvrent TOUTE la fédération : sans
|
||||||
|
# cette clé, le second site se voit proposer les règles, les VLAN et les zones du
|
||||||
|
# premier. Le matériel les accepte, aucune ne correspond jamais à un paquet, et rien
|
||||||
|
# ne le signale. Un site UNIQUE n'a rien à déclarer ; c'est le second qui se nomme.
|
||||||
|
tenants: [OPS-<tenant>]
|
||||||
routeur: <nom-du-commutateur> # racine du spanning-tree, pas un routeur
|
routeur: <nom-du-commutateur> # racine du spanning-tree, pas un routeur
|
||||||
# `stp` exige `routeur` : ne pas le déclarer tant qu'aucun commutateur ne l'est.
|
# `stp` exige `routeur` : ne pas le déclarer tant qu'aucun commutateur ne l'est.
|
||||||
routage_tenants: sdn # le routage inter-zone vit sur l'hyperviseur
|
routage_tenants: sdn # le routage inter-zone vit sur l'hyperviseur
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ from resoudre_flux import ( # noqa: E402
|
||||||
# `inventaire_de` / `admin_de` viennent de devis_reseau : les routes de retour du
|
# `inventaire_de` / `admin_de` viennent de devis_reseau : les routes de retour du
|
||||||
# switch et les regles de la frontiere doivent lire les MEMES tenants, sinon l'un
|
# switch et les regles de la frontiere doivent lire les MEMES tenants, sinon l'un
|
||||||
# certifie des routes que l'autre n'emet pas.
|
# certifie des routes que l'autre n'emet pas.
|
||||||
from devis_reseau import admin_de, decouvrir, inventaire_de, prefixe # noqa: E402
|
from devis_reseau import admin_de, decouvrir, decouvrir_du_site, inventaire_de, prefixe # noqa: E402
|
||||||
# Le lien de transit (et donc le prochain saut) vit dans l'underlay : il est PARTAGE
|
# Le lien de transit (et donc le prochain saut) vit dans l'underlay : il est PARTAGE
|
||||||
# par tous les tenants, il ne peut deriver d'aucun index. Source unique, pas un intrant.
|
# par tous les tenants, il ne peut deriver d'aucun index. Source unique, pas un intrant.
|
||||||
import underlay as underlay_mod # noqa: E402
|
import underlay as underlay_mod # noqa: E402
|
||||||
|
|
@ -792,28 +792,14 @@ def main(argv: list[str]) -> int:
|
||||||
ap.add_argument("--verifier", action="store_true", help="Valide le devis sans l'afficher.")
|
ap.add_argument("--verifier", action="store_true", help="Valide le devis sans l'afficher.")
|
||||||
args = ap.parse_args(argv)
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
tenants = decouvrir()
|
# UNE FRONTIERE NE POLICE QUE LES TENANTS DE SON SITE (2026-08-14). Le filtre est
|
||||||
|
# devenu la source unique des trois devis qui equipent un site — commutateur, SDN et
|
||||||
|
# frontiere : cf. `devis_reseau.decouvrir_du_site`.
|
||||||
|
tenants = decouvrir_du_site()
|
||||||
if not tenants:
|
if not tenants:
|
||||||
print("Aucune instance federee trouvee (../*/plan/nomenclature.yml).", file=sys.stderr)
|
print("Aucune instance federee trouvee (../*/plan/nomenclature.yml).", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
# UNE FRONTIERE NE POLICE QUE LES TENANTS DE SON SITE. `decouvrir()` rend toute la
|
|
||||||
# federation ; l'hebergeur declare dans SON underlay ceux qu'il porte reellement.
|
|
||||||
# Sans ce filtre, la frontiere du second site recevait les regles et les routes du
|
|
||||||
# premier — acceptees par le boitier, jamais correspondantes, jamais signalees.
|
|
||||||
portes = underlay_mod.tenants_du_site(underlay_mod.charger())
|
|
||||||
if portes is not None:
|
|
||||||
connus = {nom for nom, _, _ in tenants}
|
|
||||||
for nom in portes:
|
|
||||||
if nom not in connus:
|
|
||||||
print(f"ATTENTION : `tenants` de l'underlay nomme « {nom} », "
|
|
||||||
f"qu'aucun dossier frere ne fournit.", file=sys.stderr)
|
|
||||||
tenants = [t for t in tenants if t[0] in set(portes)]
|
|
||||||
if not tenants:
|
|
||||||
print("Aucun tenant heberge sur ce site : `underlay.tenants` n'en retient "
|
|
||||||
"aucun de connu.", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
|
|
||||||
devis = construire(tenants)
|
devis = construire(tenants)
|
||||||
|
|
||||||
if args.verifier:
|
if args.verifier:
|
||||||
|
|
|
||||||
|
|
@ -451,14 +451,17 @@ def admin_de(nom_instance: str) -> list[str]:
|
||||||
|
|
||||||
|
|
||||||
def admin_tous_tenants() -> list[str]:
|
def admin_tous_tenants() -> list[str]:
|
||||||
"""Union des reseaux d'administration de TOUS les tenants federes.
|
"""Union des reseaux d'administration des tenants heberges sur CE SITE.
|
||||||
|
|
||||||
Le switch routeur est partage : il doit savoir revenir vers chaque plan de gestion,
|
Le switch routeur est partage : il doit savoir revenir vers chaque plan de gestion,
|
||||||
pas seulement celui de l'instance active. Router n'est pas autoriser — le
|
pas seulement celui de l'instance active. Router n'est pas autoriser — le
|
||||||
cloisonnement se fait a la frontiere, par un alias distinct par tenant.
|
cloisonnement se fait a la frontiere, par un alias distinct par tenant.
|
||||||
SOURCE UNIQUE : `devis_opnsense` importe cette fonction plutot que d'en refaire une.
|
SOURCE UNIQUE : `devis_opnsense` importe cette fonction plutot que d'en refaire une.
|
||||||
|
|
||||||
|
Bornee au site depuis le 2026-08-18 : le routeur d'un site n'a aucune raison de
|
||||||
|
savoir revenir vers le plan de gestion d'un tenant qu'il ne porte pas.
|
||||||
"""
|
"""
|
||||||
return sorted({c for nom, _pfx, _n in decouvrir() for c in admin_de(nom)})
|
return sorted({c for nom, _pfx, _n in decouvrir_du_site() for c in admin_de(nom)})
|
||||||
|
|
||||||
|
|
||||||
def route_statique(reseau: str, saut: str, dialecte: str) -> str:
|
def route_statique(reseau: str, saut: str, dialecte: str) -> str:
|
||||||
|
|
@ -569,6 +572,44 @@ def decouvrir() -> list[tuple[str, str, dict]]:
|
||||||
return tenants
|
return tenants
|
||||||
|
|
||||||
|
|
||||||
|
def decouvrir_du_site() -> list[tuple[str, str, dict]]:
|
||||||
|
"""Les tenants que CE SITE heberge — `decouvrir()` restreint par `underlay.tenants`.
|
||||||
|
|
||||||
|
POURQUOI CE FILTRE (mesure du 2026-08-14, sur le second site). `decouvrir()` rend
|
||||||
|
TOUTE la federation : tout dossier frere portant une nomenclature avec un index.
|
||||||
|
C'etait juste tant qu'il n'y avait qu'un site — l'hebergeur unique portait bien tous
|
||||||
|
les tenants. Des le second, c'est faux : `make frontiere-plan` voulait poser sur la
|
||||||
|
frontiere de Technolibre les regles ET les routes de Chezlepro, dont six routes vers
|
||||||
|
des sous-reseaux qui n'existent pas la-bas. Le boitier les aurait acceptees, aucune
|
||||||
|
n'aurait jamais correspondu a un paquet, et rien ne l'aurait signale.
|
||||||
|
|
||||||
|
Le meme raisonnement vaut pour les DEUX autres devis qui equipent un site : le
|
||||||
|
commutateur ne porte que les VLAN et les SVI des tenants poses dessus, et le SDN de
|
||||||
|
l'hyperviseur ne cree que leurs zones. Un objet provisionne pour un tenant absent est
|
||||||
|
au mieux inutile, au pire un chemin ouvert que personne ne relit.
|
||||||
|
|
||||||
|
CLE ABSENTE = toute la federation : un site unique n'a rien a declarer, c'est le
|
||||||
|
second qui doit se nommer. Un nom declare qu'aucun dossier frere ne fournit est
|
||||||
|
signale, pas ignore. Un filtre qui ne retient RIEN est un refus, pas un devis vide.
|
||||||
|
"""
|
||||||
|
tenants = decouvrir()
|
||||||
|
if not tenants:
|
||||||
|
return tenants # « aucune instance federee » : au caller de le dire
|
||||||
|
portes = underlay_mod.tenants_du_site(underlay_mod.charger())
|
||||||
|
if portes is None:
|
||||||
|
return tenants
|
||||||
|
connus = {nom for nom, _pfx, _n in tenants}
|
||||||
|
for nom in portes:
|
||||||
|
if nom not in connus:
|
||||||
|
print(f"ATTENTION : `tenants` de l'underlay nomme « {nom} », "
|
||||||
|
f"qu'aucun dossier frere ne fournit.", file=sys.stderr)
|
||||||
|
retenus = [t for t in tenants if t[0] in set(portes)]
|
||||||
|
if not retenus:
|
||||||
|
raise SystemExit("Aucun tenant heberge sur ce site : `underlay.tenants` n'en "
|
||||||
|
"retient aucun de connu.")
|
||||||
|
return retenus
|
||||||
|
|
||||||
|
|
||||||
def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -> str:
|
def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -> str:
|
||||||
dialecte = dialecte_effectif(dialecte)
|
dialecte = dialecte_effectif(dialecte)
|
||||||
underlay = underlay_mod.charger()
|
underlay = underlay_mod.charger()
|
||||||
|
|
@ -756,7 +797,7 @@ def main() -> None:
|
||||||
ap.add_argument("--dialecte", choices=DIALECTES, default=None,
|
ap.add_argument("--dialecte", choices=DIALECTES, default=None,
|
||||||
help="CLI du commutateur (defaut : SETOPS_DIALECTE, sinon underlay.dialecte)")
|
help="CLI du commutateur (defaut : SETOPS_DIALECTE, sinon underlay.dialecte)")
|
||||||
args = ap.parse_args()
|
args = ap.parse_args()
|
||||||
tenants = decouvrir()
|
tenants = decouvrir_du_site() # le commutateur ne porte que les tenants de son site
|
||||||
if not tenants:
|
if not tenants:
|
||||||
print("Aucune instance 'ip-miroir' federee trouvee (../*/plan/nomenclature.yml).",
|
print("Aucune instance 'ip-miroir' federee trouvee (../*/plan/nomenclature.yml).",
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ RACINE = Path(__file__).resolve().parents[1]
|
||||||
sys.path.insert(0, str(RACINE / "scripts"))
|
sys.path.insert(0, str(RACINE / "scripts"))
|
||||||
|
|
||||||
from inventory_rules import passerelle_de, sous_reseau_de, supernet_de, vlan_de # noqa: E402
|
from inventory_rules import passerelle_de, sous_reseau_de, supernet_de, vlan_de # noqa: E402
|
||||||
from devis_reseau import decouvrir # noqa: E402
|
from devis_reseau import decouvrir_du_site # noqa: E402
|
||||||
import underlay as underlay_mod # noqa: E402
|
import underlay as underlay_mod # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -334,7 +334,8 @@ def main(argv: list[str]) -> int:
|
||||||
ap.add_argument("--verifier", action="store_true")
|
ap.add_argument("--verifier", action="store_true")
|
||||||
args = ap.parse_args(argv)
|
args = ap.parse_args(argv)
|
||||||
|
|
||||||
devis = construire(decouvrir())
|
# Le SDN d'un hyperviseur ne cree que les zones des tenants qu'il porte.
|
||||||
|
devis = construire(decouvrir_du_site())
|
||||||
if args.verifier:
|
if args.verifier:
|
||||||
zones = devis["zones"]
|
zones = devis["zones"]
|
||||||
if not zones:
|
if not zones:
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,20 @@
|
||||||
# Storage/Ceph restent en general L2 pur (pas de passerelle).
|
# Storage/Ceph restent en general L2 pur (pas de passerelle).
|
||||||
---
|
---
|
||||||
underlay:
|
underlay:
|
||||||
|
# LES TENANTS QUE CE SITE PORTE — noms de DOSSIER des instances (../OPS-<nom>).
|
||||||
|
#
|
||||||
|
# Les trois devis qui equipent un site (frontiere, commutateur, SDN) decouvrent TOUTE
|
||||||
|
# la federation : tout dossier frere portant une nomenclature avec un index. C'est
|
||||||
|
# juste tant qu'il n'y a qu'un site ; des le second, le devis du site B veut y poser
|
||||||
|
# les regles, les VLAN et les zones du tenant du site A. Le materiel les accepte,
|
||||||
|
# aucune ne correspond jamais a un paquet, et rien ne le signale (mesure du
|
||||||
|
# 2026-08-14 : trente objets de trop, dont six routes vers des reseaux inexistants).
|
||||||
|
#
|
||||||
|
# ABSENTE = toute la federation : un site UNIQUE n'a rien a declarer, c'est le second
|
||||||
|
# qui doit se nommer. Un nom qu'aucun dossier frere ne fournit est signale ; un filtre
|
||||||
|
# qui ne retient aucun tenant connu est un refus, pas un devis vide.
|
||||||
|
#tenants: [OPS-monorganisation]
|
||||||
|
|
||||||
# FABRICS PHYSIQUES. Chaque reseau appartient a une fabric (`principal` par defaut).
|
# FABRICS PHYSIQUES. Chaque reseau appartient a une fabric (`principal` par defaut).
|
||||||
# Deux fabrics ne partagent aucun cable : le devis d'une fabric ne declare ni ne
|
# Deux fabrics ne partagent aucun cable : le devis d'une fabric ne declare ni ne
|
||||||
# transporte les VLAN d'une autre, et leurs spanning-tree sont independants.
|
# transporte les VLAN d'une autre, et leurs spanning-tree sont independants.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue