P32 / D-72 : tout intrant exige par un role est fourni par l'instance
Premier des trois chantiers rendus evidents par la reconstruction. Il aurait
trouve son defaut n1 — amorcage_acces_courriel — SANS RIEN DETRUIRE.
Un assert de role declare un contrat ; rien ne verifiait que l'instance
l'honore, et le manque ne se voit qu'au moment ou la garde s'execute — donc,
pour un intrant d'amorcage, seulement en repartant de rien.
Satisfait par : defaut non vide (vault_* compris, gardes par P18), set_fact de
resolveur, ou declaration de l'inventaire. Aucune voute dechiffree : la preuve
reste statique.
Deux fois mon instrument a accuse le composant a sa place, avant meme sa
premiere execution utile : il criait au manque sur
serveur_postfix_mailstore_hote, pourtant fourni — je ne lisais pas le fichier
d'inventaire, puis je n'y cherchais que les blocs vars: alors qu'instancier
ecrit sous le nom d'hote.
Verifie dans les deux sens : 30 exigences satisfaites sur le reel ; sur un
double sans la declaration d'hier, le defaut n1 est nomme, code 1.
Harnais : 32 preuves, 0 echec, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 09:40:25 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Tout intrant qu'un rôle EXIGE est-il fourni par l'instance ?
|
|
|
|
|
|
|
|
|
|
Un rôle qui `assert` une variable non vide déclare un **contrat** : sans cette valeur, le
|
|
|
|
|
déploiement s'arrête. Rien ne vérifiait que l'instance honore ces contrats — et le défaut
|
|
|
|
|
ne se voit qu'au moment où la tâche gardée s'exécute vraiment.
|
|
|
|
|
|
|
|
|
|
C'est ainsi que la première reconstruction from-zero s'est arrêtée sur
|
|
|
|
|
`amorcage_acces_courriel` (2026-08-08) : la valeur était obligatoire depuis le matin, aucun
|
|
|
|
|
tenant ne la déclarait, et personne ne pouvait le savoir parce que le compte existait déjà
|
|
|
|
|
— donc la garde n'avait jamais eu l'occasion de se déclencher.
|
|
|
|
|
|
|
|
|
|
**Un intrant est considéré comme SATISFAIT s'il est** :
|
|
|
|
|
- doté d'un défaut non vide dans `<role>/defaults/main.yml` (y compris un `{{ vault_* }}`,
|
|
|
|
|
dont la présence réelle est gardée par P18) ; ou
|
|
|
|
|
- posé à l'exécution par un `set_fact` d'un rôle (les résolveurs `resoudre_*`) ; ou
|
|
|
|
|
- déclaré en clair par l'inventaire de l'instance (`group_vars`, `host_vars`).
|
|
|
|
|
|
|
|
|
|
**Ce qu'il ne fait pas, et il faut le savoir** : il ne déchiffre aucune voûte, et il ne
|
|
|
|
|
tient pas compte des `when:` qui rendent une assertion conditionnelle. Un intrant exigé
|
|
|
|
|
seulement quand une option est active sera donc signalé même si l'option est éteinte.
|
|
|
|
|
Signaler à tort coûte une ligne de déclaration ; ne pas signaler coûte un déploiement.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
CI : le harnais ne se declenchait que par memoire
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>
2026-08-20 19:29:46 -04:00
|
|
|
import os
|
P32 / D-72 : tout intrant exige par un role est fourni par l'instance
Premier des trois chantiers rendus evidents par la reconstruction. Il aurait
trouve son defaut n1 — amorcage_acces_courriel — SANS RIEN DETRUIRE.
Un assert de role declare un contrat ; rien ne verifiait que l'instance
l'honore, et le manque ne se voit qu'au moment ou la garde s'execute — donc,
pour un intrant d'amorcage, seulement en repartant de rien.
Satisfait par : defaut non vide (vault_* compris, gardes par P18), set_fact de
resolveur, ou declaration de l'inventaire. Aucune voute dechiffree : la preuve
reste statique.
Deux fois mon instrument a accuse le composant a sa place, avant meme sa
premiere execution utile : il criait au manque sur
serveur_postfix_mailstore_hote, pourtant fourni — je ne lisais pas le fichier
d'inventaire, puis je n'y cherchais que les blocs vars: alors qu'instancier
ecrit sous le nom d'hote.
Verifie dans les deux sens : 30 exigences satisfaites sur le reel ; sur un
double sans la declaration d'hier, le defaut n1 est nomme, code 1.
Harnais : 32 preuves, 0 echec, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 09:40:25 -04:00
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import yaml
|
|
|
|
|
|
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.
- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
_frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.
Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.
LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.
CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.
P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.
make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00
|
|
|
from inventory_rules import instance_courante
|
|
|
|
|
|
P32 / D-72 : tout intrant exige par un role est fourni par l'instance
Premier des trois chantiers rendus evidents par la reconstruction. Il aurait
trouve son defaut n1 — amorcage_acces_courriel — SANS RIEN DETRUIRE.
Un assert de role declare un contrat ; rien ne verifiait que l'instance
l'honore, et le manque ne se voit qu'au moment ou la garde s'execute — donc,
pour un intrant d'amorcage, seulement en repartant de rien.
Satisfait par : defaut non vide (vault_* compris, gardes par P18), set_fact de
resolveur, ou declaration de l'inventaire. Aucune voute dechiffree : la preuve
reste statique.
Deux fois mon instrument a accuse le composant a sa place, avant meme sa
premiere execution utile : il criait au manque sur
serveur_postfix_mailstore_hote, pourtant fourni — je ne lisais pas le fichier
d'inventaire, puis je n'y cherchais que les blocs vars: alors qu'instancier
ecrit sous le nom d'hote.
Verifie dans les deux sens : 30 exigences satisfaites sur le reel ; sur un
double sans la declaration d'hier, le defaut n1 est nomme, code 1.
Harnais : 32 preuves, 0 echec, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 09:40:25 -04:00
|
|
|
RACINE = Path(__file__).resolve().parent.parent
|
|
|
|
|
|
|
|
|
|
# Les deux idiomes d'exigence du depot. `| bool` n'en est pas un : une valeur fausse est
|
|
|
|
|
# une reponse valable, pas une absence.
|
|
|
|
|
MOTIFS = (
|
|
|
|
|
re.compile(r"^([a-z][a-z0-9_]*)\s*\|\s*length\s*>\s*0\s*$"),
|
|
|
|
|
re.compile(r"^([a-z][a-z0-9_]*)\s+is\s+defined\s*$"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _assertions(chemin: Path) -> set[str]:
|
|
|
|
|
"""Variables exigees par les `assert` d'un fichier de taches."""
|
|
|
|
|
try:
|
|
|
|
|
taches = yaml.safe_load(chemin.read_text(encoding="utf-8")) or []
|
|
|
|
|
except yaml.YAMLError:
|
|
|
|
|
return set()
|
|
|
|
|
trouvees: set[str] = set()
|
|
|
|
|
for tache in taches if isinstance(taches, list) else []:
|
|
|
|
|
if not isinstance(tache, dict):
|
|
|
|
|
continue
|
|
|
|
|
bloc = tache.get("ansible.builtin.assert") or tache.get("assert")
|
|
|
|
|
if not isinstance(bloc, dict):
|
|
|
|
|
continue
|
|
|
|
|
conditions = bloc.get("that") or []
|
|
|
|
|
for c in [conditions] if isinstance(conditions, str) else conditions:
|
|
|
|
|
for motif in MOTIFS:
|
|
|
|
|
m = motif.match(str(c).strip())
|
|
|
|
|
if m:
|
|
|
|
|
trouvees.add(m.group(1))
|
|
|
|
|
return trouvees
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _defauts_non_vides(role: Path) -> set[str]:
|
|
|
|
|
f = role / "defaults" / "main.yml"
|
|
|
|
|
if not f.is_file():
|
|
|
|
|
return set()
|
|
|
|
|
try:
|
|
|
|
|
d = yaml.safe_load(f.read_text(encoding="utf-8")) or {}
|
|
|
|
|
except yaml.YAMLError:
|
|
|
|
|
return set()
|
|
|
|
|
return {k for k, v in d.items() if v not in (None, "", [], {})}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _poses_par_set_fact() -> set[str]:
|
|
|
|
|
"""Variables qu'un role FABRIQUE a l'execution (resolveurs)."""
|
|
|
|
|
poses: set[str] = set()
|
|
|
|
|
for f in RACINE.glob("roles/*/tasks/*.yml"):
|
|
|
|
|
try:
|
|
|
|
|
taches = yaml.safe_load(f.read_text(encoding="utf-8")) or []
|
|
|
|
|
except yaml.YAMLError:
|
|
|
|
|
continue
|
|
|
|
|
for tache in taches if isinstance(taches, list) else []:
|
|
|
|
|
if not isinstance(tache, dict):
|
|
|
|
|
continue
|
|
|
|
|
bloc = tache.get("ansible.builtin.set_fact") or tache.get("set_fact")
|
|
|
|
|
if isinstance(bloc, dict):
|
|
|
|
|
poses |= {k for k in bloc if k != "cacheable"}
|
|
|
|
|
return poses
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _cles_vars(noeud) -> set[str]:
|
|
|
|
|
"""Variables d'un inventaire YAML, a n'importe quelle profondeur.
|
|
|
|
|
|
|
|
|
|
DEUX emplacements, et oublier le second faisait crier au manque a tort : les blocs
|
|
|
|
|
`vars:` d'un groupe, ET les cles posees directement sous un nom d'hote dans `hosts:`.
|
|
|
|
|
C'est la seconde forme que `instancier` emploie pour les valeurs derivees du plan.
|
|
|
|
|
"""
|
|
|
|
|
trouvees: set[str] = set()
|
|
|
|
|
if isinstance(noeud, dict):
|
|
|
|
|
v = noeud.get("vars")
|
|
|
|
|
if isinstance(v, dict):
|
|
|
|
|
trouvees |= {k for k, val in v.items() if val not in (None, "", [], {})}
|
|
|
|
|
h = noeud.get("hosts")
|
|
|
|
|
if isinstance(h, dict):
|
|
|
|
|
for attributs in h.values():
|
|
|
|
|
if isinstance(attributs, dict):
|
|
|
|
|
trouvees |= {k for k, val in attributs.items()
|
|
|
|
|
if val not in (None, "", [], {})}
|
|
|
|
|
for sous in noeud.values():
|
|
|
|
|
trouvees |= _cles_vars(sous)
|
|
|
|
|
elif isinstance(noeud, list):
|
|
|
|
|
for sous in noeud:
|
|
|
|
|
trouvees |= _cles_vars(sous)
|
|
|
|
|
return trouvees
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _declares_par_instance(base: Path) -> set[str]:
|
|
|
|
|
"""Cles declarees EN CLAIR par l'inventaire. Les voutes ne sont jamais dechiffrees.
|
|
|
|
|
|
|
|
|
|
Le FICHIER d'inventaire compte autant que `group_vars/` : `instancier` y ecrit les
|
|
|
|
|
valeurs derivees du plan — les liens entre applications, notamment. Ne regarder que
|
|
|
|
|
les repertoires faisait crier au manque sur `serveur_postfix_mailstore_hote`, qui
|
|
|
|
|
etait bel et bien fourni. Mesure du 2026-08-09, avant meme la premiere execution
|
|
|
|
|
utile : un verificateur incomplet accuse le composant a la place de l'instrument.
|
|
|
|
|
"""
|
|
|
|
|
declares: set[str] = set()
|
|
|
|
|
for f in base.glob("inventories/*/hosts.yml"):
|
|
|
|
|
try:
|
|
|
|
|
declares |= _cles_vars(yaml.safe_load(f.read_text(encoding="utf-8")))
|
|
|
|
|
except yaml.YAMLError:
|
|
|
|
|
continue
|
|
|
|
|
for f in list(base.glob("inventories/*/group_vars/**/*.yml")) + \
|
|
|
|
|
list(base.glob("inventories/*/host_vars/**/*.yml")):
|
|
|
|
|
texte = f.read_text(encoding="utf-8", errors="ignore")
|
|
|
|
|
if texte.lstrip().startswith("$ANSIBLE_VAULT"):
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
d = yaml.safe_load(texte) or {}
|
|
|
|
|
except yaml.YAMLError:
|
|
|
|
|
continue
|
|
|
|
|
if isinstance(d, dict):
|
|
|
|
|
declares |= {k for k, v in d.items() if v not in (None, "", [], {})}
|
|
|
|
|
return declares
|
|
|
|
|
|
|
|
|
|
|
CI : le harnais ne se declenchait que par memoire
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>
2026-08-20 19:29:46 -04:00
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
P32 / D-72 : tout intrant exige par un role est fourni par l'instance
Premier des trois chantiers rendus evidents par la reconstruction. Il aurait
trouve son defaut n1 — amorcage_acces_courriel — SANS RIEN DETRUIRE.
Un assert de role declare un contrat ; rien ne verifiait que l'instance
l'honore, et le manque ne se voit qu'au moment ou la garde s'execute — donc,
pour un intrant d'amorcage, seulement en repartant de rien.
Satisfait par : defaut non vide (vault_* compris, gardes par P18), set_fact de
resolveur, ou declaration de l'inventaire. Aucune voute dechiffree : la preuve
reste statique.
Deux fois mon instrument a accuse le composant a sa place, avant meme sa
premiere execution utile : il criait au manque sur
serveur_postfix_mailstore_hote, pourtant fourni — je ne lisais pas le fichier
d'inventaire, puis je n'y cherchais que les blocs vars: alors qu'instancier
ecrit sous le nom d'hote.
Verifie dans les deux sens : 30 exigences satisfaites sur le reel ; sur un
double sans la declaration d'hier, le defaut n1 est nomme, code 1.
Harnais : 32 preuves, 0 echec, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 09:40:25 -04:00
|
|
|
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)
|
|
|
|
|
|
CI : le harnais ne se declenchait que par memoire
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>
2026-08-20 19:29:46 -04:00
|
|
|
# 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.
|
resolution d'instance : une seule, partagee — au lieu de neuf copies
Cinq jours, cinq defauts, tous de la meme famille : « quelle instance, quel inventaire ? »
Neuf modules portaient chacun leur reponse.
- 18 aout : P03 comparait chaque instance a l'inventaire d'une AUTRE ;
- 19 aout : verifier_ports codait `principal/` en dur ; verifier_intrants et
_frontiere_absente lisaient le symlink au lieu de la variable ;
- 20 aout : devis_placement rendait un verdict juste sur le mauvais tenant ;
- 22 aout : P35, puis P36 — la dixieme, trouvee par la preuve elle-meme.
Aucune n'etait une faute d'inattention : chacune avait ete ecrite de bonne foi, a un
moment ou le besoin semblait local. C'est le mode de panne de la duplication — pas
l'erreur, mais la DERIVE, invisible depuis l'interieur d'un fichier.
LA RESOLUTION UNIQUE. `inventory_rules` porte instance_courante(), inventaire_de(),
dossier_inventaire() et plan_de(). Trois niveaux de repli, dont le TROISIEME manquait a la
moitie des copies : un hosts.yml existant, puis un REPERTOIRE existant (instance neuve —
c'est ce qui faisait echouer `make instancier` sur le modele public), puis le defaut.
Vingt-huit modules y sont branches.
CE QUI REND CE REFACTOR SUR : avant de toucher quoi que ce soit, chaque module a ete
interroge sur ce qu'il resolvait, pour les DEUX ecosystemes. Apres refactor, meme mesure :
17 modules x 2 instances, diff VIDE. Aucune resolution n'a change — prouve, pas suppose.
P41 echoue des qu'un module reintroduit une copie. Eprouvee en negatif : une copie
replacee dans genome.py est signalee avec son numero de ligne. Trois exemptions nommees :
instances.py et inventory_gui.py manipulent le SYMLINK lui-meme (bascule d'instance), et
devis_opnsense lit deliberement quelle instance est ACTIVE. Elles parlent du lien, pas de
la resolution.
make verifier 41 OK, 0 echec, 0 saute ; make ci idem ; lint vert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 14:19:55 -04:00
|
|
|
base = Path(a.instance) if a.instance else instance_courante()
|
P32 / D-72 : tout intrant exige par un role est fourni par l'instance
Premier des trois chantiers rendus evidents par la reconstruction. Il aurait
trouve son defaut n1 — amorcage_acces_courriel — SANS RIEN DETRUIRE.
Un assert de role declare un contrat ; rien ne verifiait que l'instance
l'honore, et le manque ne se voit qu'au moment ou la garde s'execute — donc,
pour un intrant d'amorcage, seulement en repartant de rien.
Satisfait par : defaut non vide (vault_* compris, gardes par P18), set_fact de
resolveur, ou declaration de l'inventaire. Aucune voute dechiffree : la preuve
reste statique.
Deux fois mon instrument a accuse le composant a sa place, avant meme sa
premiere execution utile : il criait au manque sur
serveur_postfix_mailstore_hote, pourtant fourni — je ne lisais pas le fichier
d'inventaire, puis je n'y cherchais que les blocs vars: alors qu'instancier
ecrit sous le nom d'hote.
Verifie dans les deux sens : 30 exigences satisfaites sur le reel ; sur un
double sans la declaration d'hier, le defaut n1 est nomme, code 1.
Harnais : 32 preuves, 0 echec, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 09:40:25 -04:00
|
|
|
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)
|
CI : le harnais ne se declenchait que par memoire
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>
2026-08-20 19:29:46 -04:00
|
|
|
joues = _roles_joues_par(base)
|
P32 / D-72 : tout intrant exige par un role est fourni par l'instance
Premier des trois chantiers rendus evidents par la reconstruction. Il aurait
trouve son defaut n1 — amorcage_acces_courriel — SANS RIEN DETRUIRE.
Un assert de role declare un contrat ; rien ne verifiait que l'instance
l'honore, et le manque ne se voit qu'au moment ou la garde s'execute — donc,
pour un intrant d'amorcage, seulement en repartant de rien.
Satisfait par : defaut non vide (vault_* compris, gardes par P18), set_fact de
resolveur, ou declaration de l'inventaire. Aucune voute dechiffree : la preuve
reste statique.
Deux fois mon instrument a accuse le composant a sa place, avant meme sa
premiere execution utile : il criait au manque sur
serveur_postfix_mailstore_hote, pourtant fourni — je ne lisais pas le fichier
d'inventaire, puis je n'y cherchais que les blocs vars: alors qu'instancier
ecrit sous le nom d'hote.
Verifie dans les deux sens : 30 exigences satisfaites sur le reel ; sur un
double sans la declaration d'hier, le defaut n1 est nomme, code 1.
Harnais : 32 preuves, 0 echec, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 09:40:25 -04:00
|
|
|
|
|
|
|
|
manquants: list[tuple[str, str]] = []
|
|
|
|
|
exiges = 0
|
|
|
|
|
for role in sorted(RACINE.glob("roles/*")):
|
|
|
|
|
if not role.is_dir():
|
|
|
|
|
continue
|
CI : le harnais ne se declenchait que par memoire
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>
2026-08-20 19:29:46 -04:00
|
|
|
if joues is not None and role.name not in joues:
|
|
|
|
|
continue # ce role ne tournera jamais sur cette instance
|
P32 / D-72 : tout intrant exige par un role est fourni par l'instance
Premier des trois chantiers rendus evidents par la reconstruction. Il aurait
trouve son defaut n1 — amorcage_acces_courriel — SANS RIEN DETRUIRE.
Un assert de role declare un contrat ; rien ne verifiait que l'instance
l'honore, et le manque ne se voit qu'au moment ou la garde s'execute — donc,
pour un intrant d'amorcage, seulement en repartant de rien.
Satisfait par : defaut non vide (vault_* compris, gardes par P18), set_fact de
resolveur, ou declaration de l'inventaire. Aucune voute dechiffree : la preuve
reste statique.
Deux fois mon instrument a accuse le composant a sa place, avant meme sa
premiere execution utile : il criait au manque sur
serveur_postfix_mailstore_hote, pourtant fourni — je ne lisais pas le fichier
d'inventaire, puis je n'y cherchais que les blocs vars: alors qu'instancier
ecrit sous le nom d'hote.
Verifie dans les deux sens : 30 exigences satisfaites sur le reel ; sur un
double sans la declaration d'hier, le defaut n1 est nomme, code 1.
Harnais : 32 preuves, 0 echec, 0 sautee.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 09:40:25 -04:00
|
|
|
defauts = _defauts_non_vides(role)
|
|
|
|
|
vus: set[str] = set()
|
|
|
|
|
for f in sorted(role.glob("tasks/*.yml")):
|
|
|
|
|
vus |= _assertions(f)
|
|
|
|
|
for var in sorted(vus):
|
|
|
|
|
exiges += 1
|
|
|
|
|
if var in defauts or var in poses or var in declares:
|
|
|
|
|
continue
|
|
|
|
|
manquants.append((role.name, var))
|
|
|
|
|
|
|
|
|
|
if manquants:
|
|
|
|
|
print(f"{len(manquants)} intrant(s) EXIGE(s) que l'instance ne fournit pas :\n")
|
|
|
|
|
for r, v in manquants:
|
|
|
|
|
print(f" {r:26} exige `{v}` — sans defaut, sans set_fact, absent de l'inventaire")
|
|
|
|
|
print("\nLe deploiement s'arretera sur la premiere garde atteinte.")
|
|
|
|
|
return 1
|
|
|
|
|
|
|
|
|
|
print(f"CONFORME : {exiges} exigence(s) de role, toutes satisfaites "
|
|
|
|
|
f"({len(declares)} cle(s) declaree(s) par l'instance).")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
raise SystemExit(main())
|