Plan de recette (P22) : le pendant manuel de make prouver

Les 78 exercices « À toi de jouer » du wiki forment un plan de tests d'acceptation.
Formalisé sans dupliquer :

- scripts/plan_recette.py + make plan-recette : GÉNÈRE docs/audit/plan-de-recette.md
  depuis les exercices du wiki. Grille auto-contenue par unité, colonnes : ce qu'on
  éprouve · le geste · type (observe/casse-répare) · Preuve auto (le Pxx extrait du
  texte -> quels gestes manuels sont AUSSI gardés par la machine). Générée -> ne peut
  pas dériver du wiki.
- Preuve P22 : plan_recette.py --verifier échoue si le fichier committé est périmé.
  Le plan de recette devient auto-gardé.
- Honnêteté de couverture assumée : « — » = manuel seul ; pas d'exhaustivité au-delà
  des exercices du wiki.

Pendant humain de make prouver (le harnais prouve le moteur P01-P21, la recette valide
l'exploitation) ; checklist du protocole-operateur-independant (« exploitable sans IA »).

Validé : 78 gestes / 19 unités, 5 doublés d'un Pxx ; P22 détecte une dérive (testé) ;
make verifier -> CONFORME 22/22 (instance cohérente).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-07-23 15:49:16 -04:00
parent 150121089d
commit 58015f56e0
6 changed files with 407 additions and 0 deletions

View file

@ -1,5 +1,28 @@
# CHANGELOG — Set-OPS
## 2026-07-23 (suite 6)
### Ajouté — plan de recette (le pendant manuel de `make prouver`)
Constat de l'opérateur : les 78 exercices « ④ À toi de jouer » du wiki forment, ensemble, un
**plan de tests d'acceptation**. Formalisé, sans dupliquer :
- **`scripts/plan_recette.py`** + **`make plan-recette`** — **génère** `docs/audit/plan-de-recette.md`
depuis les exercices du wiki : une grille auto-contenue (le geste inline) par unité, colonnes
*Ce qu'on éprouve · Le geste · Type (observe/casse-répare) · **Preuve auto***. La dernière colonne
est extraite du texte (le `Pxx` que l'exercice mentionne) : elle montre quels gestes manuels sont
**aussi** gardés par la machine. Étant générée, la grille **ne peut pas dériver** du wiki.
- **Preuve P22**`plan_recette.py --verifier` échoue si le fichier committé n'est plus à jour
(le wiki a changé sans régénérer). Le plan de recette devient un artefact **auto-gardé**.
- **Honnêteté de couverture** assumée dans le document : un « — » = **manuel seul** (aucune preuve
machine ne le double) ; le plan ne prétend pas à l'exhaustivité au-delà des exercices du wiki.
C'est le **pendant humain** de `make prouver` : le harnais prouve le *moteur* (P01P21), la recette
valide l'*exploitation* — et sert de checklist au `protocole-operateur-independant.md` (« exploitable
sans IA »).
Validé : 78 gestes sur 19 unités, 5 doublés d'une preuve `Pxx` ; P22 testée (détecte une dérive) ;
`make verifier`**CONFORME 22/22** (contre une instance cohérente).
## 2026-07-23 (suite 5)
### Ajouté — wiki : l'axe « méthode » (KB enrichie)

View file

@ -241,6 +241,11 @@ instance-courante:
instances:
@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:
@python3 scripts/plan_recette.py
# Liste les modèles disponibles (socle + SETOPS_MODELES) pour créer une instance.
instance-modeles:
@python3 scripts/instance_creer.py --lister-modeles

View file

@ -59,6 +59,7 @@ make prouver
| P19 | GUI couvre le plan | tout champ présent dans un plan réel est éditable par le GUI (nomenclature tolérée : le seed `index` est désormais un intrant ; l'adressage est dérivé, donc rien à éditer). |
| P20 | Adressage dérivé du seed | aucune nomenclature ne **stocke** d'adressage (supernet, sous-réseau, passerelle, VLAN) : tout se dérive du seul `index`. Attrape toute rechute vers l'écriture manuelle. |
| P21 | Fédération sans collision | aucune paire d'instances **fédérées** ne partage un `index` (mêmes VLAN/VMID sur le trunk). Le garde-fou du multi-instances ; vue avec `make instances`. |
| P22 | Plan de recette à jour | `docs/audit/plan-de-recette.md` (les 78 gestes manuels, générés des exercices du wiki) est **à jour** — il ne peut pas dériver du wiki. Régénérer : `make plan-recette`. |
> **P17, P18, P19 ferment les angles morts du harnais** : il ne vérifiait qu'*une* instance
> et le seul modèle `socle`. P17 aurait attrapé l'hôte fantôme d'`integral` ; P18, les neuf

View file

@ -0,0 +1,230 @@
# Plan de recette — tests d'acceptation manuels
> **GÉNÉRÉ** depuis les exercices du wiki (`wiki/*.md`, section « ④ À toi de jouer »)
> par `scripts/plan_recette.py`**ne pas éditer à la main** ; régénérer avec
> `make plan-recette`. La preuve **P22** échoue si ce fichier n'est plus à jour.
Ce plan est le **pendant manuel** de `make prouver` : là où le harnais prouve le
*moteur* par machine (P01P21), ces gestes valident l'*exploitation* — ce qu'un
**humain** fait, voit, casse et répare. Ensemble, ils couvrent les deux moitiés ;
le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (cf.
`protocole-operateur-independant.md`).
**78 gestes** sur **19 unités** · **18** en « casse-répare »
· **5** doublés d'un garde-fou machine (colonne *Preuve auto*).
> **Honnêteté de couverture.** La colonne *Preuve auto* n'est remplie que lorsqu'une
> preuve `Pxx` de `make prouver` couvre AUSSI le geste. Un « — » signifie **manuel
> seul** : aucune machine ne le garde, seul l'œil de l'opérateur le valide. Ce plan
> ne prétend pas à l'exhaustivité au-delà des exercices présents dans le wiki.
## Autorisation & RBAC
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Sens le défaut | Avant tout rôle, un nouvel utilisateur SSO est Viewer dans Grafana : il voit le dashboard *Journaux de la flotte*, mais pas l'icône Explore. | 👁 observe | — |
| 2 | Donne Editor | testmail a le rôle grafana-editor → déconnecte/reconnecte-le (Grafana applique le rôle *à la connexion*) → Explore apparaît. | 👁 observe | — |
| 3 | Observe le claim | Dans Keycloak (console admin) → Clients → grafana → *Client scopes**Evaluate* pour testmail : le jeton contient "roles": ["grafana-editor"]. C'est ② en vrai. | 👁 observe | — |
| 4 | Casse & répare | Retire grafana-editor de testmail (ou renomme-le en grafana-viewer), reconnecte : Explore disparaît. Remets-le : il revient. Tu *sens* que c'est le rôle, pas l'identité, qui ouvre la porte. | 🔨 casse-répare | — |
*Source : [Autorisation & RBAC](Autorisation-et-RBAC) · § À toi de jouer.*
## Bases de données
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Liste les bases | (sur data-sql-01) : « commande » | 👁 observe | — |
| 2 | Vois l'isolation | Chaque app a sa base et son compte — pas de compte partagé. | 👁 observe | — |
| 3 | Suis une liaison | Ouvre plan/bases-donnees.yml : l'entrée keycloak (serveur, base, propriétaire, secret) est *exactement* ce que le rôle serveur_keycloak va lire pour se connecter. | 👁 observe | — |
| 4 | Casse & répare | Change le mot de passe d'un compte dans PostgreSQL (garde l'ancien !) sans mettre à jour la voûte : l'app ne se connecte plus. Restaure : ça repart. Tu *sens* que la connexion = *identité + secret cohérents des deux côtés*. | 🔨 casse-répare | — |
*Source : [Bases de données](Bases-de-données) · § À toi de jouer.*
## Cache
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Écris/lis avec authentification | (sur data-sql-01) : « commande » | 👁 observe | — |
| 2 | Teste le TTL | : redis-cli -a … SET court 1 EX 5 puis GET court avant/après 5 s — il expire. | 👁 observe | — |
| 3 | Vois la borne | : redis-cli -a … CONFIG GET maxmemory et … maxmemory-policy (LRU). | 👁 observe | — |
| 4 | Casse & répare | Interroge sans mot de passe : redis-cli GET essai:1 → NOAUTH (refusé). Puis vide le cache (FLUSHALL) : rien ne casse dans l'écosystème — la vraie donnée est en base. Tu *sens* qu'un cache est jetable. | 🔨 casse-répare | — |
*Source : [Cache](Cache) · § À toi de jouer.*
## Courriel (SMTP / IMAP)
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Envoie via la soumission `:587` | (client authentifié) : « commande » 235 Authentication successful puis 250 queued = ① en action. | 👁 observe | — |
| 2 | Lis la boîte | (le MDA) : doveadm search -u testmail mailbox INBOX all \| wc -l sur infra-mail-01. | 👁 observe | — |
| 3 | Casse & répare | Coupe l'annuaire (arrête OpenLDAP), renvoie un courriel : Postfix **rejette le destinataire** (il ne peut plus valider en LDAP). Rallume OpenLDAP : ça repart. Tu *sens* la dépendance requise MTA → annuaire. | 🔨 casse-répare | — |
*Source : [Courriel (SMTP / IMAP)](Courriel) · § À toi de jouer.*
## DNS & résolution de noms
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Le plancher, sans DNS | Sur un nœud : « commande » | 👁 observe | — |
| 2 | Interroge l'autoritatif | Demande à PowerDNS directement : « commande » | 👁 observe | — |
| 3 | Vois les couches | Compare getent hosts (plancher) et dig (DNS) : deux chemins, même IP. | 👁 observe | — |
| 4 | Casse & répare | Sur un nœud sans client_unbound, vide /etc/hosts de ses entrées chezlepro (garde une sauvegarde !) et coupe l'accès au DNS : la résolution interne échoue. Restaure /etc/hosts : ça remarche sans DNS. Tu viens de *sentir* pourquoi le planc… | 🔨 casse-répare | — |
*Source : [DNS & résolution de noms](DNS-et-résolution) · § À toi de jouer.*
## Identité & SSO
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Vis le SSO | Ouvre https://grafana.lab.chezlepro.internal → « *Se connecter avec Chezlepro* » → testmail. Puis ouvre Forgejo, puis Icinga : tu n'es reconnecté nulle part. | 👁 observe | — |
| 2 | Observe le flux | Rouvre Grafana en navigation privée, ouvre les outils dév (F12 → Réseau) : repère la redirection vers Keycloak, puis le retour avec un code=. C'est ① en action. | 👁 observe | — |
| 3 | Interroge l'annuaire (la source de vérité) | Sur un nœud avec ldap-utils : « commande » Tu vois l'entrée que Keycloak fédère — il ne l'a pas recopiée. | 👁 observe | — |
| 4 | Casse & répare (la fédération) | Dans la console admin Keycloak → *User Federation* → désactive le fournisseur LDAP. Reconnecte-toi : échec (l'IdP ne voit plus l'annuaire). Réactive : ça remarche. Tu viens de *sentir* la dépendance requise entre l'IdP et l'annuaire. | 🔨 casse-répare | — |
*Source : [Identité & SSO](Identité-et-SSO) · § À toi de jouer.*
## Infra as Code & idempotence
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Sens l'idempotence | Redéploie un rôle déjà en place (ex. via la GUI ou make deployer HOTE=…) : la 2ᵉ fois, changed=0. Rien à faire = rien n'est touché. | 👁 observe | — |
| 2 | Le flux déclaratif | Édite le plan (un serveur dans la GUI), ⚙ Appliquer le plan (instancier), puis Vérifier (dry-run) : tu prévisualises avant d'appliquer. | 👁 observe | — |
| 3 | La réversibilité | git diff / git checkout sur le plan : l'état est du code, donc annulable. | 👁 observe | — |
| 4 | Casse & répare | Modifie à la main un fichier géré par un rôle (ex. un .conf), puis redéploie : Ansible rétablit l'état voulu (le code gagne sur la dérive manuelle). Tu *sens* que la source de vérité, c'est le code. | 🔨 casse-répare | — |
*Source : [Infra as Code & idempotence](Infra-as-Code-et-idempotence) · § À toi de jouer.*
## La preuve — prouver, pas affirmer
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Produis une preuve | make prouver (voûte exportée). Lis docs/audit/preuve-<date>.md : chaque preuve, son verdict, l'affirmation couverte. | 👁 observe | — |
| 2 | Fais échouer une preuve — exprès | Introduis un hôte fantôme : dans applications.yml, pointe une appli vers un hôte qui n'existe pas dans serveurs.yml. make prouver : **P06 échoue**, en nommant l'hôte. Corrige (ou via le <select> de la GUI) : vert. | 👁 observe | **P06** |
| 3 | Une autre | Remets de l'adressage dans une nomenclature (vlan: 42), make prouver : P20 échoue. Retire-le : vert. | 👁 observe | **P20** |
| 4 | Lis le registre | Ouvre docs/audit/affirmations.md : trouve une affirmation ⚪ (*non prouvable localement*) — vois comment elle est assumée comme intention, jamais présentée comme prouvée. | 👁 observe | — |
| 5 | Comprends la valeur | Demande-toi : *quelle promesse est-ce que je fais sans preuve ?* C'est exactement ce que ce registre force à regarder en face. | 👁 observe | — |
*Source : [La preuve — prouver, pas affirmer](La-preuve) · § À toi de jouer.*
## Le GUI (console d'exploitation)
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Lance-la | make inventaire-ui, ouvre l'URL affichée. Repère l'inventaire actif et l'état de la flotte (en haut), les onglets, « Appliquer le plan », « Sauvegarder ». | 👁 observe | — |
| 2 | Édite → prévisualise | Vue Serveurs, change la mémoire d'un hôte, Sauvegarder, Appliquer le plan, puis Vérifier (dry-run) : tu vois ce qui *changerait* avant d'agir. | 👁 observe | — |
| 3 | Répare un hôte fantôme (l'erreur impossible) | Si une appli pointe un hôte inexistant, ouvre la vue Applications, sélectionne-la : le <select> « Hôte » ne montre **que des hôtes réels. Choisis le bon, Sauvegarder, Appliquer**. Tu ne *peux pas* re-saisir le fantôme. | 👁 observe | — |
| 4 | Bascule d'instance | Vue Réseau, bouton « Activer » sur une autre instance. Toutes les vues suivent, sans redémarrer. | 👁 observe | — |
| 5 | Sens le garde-fou | Essaie de sauvegarder un plan incohérent (ex. une base dont le consommateur n'existe pas) : la console refuse avec une raison. L'invalide ne passe pas. | 👁 observe | — |
| 6 | Casse & répare | Édite hosts.yml à la main, reviens dans la GUI, « Appliquer le plan » : ta modification est écrasée par le plan. La source de vérité, c'est le plan — pas l'inventaire. | 🔨 casse-répare | — |
*Source : [Le GUI (console d'exploitation)](Le-GUI-console-d-exploitation) · § À toi de jouer.*
## Le plan & l'adressage dérivé
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Observe la dérivation | Vue Serveurs de la GUI : chaque carte montre VMID · IP · VLAN. Aucun n'a été saisi — tous viennent du index. Note l'IP d'un hôte. | 👁 observe | — |
| 2 | Change le seed, regarde tout suivre | Panneau Intrants → Réseau, change index (ex. de 1 à 7), « Appliquer le plan ». Toutes les IP basculent de 10.11.x à 10.17.x, les VLAN de 101x à 107x — d'un seul chiffre. Puis remets ta valeur. | 👁 observe | — |
| 3 | Sens la source unique | make instancier (diff), puis make instancier-appliquer : *« DIFF VIDE : le plan reproduit exactement l'inventaire »* — le plan est la vérité. | 👁 observe | — |
| 4 | Casse & répare | Édite hosts.yml à la main (change une IP). Relance make instancier : il signale l'écart. Ré-applique : le plan écrase ta modification. Tu *sens* que hosts.yml n'est pas la vérité — le plan l'est. | 🔨 casse-répare | — |
| 5 | Éprouve le garde-fou | Ajoute une ligne supernet: 10.99.0.0/16 dans une nomenclature, puis make prouver : P20 échoue (« adressage stocké »). Retire-la : vert. La règle se *prouve*. | 👁 observe | **P20** |
*Source : [Le plan & l'adressage dérivé](Le-plan-et-l-adressage-dérivé) · § À toi de jouer.*
## Liaisons (bindings)
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Lis une liaison | Ouvre instance/plan/applications.yml : une app avec expose: (liaison *app→domaine*), et serveurs.yml : un nœud avec integrations: (liaison *nœud→service*). | 👁 observe | — |
| 2 | Vois-la se résoudre | Après make instancier, regarde l'inventaire généré : la cible est devenue une valeur concrète (FQDN, groupe) — le moteur a câblé. | 👁 observe | — |
| 3 | Requise vs optionnelle | Compare : retirer client_journal d'un nœud → aucun problème (optionnelle). Déclarer une base sans serveur → make instancier/la validation échoue (requise). Tu *sens* la différence de modalité. | 👁 observe | — |
| 4 | Casse & répare | Casse une liaison requise (ex. réfère une base à un serveur inexistant), relance l'instanciation : échec clair *avant* tout déploiement. Corrige : ça passe. Le moteur attrape le câblage manquant à ta place. | 🔨 casse-répare | — |
*Source : [Liaisons (bindings)](Liaisons-bindings) · § À toi de jouer.*
## Multi-instance & fédération
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Vois la flotte | make instances : l'active (★), les index, les VLAN, le statut fédéré/local. Repère qui est en production. | 👁 observe | — |
| 2 | Bascule | GUI, vue Réseau, bouton « Activer » sur une autre instance (ou make instance-utiliser NOM=…). Toutes les vues suivent — sans redémarrer. | 👁 observe | — |
| 3 | Crée une instance | make instance-modeles (les modèles dispo), puis make instance-creer NOM=OPS-Test MODELE=socle INDEX=4. Un écosystème neuf, en une commande. | 👁 observe | — |
| 4 | Éprouve le garde-fou de collision | Essaie de créer une instance avec un index **déjà pris : refus *avant* toute copie. Puis make prouver → P21** veille sur la fédération. | 👁 observe | **P21** |
| 5 | Casse & répare | Donne à deux instances fédérées le même index (édite une nomenclature), make instances : la bannière de collision s'allume ; make prouver : P21 échoue. Corrige l'index : tout redevient vert. | 🔨 casse-répare | **P21** |
| 6 | (Avancé) Promeus un produit | Une instance qui *tourne et se prouve* peut devenir un modèle vendable : make model-creer MODE=instance SOURCE=OPS-… NOM=… — elle est généralisée (identité → exemple.*, secrets retirés) et validée. | 👁 observe | — |
*Source : [Multi-instance & fédération](Multi-instance-et-fédération) · § À toi de jouer.*
## Métriques & journaux
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Interroge les métriques | (sur obs-01) — combien de nœuds scrapés, tous UP ? « commande » | 👁 observe | — |
| 2 | Vois les journaux | : curl -s http://localhost:3100/loki/api/v1/label/host/values → les nœuds qui expédient leurs logs. | 👁 observe | — |
| 3 | Ouvre Grafana | (https://grafana.lab.chezlepro.internal) — métriques *et* logs au même endroit. | 👁 observe | — |
| 4 | Casse & répare | Arrête prometheus-node-exporter sur un nœud : dans Prometheus, sa cible passe up=0 (DOWN). Redémarre : elle repasse UP. Tu *sens* que c'est l'agent qui nourrit le serveur (modèle pull). | 🔨 casse-répare | — |
*Source : [Métriques & journaux](Métriques-et-journaux) · § À toi de jouer.*
## PKI & confiance
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Regarde un certificat | Sur un nœud avec client_pki : « commande » Repère le sujet (l'identité), l'émetteur (l'intermédiaire) et la validité. | 👁 observe | — |
| 2 | Suis la chaîne de confiance | « commande » OK = la racine valide bien le certificat du serveur. C'est ① en action. | 👁 observe | — |
| 3 | Vois-le servir en vrai | Le LDAPS d'OpenLDAP utilise ce certificat : « commande » 0 (ok) = confiance vérifiée. | 👁 observe | — |
| 4 | Casse & répare | Retire la racine du magasin système, refais un curl HTTPS interne : avertissement de certificat (plus de confiance). Réinstalle la racine : ça remarche. Tu viens de *sentir* pourquoi « faire confiance à la racine » est la clé de voûte. | 🔨 casse-répare | — |
*Source : [PKI & confiance](PKI-et-confiance) · § À toi de jouer.*
## Reverse-proxy & TLS
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Route par nom | Deux noms, un seul edge (192.168.15.21) : « commande » Change grafana en forge : même IP, backend différent. C'est le routage par SNI. | 👁 observe | — |
| 2 | Vois la terminaison TLS | Le certificat présenté est celui de l'edge (avec les SAN des exposés) : openssl s_client -connect 192.168.15.21:443 -servername grafana.lab… \| openssl x509 -noout -text \| grep -A1 'Subject Alternative'. | 👁 observe | — |
| 3 | Casse & répare | Arrête le backend (ex. systemctl stop grafana-server sur obs-01) et rouvre Grafana : l'edge répond 502 Bad Gateway (le proxy est là, le service non). Redémarre : ça remarche. Tu distingues le proxy de ce qu'il sert. | 🔨 casse-répare | — |
*Source : [Reverse-proxy & TLS](Reverse-proxy-et-TLS) · § À toi de jouer.*
## Sauvegardes (3-2-1)
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Lance une sauvegarde | Sur un nœud avec client_backup : « commande » | 👁 observe | — |
| 2 | Liste les instantanés | (le dépôt vit hors-nœud) : « commande » | 👁 observe | — |
| 3 | Restaure — le vrai test | Restaure dans un dossier temporaire et compare : « commande » *(sur infra-pki-01 ; ailleurs, compare le dump correspondant.)* | 👁 observe | — |
| 4 | Casse & répare | Supprime un fichier de donnée (une copie de test !), restaure-le depuis l'instantané, vérifie qu'il est identique. Tu viens de *sentir* que la valeur d'une sauvegarde est la restauration, pas la sauvegarde. | 🔨 casse-répare | — |
*Source : [Sauvegardes (3-2-1)](Sauvegardes) · § À toi de jouer.*
## Supervision & impact
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Ouvre Icinga Web 2 | (https://icinga.lab.chezlepro.internal, via le SSO) : la liste des hôtes et services supervisés, avec leur état (vert/jaune/rouge). | 👁 observe | — |
| 2 | Vois l'impact | Menu *Business Processes* → « Supervision Chezlepro » : un processus qui agrège des checks (load, procs, ping…) en un état roulé. C'est l'impact, pas une case. | 👁 observe | — |
| 3 | Casse & répare | Provoque l'échec d'un check (ex. arrête un service surveillé) : l'état passe CRITICAL, et le processus BPM qui en dépend rougit (l'impact remonte). Répare : tout reverdit. Tu *sens* la différence entre *mesurer* et *superviser/alerter*. | 🔨 casse-répare | — |
*Source : [Supervision & impact](Supervision-et-impact) · § À toi de jouer.*
## Sécurité & durcissement
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Constate les couches | (sur n'importe quel nœud) : « commande » | 👁 observe | — |
| 2 | Moindre privilège | : PermitRootLogin est à no, l'accès se fait par le compte ansible + clé SSH. Vérifie : sshd -T \| grep -E 'permitrootlogin\|passwordauthentication'. | 👁 observe | — |
| 3 | Casse & répare (avec prudence, en lab) | Assouplis un réglage sysctl, observe, puis remets-le. Tu *sens* que chaque ligne de durcissement ferme une porte précise. | 🔨 casse-répare | — |
*Source : [Sécurité & durcissement](Sécurité-et-durcissement) · § À toi de jouer.*
## Virtualisation & clonage
| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |
|---|---|---|---|---|
| 1 | Regarde un clone | Dans la GUI (make inventaire-ui), un serveur *actif* a été cloné du template (bouton « 🖥 Créer la VM »). Son VMID/IP sont dérivés de la nomenclature. | 👁 observe | — |
| 2 | Vois l'identité cloud-init | Sur un nœud : cloud-init query hostname, hostname -f, et l'agrandissement du disque racine (df -h /). | 👁 observe | — |
| 3 | Sépare les deux couches | cloud-init a posé *l'identité* ; tout le reste (paquets, services, durcissement) vient d'Ansible. Le template, lui, ne contient aucune donnée de clone. | 👁 observe | — |
| 4 | Casse & répare (mentalement + lab) | Supprime un nœud non critique et reclone-le depuis le template, puis redéploie : il revient à l'identique. Tu *sens* que la machine est reconstructible. | 🔨 casse-répare | — |
*Source : [Virtualisation & clonage](Virtualisation-et-clonage) · § À toi de jouer.*

146
scripts/plan_recette.py Normal file
View file

@ -0,0 +1,146 @@
#!/usr/bin/env python3
"""Génère le PLAN DE RECETTE (tests d'acceptation manuels) depuis les exercices du wiki.
Le wiki porte, dans chaque unité, une section « À toi de jouer » : des exercices
hands-on (observe / casse-répare). Pris ensemble, ils forment un **plan de recette**
manuel le pendant humain de `make prouver` (qui, lui, prouve le moteur par machine).
Ce script LIT ces exercices et produit `docs/audit/plan-de-recette.md` : une grille
auto-contenue (le geste est inline), avec une colonne **Preuve auto** extraite du texte
(le `Pxx` que l'exercice mentionne, quand un garde-fou machine couvre AUSSI le geste).
La grille étant GÉNÉRÉE, elle ne peut pas dériver du wiki : `--verifier` échoue si le
fichier committé n'est plus à jour (à régénérer par `make plan-recette`).
Usage :
python3 scripts/plan_recette.py # (ré)génère docs/audit/plan-de-recette.md
python3 scripts/plan_recette.py --verifier # rc=2 si le fichier committé est périmé
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
RACINE = Path(__file__).resolve().parents[1]
WIKI = RACINE / "wiki"
SORTIE = RACINE / "docs" / "audit" / "plan-de-recette.md"
def _titre(md: str) -> str:
m = re.search(r"(?m)^#\s+(.+)$", md)
return m.group(1).strip() if m else "?"
def _section_exercices(md: str) -> str | None:
m = re.search(r"## ④ À toi de jouer(.*?)(?:\n## |\Z)", md, re.S)
return m.group(1) if m else None
def _items(section: str) -> list[str]:
"""Découpe la section en items numérotés (chacun peut contenir du multi-ligne / code)."""
bornes = [m.start() for m in re.finditer(r"(?m)^\d+\.\s", section)]
bornes.append(len(section))
return [section[bornes[i]:bornes[i + 1]].strip() for i in range(len(bornes) - 1)]
def _nettoyer(texte: str) -> tuple[str, str]:
"""(intro, geste) : l'intro = 1er gras (le titre de l'exercice) ; le geste = le reste, nettoyé."""
corps = re.sub(r"^\d+\.\s*", "", texte)
mi = re.match(r"\*\*(.+?)\.?\*\*\s*", corps)
intro = mi.group(1).strip() if mi else ""
reste = corps[mi.end():] if mi else corps
reste = re.sub(r"```.*?```", "« commande »", reste, flags=re.S) # blocs de code
reste = re.sub(r"`([^`]+)`", r"\1", reste) # code inline
reste = re.sub(r"\*\*(.+?)\*\*", r"\1", reste) # gras
reste = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", reste) # liens
reste = re.sub(r"-{3,}", " ", reste) # filets --- de fin de section
reste = re.sub(r"\s+", " ", reste).strip()
if len(reste) > 240:
reste = reste[:237].rstrip() + ""
return intro, reste
def _type(texte: str) -> str:
return "🔨 casse-répare" if re.search(r"[Cc]asse\s*&?\s*répare", texte) else "👁 observe"
def _preuves(texte: str) -> str:
px = sorted(set(re.findall(r"P\d{2}", texte)))
return " ".join(f"**{p}**" for p in px) if px else ""
def generer() -> str:
unites = []
for f in sorted(WIKI.glob("*.md")):
md = f.read_text(encoding="utf-8")
sec = _section_exercices(md)
if not sec:
continue
items = _items(sec)
if items:
unites.append((_titre(md), f.stem, items))
total = sum(len(it) for _, _, it in unites)
auto = sum(1 for _, _, its in unites for it in its if re.search(r"P\d{2}", it))
casse = sum(1 for _, _, its in unites for it in its if "casse" in it.lower())
L = []
L.append("# Plan de recette — tests d'acceptation manuels")
L.append("")
L.append("> **GÉNÉRÉ** depuis les exercices du wiki (`wiki/*.md`, section « ④ À toi de jouer »)")
L.append("> par `scripts/plan_recette.py` — **ne pas éditer à la main** ; régénérer avec")
L.append("> `make plan-recette`. La preuve **P22** échoue si ce fichier n'est plus à jour.")
L.append("")
L.append("Ce plan est le **pendant manuel** de `make prouver` : là où le harnais prouve le")
L.append("*moteur* par machine (P01P21), ces gestes valident l'*exploitation* — ce qu'un")
L.append("**humain** fait, voit, casse et répare. Ensemble, ils couvrent les deux moitiés ;")
L.append("le côté humain **est** la preuve de l'affirmation « exploitable sans IA » (cf.")
L.append("`protocole-operateur-independant.md`).")
L.append("")
L.append(f"**{total} gestes** sur **{len(unites)} unités** · **{casse}** en « casse-répare »")
L.append(f"· **{auto}** doublés d'un garde-fou machine (colonne *Preuve auto*).")
L.append("")
L.append("> **Honnêteté de couverture.** La colonne *Preuve auto* n'est remplie que lorsqu'une")
L.append("> preuve `Pxx` de `make prouver` couvre AUSSI le geste. Un « — » signifie **manuel")
L.append("> seul** : aucune machine ne le garde, seul l'œil de l'opérateur le valide. Ce plan")
L.append("> ne prétend pas à l'exhaustivité au-delà des exercices présents dans le wiki.")
L.append("")
for titre, stem, items in unites:
L.append(f"## {titre}")
L.append("")
L.append("| # | Ce qu'on éprouve | Le geste (avec l'attendu) | Type | Preuve auto |")
L.append("|---|---|---|---|---|")
for i, it in enumerate(items, 1):
intro, geste = _nettoyer(it)
geste = geste.replace("|", "\\|")
intro = intro.replace("|", "\\|")
L.append(f"| {i} | {intro or ''} | {geste} | {_type(it)} | {_preuves(it)} |")
L.append("")
L.append(f"*Source : [{titre}]({stem}) · § À toi de jouer.*")
L.append("")
return "\n".join(L).rstrip() + "\n"
def main() -> int:
verifier = "--verifier" in sys.argv[1:]
attendu = generer()
if verifier:
actuel = SORTIE.read_text(encoding="utf-8") if SORTIE.exists() else ""
if actuel != attendu:
print("erreur: docs/audit/plan-de-recette.md est PÉRIMÉ (le wiki a changé). "
"Régénérer : make plan-recette.", file=sys.stderr)
return 2
n = attendu.count("\n| ")
print(f"Plan de recette à jour ({attendu.count('## ') - 0} sections).")
return 0
SORTIE.write_text(attendu, encoding="utf-8")
print(f"Écrit : {SORTIE.relative_to(RACINE)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -288,6 +288,8 @@ PREUVES: list[dict] = [
"refs": ["AFF-001", "AFF-003"], "func": preuve_nomenclature_derivee},
{"id": "P21", "titre": "Federation : aucun index en collision", "refs": ["AFF-001"],
"cmds": [[sys.executable, "scripts/instances.py", "--verifier"]]},
{"id": "P22", "titre": "Plan de recette a jour (genere du wiki)", "refs": ["AFF-002"],
"cmds": [[sys.executable, "scripts/plan_recette.py", "--verifier"]]},
]