D-70 / P31 : l'exigence de documentation devient une preuve

Directive de l'exploitant : la doc dit et explique tout ce que Set-OPS fait.
Une exigence seulement enoncee pourrit en silence — trois exemples le jour
meme dans la carte.

Ecart mesure : 66 cibles make sur 85 sans texte d'aide (make aide en montrait
19), 11 scripts sur 35 cites nulle part. Les 66 cibles ont recu leur aide :
85 commandes documentees.

P31 garde le couvert. Le chemin pour l'ecrire a ete instructif : deux fois mon
critere s'est revele creux. D'abord « le nom apparait dans un document » — le
rapport d'audit GENERE recopiait les noms manquants dans son message d'echec.
Puis j'ai failli refaire le trou en plus grand : generer un inventaire de
l'outillage aurait satisfait le critere par construction. Un critere qu'on
peut satisfaire en generant du texte ne prouve rien.

P31 teste donc que chaque script porte une docstring qui l'explique et reste
ATTEIGNABLE (cible make ou autre outil), que chaque cible porte son aide (sauf
les internes prefixees _, exemption nommee), que chaque role a son README.
Verifiee dans les deux sens.

Ce qu'elle ne garde pas, et c'est dit dans son code : que l'explication soit
bonne. Le pourquoi se juge en revue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-08 13:21:23 -04:00
parent c5fd3aa70b
commit 5308574730
6 changed files with 202 additions and 68 deletions

View file

@ -1,5 +1,44 @@
# CHANGELOG — Set-OPS # 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 ## 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 Question de l'exploitant : faut-il refondre la documentation ? **Non.** L'état mesuré ne le

132
Makefile
View file

@ -80,7 +80,7 @@ DOSSIER_PLAYBOOKS_GROUPES := playbooks/groupes
.DEFAULT_GOAL := aide .DEFAULT_GOAL := aide
.PHONY: ansible-runtime .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_LOCAL_TEMP)"
@mkdir -p "$(ANSIBLE_SSH_CONTROL_PATH_DIR)" @mkdir -p "$(ANSIBLE_SSH_CONTROL_PATH_DIR)"
@ -95,7 +95,7 @@ _instance-requise:
fi fi
.PHONY: aide .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' 'Set-OPS — moteur d ecosystemes numeriques souverains'
@printf '%s\n' '' @printf '%s\n' ''
@printf '%s\n' 'Nouveau ? -> QUICKSTART.md (de zero a ton ecosysteme sur Proxmox)' @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' @printf '%s\n' ' FICHIER_INVENTAIRE=$(SETOPS_INSTANCE)/inventories/production/hosts.yml FICHIER_DEPENDANCES=docs/dependances-groupes.yml CONFIRMER=true'
.PHONY: lint .PHONY: lint
lint: ansible-runtime lint: ansible-runtime ## Passe ansible-lint sur tout le depot
ansible-lint ansible-lint
.PHONY: syntaxe syntaxe-modele syntaxe-nettoyage syntaxe-verification-modele syntaxe-verification-hote syntaxe-groupes syntaxe-proxmox .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 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 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 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 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 \ @for playbook in $(DOSSIER_PLAYBOOKS_GROUPES)/*.yml; do \
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --syntax-check; \ ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --syntax-check; \
done 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 ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) --syntax-check
.PHONY: test .PHONY: test
test: test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
python3 scripts/tests/test_inventory_host.py python3 scripts/tests/test_inventory_host.py
.PHONY: verifier .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 python3 scripts/prouver.py --verifier
# Harnais de preuve : rejoue les preuves automatisables du registre et ecrit # Harnais de preuve : rejoue les preuves automatisables du registre et ecrit
# docs/audit/preuve-<date>.md (piece justificative horodatee, rejouable). # docs/audit/preuve-<date>.md (piece justificative horodatee, rejouable).
# `make verifier` l'appelle en mode --verifier (preuves seules, aucun rapport ecrit). # `make verifier` l'appelle en mode --verifier (preuves seules, aucun rapport ecrit).
.PHONY: prouver .PHONY: prouver
prouver: ansible-runtime _instance-requise prouver: ansible-runtime _instance-requise ## Execute les preuves et ecrit docs/audit/preuve-<date>.md
python3 scripts/prouver.py 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 .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 # 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 : 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=<dossier>
@if [[ -z "$(NOM)" ]]; then printf '%s\n' "Usage: make instance-utiliser NOM=<dossier-frère> (ex. OPS-Chezlepro-lab)"; exit 2; fi @if [[ -z "$(NOM)" ]]; then printf '%s\n' "Usage: make instance-utiliser NOM=<dossier-frère> (ex. OPS-Chezlepro-lab)"; exit 2; fi
@if [[ ! -d "../$(NOM)" ]]; then printf '%s\n' "Introuvable: ../$(NOM)"; 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 @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 @rm -f instance && ln -s "../$(NOM)" instance
@printf 'instance -> %s\n' "$$(readlink 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é)')" @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, # 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. # 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 @python3 scripts/instances.py
# (Re)génère le plan de recette (docs/audit/plan-de-recette.md) depuis les exercices # (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. # 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 @python3 scripts/plan_recette.py
# Liste les modèles disponibles (socle + SETOPS_MODELES) pour créer une instance. # 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 @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. # 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 # Ex. : make instance-creer NOM=OPS-ClientX MODELE=socle INDEX=4
instance-creer: instance-creer: ## Cree un nouvel ecosysteme depuis un modele — NOM=<nom> MODELE=<modele>
@python3 scripts/instance_creer.py --nom "$(NOM)" --modele "$(MODELE)" \ @python3 scripts/instance_creer.py --nom "$(NOM)" --modele "$(MODELE)" \
$(if $(INDEX),--index $(INDEX),) $(if $(INDEX),--index $(INDEX),)
# Crée un MODÈLE (dépôt privé). Deux modes : # 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 # 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 # instance : promouvoir une instance -> make model-creer MODE=instance SOURCE=OPS-Chezlepro NOM=cabinet
model-creer: model-creer: ## Cree un modele d'ecosysteme — MODE=<mode> NOM=<nom>
@python3 scripts/model_creer.py --mode "$(MODE)" --nom "$(NOM)" \ @python3 scripts/model_creer.py --mode "$(MODE)" --nom "$(NOM)" \
$(if $(BASE),--base $(BASE),) $(if $(SOURCE),--source $(SOURCE),) $(if $(DEST),--dest $(DEST),) $(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 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) python3 scripts/inventory_gui.py --inventaire $(FICHIER_INVENTAIRE)
hote-ajouter hote-planifier hote-groupes: 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.)' @printf '%s\n' '(creer-vm lit desormais VMID/IP/VLAN/passerelle directement dans l inventaire genere.)'
@exit 2 @exit 2
hote-afficher: ansible-runtime hote-afficher: ansible-runtime ## Affiche tout ce que le plan derive pour un hote — HOTE=<nom>
@if [[ -z "$(HOTE)" ]]; then \ @if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
exit 2; \ exit 2; \
fi fi
python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) afficher --hote $(HOTE) python3 scripts/inventory_host.py --inventaire $(FICHIER_INVENTAIRE) afficher --hote $(HOTE)
appliquer: ansible-runtime appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
@if [[ -z "$(GROUPE)" ]]; then \ @if [[ -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \ printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
exit 2; \ 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) 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)' 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=<nom>
@set -e; \ @set -e; \
if [[ -z "$(HOTE)" ]]; then \ if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
@ -328,15 +328,15 @@ deployer: _instance-requise
$(MAKE) verifier-hote LIMITE="$(HOTE)" $(MAKE) verifier-hote LIMITE="$(HOTE)"
.PHONY: site site-verifier deployer-tout .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 python3 scripts/orchestrer.py ecrire
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/site.yml --syntax-check 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 python3 scripts/orchestrer.py verifier
.PHONY: flux flux-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 registre
python3 scripts/resoudre_flux.py nftables 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 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,) 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 python3 scripts/devis_proxmox_fw.py --verifier
.PHONY: devis-proxmox-pools devis-proxmox-pools-verifier .PHONY: devis-proxmox-pools devis-proxmox-pools-verifier
devis-proxmox-pools: ansible-runtime ## Devis des pools Proxmox (un par tenant), derive du plan devis-proxmox-pools: ansible-runtime ## Devis des pools Proxmox (un par tenant), derive du plan
python3 scripts/devis_proxmox_pools.py $(if $(JSON),--json,) 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 python3 scripts/devis_proxmox_pools.py --verifier
.PHONY: devis-sdn devis-sdn-verifier .PHONY: devis-sdn devis-sdn-verifier
devis-sdn: ansible-runtime ## Devis SDN EVPN (zone + VNets + sous-reseaux par tenant), derive du seed devis-sdn: ansible-runtime ## Devis SDN EVPN (zone + VNets + sous-reseaux par tenant), derive du seed
python3 scripts/devis_sdn.py $(if $(JSON),--json,) 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 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 python3 scripts/devis_opnsense.py --verifier
.PHONY: underlay .PHONY: underlay
underlay: ## Underlay (fabric physique cluster-global : mgmt/iSCSI/Ceph) : affiche + valide (P23) underlay: ## Underlay (fabric physique cluster-global : mgmt/iSCSI/Ceph) : affiche + valide (P23)
python3 scripts/underlay.py 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 python3 scripts/resoudre_flux.py verifier
.PHONY: valider .PHONY: valider
valider: ansible-runtime valider: ansible-runtime ## Passe la recette de validation sur la flotte
ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/valider.yml ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/valider.yml
.PHONY: wiki-publier .PHONY: wiki-publier
wiki-publier: wiki-publier: ## Publie le wiki (wiki/) vers la forge
@set -e; \ @set -e; \
if [[ -z "$(WIKI_REMOTE)" ]]; then \ if [[ -z "$(WIKI_REMOTE)" ]]; then \
printf '%s\n' 'Refus: URL du wiki Forgejo requise.'; \ printf '%s\n' 'Refus: URL du wiki Forgejo requise.'; \
@ -524,7 +524,7 @@ wiki-publier:
git push --quiet; \ git push --quiet; \
printf '%s\n' 'Wiki publie.' printf '%s\n' 'Wiki publie.'
deployer-tout: _instance-requise deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des couches
@set -e; \ @set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \ if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: deploiement ORCHESTRE de TOUTE la flotte (action impactante).'; \ 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) --- # --- Reconstruction from-zero : creer TOUTES les VM (2a) puis deployer (2b) ---
.PHONY: flotte-creer .PHONY: flotte-creer
flotte-creer: _instance-requise flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le plan
@set -e; \ @set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \ if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: creation de TOUTES les VM actives du plan (clone Proxmox).'; \ 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' printf 'Flotte joignable.\n'
.PHONY: reconstruire .PHONY: reconstruire
reconstruire: _instance-requise reconstruire: _instance-requise ## Reconstruit un ecosysteme depuis zero : VM puis deploiement complet
@set -e; \ @set -e; \
if [[ "$(CONFIRMER)" != "true" ]]; then \ if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: RECONSTRUCTION — cree les VM manquantes (2a) PUIS deploie tout (2b).'; \ printf '%s\n' 'Refus: RECONSTRUCTION — cree les VM manquantes (2a) PUIS deploie tout (2b).'; \
@ -597,7 +597,7 @@ reconstruire: _instance-requise
.PHONY: myDay .PHONY: myDay
myDay: reconstruire myDay: reconstruire
deployer-groupe: deployer-groupe: ## Deploie un seul groupe sur toute la flotte — GROUPE=<groupe>
@if [[ -z "$(GROUPE)" ]]; then \ @if [[ -z "$(GROUPE)" ]]; then \
printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \ printf '%s\n' 'Refus: relancer avec GROUPE=nom_groupe.'; \
exit 2; \ exit 2; \
@ -605,7 +605,7 @@ deployer-groupe:
$(MAKE) appliquer GROUPE="$(GROUPE)" $(MAKE) appliquer GROUPE="$(GROUPE)"
.PHONY: verifier-deploiement .PHONY: verifier-deploiement
verifier-deploiement: ansible-runtime verifier-deploiement: ansible-runtime ## Verifie l'etat de la flotte apres deploiement
@set -e; \ @set -e; \
if [[ -z "$(HOTE)" ]]; then \ if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ 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; \ ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)" --check --diff; \
done done
cloner-vm: ansible-runtime cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE=<nom> VMID=<id>
@if [[ -z "$(HOTE)" || -z "$(VMID)" ]]; then \ @if [[ -z "$(HOTE)" || -z "$(VMID)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.'; \ printf '%s\n' 'Refus: relancer avec HOTE=nom VMID=id_clone.'; \
exit 2; \ exit 2; \
@ -704,7 +704,7 @@ cloner-vm: ansible-runtime
fi; \ fi; \
ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) "$${vault_args[@]}" "$${extra_vars[@]}" 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=<nom>
@set -e; \ @set -e; \
if [[ -z "$(HOTE)" ]]; then \ if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote (declare dans le plan).'; \ 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)"; \ $(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \
fi 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_LAB) --list > /dev/null
ansible-inventory -i $(INVENTAIRE_PRODUCTION) --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) 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 python3 scripts/domaines.py verifier
.PHONY: bases bases-verifier domaines domaines-verifier applications applications-verifier applications-bootstrap serveurs serveurs-verifier serveurs-bootstrap .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 python3 scripts/serveurs.py lister
serveurs-verifier: serveurs-verifier: ## Valide le registre des serveurs
python3 scripts/serveurs.py verifier python3 scripts/serveurs.py verifier
serveurs-bootstrap: serveurs-bootstrap: ## Amorce l'acces SSH aux serveurs neufs
python3 scripts/serveurs.py bootstrap python3 scripts/serveurs.py bootstrap
.PHONY: instancier instancier-appliquer .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 generer
python3 scripts/instancier.py comparer 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) python3 scripts/instancier.py appliquer $(if $(FORCE),--force)
bases: bases: ## Liste les bases de donnees declarees au plan
python3 scripts/bases_donnees.py lister python3 scripts/bases_donnees.py lister
bases-verifier: bases-verifier: ## Valide le registre des bases de donnees
python3 scripts/bases_donnees.py verifier python3 scripts/bases_donnees.py verifier
domaines: domaines: ## Liste les domaines declares au plan
python3 scripts/domaines.py lister python3 scripts/domaines.py lister
domaines-verifier: domaines-verifier: ## Valide le registre des domaines
python3 scripts/domaines.py verifier python3 scripts/domaines.py verifier
applications: applications: ## Liste les applications declarees au plan
python3 scripts/applications.py lister python3 scripts/applications.py lister
applications-verifier: applications-verifier: ## Valide le registre des applications
python3 scripts/applications.py verifier python3 scripts/applications.py verifier
applications-bootstrap: applications-bootstrap: ## Amorce les applications declarees au plan
python3 scripts/applications.py bootstrap python3 scripts/applications.py bootstrap
inventaire-lister: ansible-runtime inventaire-lister: ansible-runtime ## Affiche l'inventaire complet (JSON)
ansible-inventory -i $(FICHIER_INVENTAIRE) --list 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 ansible-inventory -i $(FICHIER_INVENTAIRE) --graph
inventaire-hote: ansible-runtime inventaire-hote: ansible-runtime ## Affiche les variables derivees d'un hote — HOTE=<nom>
@if [[ -z "$(HOTE)" ]]; then \ @if [[ -z "$(HOTE)" ]]; then \
printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote.'; \
exit 2; \ exit 2; \
fi fi
ansible-inventory -i $(FICHIER_INVENTAIRE) --host $(HOTE) 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)" $(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)" $(MAKE) inventaire-graphe FICHIER_INVENTAIRE="$(INVENTAIRE_PRODUCTION)"
.PHONY: _verifier-acces-modele _verifier-privileges-modele preparer-modele verifier-modele nettoyer-modele .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 _verifier-privileges-modele: ansible-runtime
ansible -i $(INVENTAIRE_LAB) $(GROUPE_MODELE) -b -m command -a "whoami" 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) 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) 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 \ @if [[ "$(CONFIRMER)" != "true" ]]; then \
printf '%s\n' 'Refus: relancer avec CONFIRMER=true pour le nettoyage final du modele.'; \ printf '%s\n' 'Refus: relancer avec CONFIRMER=true pour le nettoyage final du modele.'; \
exit 2; \ exit 2; \
@ -880,8 +880,8 @@ _verifier-acces-hote: ansible-runtime
_verifier-privileges-hote: ansible-runtime _verifier-privileges-hote: ansible-runtime
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m command -a "whoami" ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -b -m command -a "whoami"
faits: ansible-runtime faits: ansible-runtime ## Interroge les faits Ansible de la flotte — LIMITE=<motif>
ansible -i $(INVENTAIRE_PRODUCTION) $(LIMITE) -m setup -a "filter=ansible_distribution*" 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) ansible-playbook -i $(INVENTAIRE_PRODUCTION) $(PLAYBOOK_VERIFIER_HOTE) $(OPTIONS_PLAYBOOK)

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md). > [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml` - **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 ## 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. | | 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 | | 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. | | 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 ## Couverture des affirmations ✅ du registre

View file

@ -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`) | | **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é ?* | | **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` | | **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`**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 | | **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 | | **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` | | **Exploitation courante** | `docs/runbooks-exploitation.md`, `docs/intrants-communs.md`, `docs/intrants-base-gui-conception.md`, `docs/theme-forgejo-hors-flotte.md` |

View file

@ -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-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-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-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** |
--- ---

View file

@ -21,7 +21,9 @@ Usage :
from __future__ import annotations from __future__ import annotations
import datetime as _dt import datetime as _dt
import ast
import json import json
import re
import os import os
import subprocess import subprocess
import sys 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)." 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) ----------------------- # --- Registre des preuves : (id, titre, refs AFF, executeur) -----------------------
# #
# executeur = liste de commandes argv (toutes doivent renvoyer 0), ou callable -> (ok, detail). # executeur = liste de commandes argv (toutes doivent renvoyer 0), ou callable -> (ok, detail).
@ -472,6 +563,8 @@ PREUVES: list[dict] = [
"func": preuve_authentification}, "func": preuve_authentification},
{"id": "P30", "titre": "SDN EVPN : zones, VNets et sous-reseaux derives", "refs": ["AFF-112"], {"id": "P30", "titre": "SDN EVPN : zones, VNets et sous-reseaux derives", "refs": ["AFF-112"],
"cmds": [[sys.executable, "scripts/devis_sdn.py", "--verifier"]]}, "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},
] ]