diff --git a/.forgejo/workflows/verifier.yml b/.forgejo/workflows/verifier.yml new file mode 100644 index 0000000..5c11bf8 --- /dev/null +++ b/.forgejo/workflows/verifier.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f972d..9af0616 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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** : diff --git a/Makefile b/Makefile index 7a1e546..d0f18d2 100644 --- a/Makefile +++ b/Makefile @@ -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-.md (piece justificative horodatee, rejouable). # `make verifier` l'appelle en mode --verifier (preuves seules, aucun rapport ecrit). diff --git a/docs/audit/preuve-2026-08-20.md b/docs/audit/preuve-2026-08-20.md new file mode 100644 index 0000000..1247d29 --- /dev/null +++ b/docs/audit/preuve-2026-08-20.md @@ -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._ diff --git a/exemples/modeles/socle/inventories/production/group_vars/all/10-intrants.yml b/exemples/modeles/socle/inventories/production/group_vars/all/10-intrants.yml index d96622e..9467f26 100644 --- a/exemples/modeles/socle/inventories/production/group_vars/all/10-intrants.yml +++ b/exemples/modeles/socle/inventories/production/group_vars/all/10-intrants.yml @@ -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" diff --git a/scripts/prouver.py b/scripts/prouver.py index 413ec4f..5b4af99 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -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]: diff --git a/scripts/verifier_intrants.py b/scripts/verifier_intrants.py index 2a93b21..3e74c5b 100755 --- a/scripts/verifier_intrants.py +++ b/scripts/verifier_intrants.py @@ -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")): diff --git a/scripts/verifier_ports.py b/scripts/verifier_ports.py index 2635f6e..602799e 100755 --- a/scripts/verifier_ports.py +++ b/scripts/verifier_ports.py @@ -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