diff --git a/CHANGELOG.md b/CHANGELOG.md index 2db18cc..bc164bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # CHANGELOG — Set-OPS +## 2026-08-18 — `make underlay` confronte les tenants déclarés aux dossiers réels + +Suite immédiate du filtre de portée : `underlay.tenants` nomme des **dossiers frères**. +Une faute de frappe y était invisible — le tenant disparaissait simplement des trois +devis du site, qui restaient « conformes » sur ce qu'il en restait. + +Sur un site à **un seul tenant** — le cas de la prochaine implantation — la faute de +frappe rend un devis **vide** : une frontière sans règle, un commutateur sans VLAN. Et +rien dans le mot « conforme » ne dirait qu'on vient de dessiner le vide. + +L'écart est entièrement lisible sans toucher au matériel : d'un côté une liste de noms, +de l'autre les dossiers présents. Il se dit donc à `make underlay` (D-75), pas au moment +où l'on pousse dans un boîtier. Quatre situations, quatre messages distincts : + +``` +dossier absent → aucun dossier frere de ce nom (attendu : …/OPS-Fantome) +dossier sans nomenclature → dossier present, mais sans plan/nomenclature.yml +nomenclature non fédérée → `index` absent, `categories` vide ou `federe: false` +plus rien ne correspond → les devis de ce site n'auraient rien a poser +``` + +### Ce qu'un gabarit ne doit surtout pas subir + +Un **modèle** décrit du matériel, pas un site déployé : il ne peut nommer aucun tenant +réel. Sans garde, tout modèle portant un exemple de `tenants` échouerait chez quiconque +n'a pas ce dossier — et P17 (« tous les modèles valident ») deviendrait rouge sur la +machine du voisin. La distinction existait déjà dans le code : `modeles.py` passe des +repères de tenants **explicites**, ce qui dit « gabarit » ; le site, lui, les laisse +dériver. La vérification ne s'applique qu'au second cas. + +Trois tests ajoutés à `test_adressage_derive.py` — le nom introuvable, la clé absente, et +le gabarit épargné — avec un nom volontairement absurde pour qu'aucun test ne dépende +des dossiers de la machine qui l'exécute. `make test` 15 + 9 ; `prouver` 37/37. + + ## 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 diff --git a/scripts/tests/test_adressage_derive.py b/scripts/tests/test_adressage_derive.py index 25031b7..f09e0ba 100644 --- a/scripts/tests/test_adressage_derive.py +++ b/scripts/tests/test_adressage_derive.py @@ -118,6 +118,37 @@ def test_le_vlan_reste_sous_le_plafond_802_1q(): assert vlan_de(INDEX_MAX, 9) < 4094 +def test_un_tenant_declare_introuvable_est_refuse_statiquement(): + """`tenants` nomme des dossiers freres : une faute de frappe doit se voir ICI. + + Sinon elle ne se voit nulle part — le tenant disparait des trois devis du site, qui + restent « conformes » sur ce qu'il en reste. Le nom est volontairement absurde : le + test ne doit dependre d'aucun dossier reel de la machine qui l'execute. + """ + erreurs = u._erreurs_tenants_declares({"tenants": ["OPS-NExistePas-9f2c"]}) + assert any("aucun dossier frere" in e for e in erreurs), erreurs + assert any("aucun nom declare ne correspond" in e for e in erreurs), erreurs + + +def test_sans_la_clef_tenants_rien_n_est_verifie(): + """Clef absente = toute la federation : un site unique n'a rien a declarer.""" + assert u._erreurs_tenants_declares({"reseaux": []}) == [] + assert u._erreurs_tenants_declares(None) == [] + + +def test_un_gabarit_n_est_pas_confronte_aux_dossiers_freres(): + """Un modele decrit du MATERIEL : il ne peut pas nommer de tenants reels. + + La distinction tient a l'appelant — `modeles.py` passe des reperes de tenants + explicites, ce qui dit « gabarit ». Sans cette garde, tout modele portant un exemple + de `tenants` echouerait chez quiconque n'a pas ce dossier. + """ + gabarit = {"tenants": ["OPS-NExistePas-9f2c"], + "reseaux": [{"nom": "g", "vlan": 10, "sous_reseau": "10.11.0.0/24"}]} + erreurs = u.valider(gabarit, tenants_vlans=set(), tenants_supernets=SUPERNETS) + assert not any("tenants:" in e for e in erreurs), erreurs + + if __name__ == "__main__": n = 0 for nom, fn in sorted(globals().items()): diff --git a/scripts/underlay.py b/scripts/underlay.py index cdcc42b..3bf52e6 100644 --- a/scripts/underlay.py +++ b/scripts/underlay.py @@ -338,6 +338,44 @@ def _tenants_reperes() -> tuple[set[int], list[str]]: return vlans, supernets +def _erreurs_tenants_declares(underlay: dict | None) -> list[str]: + """`tenants` ne nomme que des tenants que la decouverte trouve REELLEMENT. + + POURQUOI STATIQUEMENT (D-75). Un nom mal orthographie dans `tenants` ne se voit pas : + le tenant disparait simplement des trois devis du site, qui restent « conformes » sur + ce qu'il en reste. Sur un site a un seul tenant, la faute de frappe rend un devis VIDE + — une frontiere sans regle, un commutateur sans VLAN — et rien dans le mot + « conforme » ne dit qu'on vient de dessiner le vide. + + L'ecart est entierement lisible sans toucher au materiel : d'un cote une liste de + noms, de l'autre les dossiers freres. On le dit donc a `make underlay`, pas au + moment ou l'on pousse dans un boitier. + """ + portes = tenants_du_site(underlay) + if portes is None: + return [] # clef absente = toute la federation : rien a verifier + import devis_reseau # import paresseux : meme cycle que _tenants_reperes + connus = {nom for nom, _pfx, _n in devis_reseau.decouvrir()} + erreurs: list[str] = [] + for nom in portes: + if nom in connus: + continue + dossier = RACINE.parent / nom + if not dossier.is_dir(): + erreurs.append(f"tenants: « {nom} » — aucun dossier frere de ce nom " + f"(attendu : {dossier})") + elif not (dossier / "plan" / "nomenclature.yml").is_file(): + erreurs.append(f"tenants: « {nom} » — dossier present, mais sans " + f"plan/nomenclature.yml : ce n'est pas une instance") + else: + erreurs.append(f"tenants: « {nom} » — nomenclature presente mais non federee " + f"(`index` absent, `categories` vide ou `federe: false`)") + if not (set(portes) & connus): + erreurs.append("tenants: aucun nom declare ne correspond a un tenant federe — " + "les devis de ce site n'auraient rien a poser") + return erreurs + + def valider(underlay: dict | None, tenants_vlans: set[int] | None = None, tenants_supernets: list[str] | None = None, @@ -346,6 +384,10 @@ def valider(underlay: dict | None, surtout l'absence de collision avec la plage tenant (VLAN et sous-reseaux).""" if underlay is None: return [] # absent = rien a valider (le devis omettra la section) + # Les tenants reperes fournis par l'appelant = validation d'un GABARIT (modeles.py) : + # un modele decrit du materiel, pas un site deploye, et ne peut pas nommer de tenants + # reels. Seule la validation d'un SITE confronte `tenants` aux dossiers freres. + mode_site = tenants_vlans is None or tenants_supernets is None if tenants_vlans is None or tenants_supernets is None: tenants_vlans, tenants_supernets = _tenants_reperes() t_supernets = [ipaddress.ip_network(s, strict=False) for s in tenants_supernets] @@ -354,6 +396,8 @@ def valider(underlay: dict | None, idx_site = index_site(underlay) erreurs: list[str] = [] + if mode_site: + erreurs += _erreurs_tenants_declares(underlay) vus_vlan: dict[int, str] = {} vus_sous: list[tuple[ipaddress.IPv4Network, str]] = [] noms_reseaux: set[str] = set() diff --git a/underlay.yml.example b/underlay.yml.example index 7fc0399..2a6c119 100644 --- a/underlay.yml.example +++ b/underlay.yml.example @@ -27,8 +27,9 @@ underlay: # 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. + # qui doit se nommer. Un nom qu'aucun dossier frere ne fournit est REFUSE par + # `make underlay` (ecart statique, D-75) ; 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).