diff --git a/CHANGELOG.md b/CHANGELOG.md index dc1c00c..3fd37ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,44 @@ # CHANGELOG — Set-OPS +## 2026-08-08 — D-70 : l'exigence de documentation devient une preuve (P31) + +Directive de l'exploitant : « la doc dit et explique tout ce que Set-OPS fait, et pourquoi +c'est ainsi. » Une exigence qu'on se contente d'énoncer pourrit en silence — on venait +d'en avoir trois exemples le jour même dans la carte. + +**L'écart mesuré avant de le combler :** + +``` +cibles make sans texte d'aide : 66 sur 85 → `make help` en montrait 19 +scripts jamais cités en doc : 11 sur 35 → dont 3 applicateurs et 4 devis du jour +``` + +Les 66 cibles ont reçu leur aide : `make aide` couvre maintenant **85 commandes** au lieu +de 19. C'est ce qui rend le moteur utilisable par quelqu'un qui ne lit pas le Makefile — +la règle « un sysadmin l'exploite sans IA » n'a pas d'autre traduction concrète. + +**P31 garde l'exigence**, et le chemin pour l'écrire a été instructif : *deux fois* mon +critère s'est révélé creux. + +D'abord « le nom du script apparaît dans un document » : le rapport d'audit **généré** +recopiait les noms manquants dans son message d'échec, ce qui les rendait cités au tour +suivant. Une preuve qui se nourrit de sa propre sortie passe au vert sans qu'une ligne +soit écrite. + +Puis, en corrigeant, j'ai failli créer le même trou en plus grand : générer un inventaire +de l'outillage aurait satisfait le critère par construction. **Un critère qu'on peut +satisfaire en générant du texte ne prouve rien.** P31 teste donc que chaque script porte +une docstring qui l'explique et qu'il reste **atteignable** — par une cible `make`, ou par +un autre outil. + +Vérifiée dans les deux sens, comme les devis : on retire l'aide d'une cible et la +docstring d'un script, les deux défauts sont nommés ; on restaure, `CONFORME`. + +**Ce que P31 ne garde pas, et c'est dit dans son propre code** : que l'explication soit +*bonne*. Le « pourquoi » se juge en revue. Il vit dans ce journal — qui porte le fait +mesuré, pas seulement le changement — et dans le registre des décisions. Prétendre le +mesurer mécaniquement serait se mentir. + ## 2026-08-08 — Tisser le travail du jour dans les points d'entrée Question de l'exploitant : faut-il refondre la documentation ? **Non.** L'état mesuré ne le diff --git a/Makefile b/Makefile index 319c6a9..f257520 100644 --- a/Makefile +++ b/Makefile @@ -80,7 +80,7 @@ DOSSIER_PLAYBOOKS_GROUPES := playbooks/groupes .DEFAULT_GOAL := aide .PHONY: ansible-runtime -ansible-runtime: +ansible-runtime: ## Prepare le repertoire temporaire local d'Ansible (prerequis interne des cibles qui deploient) @mkdir -p "$(ANSIBLE_LOCAL_TEMP)" @mkdir -p "$(ANSIBLE_SSH_CONTROL_PATH_DIR)" @@ -95,7 +95,7 @@ _instance-requise: fi .PHONY: aide -aide: +aide: ## Affiche l'aide detaillee du moteur (au-dela de cette liste) @printf '%s\n' 'Set-OPS — moteur d ecosystemes numeriques souverains' @printf '%s\n' '' @printf '%s\n' 'Nouveau ? -> QUICKSTART.md (de zero a ton ecosysteme sur Proxmox)' @@ -180,93 +180,93 @@ aide: @printf '%s\n' ' FICHIER_INVENTAIRE=$(SETOPS_INSTANCE)/inventories/production/hosts.yml FICHIER_DEPENDANCES=docs/dependances-groupes.yml CONFIRMER=true' .PHONY: lint -lint: ansible-runtime +lint: ansible-runtime ## Passe ansible-lint sur tout le depot ansible-lint .PHONY: syntaxe syntaxe-modele syntaxe-nettoyage syntaxe-verification-modele syntaxe-verification-hote syntaxe-groupes syntaxe-proxmox -syntaxe: syntaxe-modele syntaxe-verification-modele syntaxe-nettoyage syntaxe-verification-hote syntaxe-groupes syntaxe-proxmox +syntaxe: syntaxe-modele syntaxe-verification-modele syntaxe-nettoyage syntaxe-verification-hote syntaxe-groupes syntaxe-proxmox ## Verifie la syntaxe de TOUS les playbooks (modele, hote, groupes, proxmox) -syntaxe-modele: ansible-runtime +syntaxe-modele: ansible-runtime ## Verifie la syntaxe du playbook de preparation du gabarit dore ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_PREPARER_MODELE) --syntax-check -syntaxe-verification-modele: ansible-runtime +syntaxe-verification-modele: ansible-runtime ## Verifie la syntaxe du playbook de verification du gabarit ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_VERIFIER_MODELE) --syntax-check -syntaxe-nettoyage: ansible-runtime +syntaxe-nettoyage: ansible-runtime ## Verifie la syntaxe du playbook de nettoyage du gabarit ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_NETTOYER_MODELE) --syntax-check -syntaxe-verification-hote: ansible-runtime +syntaxe-verification-hote: ansible-runtime ## Verifie la syntaxe du playbook de verification d'hote ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) --syntax-check -syntaxe-groupes: ansible-runtime +syntaxe-groupes: ansible-runtime ## Verifie la syntaxe des 30 playbooks de groupe @for playbook in $(DOSSIER_PLAYBOOKS_GROUPES)/*.yml; do \ ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --syntax-check; \ done -syntaxe-proxmox: ansible-runtime +syntaxe-proxmox: ansible-runtime ## Verifie la syntaxe du playbook de clonage de VM ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) --syntax-check .PHONY: test -test: +test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire) python3 scripts/tests/test_inventory_host.py .PHONY: verifier -verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe +verifier: lint test inventaire-verifier site-verifier flux-verifier syntaxe ## Rejoue les preuves SANS reecrire le rapport (verification rapide) python3 scripts/prouver.py --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). .PHONY: prouver -prouver: ansible-runtime _instance-requise +prouver: ansible-runtime _instance-requise ## Execute les preuves et ecrit docs/audit/preuve-.md python3 scripts/prouver.py .PHONY: inventaire hote-planifier hote-ajouter hote-groupes hote-afficher appliquer deployer deployer-groupe cloner-vm creer-vm config inventaire-ui inventaire-verifier inventaire-lister inventaire-graphe inventaire-hote inventaire-lab inventaire-production instance-utiliser instance-courante -inventaire: inventaire-production +inventaire: inventaire-production ## Verifie l'inventaire et en affiche le graphe (lab puis production) # Bascule le symlink 'instance' vers un autre dépôt d'instance (séparation par # instance : prod vs bac à sable). Ex. : make instance-utiliser NOM=OPS-Chezlepro-lab -instance-utiliser: +instance-utiliser: ## Bascule l'instance active (symlink instance/) vers un dossier frere — NOM= @if [[ -z "$(NOM)" ]]; then printf '%s\n' "Usage: make instance-utiliser NOM= (ex. OPS-Chezlepro-lab)"; exit 2; fi @if [[ ! -d "../$(NOM)" ]]; then printf '%s\n' "Introuvable: ../$(NOM)"; exit 2; fi @if [[ -e instance && ! -L instance ]]; then printf '%s\n' "Refus: 'instance' existe et n'est pas un symlink."; exit 2; fi @rm -f instance && ln -s "../$(NOM)" instance @printf 'instance -> %s\n' "$$(readlink instance)" -instance-courante: +instance-courante: ## Affiche vers quel ecosysteme pointe l'instance active @printf 'instance -> %s\n' "$$(readlink instance 2>/dev/null || echo '(non monté)')" # Vue d'ensemble : toutes les instances de la fédération, l'active (*), leur index, # plage VLAN, statut fédéré/prod ; signale les collisions d'index. Lecture seule. -instances: +instances: ## Liste les ecosystemes decouverts (dossiers freres) et signale les collisions d'index @python3 scripts/instances.py # (Re)génère le plan de recette (docs/audit/plan-de-recette.md) depuis les exercices # du wiki. La preuve P22 vérifie qu'il reste à jour. -plan-recette: +plan-recette: ## Regenere docs/audit/plan-de-recette.md depuis le wiki @python3 scripts/plan_recette.py # Liste les modèles disponibles (socle + SETOPS_MODELES) pour créer une instance. -instance-modeles: +instance-modeles: ## Liste les modeles d'ecosysteme disponibles @python3 scripts/instance_creer.py --lister-modeles # Crée un dépôt d'instance frère depuis un modèle. Ne bascule pas le symlink. # Ex. : make instance-creer NOM=OPS-ClientX MODELE=socle INDEX=4 -instance-creer: +instance-creer: ## Cree un nouvel ecosysteme depuis un modele — NOM= MODELE= @python3 scripts/instance_creer.py --nom "$(NOM)" --modele "$(MODELE)" \ $(if $(INDEX),--index $(INDEX),) # Crée un MODÈLE (dépôt privé). Deux modes : # base : copier un modèle générique -> make model-creer MODE=base BASE=identite NOM=maison-obnl # instance : promouvoir une instance -> make model-creer MODE=instance SOURCE=OPS-Chezlepro NOM=cabinet -model-creer: +model-creer: ## Cree un modele d'ecosysteme — MODE= NOM= @python3 scripts/model_creer.py --mode "$(MODE)" --nom "$(NOM)" \ $(if $(BASE),--base $(BASE),) $(if $(SOURCE),--source $(SOURCE),) $(if $(DEST),--dest $(DEST),) -config: +config: ## Affiche la configuration Proxmox lue par le moteur python3 scripts/config_proxmox.py -inventaire-ui: _instance-requise +inventaire-ui: _instance-requise ## Ouvre la console d'exploitation (GUI web) sur l'inventaire actif python3 scripts/inventory_gui.py --inventaire $(FICHIER_INVENTAIRE) hote-ajouter hote-planifier hote-groupes: @@ -276,14 +276,14 @@ hote-ajouter hote-planifier hote-groupes: @printf '%s\n' '(creer-vm lit desormais VMID/IP/VLAN/passerelle directement dans l inventaire genere.)' @exit 2 -hote-afficher: ansible-runtime +hote-afficher: ansible-runtime ## Affiche tout ce que le plan derive pour un hote — HOTE= @if [[ -z "$(HOTE)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ exit 2; \ fi python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) afficher --hote $(HOTE) -appliquer: ansible-runtime +appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE= @if [[ -z "$(GROUPE)" ]]; then \ printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \ exit 2; \ @@ -295,7 +295,7 @@ appliquer: ansible-runtime python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-groupe --groupe $(GROUPE) ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)' -deployer: _instance-requise +deployer: _instance-requise ## Deploie un hote, couche par couche, dans l'ordre du graphe — HOTE= @set -e; \ if [[ -z "$(HOTE)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ @@ -328,15 +328,15 @@ deployer: _instance-requise $(MAKE) verifier-hote LIMITE="$(HOTE)" .PHONY: site site-verifier deployer-tout -site: ansible-runtime +site: ansible-runtime ## Regenere playbooks/site.yml depuis les couches et le graphe de dependances python3 scripts/orchestrer.py ecrire ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/site.yml --syntax-check -site-verifier: +site-verifier: ## Verifie que playbooks/site.yml correspond aux couches declarees python3 scripts/orchestrer.py verifier .PHONY: flux flux-verifier -flux: ansible-runtime +flux: ansible-runtime ## Regenere le registre des flux et les regles nftables depuis les meta/flux.yml python3 scripts/resoudre_flux.py registre python3 scripts/resoudre_flux.py nftables @@ -455,39 +455,39 @@ frontiere-appliquer: ansible-runtime ## Reconcilie la frontiere : cree ce qui ma devis-proxmox-fw: ansible-runtime ## Devis pare-feu Proxmox (est-ouest intra-tenant), derive du registre des flux python3 scripts/devis_proxmox_fw.py $(if $(JSON),--json,) -devis-proxmox-fw-verifier: +devis-proxmox-fw-verifier: ## Verifie le devis du pare-feu est-ouest Proxmox (aucune ecriture) python3 scripts/devis_proxmox_fw.py --verifier .PHONY: devis-proxmox-pools devis-proxmox-pools-verifier devis-proxmox-pools: ansible-runtime ## Devis des pools Proxmox (un par tenant), derive du plan python3 scripts/devis_proxmox_pools.py $(if $(JSON),--json,) -devis-proxmox-pools-verifier: +devis-proxmox-pools-verifier: ## Verifie le devis des pools Proxmox (aucune ecriture) python3 scripts/devis_proxmox_pools.py --verifier .PHONY: devis-sdn devis-sdn-verifier devis-sdn: ansible-runtime ## Devis SDN EVPN (zone + VNets + sous-reseaux par tenant), derive du seed python3 scripts/devis_sdn.py $(if $(JSON),--json,) -devis-sdn-verifier: +devis-sdn-verifier: ## Verifie le devis SDN EVPN — zones, VNets, sous-reseaux (aucune ecriture) python3 scripts/devis_sdn.py --verifier -devis-opnsense-verifier: +devis-opnsense-verifier: ## Verifie le devis de la frontiere nord/sud (aucune ecriture) python3 scripts/devis_opnsense.py --verifier .PHONY: underlay underlay: ## Underlay (fabric physique cluster-global : mgmt/iSCSI/Ceph) : affiche + valide (P23) python3 scripts/underlay.py -flux-verifier: +flux-verifier: ## Verifie que le registre des flux correspond aux meta/flux.yml des roles python3 scripts/resoudre_flux.py verifier .PHONY: valider -valider: ansible-runtime +valider: ansible-runtime ## Passe la recette de validation sur la flotte ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/valider.yml .PHONY: wiki-publier -wiki-publier: +wiki-publier: ## Publie le wiki (wiki/) vers la forge @set -e; \ if [[ -z "$(WIKI_REMOTE)" ]]; then \ printf '%s\n' 'Refus: URL du wiki Forgejo requise.'; \ @@ -524,7 +524,7 @@ wiki-publier: git push --quiet; \ printf '%s\n' 'Wiki publie.' -deployer-tout: _instance-requise +deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des couches @set -e; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: deploiement ORCHESTRE de TOUTE la flotte (action impactante).'; \ @@ -553,7 +553,7 @@ deployer-tout: _instance-requise # --- Reconstruction from-zero : creer TOUTES les VM (2a) puis deployer (2b) --- .PHONY: flotte-creer -flotte-creer: _instance-requise +flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le plan @set -e; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: creation de TOUTES les VM actives du plan (clone Proxmox).'; \ @@ -580,7 +580,7 @@ _attendre-flotte: ansible-runtime printf 'Flotte joignable.\n' .PHONY: reconstruire -reconstruire: _instance-requise +reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM puis deploiement complet @set -e; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: RECONSTRUCTION — cree les VM manquantes (2a) PUIS deploie tout (2b).'; \ @@ -597,7 +597,7 @@ reconstruire: _instance-requise .PHONY: myDay myDay: reconstruire -deployer-groupe: +deployer-groupe: ## Deploie un seul groupe sur toute la flotte — GROUPE= @if [[ -z "$(GROUPE)" ]]; then \ printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \ exit 2; \ @@ -605,7 +605,7 @@ deployer-groupe: $(MAKE) appliquer GROUPE="$(GROUPE)" .PHONY: verifier-deploiement -verifier-deploiement: ansible-runtime +verifier-deploiement: ansible-runtime ## Verifie l'etat de la flotte apres deploiement @set -e; \ if [[ -z "$(HOTE)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ @@ -634,7 +634,7 @@ verifier-deploiement: ansible-runtime ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)" --check --diff; \ done -cloner-vm: ansible-runtime +cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE= VMID= @if [[ -z "$(HOTE)" || -z "$(VMID)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.'; \ exit 2; \ @@ -704,7 +704,7 @@ cloner-vm: ansible-runtime fi; \ ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) "$${vault_args[@]}" "$${extra_vars[@]}" -creer-vm: _instance-requise +creer-vm: _instance-requise ## Cree une VM et attend qu'elle soit joignable — HOTE= @set -e; \ if [[ -z "$(HOTE)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote (declare dans le plan).'; \ @@ -741,7 +741,7 @@ creer-vm: _instance-requise $(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \ fi -inventaire-verifier: ansible-runtime _instance-requise +inventaire-verifier: ansible-runtime _instance-requise ## Verifie que l'inventaire se parse (voute dechiffree) ansible-inventory -i $(INVENTAIRE_LAB) --list > /dev/null ansible-inventory -i $(INVENTAIRE_PRODUCTION) --list > /dev/null python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) verifier-playbooks --dossier-playbooks $(DOSSIER_PLAYBOOKS_GROUPES) @@ -753,61 +753,61 @@ inventaire-verifier: ansible-runtime _instance-requise python3 scripts/domaines.py verifier .PHONY: bases bases-verifier domaines domaines-verifier applications applications-verifier applications-bootstrap serveurs serveurs-verifier serveurs-bootstrap -serveurs: +serveurs: ## Liste les serveurs declares au plan python3 scripts/serveurs.py lister -serveurs-verifier: +serveurs-verifier: ## Valide le registre des serveurs python3 scripts/serveurs.py verifier -serveurs-bootstrap: +serveurs-bootstrap: ## Amorce l'acces SSH aux serveurs neufs python3 scripts/serveurs.py bootstrap .PHONY: instancier instancier-appliquer -instancier: _instance-requise +instancier: _instance-requise ## Genere hosts.yml depuis le plan (sans l'appliquer) python3 scripts/instancier.py generer python3 scripts/instancier.py comparer -instancier-appliquer: _instance-requise +instancier-appliquer: _instance-requise ## Applique l'inventaire genere — FORCE=1 pour passer outre le diff python3 scripts/instancier.py appliquer $(if $(FORCE),--force) -bases: +bases: ## Liste les bases de donnees declarees au plan python3 scripts/bases_donnees.py lister -bases-verifier: +bases-verifier: ## Valide le registre des bases de donnees python3 scripts/bases_donnees.py verifier -domaines: +domaines: ## Liste les domaines declares au plan python3 scripts/domaines.py lister -domaines-verifier: +domaines-verifier: ## Valide le registre des domaines python3 scripts/domaines.py verifier -applications: +applications: ## Liste les applications declarees au plan python3 scripts/applications.py lister -applications-verifier: +applications-verifier: ## Valide le registre des applications python3 scripts/applications.py verifier -applications-bootstrap: +applications-bootstrap: ## Amorce les applications declarees au plan python3 scripts/applications.py bootstrap -inventaire-lister: ansible-runtime +inventaire-lister: ansible-runtime ## Affiche l'inventaire complet (JSON) ansible-inventory -i $(FICHIER_INVENTAIRE) --list -inventaire-graphe: ansible-runtime +inventaire-graphe: ansible-runtime ## Affiche le graphe des groupes de l'inventaire ansible-inventory -i $(FICHIER_INVENTAIRE) --graph -inventaire-hote: ansible-runtime +inventaire-hote: ansible-runtime ## Affiche les variables derivees d'un hote — HOTE= @if [[ -z "$(HOTE)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ exit 2; \ fi ansible-inventory -i $(FICHIER_INVENTAIRE) --host $(HOTE) -inventaire-lab: +inventaire-lab: ## Affiche le graphe de l'inventaire de laboratoire $(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_LAB)" -inventaire-production: +inventaire-production: ## Affiche le graphe de l'inventaire de production $(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_PRODUCTION)" .PHONY: _verifier-acces-modele _verifier-privileges-modele preparer-modele verifier-modele nettoyer-modele @@ -817,13 +817,13 @@ _verifier-acces-modele: ansible-runtime _verifier-privileges-modele: ansible-runtime ansible -i $(INVENTAIRE_LAB) $(GROUPE_MODELE) -b -m command -a "whoami" -preparer-modele: ansible-runtime _verifier-acces-modele _verifier-privileges-modele +preparer-modele: ansible-runtime _verifier-acces-modele _verifier-privileges-modele ## Prepare le gabarit dore (VM de reference clonee pour chaque hote) ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_PREPARER_MODELE) -verifier-modele: ansible-runtime +verifier-modele: ansible-runtime ## Verifie le gabarit dore ansible-playbook -i $(INVENTAIRE_LAB) $(PLAYBOOK_VERIFIER_MODELE) -nettoyer-modele: ansible-runtime +nettoyer-modele: ansible-runtime ## Nettoie le gabarit avant capture — exige CONFIRMER=true @if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: relancer avec CONFIRMER=true pour le nettoyage final du modele.'; \ exit 2; \ @@ -880,8 +880,8 @@ _verifier-acces-hote: ansible-runtime _verifier-privileges-hote: ansible-runtime ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m command -a "whoami" -faits: ansible-runtime +faits: ansible-runtime ## Interroge les faits Ansible de la flotte — LIMITE= ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m setup -a "filter=ansible_distribution*" -verifier-hote: ansible-runtime +verifier-hote: ansible-runtime ## Passe le playbook de verification sur un hote ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) $(OPTIONS_PLAYBOOK) diff --git a/docs/audit/preuve-2026-08-08.md b/docs/audit/preuve-2026-08-08.md index 2853877..4a07e56 100644 --- a/docs/audit/preuve-2026-08-08.md +++ b/docs/audit/preuve-2026-08-08.md @@ -7,7 +7,7 @@ > [`docs/audit/affirmations.md`](affirmations.md). - **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml` -- **Verdict** : ✅ CONFORME (29 OK · 0 echec · 1 saute) +- **Verdict** : ✅ CONFORME (30 OK · 0 echec · 1 saute) ## Preuves @@ -43,6 +43,7 @@ | 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 | 35 scripts expliques et atteignables, 85 cibles make documentees, 54 roles avec README. | ## Couverture des affirmations ✅ du registre diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index 14bd933..95acb38 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -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-.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` — **66 décisions en vigueur** (D-01 → D-69, 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` — **67 décisions en vigueur** (D-01 → D-70, 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` | diff --git a/docs/decisions-architecture.md b/docs/decisions-architecture.md index c175ce3..b986daa 100644 --- a/docs/decisions-architecture.md +++ b/docs/decisions-architecture.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-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** | --- diff --git a/scripts/prouver.py b/scripts/prouver.py index 8b02d38..f702d12 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -21,7 +21,9 @@ Usage : from __future__ import annotations import datetime as _dt +import ast import json +import re import os import subprocess import sys @@ -397,6 +399,95 @@ def preuve_inventaire_ansible() -> tuple[bool, str]: return True, f"{n_hotes} hotes, {n_groupes} groupes (inventaire dechiffre et parse)." +def preuve_documentation_outillage() -> tuple[bool, str]: + """Tout ce que Set-OPS FAIT s'explique et reste atteignable. + + Exigence de l'exploitant, 2026-08-08 : « la doc dit et explique tout ce que Set-OPS + fait, et pourquoi c'est ainsi. » Une exigence qu'on n'outille pas pourrit en silence + — la carte annoncait « 28 decisions » quand il y en avait 66, et disait les acces + « non construits » alors qu'ils tournaient en production. + + CE QU'ELLE TESTE, et pourquoi ainsi : + + - chaque `scripts/*.py` porte une docstring de module dont la premiere ligne + explique a quoi il sert. Le premier jet verifiait plutot que le nom du script + « apparaisse dans un document » : deux fois ce critere s'est revele creux — + d'abord parce que le rapport d'audit GENERE recopiait les noms manquants dans + son message d'echec, ensuite parce qu'un inventaire genere de l'outillage + aurait fait passer la preuve au vert sans qu'une ligne soit ecrite. Un critere + qu'on peut satisfaire en generant du texte ne prouve rien ; + - chaque script est ATTEIGNABLE : invoque par une cible `make`, ou importe/appele + par un autre script (bibliotheques partagees, scripts appeles par les preuves). + Un outil que rien n'atteint est du code mort qui se documente tout seul ; + - chaque cible `make` porte un texte d'aide `##`, SAUF celles prefixees `_` : + convention du depot pour les cibles internes (attentes, verifications de + privileges) qui ne sont pas des commandes d'exploitant. L'exemption est nommee + ici pour rester un choix et non un trou ; + - chaque role porte un README. + + CE QU'ELLE NE TESTE PAS : que l'explication soit BONNE. Le « pourquoi » se juge en + revue ; il vit dans `CHANGELOG.md` (les faits mesures) et dans + `decisions-architecture.md`. Pretendre le mesurer mecaniquement serait se mentir. + """ + manques: list[str] = [] + scripts = sorted((RACINE / "scripts").glob("*.py")) + textes = {f.name: f.read_text(encoding="utf-8", errors="ignore") for f in scripts} + makefile = (RACINE / "Makefile").read_text(encoding="utf-8", errors="ignore") + + # `ast`, pas une expression reguliere : les scripts commencent par un shebang, et + # mon premier motif le prenait pour l'absence de docstring — 35 faux positifs d'un + # coup. Lire du Python avec le parseur de Python. + muets = [] + for f in scripts: + try: + doc = ast.get_docstring(ast.parse(textes[f.name])) or "" + except SyntaxError: + muets.append(f"{f.name} (illisible)") + continue + if len(doc.strip().splitlines()[0] if doc.strip() else "") < 30: + muets.append(f.name) + if muets: + manques.append(f"{len(muets)} script(s) sans docstring explicative : " + + ", ".join(muets[:6]) + ("…" if len(muets) > 6 else "")) + + injoignables = [] + for f in scripts: + autres = "\n".join(v for k, v in textes.items() if k != f.name) + module = f.stem + if f"scripts/{f.name}" in makefile: + continue + if module in autres: # importe ou appele par un autre outil + continue + injoignables.append(f.name) + if injoignables: + manques.append(f"{len(injoignables)} script(s) qu'aucune cible ni aucun outil " + f"n'atteint : " + ", ".join(injoignables[:6])) + + muettes = [ + ligne.split(":")[0] + for ligne in makefile.splitlines() + if re.match(r"^[a-z][a-z0-9_-]*:", ligne) and "##" not in ligne + ] + if muettes: + manques.append(f"{len(muettes)} cible(s) make sans texte d'aide `##` : " + + ", ".join(sorted(muettes)[:6]) + ("…" if len(muettes) > 6 else "")) + + sans_readme = sorted( + d.name for d in (RACINE / "roles").iterdir() + if d.is_dir() and not (d / "README.md").exists() + ) + if sans_readme: + manques.append(f"{len(sans_readme)} role(s) sans README : " + ", ".join(sans_readme[:6])) + + if manques: + return False, " | ".join(manques) + + n_c = len([l for l in makefile.splitlines() if re.match(r"^[a-z][a-z0-9_-]*:", l)]) + n_r = len([d for d in (RACINE / "roles").iterdir() if d.is_dir()]) + return True, (f"{len(scripts)} scripts expliques et atteignables, " + f"{n_c} cibles make documentees, {n_r} roles avec README.") + + # --- Registre des preuves : (id, titre, refs AFF, executeur) ----------------------- # # executeur = liste de commandes argv (toutes doivent renvoyer 0), ou callable -> (ok, detail). @@ -472,6 +563,8 @@ PREUVES: list[dict] = [ "func": preuve_authentification}, {"id": "P30", "titre": "SDN EVPN : zones, VNets et sous-reseaux derives", "refs": ["AFF-112"], "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}, ]