P03 : la preuve comparait chaque instance a l'inventaire d'UNE SEULE

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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-18 14:35:01 -04:00
parent 29148831bf
commit 4462c13b3d
11 changed files with 218 additions and 14 deletions

View file

@ -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

View file

@ -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._

View file

@ -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():

View file

@ -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)

View file

@ -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)

View file

@ -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)

View file

@ -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():

View file

@ -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"

View file

@ -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/<groupe>/meta/liens.yml -> setops_liens.accepte.

View file

@ -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:

View file

@ -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():