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:
parent
29148831bf
commit
4462c13b3d
11 changed files with 218 additions and 14 deletions
67
CHANGELOG.md
67
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
|
||||
|
|
|
|||
73
docs/audit/preuve-2026-08-18.md
Normal file
73
docs/audit/preuve-2026-08-18.md
Normal 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 | [0m[0m |
|
||||
| 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._
|
||||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
Loading…
Reference in a new issue