From 0b653170fafe564d08dbe9fceb17aafda916caaf Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Tue, 1 Sep 2026 08:36:28 -0400 Subject: [PATCH] =?UTF-8?q?emancipation=20:=20l=20instrument=20de=20la=20q?= =?UTF-8?q?uatrieme=20ligne=20=E2=80=94=20couper,=20pas=20sonder?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/filiation-emancipation.md decrit quatre temps et n en outillait que trois. Le quatrieme est celui qu on oublie : « une emancipation non prouvee est une emancipation non faite ». SONDER NE PROUVE RIEN. Verifier que le service local repond ne dit pas si l amont sert encore — le depot le disait deja du cache : « tant qu internet repond, un apt update qui reussit ne dit pas d ou vient l octet ». L instrument COUPE donc l amont et refait marcher la chose. LE MEME ESSAI REND LES DEUX VERDICTS, et c est ce qui le rend honnete : coupe, la fonction marche -> EMANCIPE, et c est prouve coupe, la fonction casse -> PAS EMANCIPE, dependance prouvee REELLE Le second n est pas un echec de l outil, c est son CONTROLE NEGATIF rendu par la meme commande. Une preuve d emancipation incapable de montrer la dependance qu elle mesure ne prouverait rien le jour ou elle passerait au vert. UN TEMOIN PRECEDE LA COUPURE : la fonction marchait-elle seulement avant ? Sans lui, une panne preexistante se lirait comme une dependance. LA COUPURE EST GARANTIE REVERSIBLE : une TABLE nftables dediee, jamais une regle glissee dans une table existante — elle se retire d un geste et ne peut pas laisser d etat partiel. Le bloc `always` la retire meme si la mesure echoue ou si le play est interrompu, et une tache verifie ensuite qu elle a bien disparu. MESURE LE JOUR DE SA NAISSANCE, les deux verdicts sur du vrai materiel : obs-01 / resolveur PAS EMANCIPE — plus aucune resolution des la coupure forge-01 / artefacts EMANCIPE — apt installe, cache du site coupe Ce second verdict a ete DOUTE puis verifie : apt aurait pu reussir en rejouant des listes fraiches. Refait avec un dossier de listes NEUF, amont coupe : reussit quand meme. Le cache sert vraiment son contenu. make verifier : vert. make prouver : CONFORME, 55 OK, 0 echec. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on --- CHANGELOG.md | 43 +++++ Makefile | 29 ++++ docs/audit/preuve-2026-09-01.md | 91 ++++++++++ docs/carte-set-ops.md | 2 +- docs/filiation-emancipation.md | 32 +++- .../maintenance/emancipation-prouver.yml | 160 ++++++++++++++++++ 6 files changed, 354 insertions(+), 3 deletions(-) create mode 100644 docs/audit/preuve-2026-09-01.md create mode 100644 playbooks/maintenance/emancipation-prouver.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f1b6cc..e0fbec2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # CHANGELOG — Set-OPS +## 2026-09-01 — `make emancipation-prouver` : couper, pas sonder + +**55 preuves.** `docs/filiation-emancipation.md` decrivait quatre temps et n'en outillait +que trois. Le quatrieme est celui qu'on oublie — *« une emancipation non prouvee est une +emancipation non faite »*. + +**Sonder ne prouve rien.** Verifier que le service local repond ne dit pas si l'amont sert +encore ; le depot le disait deja du cache : *« tant qu'internet repond, un `apt update` qui +reussit ne dit pas d'ou vient l'octet »*. L'instrument **coupe** donc l'amont, et refait +marcher la chose. + +### Le meme essai rend les deux verdicts + +``` +coupe, la fonction marche -> ÉMANCIPÉ, et c'est prouvé +coupe, la fonction casse -> PAS ÉMANCIPÉ, dépendance prouvée RÉELLE +``` + +Le second n'est pas un echec de l'outil : c'est son **controle negatif**, rendu par la meme +commande. *Une preuve d'emancipation incapable de montrer la dependance qu'elle mesure ne +prouverait rien le jour ou elle passerait au vert.* + +Un **temoin** precede la coupure — la fonction marchait-elle seulement avant ? Sans lui, +une panne preexistante se lirait comme une dependance. + +### La coupure est garantie reversible + +Une **table** `nftables` dediee, jamais une regle glissee dans une table existante : elle se +retire d'un geste et ne peut pas laisser d'etat partiel. Le bloc `always` la retire meme si +la mesure echoue ou si le play est interrompu, et une tache verifie ensuite qu'elle a bien +disparu. + +### Mesure le jour de sa naissance + +``` +obs-01 / resolveur PAS ÉMANCIPÉ — plus aucune resolution des la coupure +forge-01 / artefacts ÉMANCIPÉ — apt installe, cache du site coupe +``` + +**Ce second verdict a ete doute avant d'etre cru** : `apt` aurait pu reussir en rejouant des +listes deja fraiches. Refait avec un dossier de listes **neuf**, amont coupe — il reussit +quand meme. Le cache sert vraiment son contenu. + ## 2026-08-31 — D-82 : patient 0 n'est le parent de personne **55 preuves.** Le dilemme ouvert le 2026-08-28 est referme, et ce sont les faits qui l'ont diff --git a/Makefile b/Makefile index cd39eae..85dd64d 100644 --- a/Makefile +++ b/Makefile @@ -338,6 +338,35 @@ inseminer: ansible-runtime ## Le SITE amorce le runner d'un tenant — TENANT= [HOTE=] CONFIRMER=true + @set -e; \ + if [[ -z "$(SERVICE)" ]]; then \ + printf '%s\n' 'Refus: relancer avec SERVICE=.'; \ + exit 2; \ + fi; \ + if [[ "$(CONFIRMER)" != "true" ]]; then \ + printf '%s\n' 'Refus: cette preuve COUPE l amont quelques secondes pour mesurer.'; \ + printf '%s\n' 'La coupure est retiree quoi qu il arrive (bloc `always`), mais'; \ + printf '%s\n' 'pendant ce temps la fonction eprouvee peut echouer sur cet hote.'; \ + printf '%s\n' 'Relancer avec CONFIRMER=true.'; \ + exit 2; \ + fi; \ + ansible-playbook -i $(INVENTAIRE_PRODUCTION) playbooks/maintenance/emancipation-prouver.yml \ + -e emancipation_service=$(SERVICE) \ + $(if $(HOTE),-e emancipation_hotes=$(HOTE)) $(ARGS) + .PHONY: sonder sonder: ## Sonde une cible et DIT ce qui distingue — CIBLE= [PORTS="22 443"] @if [[ -z "$(CIBLE)" ]]; then \ diff --git a/docs/audit/preuve-2026-09-01.md b/docs/audit/preuve-2026-09-01.md new file mode 100644 index 0000000..1585276 --- /dev/null +++ b/docs/audit/preuve-2026-09-01.md @@ -0,0 +1,91 @@ +# Preuve de conformite — Set-OPS — 2026-09-01 + +> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer +> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ; +> aucune validation n'est reimplementee ici. Voir le mode d'emploi : +> [`docs/audit/README.md`](README.md), et le registre trace : +> [`docs/audit/affirmations.md`](affirmations.md). + +- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml` +- **Verdict** : ✅ CONFORME (55 OK · 0 echec · 0 saute) + +## Preuves + +| # | Preuve | Affirmations | Statut | Detail | +|---|---|---|---|---| +| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  | +| P02 | Tests unitaires (inventory_host) | — | ✅ OK | >>> le verrou tient : aucune VM n'aurait ete touchee | +| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. | +| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | | +| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | +| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | +| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 39 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 37 rôles, 97 flux, schéma + matrice OK. | +| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | +| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site | +| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | +| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. | +| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. | +| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. | +| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 15 hotes, 36 groupes (inventaire dechiffre et parse). | +| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. | +| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) | +| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. | +| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. | +| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. | +| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). | +| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 11 reseau(x), aucune collision avec la plage tenant. | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 51 groupe(s), 81 regle(s). | +| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 15 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). | +| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | +| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. | +| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 31 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 20, 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, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 53 scripts expliques et atteignables, 107 cibles make documentees, 63 roles avec README. | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 41 exigence(s) de role, toutes satisfaites (130 cle(s) declaree(s) par l'instance). | +| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (38 groupes). | +| P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (29 genere(s) exempte(s)). | +| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | +| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | +| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. | +| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 38 role(s) serveur/client tous nommes, 39 groupe(s) cite(s) en table existent tous. | +| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. | +| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. | +| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 49 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | +| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product | +| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 5 machine(s) du plan retrouvees, 66 regle(s) du site. | +| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. | +| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). | +| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos | +| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). | +| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 84 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. | +| P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (115 lignes). | +| P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 131 regles `pass`), tous non consignes et tous motives. | +| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 | +| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. | +| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b | +| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 10 role(s) applique(s), aucun secret de tenant reclame. | +| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 15 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 15. | + +## Couverture des affirmations ✅ du registre + +Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus. +Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005 +`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite +d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ; +elles restent hors du harnais recurrent (rien d'executable a rejouer). + +## Declarations d'intention (⚪ invérifiables localement — assumees) + +Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees** +comme declarations d'intention, non comme preuves : + +- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement + contre une flotte vivante. +- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles. +- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee. +- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume. + +_Rapport genere le 2026-09-01._ diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index 18361a7..3634c00 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous. | rôles | 63 | `roles/*/` | | README de rôles | 63 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette | | documents | 38 | `docs/*.md` | -| pièces d'audit | 31 | `docs/audit/*` | +| pièces d'audit | 32 | `docs/audit/*` | | unités de wiki | 27 | `wiki/*.md` | | décisions en vigueur | 79 | lignes `\| **D-nn** \|` de `decisions-architecture.md` | | décisions renversées | 3 | lignes `\| **D-nn** —` du même document | diff --git a/docs/filiation-emancipation.md b/docs/filiation-emancipation.md index df32550..6237555 100644 --- a/docs/filiation-emancipation.md +++ b/docs/filiation-emancipation.md @@ -138,8 +138,36 @@ servi. Sans elle, on croirait s'être émancipé en restant dépendant sans le s exactement le défaut que ce dépôt traque partout ailleurs : un vert sur un périmètre vide. **Une émancipation non prouvée est une émancipation non faite.** -*L'instrument de la quatrième ligne n'existe pas encore. C'est la pièce manquante nommée -au bas de ce document.* +### L'instrument de la quatrième ligne — `make emancipation-prouver` + +Il existe depuis le 2026-09-01, et il **coupe** au lieu de sonder. + +Vérifier que le service local répond ne prouve rien : *tant que l'amont répond, une +fonction qui marche ne dit pas d'où vient l'octet.* L'instrument bloque donc l'amont — +table `nftables` dédiée, retirée par un bloc `always` quoi qu'il arrive — puis refait +marcher la chose. + +**Le même essai rend les deux verdicts, et c'est ce qui le rend honnête :** + +``` +coupé, la fonction marche -> ÉMANCIPÉ, et c'est prouvé +coupé, la fonction casse -> PAS ÉMANCIPÉ, et la dépendance est prouvée RÉELLE +``` + +Le second n'est pas un échec de l'outil : c'est son **contrôle négatif**, rendu par la +même commande. Une preuve d'émancipation incapable de montrer la dépendance qu'elle mesure +ne prouverait rien le jour où elle passerait au vert. + +Un témoin précède la coupure — *la fonction marchait-elle seulement avant ?* Sans lui, une +panne préexistante se lirait comme une dépendance. + +``` +make emancipation-prouver SERVICE=artefacts HOTE=forge-01 CONFIRMER=true +``` + +*Mesuré le jour de sa naissance : `obs-01` ne résout plus rien dès que le résolveur du +site est coupé — dépendance réelle ; `forge-01` installe des paquets le cache du site +coupé, listes vidées — émancipation prouvée sur ce service.* ## Forme attendue d'une déclaration diff --git a/playbooks/maintenance/emancipation-prouver.yml b/playbooks/maintenance/emancipation-prouver.yml new file mode 100644 index 0000000..9b6e72a --- /dev/null +++ b/playbooks/maintenance/emancipation-prouver.yml @@ -0,0 +1,160 @@ +--- +# PROUVER QU'UN LIEN EST COUPÉ — l'instrument de la quatrième ligne. +# +# `docs/filiation-emancipation.md` décrit quatre temps, et n'en outillait que trois : +# +# le lien de filiation déclaré au plan le moteur +# le constat d'aptitude « tu pourrais » la machine INSTRUIT +# l'acte d'émancipation retirer le lien l'HUMAIN, CONFIRMER=true +# la preuve que c'est coupé ← CE PLAYBOOK la machine, APRÈS l'acte +# +# « Une émancipation non prouvée est une émancipation non faite. » +# +# ─── POURQUOI SONDER NE SUFFIT PAS ─────────────────────────────────────────── +# +# Vérifier que le service local répond ne prouve RIEN. Le dépôt le dit déjà, à propos du +# cache : « tant qu'internet répond, un `apt update` qui réussit ne dit pas d'où vient +# l'octet ». Un écosystème peut avoir monté son propre service ET continuer à tirer de +# l'amont sans que personne ne le voie. +# +# On COUPE donc, et on refait marcher la chose. C'est le seul geste qui distingue +# « j'ai un service local » de « je n'ai plus besoin du tien ». +# +# ─── LE MÊME ESSAI REND LES DEUX VERDICTS ──────────────────────────────────── +# +# Le verdict vient de la MESURE, jamais d'une déclaration : +# +# coupé, la fonction marche -> ÉMANCIPÉ, et c'est prouvé +# coupé, la fonction casse -> PAS ÉMANCIPÉ — et la dépendance est prouvée REELLE +# +# Le second n'est pas un échec de l'outil : c'est son contrôle négatif, rendu par la même +# commande. Une preuve d'émancipation qui ne saurait pas montrer la dépendance qu'elle +# mesure ne prouverait rien le jour où elle passerait au vert. +# +# ─── LA COUPURE EST GARANTIE RÉVERSIBLE ────────────────────────────────────── +# +# Une TABLE nftables dédiée, jamais une règle glissée dans une table existante : elle se +# retire d'un seul geste, et ne peut pas laisser d'état partiel si l'essai s'interrompt. +# Le `always` la retire quoi qu'il arrive — y compris si la mesure échoue. +# +# Usage : +# make emancipation-prouver SERVICE=artefacts CONFIRMER=true +# make emancipation-prouver SERVICE=genome CONFIRMER=true +# make emancipation-prouver SERVICE=resolveur CONFIRMER=true + +- name: Prouver qu'un lien de filiation est coupé + hosts: "{{ emancipation_hotes | default('hotes_actifs') }}" + gather_facts: false + become: true + vars: + # CE QU'ON COUPE, ET CE QU'ON REFAIT MARCHER. Chaque service nomme son amont (lu au + # plan, jamais écrit ici) et l'épreuve qui dit s'il tient sans lui. + emancipation_services: + artefacts: + amont: "{{ serveur_artefacts_amont | default(artefacts_amorcage | default('')) }}" + epreuve: "apt-get -qq update" + dit: "installer des paquets" + genome: + amont: "{{ serveur_ops_forge_amont | default('') }}" + epreuve: "git ls-remote {{ serveur_ops_forge_amont | default('') }}/genome/set-ops-public.git HEAD" + dit: "cloner le génome" + resolveur: + amont: "{{ dns_amorcage | default('') }}" + epreuve: "getent hosts deb.debian.org" + dit: "résoudre un nom public" + emancipation_ce_service: "{{ emancipation_services[emancipation_service] }}" + # L'ADRESSE SEULE, sans schéma ni port : c'est elle qu'on bloque. + emancipation_adresse: >- + {{ (emancipation_ce_service.amont | regex_replace('^\w+://', '') + | regex_replace('[:/].*$', '')) }} + + tasks: + - name: Exiger un service connu et un amont déclaré + ansible.builtin.assert: + that: + - emancipation_service is defined + - emancipation_service in emancipation_services + - emancipation_adresse | length > 0 + fail_msg: >- + SERVICE doit valoir l'un de {{ emancipation_services.keys() | join(', ') }}, + et cet écosystème doit DÉCLARER l'amont correspondant. Un emprunt qu'aucune + variable ne nomme ne peut pas être prouvé coupé — c'est la forme normée de + `docs/filiation-emancipation.md` qui rend cette preuve possible. + # PAS de `run_once` : l'amont se lit dans les variables de CHAQUE hote. Un + # ecosysteme peut tres bien emprunter sur une machine et pas sur une autre — c'est + # meme le cas ici, ou seul le porteur du cache chaine vers celui du site. Verifier + # une fois pour toutes lirait le premier hote et parlerait au nom des autres. + + - name: Prouver + block: + # ── L'ÉTAT AVANT : la fonction marche-t-elle seulement ? ──────────── + # + # Sans ce témoin, une fonction déjà cassée AVANT la coupure se lirait comme une + # dépendance prouvée. On mesure donc le point de départ. + - name: Témoin — la fonction marche-t-elle AVANT toute coupure ? + ansible.builtin.command: + cmd: "{{ emancipation_ce_service.epreuve }}" + register: emancipation_temoin + failed_when: false + changed_when: false + + - name: Refuser de mesurer une fonction déjà cassée + ansible.builtin.assert: + that: + - emancipation_temoin.rc == 0 + fail_msg: >- + « {{ emancipation_ce_service.dit }} » ne marche DÉJÀ PAS sur cet hôte, avant + qu'on ait rien coupé. Une coupure ne prouverait rien ici : on lirait une + panne préexistante comme une dépendance. Réparer d'abord. + + # ── LA COUPURE ───────────────────────────────────────────────────── + - name: Couper le lien vers l'amont + ansible.builtin.command: + argv: + - nft + - -f + - "-" + stdin: | + table inet setops_emancipation { + chain output { + type filter hook output priority -10; policy accept; + ip daddr {{ emancipation_adresse }} reject with icmpx type admin-prohibited + } + } + changed_when: true + + # ── L'ÉPREUVE ────────────────────────────────────────────────────── + - name: L'épreuve, l'amont coupé + ansible.builtin.command: + cmd: "{{ emancipation_ce_service.epreuve }}" + register: emancipation_epreuve + failed_when: false + changed_when: false + + always: + # RETIRÉE QUOI QU'IL ARRIVE — y compris si l'épreuve échoue, si le play est + # interrompu, ou si la mesure lève. Une coupure qui survit à son essai serait une + # panne qu'on aurait fabriquée soi-même. + - name: Rétablir le lien + ansible.builtin.command: + argv: ["nft", "delete", "table", "inet", "setops_emancipation"] + failed_when: false + changed_when: true + + - name: Vérifier que le lien est bien rétabli + ansible.builtin.command: + argv: ["nft", "list", "table", "inet", "setops_emancipation"] + register: emancipation_reste + failed_when: emancipation_reste.rc == 0 + changed_when: false + + - name: "✔ Émancipation — {{ emancipation_service }}" + ansible.builtin.debug: + msg: >- + {{ (emancipation_epreuve.rc == 0) | ternary( + 'ÉMANCIPÉ (prouvé) — « ' ~ emancipation_ce_service.dit ~ ' » fonctionne + l amont ' ~ emancipation_adresse ~ ' COUPÉ. Le lien peut être retiré du + plan : il ne sert plus.', + 'PAS ÉMANCIPÉ — « ' ~ emancipation_ce_service.dit ~ ' » casse dès que + l amont ' ~ emancipation_adresse ~ ' est coupé. La dépendance est REELLE, + et cette mesure est le contrôle négatif de la preuve future.') }}