From 93a92aae86613fdd7fe029b7c91585bb80ed13e7 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Wed, 12 Aug 2026 17:21:09 -0400 Subject: [PATCH] =?UTF-8?q?P03=20:=20la=20preuve=20regarde=20TOUTES=20les?= =?UTF-8?q?=20instances=20=E2=80=94=20et=20trouve=20une=20collision=20d'em?= =?UTF-8?q?blee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P03 ne verifiait la fraicheur de l'inventaire que pour l'instance ACTIVE. Or la frontiere nord/sud est PARTAGEE : ses alias d'hotes sont construits depuis le hosts.yml de CHAQUE tenant. Un tenant qu'on ne regarde pas — parce qu'il n'a aucune VM, precisement — impose donc ses adresses au pare-feu de tout le monde. Elle boucle desormais sur les instances decouvertes, chacune verifiee via SETOPS_INSTANCE (que instancier.py honore deja). AU PREMIER PASSAGE, elle a trouve une troisieme instance perimee et une collision franche : OPS-Chezlepro-lab (index 1) applique : 10.11.18.21 <- ancienne derivation OPS-Technolibre (index 11) derive : 10.11.x.x <- nouvelle derivation Le lab occupait EXACTEMENT la plage desormais attribuee a Technolibre. P21 garde les index ; rien ne gardait les inventaires APPLIQUES. La collision serait apparue le jour ou les deux auraient tourne ensemble. Les trois instances sont alignees : 10.1 (lab), 10.11 (Technolibre), 10.17 (Chezlepro). Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 25 ++++++++++++++ docs/audit/preuve-2026-08-12.md | 2 +- scripts/prouver.py | 58 ++++++++++++++++++++++++++++++--- 3 files changed, 80 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 88b1c81..fc132ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,30 @@ # CHANGELOG — Set-OPS +## 2026-08-12 — P03 regarde TOUTES les instances, et trouve une collision au premier essai + +P03 ne vérifiait la fraîcheur de l'inventaire que pour l'instance **active** — laissant un +tenant qu'on ne regarde pas imposer ses adresses à la frontière partagée. Elle boucle +désormais sur les instances **découvertes**, chacune vérifiée avec `SETOPS_INSTANCE`. + +**Au premier passage, elle a trouvé une troisième instance périmée** — et une collision +franche que personne n'avait vue : + +``` +OPS-Chezlepro-lab (index 1) applique : 10.11.18.21 ← ancienne derivation (10+1) +OPS-Technolibre (index 11) derive : 10.11.x.x ← nouvelle derivation +``` + +Le lab occupait **exactement la plage désormais attribuée à Technolibre**. Sans cette +preuve, la collision serait apparue le jour où les deux auraient tourné ensemble — c'est +`P21` qui garde les index, rien ne gardait les inventaires *appliqués*. + +Les trois instances sont maintenant alignées : `10.1` (lab), `10.11` (Technolibre), +`10.17` (Chezlepro). + +> **Ce que la preuve ne fait pas** : vérifier que le boîtier porte ce que le devis dit — +> c'est `make frontiere-plan`. Elle garde l'**intrant** de ce devis, pas sa sortie. Les +> deux sont nécessaires, et c'est l'intrant qui manquait. + ## 2026-08-12 — Un tenant périmé injecte ses vieilles adresses dans le pare-feu partagé Après le renumérotage de Chezlepro, la frontière portait encore **17 adresses `10.21.x`** diff --git a/docs/audit/preuve-2026-08-12.md b/docs/audit/preuve-2026-08-12.md index 527e052..c17e9f7 100644 --- a/docs/audit/preuve-2026-08-12.md +++ b/docs/audit/preuve-2026-08-12.md @@ -15,7 +15,7 @@ |---|---|---|---|---| | P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  | | P02 | Tests unitaires (inventory_host) | — | ✅ OK | >>> le verrou tient : aucune VM n'aurait ete touchee | -| P03 | Diff-vide du plan (inventaire genere) | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible. | +| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 3 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Technolibre, OPS-Chezlepro : plan et inventaire applique coincident. | | P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | | | P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | | P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | diff --git a/scripts/prouver.py b/scripts/prouver.py index 961a955..644e24e 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -472,6 +472,58 @@ def preuve_base_par_consommateur() -> tuple[bool, str]: f"({len(bases)} entree(s) au registre).") +def preuve_diff_vide_toutes_instances() -> tuple[bool, str]: + """CHAQUE instance decouverte a un inventaire applique conforme a son plan. + + Pourquoi toutes, et pas seulement l'active. La frontiere nord/sud est PARTAGEE : + ses alias d'hotes (`SETOPS__SERVEUR_*`) sont construits depuis le `hosts.yml` de + CHAQUE tenant, pas depuis la formule. Le 2026-08-12, l'inventaire de Technolibre est + reste a `10.21.x` apres un changement de derivation — et la frontiere a garde ces + adresses pendant que ses alias RESEAU, eux, passaient a `10.11`. Le devis annoncait + « conforme » : il lisait fidelement un intrant perime. + + Un tenant qu'on ne regarde pas — parce qu'il n'a aucune VM, precisement — impose donc + ses adresses au pare-feu de tout le monde. Verifier la seule instance active laissait + cet angle entier. + + CE QU'ELLE NE TESTE PAS : que le boitier porte ce que le devis dit — c'est + `make frontiere-plan`. Ici on garde l'INTRANT de ce devis, pas sa sortie. + """ + sys.path.insert(0, str(RACINE / "scripts")) + import instances as mod_instances + + trouvees = mod_instances.decouvrir() + if not trouvees: + return True, "Aucune instance decouverte : rien a comparer." + + ecarts: list[str] = [] + verifiees: list[str] = [] + for i in trouvees: + chemin = RACINE.parent / i["nom"] + if not (chemin / "inventories").is_dir(): + continue + env = {**os.environ, "SETOPS_INSTANCE": str(chemin)} + gen = subprocess.run([sys.executable, "scripts/instancier.py", "generer"], + cwd=RACINE, env=env, capture_output=True, text=True, timeout=300) + if gen.returncode != 0: + ecarts.append(f"{i['nom']} : generation impossible") + continue + cmp_ = subprocess.run([sys.executable, "scripts/instancier.py", "comparer", "--strict"], + cwd=RACINE, env=env, capture_output=True, text=True, timeout=300) + verifiees.append(i["nom"]) + if cmp_.returncode != 0: + lignes = [l.strip() for l in cmp_.stdout.splitlines() if l.strip()] + resume = next((l for l in lignes if "ecart" in l), "diff non vide") + ecarts.append(f"{i['nom']} : {resume}") + + if ecarts: + return False, (f"{len(ecarts)}/{len(verifiees)} instance(s) dont l'inventaire " + f"applique ne suit plus le plan : " + " | ".join(ecarts) + + " — basculer avec `make instancier-appliquer` sur chacune.") + return True, (f"{len(verifiees)} instance(s) verifiee(s) — " + f"{', '.join(verifiees)} : plan et inventaire applique coincident.") + + def preuve_etat_sauvegarde() -> tuple[bool, str]: """Tout hote qui detient de l'etat non regenerable porte `client_backup`. @@ -700,10 +752,8 @@ PREUVES: list[dict] = [ {"id": "P02", "titre": "Tests unitaires (inventory_host)", "refs": [], "cmds": [[sys.executable, "scripts/tests/test_inventory_host.py"], [sys.executable, "scripts/tests/test_raser.py"]]}, - {"id": "P03", "titre": "Diff-vide du plan (inventaire genere)", "refs": ["AFF-001", "AFF-004", "AFF-030", "AFF-031", "AFF-032"], - "refs": ["AFF-001", "AFF-004", "AFF-030", "AFF-031", "AFF-032"], - "cmds": [[sys.executable, "scripts/instancier.py", "generer"], - [sys.executable, "scripts/instancier.py", "comparer", "--strict"]]}, + {"id": "P03", "titre": "Diff-vide du plan — TOUTES les instances", "refs": ["AFF-001", "AFF-004", "AFF-030", "AFF-031", "AFF-032"], + "func": preuve_diff_vide_toutes_instances}, {"id": "P04", "titre": "Groupes <-> playbooks homonymes", "refs": ["AFF-008"], "cmds": [[sys.executable, "scripts/inventory_host.py", "--inventaire", INV, "verifier-playbooks", "--dossier-playbooks", GROUPES]]},