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>
This commit is contained in:
parent
cbb186d2fa
commit
d52f256366
7 changed files with 236 additions and 5 deletions
34
CHANGELOG.md
34
CHANGELOG.md
|
|
@ -1,5 +1,39 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-09 — P32 : un `assert` de rôle est un contrat, et l'instance doit l'honorer
|
||||
|
||||
Le premier des trois chantiers que la reconstruction avait rendus évidents. Il aurait
|
||||
trouvé son défaut nº 1 — `amorcage_acces_courriel` — **sans rien détruire**.
|
||||
|
||||
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 les honore, et le manque ne se voit
|
||||
qu'au moment où la garde s'exécute pour de vrai — c'est-à-dire, pour un intrant d'amorçage,
|
||||
seulement quand on repart de rien.
|
||||
|
||||
**Un intrant est satisfait** par un défaut non vide dans le rôle (y compris un
|
||||
`{{ vault_* }}`, dont la présence réelle relève de P18), par un `set_fact` de résolveur, ou
|
||||
par une déclaration de l'inventaire. Aucune voûte n'est déchiffrée : la preuve reste
|
||||
statique, comme les 31 autres.
|
||||
|
||||
**Deux fois mon instrument a accusé le composant à sa place**, et les deux fois avant la
|
||||
première exécution utile. Il criait au manque sur `serveur_postfix_mailstore_hote`, qui est
|
||||
pourtant bel et bien fourni — d'abord parce que je ne lisais que `group_vars/` et
|
||||
`host_vars/` en oubliant le fichier d'inventaire lui-même, ensuite parce que je n'y
|
||||
cherchais que les blocs `vars:` alors que `instancier` écrit les valeurs dérivées
|
||||
**directement sous le nom d'hôte**. Un vérificateur incomplet est pire qu'absent : il fait
|
||||
douter de ce qui marche.
|
||||
|
||||
**Vérifié dans les deux sens.** Sur l'instance réelle : 30 exigences, toutes satisfaites.
|
||||
Sur un double où l'on retire la déclaration ajoutée la veille : `amorcage_acces_courriel`
|
||||
nommé, code de sortie 1 — le défaut nº 1 reproduit à froid.
|
||||
|
||||
Ce qu'il ne fait pas, et c'est écrit dans son en-tête : il ignore les `when:` qui rendent
|
||||
une assertion conditionnelle, donc il peut signaler un intrant exigé seulement quand une
|
||||
option est active. Signaler à tort coûte une ligne de déclaration ; ne pas signaler coûte
|
||||
un déploiement.
|
||||
|
||||
Harnais : **32 preuves, 0 échec, 0 sautée.**
|
||||
|
||||
## 2026-08-09 — La reconstruction from-zero est prouvée : cinq devis sur cinq
|
||||
|
||||
Écosystème `chezlepro` détruit — 14 VM, disques compris — puis **rejoué depuis le plan
|
||||
|
|
|
|||
3
Makefile
3
Makefile
|
|
@ -438,6 +438,9 @@ certificats-plan: ansible-runtime ## Ecart entre les certificats sur disque et c
|
|||
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-certificats.yml >/dev/null
|
||||
@python3 scripts/devis_certificats.py
|
||||
|
||||
intrants-verifier: ## Tout intrant qu'un role EXIGE est-il fourni par l'instance ? (lecture seule)
|
||||
@python3 scripts/verifier_intrants.py
|
||||
|
||||
identite-plan: ansible-runtime ## Ecart entre l'identite deployee et ce que le plan derive (aucune ecriture)
|
||||
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-identite.yml >/dev/null
|
||||
@python3 scripts/devis_identite.py
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (30 OK · 0 echec · 1 saute)
|
||||
- **Verdict** : ✅ CONFORME (32 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
|
|
@ -28,9 +28,9 @@
|
|||
| 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 | ⚪ SAUTE | Voute chiffree sans ANSIBLE_VAULT_PASSWORD_FILE (prerequis AFF-026). |
|
||||
| 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 : 25 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 25 secret(s) exige(s), tous presents. Voute reelle : 28 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. |
|
||||
|
|
@ -43,7 +43,8 @@
|
|||
| 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 | 37 scripts expliques et atteignables, 87 cibles make documentees, 54 roles avec README. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 38 scripts expliques et atteignables, 88 cibles make documentees, 54 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 30 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ code + les README de rôles). Cette page comble ces deux trous.
|
|||
| **Ordre de déploiement** | `docs/couches-deploiement.yml` (couches) + `docs/dependances-groupes.yml` (graphe) → `playbooks/site.yml` (**généré**, `make site`) |
|
||||
| **Conformité du déployé** | `docs/devis-services.md` — les **cinq devis de service** (`make identite-plan`, `certificats-plan`, `expositions-plan`, `postgresql-plan`, `courriel-plan`). Répondent à ce que `make prouver` ne demande jamais : *ce qui tourne correspond-il à ce qui est déclaré ?* |
|
||||
| **Preuve / recette** | `docs/audit/affirmations.md` (registre), `make prouver` → `docs/audit/preuve-<date>.md` — **statique** : lit le dépôt, aucun appel réseau ; la conformité du déployé est l'affaire des devis de service (ligne au-dessus), `docs/audit/plan-de-recette.md` (**généré** du wiki), `docs/audit/protocole-operateur-independant.md` |
|
||||
| **Décisions d'architecture** | `docs/decisions-architecture.md` — **68 décisions en vigueur** (D-01 → D-71, 3 renversées), pourquoi, où lire le détail, et ce qui les garde ; plus les **décisions renversées** et leur cause |
|
||||
| **Décisions d'architecture** | `docs/decisions-architecture.md` — **69 décisions en vigueur** (D-01 → D-72, 3 renversées), pourquoi, où lire le détail, et ce qui les garde ; plus les **décisions renversées** et leur cause |
|
||||
| **SDN / routage** | `docs/sdn-evpn.md` — décision du 2026-08-02 : le routage inter-zone passe des commutateurs aux hyperviseurs (zones EVPN = VRF). **Non éprouvé** : spike avant génération |
|
||||
| **Migration de tenant** | `docs/migration-tenant.md` — recette en 8 étapes, machine à états, gardes ; le receveur se construit **avant** tout gel |
|
||||
| **Exploitation courante** | `docs/runbooks-exploitation.md`, `docs/intrants-communs.md`, `docs/intrants-base-gui-conception.md`, `docs/theme-forgejo-hors-flotte.md` |
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@ sont les seules vérifiables.
|
|||
| **D-34** | Une **exemption** se dérive du **service rendu** (`sauf_role`), jamais d'un nom d'hôte | l'AC ne s'enrôle pas auprès d'elle-même ; l'exemption doit suivre step-ca si on le déplace | `roles/client_pki/meta/integration.yml` | P26 |
|
||||
| **D-68** | On **écrit, puis on relit et on compare** — quelle que soit l'interface ; on choisit celle dont le chemin de **lecture** parle le même langage que le chemin d'**écriture** | « toujours préférer l'API » n'aurait prédit aucune des pannes du 2026-08-08 : sur six familles de défauts, deux venaient d'un CLI, une d'un module Ansible (`ldap_entry` crée sans jamais modifier), une d'un `grep` de fichier, une de la précédence Ansible, une de mon comparateur. Le facteur commun est d'avoir écrit sans relire. Et la plupart de la flotte n'a **pas** d'API — Postfix, Dovecot, nginx, slapd, nftables : `postconf -h` / `postconf -e` sont symétriques, c'est tout ce qu'on demande | `devis-services.md` | les 5 devis |
|
||||
| **D-69** | Sur Keycloak : **l'API pour toute map ou collection** (`smtpServer`, `attributes`, `config`), `kcadm` pour les scalaires et les créations | `kcadm -s` sur une map accepte la commande, **sort en succès et n'écrit rien** — mesuré deux fois le 2026-08-08 (`smtpServer` resté vide après deux déploiements verts, puis `post.logout.redirect.uris`). Le CLI reste préféré ailleurs : c'est le vocabulaire de la documentation du produit, donc lisible sans IA | `roles/serveur_keycloak/tasks/` | `make identite-plan` |
|
||||
| **D-72** | Un `assert` de rôle est un **contrat d'intrant**, et l'instance doit l'honorer — vérifié hors ligne | la première reconstruction from-zero s'est arrêtée sur `amorcage_acces_courriel` : obligatoire depuis le matin, déclaré par aucun tenant, et invisible parce que le compte existait déjà — la garde n'avait jamais eu l'occasion de se déclencher. Un intrant est satisfait par un **défaut non vide**, un `set_fact` de résolveur, ou une déclaration de l'inventaire (fichier `hosts.yml` compris) | `scripts/verifier_intrants.py`, `make intrants-verifier` | **P32** |
|
||||
| **D-70** | La documentation **dit et explique tout ce que le dépôt fait** — et l'exigence est **outillée**, pas seulement énoncée | une exigence qu'on n'outille pas pourrit en silence : la carte annonçait « 28 décisions » quand il y en avait 66, et disait les accès « non construits » alors qu'ils tournaient en production. **P31** garde le couvert — chaque script s'explique et reste atteignable, chaque cible `make` porte son aide (sauf les internes préfixées `_`), chaque rôle a son README. Elle ne garde **pas** la qualité du « pourquoi » : ça se juge en revue, et ça vit dans `CHANGELOG.md` et ici | `devis-services.md`, `CHANGELOG.md` | **P31** |
|
||||
| **D-71** | **Une PKI et un DNS fonctionnels avant toute chose** ; puis, par VM : socle → enrôlement PKI → enregistrement DNS (A **et** PTR) | `deployer-tout` déroule par COUCHES — correct, mais chaque VM réclame alors un certificat à une autorité pas encore debout, et l'échec se lit comme un défaut du rôle et non d'ordre. Les deux hôtes d'amorçage se **dérivent** de `applications.<step_ca\|powerdns>.hote` : déplacer l'autorité déplace l'amorçage. **Deux exceptions structurelles assumées** — l'AC s'auto-signe, le DNS pose son propre enregistrement | `Makefile` `_amorcer-socle`, `scripts/socle_amorcage.py` | — |
|
||||
|
||||
|
|
|
|||
|
|
@ -571,6 +571,8 @@ PREUVES: list[dict] = [
|
|||
"cmds": [[sys.executable, "scripts/devis_sdn.py", "--verifier"]]},
|
||||
{"id": "P31", "titre": "Documentation : tout ce que le depot FAIT est nomme", "refs": [],
|
||||
"func": preuve_documentation_outillage},
|
||||
{"id": "P32", "titre": "Intrants exiges par les roles : tous fournis", "refs": [],
|
||||
"cmds": [[sys.executable, "scripts/verifier_intrants.py"]]},
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
190
scripts/verifier_intrants.py
Executable file
190
scripts/verifier_intrants.py
Executable file
|
|
@ -0,0 +1,190 @@
|
|||
#!/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
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
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
|
||||
|
||||
|
||||
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")
|
||||
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)
|
||||
|
||||
manquants: list[tuple[str, str]] = []
|
||||
exiges = 0
|
||||
for role in sorted(RACINE.glob("roles/*")):
|
||||
if not role.is_dir():
|
||||
continue
|
||||
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())
|
||||
Loading…
Reference in a new issue