From 4462c13b3da600401b434e317ee48c30bf0ad3a5 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Tue, 18 Aug 2026 14:35:01 -0400 Subject: [PATCH] P03 : la preuve comparait chaque instance a l'inventaire d'UNE SEULE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trouve en validant une mise a jour du CHANGELOG. Deux invocations de la meme preuve, deux verdicts : `make prouver` -> NON CONFORME (« lab : 17 hotes avec ecart »), `python3 scripts/prouver.py` -> CONFORME 37/37. Le lab n'avait aucun ecart. DEUX VARIABLES DESIGNENT LA CIBLE, ET LA SECONDE GAGNE. Le Makefile exporte SETOPS_INVENTAIRE (ligne 13), derive de l'instance ACTIVE ; instancier.py:68 lui fait FORCER la cible par-dessus SETOPS_INSTANCE. P03 (prouver.py:505) ne redirigeait que SETOPS_INSTANCE : elle generait le plan de CHAQUE instance federee et le comparait a l'inventaire applique de la SEULE instance active. LE ROUGE N'ETAIT PAS LE PROBLEME, LE VERT L'ETAIT. Sous `make`, l'inventaire applique de lab et de Technolibre n'etait JAMAIS lu — l'angle meme pour lequel P03 a ete ecrite le 2026-08-12 (un tenant qu'on ne regarde pas imposant ses vieilles adresses au pare-feu partage). La preuve etait aveugle a son propre cas, par l'invocation documentee. Les rapports du 13 et du 14 sortent de cette invocation-la. Signature visible sans lire le code : les hosts.genere.yml de lab et de Technolibre ne bougeaient pas. CORRECTIF, cinq sites : env.pop("SETOPS_INVENTAIRE") partout ou l'on redirige SETOPS_INSTANCE — P03 et P15, plus les trois applicateurs (opnsense, proxmox_fw, sdn) qui pointent vers l'HEBERGEUR. Ces trois sont sans effet tant qu'hebergeur et tenant actif coincident, c'est-a-dire jusqu'au second site. Le geste existait deja (modeles.py:96). GARDE, pour que la classe cesse d'etre silencieuse : inventory_rules.inventaire_force() REFUSE une cible hors de l'instance visee, en nommant les deux valeurs. Eprouvee dans les deux sens (contradiction -> code 1 ; cible legitime dans l'instance -> passe). Branchee sur les quatre resolutions de _inventaire (instancier, serveurs, applications, config_proxmox). Le GUI garde la sienne : il ne redirige jamais SETOPS_INSTANCE pour un fils et resout par symlink a chaque requete. make prouver : 37 OK, 0 echec, 0 saute — et les hosts.genere.yml des TROIS instances portent l'horodatage du passage. make test 0. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 67 ++++++++++++++++++++++++++++++ docs/audit/preuve-2026-08-18.md | 73 +++++++++++++++++++++++++++++++++ scripts/applications.py | 5 ++- scripts/appliquer_opnsense.py | 8 +++- scripts/appliquer_proxmox_fw.py | 7 +++- scripts/appliquer_sdn.py | 7 +++- scripts/config_proxmox.py | 6 ++- scripts/instancier.py | 7 ++-- scripts/inventory_rules.py | 36 ++++++++++++++++ scripts/prouver.py | 11 +++++ scripts/serveurs.py | 5 ++- 11 files changed, 218 insertions(+), 14 deletions(-) create mode 100644 docs/audit/preuve-2026-08-18.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b0f86ce..98f5b4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,72 @@ # CHANGELOG — Set-OPS +## 2026-08-18 — P03 mesurait toutes les instances contre l'inventaire d'**une seule** + +Trouvé en validant une simple mise à jour du CHANGELOG. Deux invocations de la même +preuve, deux verdicts : + +``` +make prouver NON CONFORME — « lab : 17 hôtes avec écart » +python3 scripts/prouver.py CONFORME 37/37 +``` + +Le lab n'avait aucun écart : `SETOPS_INSTANCE=…lab instancier comparer --strict` dit +**DIFF VIDE**. C'est l'instrument qui mesurait ailleurs. + +### Deux variables désignent la cible, et c'est la seconde qui gagne + +``` +Makefile:13 export SETOPS_INVENTAIRE → instance/inventories/principal/hosts.yml +instancier.py:68 SETOPS_INVENTAIRE FORCE la cible, par-dessus SETOPS_INSTANCE +prouver.py:505 env = {**os.environ, "SETOPS_INSTANCE": str(chemin)} ← rien de retiré +``` + +P03 générait donc le plan de **chaque** instance fédérée et le comparait à l'inventaire +appliqué de la **seule** instance active. D'où un rouge sur un lab sain. + +### Le rouge n'était pas le problème — le vert l'était + +Sous `make`, l'inventaire appliqué de lab et de Technolibre **n'était jamais lu**. Or P03 +a été écrite le 2026-08-12 pour exactement cet angle : un tenant qu'on ne regarde pas — +parce qu'il n'a aucune VM, précisément — imposant ses vieilles adresses au pare-feu +partagé. **La preuve était aveugle au cas pour lequel elle existe**, quand on l'invoque de +la façon documentée. Les rapports du 13 et du 14 sortent de cette invocation-là. + +La signature était visible sans lire une ligne de code : sous `make prouver`, les +`hosts.genere.yml` de lab et de Technolibre **ne bougeaient pas** — tout était écrit dans +le répertoire de Chezlepro. + +### Corrigé aux cinq sites, et rendu bruyant + +`env.pop("SETOPS_INVENTAIRE", None)` partout où l'on redirige `SETOPS_INSTANCE` : P03 et +P15 (`prouver.py`), et les trois applicateurs `appliquer_opnsense` / `appliquer_proxmox_fw` +/ `appliquer_sdn`, qui pointent `SETOPS_INSTANCE` vers l'**hébergeur**. Ces trois-là sont +sans effet tant qu'hébergeur et tenant actif coïncident — c'est-à-dire **jusqu'au second +site**. Le geste correct existait déjà dans le dépôt (`modeles.py:96`) ; il n'avait +simplement jamais été repris. + +Et pour que la classe cesse d'être silencieuse, `inventory_rules.inventaire_force()` +**refuse** une cible hors de l'instance visée, en nommant les deux valeurs : + +``` +REFUS : SETOPS_INVENTAIRE designe un inventaire HORS de l'instance demandee. + SETOPS_INSTANCE …/OPS-Chezlepro-lab + SETOPS_INVENTAIRE …/OPS-Chezlepro/inventories/principal/hosts.yml +``` + +Éprouvée dans les deux sens : la contradiction sort en code 1, une cible légitime **dans** +l'instance passe. Branchée sur les quatre résolutions de `_inventaire` (`instancier`, +`serveurs`, `applications`, `config_proxmox`). Le GUI garde la sienne : il ne redirige +jamais `SETOPS_INSTANCE` pour un fils, et sa résolution suit le symlink à chaque requête. + +**`make prouver` : 37 OK, 0 échec, 0 sauté** — et cette fois les `hosts.genere.yml` des +trois instances portent l'horodatage du passage, preuve que chacune a été lue chez elle. + +> **Vérifier d'où l'instrument mesure.** Le dépôt porte déjà la règle ; c'est ici la +> quatrième fois qu'elle paye. Une preuve qui change de verdict selon qu'on l'appelle par +> `make` ou à la main ne mesurait pas ce qu'elle annonçait dans au moins un des deux cas. + + ## 2026-08-14 — Une frontière ne police que les tenants de **son** site Premier `make frontiere-plan` sur le second site. Le devis voulait poser sur la frontière diff --git a/docs/audit/preuve-2026-08-18.md b/docs/audit/preuve-2026-08-18.md new file mode 100644 index 0000000..572734e --- /dev/null +++ b/docs/audit/preuve-2026-08-18.md @@ -0,0 +1,73 @@ +# Preuve de conformite — Set-OPS — 2026-08-18 + +> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer +> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ; +> aucune validation n'est reimplementee ici. Voir le mode d'emploi : +> [`docs/audit/README.md`](README.md), et le registre trace : +> [`docs/audit/affirmations.md`](affirmations.md). + +- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml` +- **Verdict** : ✅ CONFORME (37 OK · 0 echec · 0 saute) + +## Preuves + +| # | Preuve | Affirmations | Statut | Detail | +|---|---|---|---|---| +| 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 — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 3 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : 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. | +| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 30 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 29 rôles, 77 flux, schéma + matrice OK. | +| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | +| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | playbook: playbooks/proxmox/cloner_vm_debian.yml | +| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | +| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. | +| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. | +| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. | +| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 31 groupes (inventaire dechiffre et parse). | +| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. | +| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 26 secret(s) exige(s), tous presents. Voute reelle : 29 cle(s), aucun manque. | +| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. | +| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. | +| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. | +| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (20 sections). | +| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 6 reseau(x), aucune collision avec la plage tenant. | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 35 regles, 12 routes, admin=10.0.0.0/24,10.17.0.0/24,192.168.254.2/32,192.168.255.2/32. | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 42 groupe(s), 68 regle(s). | +| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 4 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). | +| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 0 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | +| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. | +| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | +| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 45 scripts expliques et atteignables, 93 cibles make documentees, 54 roles avec README. | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 35 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). | +| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). | +| P34 | Chaque document declare son lecteur | — | ✅ OK | 40 document(s) declarent leur lecteur (18 genere(s) exempte(s)). | +| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | +| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | +| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (OPS-Chezlepro) : noeud, stockage, pont — tous offerts. | + +## Couverture des affirmations ✅ du registre + +Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus. +Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005 +`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite +d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ; +elles restent hors du harnais recurrent (rien d'executable a rejouer). + +## Declarations d'intention (⚪ invérifiables localement — assumees) + +Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees** +comme declarations d'intention, non comme preuves : + +- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement + contre une flotte vivante. +- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles. +- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee. +- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume. + +_Rapport genere le 2026-08-18._ diff --git a/scripts/applications.py b/scripts/applications.py index 27b33ff..d2b3008 100644 --- a/scripts/applications.py +++ b/scripts/applications.py @@ -19,6 +19,7 @@ from inventory_rules import ( charger_bases_donnees, charger_domaines, charger_serveurs, + inventaire_force, valider_applications, ) @@ -29,9 +30,9 @@ FICHIER_BASES = INSTANCE / "plan/bases-donnees.yml" FICHIER_DOMAINES = INSTANCE / "plan/domaines.yml" FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml" def _inventaire(instance: Path, *noms: str) -> Path: - forced = os.environ.get("SETOPS_INVENTAIRE") + forced = inventaire_force(instance) # refuse une cible hors de `instance` if forced: - return Path(forced) + return forced for nom in noms: p = instance / "inventories" / nom / "hosts.yml" if p.exists(): diff --git a/scripts/appliquer_opnsense.py b/scripts/appliquer_opnsense.py index b8c9b6f..54bbd38 100644 --- a/scripts/appliquer_opnsense.py +++ b/scripts/appliquer_opnsense.py @@ -376,9 +376,15 @@ def main() -> int: api = Frontiere(url, v["vault_opnsense_api_key"], v["vault_opnsense_api_secret"], bool(intr.get("opnsense_api_verifier_certs"))) + # SETOPS_INSTANCE designe ici l'HEBERGEUR, pas le tenant actif. SETOPS_INVENTAIRE, + # exporte par le Makefile, gagnerait sur lui (instancier.py) : le devis d'un site + # lirait l'inventaire d'un tenant d'ailleurs. Sans effet tant qu'hebergeur et tenant + # actif coincident — c'est-a-dire jusqu'au second site (mesure du 2026-08-18). + env_heb = {**os.environ, "SETOPS_INSTANCE": str(base)} + env_heb.pop("SETOPS_INVENTAIRE", None) sortie = subprocess.run([sys.executable, str(RACINE / "scripts" / "devis_opnsense.py"), "--json"], cwd=RACINE, capture_output=True, text=True, - env={**os.environ, "SETOPS_INSTANCE": str(base)}) + env=env_heb) if sortie.returncode != 0: raise SystemExit("Le devis ne se genere pas :\n" + sortie.stderr.strip()[:400]) devis = json.loads(sortie.stdout) diff --git a/scripts/appliquer_proxmox_fw.py b/scripts/appliquer_proxmox_fw.py index 314e045..0fea63f 100644 --- a/scripts/appliquer_proxmox_fw.py +++ b/scripts/appliquer_proxmox_fw.py @@ -260,9 +260,12 @@ def appliquer(api: Cluster, p: dict) -> int: def main() -> int: api, base = Cluster.depuis_hebergeur() + # `base` est l'HEBERGEUR : SETOPS_INVENTAIRE (exporte par le Makefile) gagnerait sur + # SETOPS_INSTANCE et ferait lire l'inventaire du tenant actif. Cf. prouver.py P03. + env_heb = {**os.environ, "SETOPS_INSTANCE": str(base)} + env_heb.pop("SETOPS_INVENTAIRE", None) r = subprocess.run([sys.executable, str(RACINE / "scripts" / "devis_proxmox_fw.py"), "--json"], - cwd=RACINE, capture_output=True, text=True, - env={**os.environ, "SETOPS_INSTANCE": str(base)}) + cwd=RACINE, capture_output=True, text=True, env=env_heb) if r.returncode != 0: raise SystemExit("Le devis ne se genere pas :\n" + r.stderr.strip()[:400]) devis = json.loads(r.stdout) diff --git a/scripts/appliquer_sdn.py b/scripts/appliquer_sdn.py index 96143a2..521add7 100644 --- a/scripts/appliquer_sdn.py +++ b/scripts/appliquer_sdn.py @@ -253,9 +253,12 @@ def appliquer(api: Cluster, p: dict, devis: dict) -> int: def main() -> int: api, base = Cluster.depuis_hebergeur() + # `base` est l'HEBERGEUR : SETOPS_INVENTAIRE (exporte par le Makefile) gagnerait sur + # SETOPS_INSTANCE et ferait lire l'inventaire du tenant actif. Cf. prouver.py P03. + env_heb = {**os.environ, "SETOPS_INSTANCE": str(base)} + env_heb.pop("SETOPS_INVENTAIRE", None) r = subprocess.run([sys.executable, str(RACINE / "scripts" / "devis_sdn.py"), "--json"], - cwd=RACINE, capture_output=True, text=True, - env={**os.environ, "SETOPS_INSTANCE": str(base)}) + cwd=RACINE, capture_output=True, text=True, env=env_heb) if r.returncode != 0: raise SystemExit("Le devis ne se genere pas :\n" + r.stderr.strip()[:400]) devis = json.loads(r.stdout) diff --git a/scripts/config_proxmox.py b/scripts/config_proxmox.py index 84b41a6..ae92112 100644 --- a/scripts/config_proxmox.py +++ b/scripts/config_proxmox.py @@ -11,6 +11,8 @@ import sys import yaml +from inventory_rules import inventaire_force + RACINE = Path(__file__).resolve().parents[1] INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance")) @@ -18,9 +20,9 @@ INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance")) def _inventaire_dir(instance: Path, *noms: str) -> Path: """Répertoire de l'inventaire modèle (porte la config Proxmox / le clonage).""" - forced = os.environ.get("SETOPS_INVENTAIRE") + forced = inventaire_force(instance) # refuse une cible hors de `instance` if forced: - return Path(forced).parent + return forced.parent for nom in noms: d = instance / "inventories" / nom if (d / "hosts.yml").exists(): diff --git a/scripts/instancier.py b/scripts/instancier.py index e12f585..d7cad88 100644 --- a/scripts/instancier.py +++ b/scripts/instancier.py @@ -40,6 +40,7 @@ from inventory_rules import ( fonction_seq, integrations_de, integrations_universelles, + inventaire_force, liens_acceptes, supernet_de, ) @@ -64,9 +65,9 @@ def _inventaire(instance: Path, *noms: str) -> Path: # Inventaire unique de l'instance (rétro-compat : principal > production). -# SETOPS_INVENTAIRE force la cible. -_force = os.environ.get("SETOPS_INVENTAIRE") -INVENTAIRE = Path(_force) if _force else _inventaire(INSTANCE, "principal", "production") +# SETOPS_INVENTAIRE force la cible — mais seulement A L'INTERIEUR de l'instance visée : +# `inventaire_force` refuse une cible qui désigne une AUTRE instance (2026-08-18, P03). +INVENTAIRE = inventaire_force(INSTANCE) or _inventaire(INSTANCE, "principal", "production") GENERE = INVENTAIRE.with_name("hosts.genere.yml") FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml" FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml" diff --git a/scripts/inventory_rules.py b/scripts/inventory_rules.py index da2f640..4786bda 100644 --- a/scripts/inventory_rules.py +++ b/scripts/inventory_rules.py @@ -3,6 +3,7 @@ from __future__ import annotations +import os import re from pathlib import Path @@ -17,6 +18,41 @@ GROUPES_ETAT_HOTE = {GROUPE_HOTES_ACTIFS, GROUPE_HOTES_PLANIFIES} RACINE_DEPOT = Path(__file__).resolve().parents[1] +def inventaire_force(instance: Path) -> Path | None: + """Inventaire impose par SETOPS_INVENTAIRE, ou None. REFUSE s'il vise AILLEURS. + + POURQUOI CETTE GARDE (mesure du 2026-08-18). Deux variables designent la cible, et + la seconde gagne : `SETOPS_INSTANCE` dit QUELLE instance, `SETOPS_INVENTAIRE` FORCE + le fichier. Le Makefile exporte la seconde (ligne 13), derivee de l'instance ACTIVE. + Tout code qui redirige la premiere vers une AUTRE instance sans retirer la seconde + lit donc le plan de l'une et l'inventaire de l'autre. + + C'est arrive a P03, la preuve du diff-vide : sous `make prouver` elle generait le + plan de chaque instance federee et le comparait a l'inventaire applique de la seule + instance active. Elle annoncait « 17 hotes avec ecart » sur un lab qui n'en avait + aucun — et, plus grave, l'inventaire applique des autres tenants n'etait JAMAIS lu, + l'angle meme que cette preuve existe pour couvrir (D-77, tenant perime). + + Une contradiction entre les deux ne peut pas se deviner : on refuse, en nommant les + deux valeurs. Le geste correct au site d'appel est `env.pop("SETOPS_INVENTAIRE")`. + """ + force = os.environ.get("SETOPS_INVENTAIRE") + if not force: + return None + cible = Path(force) + base = Path(instance).resolve() + if base != cible.resolve() and base not in cible.resolve().parents: + raise SystemExit( + "REFUS : SETOPS_INVENTAIRE designe un inventaire HORS de l'instance demandee.\n" + f" SETOPS_INSTANCE {base}\n" + f" SETOPS_INVENTAIRE {cible.resolve()}\n" + "Comparer le plan d'une instance a l'inventaire d'une AUTRE rend un verdict\n" + "qui ne veut rien dire. Retirer SETOPS_INVENTAIRE de l'environnement fils\n" + "(env.pop) partout ou l'on redirige SETOPS_INSTANCE." + ) + return cible + + def liens_acceptes(groupe: str, racine: Path | None = None) -> dict: """Liens qu'un role accepte : roles//meta/liens.yml -> setops_liens.accepte. diff --git a/scripts/prouver.py b/scripts/prouver.py index 443a1a4..fbacd18 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -157,7 +157,10 @@ def preuve_structurels() -> tuple[bool, str]: def preuve_modele_socle() -> tuple[bool, str]: """Le modele public `socle` valide (registres) — garde contre une regression d'exemple.""" socle = "exemples/modeles/socle" + # Meme retrait qu'en P03 : les validateurs lisent SETOPS_INVENTAIRE, qui gagnerait + # sur le socle et ferait valider le modele public contre l'inventaire du tenant actif. env = {**os.environ, "SETOPS_INSTANCE": socle} + env.pop("SETOPS_INVENTAIRE", None) for s in ("domaines", "serveurs", "applications", "bases_donnees"): r = subprocess.run([sys.executable, f"scripts/{s}.py", "verifier"], cwd=RACINE, env=env, capture_output=True, text=True) @@ -502,7 +505,15 @@ def preuve_diff_vide_toutes_instances() -> tuple[bool, str]: chemin = RACINE.parent / i["nom"] if not (chemin / "inventories").is_dir(): continue + # SETOPS_INVENTAIRE FORCE la cible et GAGNE sur SETOPS_INSTANCE (instancier.py). + # Le Makefile l'exporte (ligne 13), pointe sur l'inventaire de l'instance ACTIVE. + # Sans ce retrait, la boucle generait le plan de CHAQUE instance et le comparait + # a l'inventaire applique de la SEULE instance active : « 17 hotes avec ecart » + # sur un lab qui n'en a aucun (mesure du 2026-08-18). Et le vert etait pire que + # le rouge — l'inventaire applique des autres tenants n'etait JAMAIS lu, ce qui + # est precisement l'angle que cette preuve existe pour couvrir. env = {**os.environ, "SETOPS_INSTANCE": str(chemin)} + env.pop("SETOPS_INVENTAIRE", None) gen = subprocess.run([sys.executable, "scripts/instancier.py", "generer"], cwd=RACINE, env=env, capture_output=True, text=True, timeout=300) if gen.returncode != 0: diff --git a/scripts/serveurs.py b/scripts/serveurs.py index 42d8867..5162622 100644 --- a/scripts/serveurs.py +++ b/scripts/serveurs.py @@ -22,6 +22,7 @@ from inventory_rules import ( charger_serveurs, fonction_seq, integrations_universelles, + inventaire_force, reconcilier_serveur, valider_serveurs, ) @@ -31,9 +32,9 @@ INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance")) FICHIER = INSTANCE / "plan/serveurs.yml" FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml" def _inventaire(instance: Path, *noms: str) -> Path: - forced = os.environ.get("SETOPS_INVENTAIRE") + forced = inventaire_force(instance) # refuse une cible hors de `instance` if forced: - return Path(forced) + return forced for nom in noms: p = instance / "inventories" / nom / "hosts.yml" if p.exists():