CI : le harnais ne se declenchait que par memoire
Some checks are pending
verifier / verifier (push) Waiting to run

Trente-huit preuves, des tests, un lint — et RIEN ne les executait sans qu'un humain tape
`make`. Le meilleur atout du depot dependait de ne pas oublier. Il a desormais une CI
(.forgejo/workflows/verifier.yml) et une cible qui la rejoue a l'identique : `make ci`.

CE QUE LA CI A TROUVE AVANT D'EXISTER. Ecrire le workflow supposait de repondre a une
question jamais posee : est-ce qu'un depot PUBLIC, seul, se tient ? Mesure sur un clone
nu : non, a cinq endroits.

- `make instancier` echouait sur le modele public — le tout premier geste du QUICKSTART.
  Le Makefile forcait `principal/hosts.yml` alors que le modele vit en `production/` ; sa
  precedence suit desormais celle du code (fichier, puis REPERTOIRE existant, puis defaut).
- P32 parcourait les 54 roles sans regarder ce que l'instance deploie. Elle passait sur
  l'ecosysteme de reference PARCE QU'IL PORTE TOUT. Or les modeles sont des OFFRES : toute
  offre plus petite que l'ecosysteme complet echouait son propre harnais, pour des services
  qu'elle ne vend pas. Le perimetre se lit maintenant du plan (groupes de l'inventaire,
  puis roles composes par leur playbook).
- P24 : le modele public ne declarait aucun reseau d'administration — une flotte qu'on
  construit et ou l'on n'entre plus. `nftables_admin_ssh` est pose, avec le pourquoi.
- P33 : verifier_ports.py codait `instance/inventories/principal/hosts.yml` en dur.
- P32 et P24 lisaient le symlink `instance/` au lieu de SETOPS_INSTANCE.

Toutes de la MEME FAMILLE que P03 avant-hier : une resolution d'inventaire recopiee, une
variable d'environnement qui deborde de sa portee. Le depot en compte SEPT ; deux de plus
sont corrigees ici, et la septieme le dit en commentaire plutot que de le taire.

`make ci` NE TOUCHE AUCUN SYMLINK : le modele public est monte comme instance jetable,
vise par SETOPS_INSTANCE/SETOPS_UNDERLAY, detruit en sortant. Deux details mesures parce
que devines faux d'abord : l'instance jetable est un DOSSIER FRERE (la federation se
decouvre ainsi ; ailleurs, quatre preuves tombent) ; et SETOPS_UNDERLAY n'est pose QUE
pour la verification, sinon l'inventaire est ecrit avec une fabric et regenere avec une
autre — la commande fabriquait l'ecart qu'elle denonce.

RESULTAT : clone nu sans instance ni frere -> 38 OK, 0 echec, 0 saute. Depot de
l'exploitant avec ses 3 instances -> 38 OK, 0 echec, 0 saute. Aucun residu.

Et le lint du depot a refuse mon propre fichier de CI avant qu'il ne tourne une seule fois
(`on:` lu par YAML comme le booleen vrai). Le harnais mordait deja.

A AJUSTER AU PREMIER PASSAGE, ecrit en tete du workflow : l'etiquette `runs-on` doit
correspondre a un runner Forgejo enregistre, et le runner a besoin du reseau pour pip et
ansible-galaxy. Le vert de cette CI dira que le moteur et son modele public se tiennent —
pas que la flotte va bien : aucune VM jointe, aucune voute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-20 19:29:46 -04:00
parent f01f06df5e
commit a68b9cd10e
8 changed files with 341 additions and 6 deletions

View file

@ -0,0 +1,51 @@
# Verification automatique du depot, a chaque poussee.
#
# POURQUOI CE FICHIER EXISTE (2026-08-20). Le depot portait 38 preuves rejouables, des
# tests unitaires et un lint — et RIEN ne les executait sans qu'un humain tape `make`.
# Le meilleur atout du projet ne se declenchait que par memoire. Un harnais qu'on peut
# oublier de lancer ne garde rien.
#
# CE QUE CETTE CI VERIFIE : que le depot PUBLIC, seul, se tient. Elle monte le modele
# public `exemples/modeles/socle` comme instance jetable, l'applique, puis rejoue lint,
# tests, syntaxe et les 38 preuves (`make ci`). C'est exactement ce qu'obtient un inconnu
# qui clone le depot — et c'est pour ca que ca vaut la peine : jusqu'au 2026-08-19, ce
# chemin-la ne passait pas.
#
# CE QU'ELLE NE PEUT PAS VERIFIER, et il faut le savoir :
# - la flotte reelle. Aucune VM n'est jointe, aucun deploiement n'est fait ;
# - la voute. Aucun secret n'entre ici ; les preuves qui exigent une voute reelle
# travaillent sur le modele, qui n'en a pas ;
# - l'instance de production. Elle vit dans un depot prive, absent d'ici.
# Le vert de cette CI dit « le moteur et son modele public se tiennent », pas « la flotte
# va bien ». Cette derniere question, c'est `make prouver` sur le poste de l'exploitant.
#
# DEUX CHOSES A AJUSTER AU PREMIER PASSAGE :
# - `runs-on` doit correspondre a une etiquette d'un runner Forgejo enregistre ;
# - le runner a besoin d'un acces reseau pour pip et ansible-galaxy.
name: verifier
# « on » entre guillemets : YAML lit `on:` nu comme le booleen vrai, et le lint du depot
# le refuse (yaml[truthy]). Le harnais a attrape son propre fichier de CI avant qu'il ne
# tourne une seule fois.
"on":
push:
pull_request:
workflow_dispatch:
jobs:
verifier:
runs-on: ubuntu-latest
steps:
- name: Recuperer le depot
uses: actions/checkout@v4
- name: Outils et collections, puis `make ci`
run: |
set -e
python3 -m venv /tmp/venv
. /tmp/venv/bin/activate
pip install --quiet --upgrade pip
pip install --quiet 'ansible-core>=2.18,<2.19' ansible-lint pyyaml
ansible-galaxy collection install -r requirements.yml
make ci

View file

@ -1,5 +1,70 @@
# CHANGELOG — Set-OPS
## 2026-08-20 — Le harnais ne se déclenchait que par mémoire
Trente-huit preuves, des tests, un lint — et **rien** ne les exécutait sans qu'un humain
tape `make`. Le meilleur atout du dépôt dépendait de ne pas oublier. Il a maintenant une
CI (`.forgejo/workflows/verifier.yml`) et une cible qui la rejoue à l'identique :
`make ci`.
### Ce que la CI a trouvé avant d'exister
Écrire le workflow supposait de répondre à une question jamais posée : **est-ce qu'un
dépôt public, seul, se tient ?** Réponse mesurée sur un clone nu : non, à cinq endroits.
| | |
|---|---|
| `make instancier` | échouait sur le modèle public — **le tout premier geste du QUICKSTART** |
| P32 | exigeait les intrants d'oauth2-proxy d'une instance qui ne le déploie pas |
| P24 | le modèle ne déclarait aucun réseau d'administration |
| P33 | `verifier_ports.py` codait `principal/` en dur |
| P32, P24 (bis) | lisaient le symlink `instance/` au lieu de `SETOPS_INSTANCE` |
**Toutes de la même famille** — celle de P03 avant-hier : une résolution d'inventaire
recopiée, une variable d'environnement qui déborde de sa portée. Le dépôt en compte
**sept** ; deux de plus ont été corrigées ici, et le commentaire de la septième le dit
plutôt que de le taire.
### Celle qui comptait le plus
P32 parcourait les 54 rôles sans regarder ce que l'instance déploie. Elle passait sur
l'écosystème de référence **parce qu'il porte tout**. La conséquence dépassait le modèle :
les modèles sont des **offres**, et toute offre plus petite que l'écosystème complet —
c'est-à-dire toute offre réelle — échouait son propre harnais, pour des services qu'elle
ne vend pas. Le périmètre juste se lit du plan : les groupes de l'inventaire, puis les
rôles que leur playbook compose.
### Ce que `make ci` ne fait pas
Il ne touche **aucun symlink**. Le modèle public est monté comme instance jetable, visé
par `SETOPS_INSTANCE` / `SETOPS_UNDERLAY`, et détruit en sortant — ton instance reste
montée pendant l'exécution. Deux détails, mesurés parce que devinés faux d'abord :
- l'instance jetable est un **dossier frère**, pas un `/tmp` : la fédération se découvre
par les dossiers frères, et ailleurs quatre preuves tombent en disant « aucun tenant
fédéré découvert » ;
- `SETOPS_UNDERLAY` n'est posé **que pour la vérification**, jamais pour l'application —
sinon l'inventaire est écrit avec une fabric et régénéré avec une autre, et la commande
fabrique elle-même l'écart qu'elle dénonce.
### Le résultat
```
clone nu, aucune instance, aucun frère → make ci : 38 OK, 0 échec, 0 sauté
dépôt de l'exploitant, 3 instances → make ci : 38 OK, 0 échec, 0 sauté
```
Et une dernière chose, qui dit bien où on en est : **le lint du dépôt a refusé mon propre
fichier de CI** avant qu'il ne tourne une seule fois — `on:` que YAML lit comme le booléen
vrai. Le harnais mordait déjà.
> **Ce que le vert de cette CI dira, et ce qu'il ne dira pas.** Que le moteur et son
> modèle public se tiennent — pas que la flotte va bien. Aucune VM n'est jointe, aucune
> voûte n'entre là. La santé de la flotte reste une question qu'on pose sur le poste de
> l'exploitant, avec `make prouver`. C'est écrit en tête du workflow, pour que personne
> ne lise ce vert pour plus qu'il ne vaut.
## 2026-08-19 — La carte des services avait quatre mois de retard sur le moteur
`catalogue-services.md` est le document qu'on lit pour savoir **ce que Set-OPS fait** :

View file

@ -9,7 +9,19 @@ export SETOPS_INSTANCE ?= instance
# Inventaire de l'instance : un seul par instance dans le modèle « séparation par
# instance ». Détection rétro-compatible : principal > production > lab.
# Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml
SETOPS_INVENTAIRE ?= $(firstword $(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) $(wildcard $(SETOPS_INSTANCE)/inventories/production/hosts.yml) $(SETOPS_INSTANCE)/inventories/principal/hosts.yml)
#
# LA PRÉCÉDENCE SUIT CELLE DU CODE (`instancier._inventaire`) : fichier existant, puis
# RÉPERTOIRE existant, puis défaut. Le troisième niveau manquait, et comme cette variable
# FORCE la cible, `make instancier` sur une instance neuve visait un `principal/`
# inexistant alors que le plan vivait dans `production/` — l'échec du tout premier geste
# du QUICKSTART, sur le modèle public (mesuré le 2026-08-19). Sans `make`, la même
# commande réussissait : c'est la variable exportée qui décidait, pas le code.
SETOPS_INVENTAIRE ?= $(firstword \
$(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) \
$(wildcard $(SETOPS_INSTANCE)/inventories/production/hosts.yml) \
$(patsubst %/,%/hosts.yml,$(wildcard $(SETOPS_INSTANCE)/inventories/principal/)) \
$(patsubst %/,%/hosts.yml,$(wildcard $(SETOPS_INSTANCE)/inventories/production/)) \
$(SETOPS_INSTANCE)/inventories/principal/hosts.yml)
export SETOPS_INVENTAIRE
# Inventaire « modèle » (construction du golden template) : lab > principal > production.
INVENTAIRE_LAB ?= $(firstword $(wildcard $(SETOPS_INSTANCE)/inventories/lab/hosts.yml) $(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) $(SETOPS_INSTANCE)/inventories/production/hosts.yml)
@ -218,6 +230,36 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide)
python3 scripts/prouver.py --verifier
# Ce que la CI execute, et que tu peux rejouer a l'identique.
#
# Le modele PUBLIC est monte comme instance jetable et vise par SETOPS_INSTANCE /
# SETOPS_UNDERLAY : AUCUN symlink n'est touche, ton instance reste montee pendant
# l'execution. Deux details ne sont pas negociables, mesures le 2026-08-20 :
#
# - l'instance jetable est un DOSSIER FRERE du depot, pas un /tmp. La federation se
# decouvre par les dossiers freres (`devis_reseau.decouvrir`) : ailleurs, elle est
# invisible, et quatre preuves tombent en disant « aucun tenant federe decouvert » ;
# - `env -u SETOPS_INVENTAIRE`, sinon la variable exportee par CE make survit dans le
# sous-make et designe l'inventaire de l'instance ACTIVE — le piege de P03.
#
# Et SETOPS_UNDERLAY n'est pose QUE pour la verification, jamais pour l'application :
# l'inventaire de l'instance jetable doit etre ecrit avec la meme fabric que celle avec
# laquelle P03 le REGENERERA ensuite. En posant l'underlay du modele des l'application,
# `make ci` passait sur un depot nu et echouait chez un exploitant dont le symlink
# `underlay.yml` designe une autre fabric — quatre hotes « en ecart », pour un ecart
# fabrique par la commande elle-meme.
.PHONY: ci
ci: ## Verifie le depot comme la CI : modele public monte a l'ecart, sans toucher ton instance
@set -e ; \
frere="../instance-ci-$$$$" ; \
trap 'rm -rf "$$frere"' EXIT ; \
rm -rf "$$frere" ; cp -r exemples/modeles/socle "$$frere" ; \
env -u SETOPS_INVENTAIRE SETOPS_INSTANCE="$$frere" \
$(MAKE) --no-print-directory instancier-appliquer FORCE=1 ; \
env -u SETOPS_INVENTAIRE SETOPS_INSTANCE="$$frere" \
SETOPS_UNDERLAY="$$frere/underlay.yml" \
$(MAKE) --no-print-directory verifier
# Harnais de preuve : rejoue les preuves automatisables du registre et ecrit
# docs/audit/preuve-<date>.md (piece justificative horodatee, rejouable).
# `make verifier` l'appelle en mode --verifier (preuves seules, aucun rapport ecrit).

View file

@ -0,0 +1,74 @@
# Preuve de conformite — Set-OPS — 2026-08-20
> 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 (38 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, 94 cibles make documentees, 54 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 34 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 (20 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. |
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 29 role(s) serveur/client tous nommes, 30 groupe(s) cite(s) en table existent tous. |
## 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-20._

View file

@ -1,4 +1,17 @@
---
# RÉSEAU(X) D'ADMINISTRATION — la seule source autorisée à ouvrir SSH sur la flotte.
#
# À REMPLACER par le tien avant tout déploiement. Ici, la valeur suit le réseau de
# gestion que déclare l'`underlay.yml` de ce même modèle (VLAN 10, 10.0.0.0/24) : les
# deux doivent parler du même réseau, sinon la règle SSH entrante n'a aucune source et
# le `default deny` ferme l'accès d'administration — sur une flotte qu'on vient de
# construire et où l'on n'entrera plus. C'est ce que refuse la preuve P24.
#
# Y mettre AUSSI le CIDR du VPN, s'il y en a un : un accès distant qui n'est pas déclaré
# ici ne franchit pas la frontière.
nftables_admin_ssh:
- 10.0.0.0/24
domaine_interne: "exemple.internal"
fuseau_horaire: "America/Toronto"
organisation: "Exemple"

View file

@ -514,6 +514,12 @@ def preuve_diff_vide_toutes_instances() -> tuple[bool, str]:
# est precisement l'angle que cette preuve existe pour couvrir.
env = {**os.environ, "SETOPS_INSTANCE": str(chemin)}
env.pop("SETOPS_INVENTAIRE", None)
# SETOPS_UNDERLAY aussi, et pour la meme raison (2026-08-20). L'inventaire APPLIQUE
# d'un tenant a ete produit avec la fabric de SON site ; le regenerer contre une
# autre fabric fabrique un ecart sur chaque hote — les ponts derivent des VNet de
# l'underlay. Vu en lancant `make ci`, qui vise l'underlay du modele public : les
# trois instances reelles sont apparues « en ecart », toutes les 14 machines.
env.pop("SETOPS_UNDERLAY", 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:
@ -993,8 +999,14 @@ def _underlay_absent() -> bool:
def _frontiere_absente() -> bool:
"""Vrai si aucune instance n'est active (depot public nu : aucune frontiere a prouver)."""
return not (RACINE / "instance").exists()
"""Vrai si aucune instance n'est active (depot public nu : aucune frontiere a prouver).
L'instance est celle que designe SETOPS_INSTANCE — le symlink `instance/` n'en est que
le cas courant. Lire le symlink en dur faisait SAUTER cette preuve des qu'on visait une
instance autrement (CI, modele, autre tenant) : un vert de moins, et personne pour le
dire.
"""
return not INSTANCE.exists()
def _executer(preuve: dict) -> tuple[str, str]:

View file

@ -25,6 +25,7 @@ Signaler à tort coûte une ligne de déclaration ; ne pas signaler coûte un d
from __future__ import annotations
import argparse
import os
import re
import sys
from pathlib import Path
@ -146,24 +147,78 @@ def _declares_par_instance(base: Path) -> set[str]:
return declares
def _roles_joues_par(base: Path) -> set[str] | None:
"""Les roles qui tourneront REELLEMENT sur cette instance, ou None si indeterminable.
POURQUOI CE CADRAGE (mesure du 2026-08-19). Cette preuve parcourait `roles/*` — les
54 — sans regarder ce que l'instance deploie. Elle passait sur l'ecosysteme de
reference parce qu'il porte TOUT ; elle echouait sur le modele public en exigeant
`serveur_oauth2_proxy_client_id` d'une instance qui ne deploie pas oauth2-proxy, et
`serveur_postfix_mailstore_hote` d'une instance sans Postfix.
La consequence depassait le modele : les modeles sont des OFFRES. Toute offre plus
petite que l'ecosysteme complet — c'est-a-dire toute offre reelle — echouait son
propre harnais, pour des services qu'elle ne vend pas.
Le perimetre juste se lit du plan : les GROUPES de l'inventaire, puis les roles que
leur playbook homonyme compose (`serveur_durci` en enchaine onze sans role du meme
nom). None quand aucun inventaire n'est lisible : on retombe alors sur l'ancien
comportement, large, plutot que de ne rien verifier du tout.
"""
inventaires = sorted(base.glob("inventories/*/hosts.yml"))
if not inventaires:
return None
groupes: set[str] = set()
for f in inventaires:
inv = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
def marcher(noeud):
if not isinstance(noeud, dict):
return
for nom, contenu in (noeud.get("children") or {}).items():
groupes.add(nom)
marcher(contenu or {})
marcher(inv.get("all") or {})
if not groupes:
return None
joues: set[str] = set()
for groupe in groupes:
if (RACINE / "roles" / groupe).is_dir():
joues.add(groupe)
playbook = RACINE / "playbooks" / "groupes" / f"{groupe}.yml"
if not playbook.is_file():
continue
plays = yaml.safe_load(playbook.read_text(encoding="utf-8")) or []
for play in plays if isinstance(plays, list) else []:
for r in (play or {}).get("roles") or []:
joues.add(r if isinstance(r, str) else str((r or {}).get("role", "")))
return joues or None
def main(argv: list[str] | None = None) -> int:
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--instance", default=None, help="racine de l'instance (defaut : instance/)")
a = ap.parse_args(argv)
base = Path(a.instance) if a.instance else (RACINE / "instance")
# SETOPS_INSTANCE d'abord : le symlink `instance/` n'est que le cas courant. Le coder
# en dur rendait cette preuve inapplicable des qu'on visait une autre instance.
base = Path(a.instance) if a.instance else \
Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
if not base.exists():
print("Aucune instance montee : rien a verifier.", file=sys.stderr)
return 2
poses = _poses_par_set_fact()
declares = _declares_par_instance(base)
joues = _roles_joues_par(base)
manquants: list[tuple[str, str]] = []
exiges = 0
for role in sorted(RACINE.glob("roles/*")):
if not role.is_dir():
continue
if joues is not None and role.name not in joues:
continue # ce role ne tournera jamais sur cette instance
defauts = _defauts_non_vides(role)
vus: set[str] = set()
for f in sorted(role.glob("tasks/*.yml")):

View file

@ -28,6 +28,7 @@ from __future__ import annotations
import argparse
import collections
import os
import sys
from pathlib import Path
@ -36,6 +37,25 @@ import yaml
RACINE = Path(__file__).resolve().parent.parent
def _inventaire_par_defaut() -> Path:
"""L'inventaire de l'instance montee — meme precedence que le reste du moteur.
CINQ endroits resolvent l'inventaire dans ce depot, et deux defauts en sont sortis le
meme jour (2026-08-19) : celui-ci codait `principal/` en dur, ce qui rendait la preuve
inapplicable a toute instance en `production/` — dont le modele public. A consolider :
une seule resolution partagee, plutot que cinq copies qui derivent chacune de son cote.
"""
force = os.environ.get("SETOPS_INVENTAIRE")
if force:
return Path(force)
instance = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
for nom in ("principal", "production"):
p = instance / "inventories" / nom / "hosts.yml"
if p.is_file():
return p
return instance / "inventories" / "principal" / "hosts.yml"
def _groupes(inventaire: Path) -> dict[str, set[str]]:
"""Groupe -> hotes, en descendant les `children` de l'inventaire YAML."""
inv = yaml.safe_load(inventaire.read_text(encoding="utf-8")) or {}
@ -74,8 +94,11 @@ def main(argv: list[str] | None = None) -> int:
ap.add_argument("--inventaire", default=None)
a = ap.parse_args(argv)
inventaire = Path(a.inventaire) if a.inventaire else \
(RACINE / "instance" / "inventories" / "principal" / "hosts.yml")
# MEME PRECEDENCE QUE PARTOUT AILLEURS : SETOPS_INVENTAIRE, puis l'inventaire present
# (principal > production). Le chemin `principal/` etait code en dur ici, et rendait
# cette preuve inapplicable a toute instance en `production/` — dont le modele public,
# donc dont un depot fraichement clone (mesure du 2026-08-19).
inventaire = Path(a.inventaire) if a.inventaire else _inventaire_par_defaut()
if not inventaire.is_file():
print(f"Inventaire introuvable : {inventaire}", file=sys.stderr)
return 2