Set-OPS-Public/scripts/verifier_intrants.py

247 lines
10 KiB
Python
Raw Normal View History

#!/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
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
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
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()
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)
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
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())