diff --git a/AGENTS.md b/AGENTS.md index e8b0e3a..b88fe67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -188,7 +188,7 @@ Si `ansible-lint` n’est pas disponible, le signaler clairement. Ne pas invente ## Écrire, puis relire (D-68) `--syntax-check` et `ansible-lint` prouvent que le dépôt est cohérent **avec lui-même**. -C'est aussi ce que font les 82 preuves de `make prouver` : elles lisent le dépôt, sans le +C'est aussi ce que font les 83 preuves de `make prouver` : elles lisent le dépôt, sans le moindre appel réseau. **Aucune ne demande au système déployé s'il ressemble à ce que le dépôt annonce.** diff --git a/CHANGELOG.md b/CHANGELOG.md index 392cd77..fcf0418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,88 @@ # CHANGELOG — Set-OPS +## 2026-09-20 (6) — Cent trente-deux cibles, et aucune ne dit dans quel ordre + +**83 preuves (P83 est neuve), `make test` à 0 échec.** Le `Makefile` porte 132 cibles +documentées. Chacune dit ce qu'elle fait ; aucune ne dit quand, ni après quoi, ni ce +qu'il faut avoir mesuré avant. La console offrait donc des boutons sans séquence, et +l'ordre des gestes vivait en prose dans des documents que la console ne porte pas. + +### Un exploitant devant un site neuf n'avait pas de quoi commencer + +Rien dans l'interface n'apprenait que `site-creer` précède `forge-amorcer`, que le premier +passage de `site-deployer-tout` **s'arrête** sur une forge vide — et que ce n'est pas un +échec mais le maillon suivant — ni que rien n'est « prêt » avant `valider`. C'est +exactement ce que le principe fondateur exige : *un opérateur exploite Set-OPS sans IA*. +Sans la séquence, la console ne tenait cette promesse qu'à moitié. + +La vue **Assistants** conduit **17 runbooks, 126 étapes**, du premier jour d'un site à la +remise au client. Les 132 cibles y sont : chacune portée par un assistant, ou **exemptée +avec son motif**. Une exemption muette est refusée — c'est un oubli déguisé. + +### Le registre ne recopie pas le Makefile, et la garde est écrite avec lui + +`docs/runbooks-construction.yml` déclare seulement ce que le `Makefile` ne peut pas +porter : l'ordre, la nature du geste, la portée, le pourquoi. **Le libellé de chaque étape +est lu dans le `Makefile` au moment de servir** — jamais recopié. Une cible renommée se +voit donc à l'écran, elle ne s'invente pas. + +Ce fichier est malgré tout une seconde liste à côté de la première, et une liste qui suit +une autre prend du retard : vu quatre fois dans ce dépôt en une seule journée. **P83 est +donc écrite en même temps que la liste**, pas après. Elle refuse une étape qui vise une +cible absente du `Makefile`, une cible documentée que nul runbook ne porte et que nul +motif n'exempte, une cible à la fois portée et exemptée, et une portée que la console ne +saurait pas traduire en pouvoir. Onze tests unitaires lui présentent des registres faux — +un par forme de retard — et exigent qu'elle les refuse : une garde qui rendrait toujours +« rien à signaler » passerait P83 tous les jours sans rien garder. + +### Le navigateur ne nomme pas une commande, il nomme une place + +`/api/runbook-etape` ne lance jamais la cible que la page demande : il lance ce que le +registre déclare **à cet index-là**, avec les seules variables déclarées, et refuse tout +le reste en disant pourquoi. Une page périmée — ou compromise — ne peut pas réclamer +`raser` depuis un assistant de mesure. **L'index compte** : « Le premier jour d'un site » +joue `site-deployer-tout` deux fois, et les deux places n'ont pas le même sens ; chercher +par nom seul les confondrait. + +Les valeurs imposées par une étape (`CONFIRMER=true` et compagnie) ne viennent jamais du +corps de la requête : une confirmation qu'on peut s'envoyer à soi-même n'en est pas une. +Ce qui protège, côté page, c'est d'**écrire `DETRUIRE`** — le seul garde-fou qui résiste à +un clic distrait. + +**La portée se dérive, elle ne se déclare pas.** Un assistant de site exige +`materialiser`, un de locataire `configurer`, un de poste les deux. La garde de cette +route est plus fine que celle des autres, et c'est voulu : un seul mot y serait soit trop +laxiste, soit trop sévère. Un assistant hors de portée reste **visible et lisible** — +savoir que le geste existe, et chez qui il se fait, fait partie du métier. Ce qui est +refusé, c'est de le lancer. + +### La séquence est un garde-fou, pas une décoration + +Une étape qui **écrit** attend que la précédente non facultative ait réussi : sinon on +bâtit sur un terrain qu'on n'a pas vérifié. Une étape qui **mesure** reste toujours +offerte, même après un échec — mesurer pour comprendre est exactement ce qu'on fait +ensuite. Cette asymétrie est la seule chose que l'interface impose d'elle-même. + +### Ce qui a été vérifié, et ce qui ne l'a pas été + +`python3 scripts/runbooks.py verifier` (0 écart), `make test` à 0 échec — 11 tests neufs +compris — et les 83 preuves rejouées. La console a été lancée pour de vrai : `GET /` +rend 200, `/api/runbooks` sert 17 runbooks avec les libellés lus au `Makefile`, six +requêtes malformées sont refusées une à une (cible absente du runbook, runbook inconnu, +bon nom au mauvais index, variable requise absente, valeur commençant par un tiret, +valeur hors du vocabulaire déclaré), et une étape de mesure s'exécute de bout en bout +avec son journal daté. + +**`make verifier` n'est pas vert, et ce n'est pas de ce changement.** P02 +(`scripts/tests/test_ecriture_plan.py`) échoue sur `domaines.yml` — trois erreurs dans +`_fusion_table`, où un bloc réindenté se relit comme une liste là où le code attend une +table. Mesuré sur une copie de `HEAD` avec la même instance montée : **l'échec est +identique avant ce travail**. Il reste ouvert, et il a une conséquence pour l'exploitant : +ajouter ou retirer un domaine public depuis la vue Domaines lèverait au moment +d'enregistrer. L'aller-retour sans modification, lui, fonctionne. + +Aucun rôle, playbook ou réglage n'est modifié ; aucune VM touchée, aucune voûte ouverte. + ## 2026-09-20 (5) — Celui qui pousse avance son propre clone **82 preuves dans le rapport du 17 septembre, non rejouées ici.** Le correctif de diff --git a/Makefile b/Makefile index 381ec8c..6e5cbdd 100644 --- a/Makefile +++ b/Makefile @@ -310,6 +310,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire) python3 scripts/tests/test_raser_resultat.py python3 scripts/tests/test_adressage_derive.py python3 scripts/tests/test_gui_intrants.py + python3 scripts/tests/test_runbooks.py python3 scripts/tests/test_devis_placement.py python3 scripts/tests/test_sonder.py diff --git a/docs/devis-services.md b/docs/devis-services.md index c59d674..009254d 100644 --- a/docs/devis-services.md +++ b/docs/devis-services.md @@ -30,7 +30,7 @@ make placement-plan # chaque VM est-elle là où le plan la met ## Le trou qu'il comble -`scripts/prouver.py` porte 82 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le +`scripts/prouver.py` porte 83 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le dépôt. Zéro appel réseau, zéro SSH, zéro `ansible`. Elles établissent que le dépôt est cohérent **avec lui-même** — que les handlers existent, que les intrants ont un propriétaire, que rien n'est codé en dur. diff --git a/docs/responsabilites-locataire-hebergeur.md b/docs/responsabilites-locataire-hebergeur.md index 2997207..efa5e7c 100644 --- a/docs/responsabilites-locataire-hebergeur.md +++ b/docs/responsabilites-locataire-hebergeur.md @@ -99,7 +99,7 @@ Nommer un point ouvert vaut mieux qu'une ligne rassurante sans mécanisme derri Aucune de ces responsabilités n'est laissée à la parole : ``` -make prouver le dépôt est-il cohérent avec lui-même (82 preuves, zéro réseau) +make prouver le dépôt est-il cohérent avec lui-même (83 preuves, zéro réseau) make remise-verifier le second temps de la remise est-il fait, ou en retard ? (P80) make certificats-plan ce que le disque porte contre ce que la mémoire sert make expositions-plan chaque service publié répond-il, et depuis où diff --git a/docs/runbooks-construction.yml b/docs/runbooks-construction.yml new file mode 100644 index 0000000..d9f016f --- /dev/null +++ b/docs/runbooks-construction.yml @@ -0,0 +1,821 @@ +--- +# LES RUNBOOKS DE CONSTRUCTION — l'ordre des gestes, et pourquoi celui-la. +# +# CE QUE CE FICHIER AJOUTE AU MAKEFILE, ET CE QU'IL NE REPETE PAS. +# +# Les 132 cibles documentees du Makefile disent chacune CE QU'ELLE FAIT. Aucune ne dit +# dans quel ORDRE, ni pourquoi maintenant, ni ce qu'il faut avoir mesure avant. Cette +# connaissance-la vivait en prose dans `docs/runbooks-exploitation.md`, +# `docs/implanter-un-tenant-sur-un-site.md` et `docs/preparer-un-site-hebergeur.md` — et +# la console offrait des boutons sans sequence. +# +# ON NE RECOPIE PAS LE LIBELLE D'UNE CIBLE. `scripts/runbooks.py` va le lire dans le +# Makefile au moment de servir. Un libelle recopie ici serait une seconde liste, et une +# liste qui suit une autre prend du retard sur elle. Ce fichier ne porte donc que ce que +# le Makefile ne peut pas porter : l'ordre, la nature du geste, la portee, le pourquoi. +# +# LA GARDE EST ECRITE AVEC LA LISTE, PAS APRES. `python3 scripts/runbooks.py verifier` +# (et P83) refusent qu'une cible documentee ne soit ni portee par un runbook ni exemptee +# avec un motif. Sans cela, la console cacherait des pouvoirs que le moteur possede. +# +# NATURE D'UNE ETAPE +# mesure n'ecrit rien, rejouable sans consequence, proposee meme apres un echec +# ecriture change l'etat du monde ; exige que l'etape precedente ait reussi +# destructif detruit ; exige une confirmation ecrite en plus de CONFIRMER=true +# +# PORTEE D'UN RUNBOOK — ce que la MACHINE porte, au sens de `contexte()` : +# tenant un ecosysteme est monte (`instance/`) : on le configure +# site une fabric est montee (`underlay.yml`) : on materialise +# poste les deux — l'atelier du mainteneur +# toute ni l'un ni l'autre n'est requis + +# CE QUE LA CONSOLE DOIT DEMANDER A L'EXPLOITANT. Une variable absente d'ici est refusee +# par la garde : la console ne saurait pas quoi afficher, et un champ libre sans invite +# est une invitation a se tromper. +variables: + HOTE: + invite: "La machine" + source: hotes # la liste vient de l'inventaire actif + GROUPE: + invite: "Le groupe (role)" + source: groupes + NOM: + invite: "Nom du dossier de l'ecosysteme" + exemple: "OPS-Machin" + MODELE: + invite: "Modele de depart" + source: modeles + INSTANCE: + invite: "Nom de l'ecosysteme a detruire (il doit etre ecrit en toutes lettres)" + SITE: + invite: "Depot du site a detruire (ecrit en toutes lettres)" + TENANT: + invite: "Dossier du locataire a amorcer" + exemple: "OPS-Machin" + DEPOT: + invite: "Un seul depot (vide = tous ceux que le runner porte)" + facultatif: true + VERS: + invite: "Repertoire de destination (une cle chiffree, hors du poste)" + ARCHIVE: + invite: "Fichier d'archive a restaurer" + CIBLE: + invite: "Hote ou adresse a sonder" + SERVICE: + invite: "Le lien dont on prouve la coupure" + valeurs: [artefacts, genome, resolveur] + ROLE: + invite: "Un seul role (vide = tous)" + facultatif: true + LIMITE: + invite: "Motif d'hotes" + facultatif: true + RECU_PAR: + invite: "Qui recoit la remise (nom complet)" + COURRIEL: + invite: "Courriel de la personne qui recoit" + DANS: + invite: "Jours avant le second temps" + facultatif: true + VMID: + invite: "Identifiant Proxmox de la VM" + DIALECTE: + invite: "Dialecte du commutateur" + valeurs: [cisco, binardat] + facultatif: true + PARALLELE: + invite: "Combien de VM a la fois" + facultatif: true + +runbooks: + + # ───────────────────────────────────────────────────────────────────────────── + - id: site-premier-jour + titre: "Le premier jour d'un site" + portee: site + doc: docs/runbooks-exploitation.md + but: >- + Faire naitre un site hebergeur depuis une fabric nue : les machines, puis le moteur, + puis la forge qui permettra aux locataires de se reproduire. L'ordre n'est pas une + preference — le premier passage s'ARRETE sur une forge vide, et c'est normal. + etapes: + - cible: underlay + nature: mesure + pourquoi: >- + La fabric declaree tient-elle debout toute seule ? Rien ne sert de creer des + machines sur un plan d'adressage qui se contredit. + - cible: underlay-plan + nature: mesure + pourquoi: >- + Le declare face au REEL, par l'API du cluster et une sonde TCP. C'est ici qu'on + apprend qu'un pont n'existe pas, pas au milieu de la creation des VM. + - cible: site-creer + nature: ecriture + fixes: {CONFIRMER: "true"} + duree: "~9 min" + pourquoi: >- + Les machines du site, depuis l'underlay. Elles naissent ; elles ne repondent pas + encore. + - cible: site-deployer-tout + nature: ecriture + fixes: {CONFIRMER: "true"} + duree: "~20 min" + pourquoi: >- + Premier passage. Il va jusqu'a `serveur_ops` et s'arrete sur la forge vide : + ce n'est pas un echec, c'est le maillon suivant. + - cible: forge-amorcer + nature: ecriture + fixes: {CONFIRMER: "true"} + duree: "~45 s" + pourquoi: >- + La forge tourne et n'a ni organisation ni depot. Sans cet amorcage, le runner + clone le vide et le deploiement ne finira jamais. + - cible: site-deployer-tout + nature: ecriture + fixes: {CONFIRMER: "true"} + duree: "~4 min" + pourquoi: >- + Second passage. Celui-ci doit finir a zero echec ; s'il n'y arrive pas, c'est + un vrai defaut, plus un maillon manquant. + - cible: genome-pousser + nature: ecriture + variables: [DEPOT] + pourquoi: >- + La forge du site FAIT AUTORITE : tant qu'elle est en retard, tout ecosysteme qui + s'y reproduit reproduit un moteur perime. + - cible: routes-fabric-etat + nature: mesure + pourquoi: >- + Les hyperviseurs routent-ils toutes les zones ? Une zone non routee ne se voit + qu'au moment ou un locataire y pose sa premiere machine. + - cible: gabarit-etat + nature: mesure + pourquoi: >- + Sans gabarit conforme, aucun locataire ne pourra cloner quoi que ce soit ici. + + # ───────────────────────────────────────────────────────────────────────────── + - id: site-tenir + titre: "Tenir un site en etat" + portee: site + doc: docs/hebergeur-exploitation.md + but: >- + Les gestes reguliers de l'hebergeur : porter le genome a jour, appliquer un role aux + machines du site, et regarder ce que la fabric fait vraiment. + etapes: + - cible: genome-etat + nature: mesure + pourquoi: >- + Ce que la forge porte, face au poste. Un ecart ici se paie chez TOUS les + locataires qui clonent ensuite. + - cible: genome-pousser + nature: ecriture + variables: [DEPOT] + pourquoi: "Remettre la forge au niveau du poste, depot par depot si besoin." + - cible: site-appliquer + nature: ecriture + variables: [GROUPE] + pourquoi: >- + Rejouer un role sur les machines du site — c'est ainsi qu'un runner reprend le + genome courant, rien ne le tire tout seul. + - cible: site-decrire + nature: mesure + pourquoi: "Ce que l'underlay declare comme machines de l'hebergeur." + - cible: site-inventaire + nature: mesure + pourquoi: "L'inventaire dynamique du site, tel qu'Ansible le voit — pas tel qu'on l'imagine." + - cible: site-intrants + nature: mesure + pourquoi: "Ce que ce site expose a ses locataires, derive et non declare deux fois." + - cible: site-verifier + nature: mesure + pourquoi: "Le playbook du site correspond-il encore aux couches declarees ?" + - cible: site + nature: ecriture + pourquoi: "Regenerer playbooks/site.yml quand les couches ont bouge." + - cible: routes-fabric-etat + nature: mesure + pourquoi: "Une route de zone manquante est invisible jusqu'au premier invite qui la traverse." + - cible: site-raser + nature: destructif + variables: [SITE] + fixes: {CONFIRMER: "true"} + pourquoi: >- + Detruire les VM du SITE. A ne faire que sur un site de chantier : la + reconstruction est prouvee, elle n'est pas gratuite. + + # ───────────────────────────────────────────────────────────────────────────── + - id: locataire-naitre + titre: "Faire naitre un ecosysteme de locataire" + portee: poste + doc: docs/multi-instances.md + but: >- + Du modele au plan monte : creer le dossier de l'ecosysteme, le rendre actif, puis + generer son inventaire. Tout l'adressage descend du seed `index` — on ne l'ecrit + jamais a la main. + etapes: + - cible: instance-modeles + nature: mesure + pourquoi: "Quels modeles de depart existent, avant d'en choisir un." + - cible: instances + nature: mesure + pourquoi: >- + Les ecosystemes deja decouverts, et surtout les COLLISIONS d'index : deux + ecosystemes sur le meme seed se marcheraient dessus en silence. + - cible: instance-creer + nature: ecriture + variables: [NOM, MODELE] + pourquoi: "Le dossier de l'ecosysteme, depuis un modele, avec son index reserve." + - cible: instance-utiliser + nature: ecriture + variables: [NOM] + pourquoi: >- + Basculer le symlink `instance/`. C'est lui qui decide QUEL ecosysteme la console + configure — et quelle voute elle ouvrira. + - cible: instance-courante + nature: mesure + pourquoi: "Verifier vers quoi on pointe avant d'ecrire quoi que ce soit." + - cible: intrants-verifier + nature: mesure + pourquoi: >- + Tout intrant qu'un role EXIGE est-il fourni ? Un intrant manquant ne se voit + sinon qu'au milieu d'un deploiement. + - cible: ports-verifier + nature: mesure + pourquoi: "Deux roles co-localises qui reclament le meme port ne cohabiteront pas." + - cible: instancier + nature: mesure + pourquoi: >- + Generer hosts.yml depuis le plan SANS l'appliquer — on regarde le diff avant de + le prendre. + - cible: instancier-appliquer + nature: ecriture + pourquoi: >- + Prendre l'inventaire genere. `hosts.yml` est un ARTEFACT : on edite le plan, on + ne le corrige jamais a la main. + - cible: inventaire-verifier + nature: mesure + pourquoi: "L'inventaire se parse-t-il, voute dechiffree ? Sinon rien ne partira." + + # ───────────────────────────────────────────────────────────────────────────── + - id: locataire-materialiser + titre: "Preparer le terrain d'un locataire sur la fabric" + portee: site + doc: docs/implanter-un-tenant-sur-un-site.md + but: >- + Tout ce que l'HEBERGEUR pose avant qu'un locataire puisse exister : le placement, les + pools, le SDN, les pare-feux et la frontiere. Chaque devis se mesure avant de + s'appliquer — un devis qu'on applique sans l'avoir lu est un pari. + etapes: + - cible: placement-plan + nature: mesure + pourquoi: >- + Le noeud, le stockage, le pont et le gabarit existent-ils VRAIMENT sur ce + cluster ? C'est la premiere chose qui manque, et la derniere qu'on regarde. + - cible: devis-proxmox-pools + nature: mesure + pourquoi: "Un pool par locataire, derive du plan — la cloison la plus simple." + - cible: devis-proxmox-pools-verifier + nature: mesure + pourquoi: "Le devis tient-il ses propres regles avant qu'on le pose ?" + - cible: devis-sdn + nature: mesure + pourquoi: "Zone, VNets et sous-reseaux, derives du seed de l'ecosysteme." + - cible: devis-sdn-verifier + nature: mesure + pourquoi: "Relire le devis SDN avant de toucher au reseau du cluster." + - cible: sdn-plan + nature: mesure + pourquoi: "L'ecart entre le SDN en service et ce devis — ce qui manque, ce qui est perime." + - cible: sdn-appliquer + nature: ecriture + fixes: {CONFIRMER: "true"} + pourquoi: >- + Reconcilier : creer ce qui manque et RETIRER ce qui est perime. Le retrait est la + moitie qu'on oublie. + - cible: flux + nature: ecriture + pourquoi: >- + Regenerer le registre des flux et les regles nftables depuis les `meta/flux.yml` + des roles. Tout ce qui suit en descend. + - cible: devis-proxmox-fw + nature: mesure + pourquoi: "Le pare-feu est-ouest intra-locataire, derive du registre des flux." + - cible: devis-proxmox-fw-verifier + nature: mesure + pourquoi: "Relire ce devis avant de le poser sur le cluster." + - cible: proxmox-fw-plan + nature: mesure + pourquoi: "L'ecart entre le pare-feu en service et le devis." + - cible: proxmox-fw-appliquer + nature: ecriture + fixes: {CONFIRMER: "true"} + pourquoi: "Poser IPSets, groupes et affectations — et retirer ce qui ne se declare plus." + - cible: devis-opnsense + nature: mesure + pourquoi: "La frontiere nord/sud, derivee du meme registre de flux." + - cible: devis-opnsense-verifier + nature: mesure + pourquoi: "Relire le devis de frontiere : c'est la porte de l'exterieur." + - cible: frontiere-plan + nature: mesure + pourquoi: "Ce que la frontiere porte aujourd'hui, face a ce devis." + - cible: frontiere-appliquer + nature: ecriture + fixes: {CONFIRMER: "true"} + pourquoi: >- + Reconcilier la frontiere. Les routes creees ETEINTES ont deja coute une journee : + une route eteinte compte « posee » et ne route rien. + - cible: devis-reseau + nature: mesure + variables: [DIALECTE] + pourquoi: >- + Le devis des commutateurs physiques (VLANs, SVIs, ACLs). Il se pose a la main sur + le materiel — le moteur ne configure pas les switches. + - cible: frontiere-mesurer + nature: mesure + pourquoi: >- + La frontiere refuse-t-elle ce qui n'est pas declare ? Un connect() qui aboutit ne + prouve rien : seule la LIVRAISON compte. + + # ───────────────────────────────────────────────────────────────────────────── + - id: locataire-deployer + titre: "Materialiser et deployer la flotte d'un locataire" + portee: poste + doc: docs/vm-lifecycle.md + but: >- + Des VM au service rendu : creer les machines manquantes, deployer couche par couche, + puis mesurer. Rien n'est « pret » avant la recette. + etapes: + - cible: flotte-creer + nature: ecriture + variables: [PARALLELE] + pourquoi: >- + Les VM manquantes, clonees depuis le gabarit dore. VMID, IP et VLAN sont DERIVES + du plan : on ne les saisit nulle part. + - cible: deployer-tout + nature: ecriture + duree: "long" + pourquoi: >- + Toute la flotte, dans l'ordre des couches. L'ordre vient du graphe de + dependances, pas d'une liste tenue a la main. + - cible: deployer-groupe + nature: ecriture + variables: [GROUPE] + facultative: true + pourquoi: >- + Un seul role, sur toute la flotte. C'est le geste d'apres : quand un role a change + et qu'on ne veut pas tout rejouer. + - cible: appliquer + nature: ecriture + variables: [GROUPE] + facultative: true + pourquoi: >- + Appliquer un groupe a la flotte. Meme usage que `deployer-groupe`, par le chemin + court — sans le graphe des couches. + - cible: verifier-deploiement + nature: mesure + pourquoi: "L'etat de la flotte apres coup — avant de croire que c'est fini." + - cible: valider + nature: mesure + pourquoi: >- + La recette de validation sur la flotte. C'est elle qui autorise le mot « pret », + pas l'absence d'erreur rouge. + + # ───────────────────────────────────────────────────────────────────────────── + - id: machine-une + titre: "Ajouter ou reprendre une seule machine" + portee: poste + doc: docs/vm-lifecycle.md + but: >- + Le cycle d'UNE machine, du plan au service : la voir derivee, la creer, la deployer, + la verifier. Le meme chemin sert pour une machine neuve et pour une machine a + reprendre. + etapes: + - cible: hote-afficher + nature: mesure + variables: [HOTE] + pourquoi: >- + Tout ce que le plan derive pour cette machine — avant de la creer, pour verifier + qu'on va bien poser ce qu'on croit. + - cible: creer-vm + nature: ecriture + variables: [HOTE] + duree: "~4 min 30" + pourquoi: "Cloner depuis le gabarit et ATTENDRE que la machine reponde." + - cible: deployer + nature: ecriture + variables: [HOTE] + pourquoi: "Les roles de cette machine, couche par couche, dans l'ordre du graphe." + - cible: verifier-hote + nature: mesure + variables: [HOTE] + pourquoi: "Le playbook de verification sur cette machine seule." + - cible: cloner-vm + nature: ecriture + variables: [HOTE, VMID] + facultative: true + pourquoi: >- + Cloner SANS passer par le plan. Chemin de reprise : a n'emprunter que lorsque le + plan ne peut pas encore deriver la machine. + + # ───────────────────────────────────────────────────────────────────────────── + - id: flotte-refaire + titre: "Raser et reconstruire un ecosysteme" + portee: poste + doc: docs/vm-lifecycle.md + but: >- + La preuve la plus dure du moteur : detruire un ecosysteme et le refaire depuis le + code seul. A ne lancer que sur un chantier — la prod vit ailleurs. + etapes: + - cible: raser + nature: destructif + variables: [INSTANCE] + fixes: {CONFIRMER: "true"} + pourquoi: >- + Detruire les VM derivees du plan. Le nom de l'ecosysteme s'ecrit en toutes + lettres : c'est le seul garde-fou qui resiste a un clic distrait. + - cible: reconstruire + nature: ecriture + duree: "long" + pourquoi: >- + Refaire tout depuis zero : les VM, puis le deploiement complet. Si le code ne + suffit pas, c'est ici qu'on l'apprend. + - cible: valider + nature: mesure + pourquoi: "Une reconstruction sans recette n'a rien prouve." + + # ───────────────────────────────────────────────────────────────────────────── + - id: gabarit + titre: "Le gabarit dore" + portee: site + doc: docs/procedure-template-debian13-proxmox.md + but: >- + La VM de reference que toute la flotte clone. Elle nait en q35/OVMF et ne se convertit + jamais : convertir depuis i440fx casse interfaces et disques. + etapes: + - cible: gabarit-etat + nature: mesure + pourquoi: "Le gabarit porte-t-il ce que le SITE declare ? A regarder avant d'y toucher." + - cible: preparer-modele + nature: ecriture + duree: "long" + pourquoi: "Preparer la VM de reference, celle qui sera clonee pour chaque hote." + - cible: verifier-modele + nature: mesure + pourquoi: "Le gabarit tient-il ses promesses avant qu'on le capture ?" + - cible: nettoyer-modele + nature: destructif + fixes: {CONFIRMER: "true"} + pourquoi: >- + Nettoyer avant capture. Destructif pour la VM de reference : ce qui est efface ne + se retrouve pas. + + # ───────────────────────────────────────────────────────────────────────────── + - id: mesurer + titre: "Mesurer sans rien ecrire" + portee: tenant + doc: docs/devis-services.md + but: >- + Les devis de service : ils confrontent ce que le plan derive a ce que le systeme rend + VRAIMENT, et sortent en erreur s'il y a un ecart. Une tache verte ne prouve pas qu'un + service rend son service. + etapes: + - cible: identite-plan + nature: mesure + pourquoi: "L'identite deployee face a ce que le plan derive." + - cible: certificats-plan + nature: mesure + pourquoi: >- + Les certificats sur disque face a ceux reellement SERVIS. Un cert renouvele mais + non recharge reste perime en memoire. + - cible: expositions-plan + nature: mesure + pourquoi: "Chaque exposition du plan repond-elle, depuis l'edge et depuis le poste ?" + - cible: expositions-etat + nature: mesure + pourquoi: "Chaque exposition est-elle servie sous un certificat qui la porte ?" + - cible: courriel-plan + nature: mesure + pourquoi: "La chaine Postfix → LDAP → Dovecot → IMAP, de bout en bout." + - cible: postgresql-plan + nature: mesure + pourquoi: "Le chiffrement impose et la portee reelle des acces." + - cible: mtu-mesurer + nature: mesure + pourquoi: "L'invite porte-t-il le MTU de sa zone ? Un MTU faux ne se voit qu'aux gros paquets." + - cible: versions-mesurer + nature: mesure + pourquoi: "De combien nos epinglages ont-ils vieilli face aux amonts ?" + - cible: ports-verifier + nature: mesure + pourquoi: "Deux roles co-localises revendiquent-ils le meme port ?" + - cible: intrants-verifier + nature: mesure + pourquoi: "Un intrant exige et non fourni est une panne differee." + - cible: flux-verifier + nature: mesure + pourquoi: "Le registre des flux correspond-il encore aux `meta/flux.yml` des roles ?" + - cible: site-intrants-verifier + nature: mesure + pourquoi: "Le locataire monte suit-il encore les intrants de son site ?" + - cible: sonder + nature: mesure + variables: [CIBLE] + pourquoi: >- + Sonder une cible et DIRE ce qui distingue absence, politique et frontiere. Un + « echec » nu ecrase ces trois causes et envoie chercher la panne ailleurs. + - cible: faits + nature: mesure + variables: [LIMITE] + pourquoi: "Les faits Ansible de la flotte, quand une hypothese demande un fait." + + # ───────────────────────────────────────────────────────────────────────────── + - id: recette + titre: "La recette — avant de dire « pret »" + portee: tenant + doc: docs/audit/plan-de-recette.md + but: >- + Ce qui separe « ca a tourne sans erreur » de « c'est livrable ». Les preuves lisent le + depot ; la recette interroge la flotte. Il faut les deux. + etapes: + - cible: verifier + nature: mesure + pourquoi: "Rejouer les preuves sans reecrire le rapport — la verification rapide." + - cible: prouver + nature: mesure + pourquoi: >- + Executer les preuves et ECRIRE le rapport date. C'est le document qu'on montre, + et celui qu'on relit dans six mois. + - cible: valider + nature: mesure + pourquoi: "La recette de validation sur la flotte reelle." + - cible: verifier-deploiement + nature: mesure + pourquoi: "L'etat de la flotte, apres coup." + - cible: inventaire + nature: mesure + pourquoi: "L'inventaire se verifie et se montre — lab puis production." + + # ───────────────────────────────────────────────────────────────────────────── + - id: acces-admin + titre: "L'acces d'administration (tunnel WireGuard)" + portee: site + doc: docs/acces-administration.md + but: >- + Un tunnel nominatif par locataire, declare par lui et borne a lui. Sans garde, un + ecosysteme s'ouvrirait un acces chez son voisin depuis son propre plan. + etapes: + - cible: vpn-admin-plan + nature: mesure + pourquoi: "Ce que la frontiere porte aujourd'hui, face aux pairs declares au plan." + - cible: vpn-admin-appliquer + nature: ecriture + fixes: {CONFIRMER: "true"} + pourquoi: "Poser l'instance et les pairs declares — et retirer ceux qui ne le sont plus." + + # ───────────────────────────────────────────────────────────────────────────── + - id: dns-public + titre: "Le DNS public et sa signature" + portee: tenant + doc: docs/dns-interne.md + but: >- + Publier les zones du locataire, signees avant d'etre exposees. Les DS partent chez le + registraire : c'est le seul maillon que le moteur ne peut pas poser lui-meme. + etapes: + - cible: dnssec-verifier + nature: mesure + pourquoi: "Une cle en voute pour chaque zone signee, et des DS conformes au registre." + - cible: dnssec-ds + nature: mesure + pourquoi: >- + Les DS a remettre au registraire, calcules depuis la voute du locataire. A porter + a la main chez le registraire : aucun automate ne le fera. + - cible: dns-bascule-devis + nature: mesure + pourquoi: >- + Basculer nos serveurs de noms changerait-il quelque chose ? Le plan face au DNS + reellement en service, avant de toucher a la delegation. + + # ───────────────────────────────────────────────────────────────────────────── + - id: filiation + titre: "Filiation, insemination, emancipation" + portee: tenant + doc: docs/filiation-emancipation.md + but: >- + D'ou vient cet ecosysteme, de quoi depend-il encore, et que faut-il couper pour qu'il + tienne seul. L'emancipation se mesure ; elle ne se decrete pas. + etapes: + - cible: genome + nature: mesure + pourquoi: "Les depots requis a la reproduction de cet ecosysteme, et leur etat." + - cible: genome-inscrire + nature: ecriture + pourquoi: "Inscrire la parente dans l'instance — sans quoi la filiation n'est qu'un souvenir." + - cible: genome-verifier + nature: mesure + pourquoi: "La parente inscrite tient-elle encore ? Le code de sortie repond." + - cible: inseminer + nature: ecriture + variables: [TENANT, HOTE] + pourquoi: >- + Le SITE amorce le runner d'un locataire, SANS ses secrets. Trois murs connus : + apt, pip, et le genome lui-meme. + - cible: emancipation-prouver + nature: mesure + variables: [SERVICE, HOTE] + fixes: {CONFIRMER: "true"} + pourquoi: >- + Prouver qu'un lien est coupe. La mesure instruit ; l'humain decide. Une + emancipation automatique serait une expulsion. + + # ───────────────────────────────────────────────────────────────────────────── + - id: remise + titre: "La remise au client" + portee: poste + doc: docs/remise-au-client.md + but: >- + Deux temps qui ne se confondent pas : le paquet qu'on remet, puis le re-cle qui + mesure la revocation reelle. Tant que le temps 2 n'est pas fait, on detient encore + les cles de quelqu'un d'autre. + etapes: + - cible: remise-recenser + nature: mesure + pourquoi: "Ce qu'une remise emporterait, sans rien ecrire. A lire avant de fabriquer." + - cible: remise-paquet + nature: ecriture + variables: [VERS] + pourquoi: "Temps 1 : le paquet chiffre, et relu apres ecriture." + - cible: remise-inscrire + nature: ecriture + variables: [RECU_PAR, COURRIEL, DANS] + pourquoi: >- + Inscrire la remise chez le locataire : qui a recu, quand, et dans combien de + jours le second temps est du. + - cible: remise-verifier + nature: mesure + pourquoi: "Temps 1 fait ? Temps 2 du, ou echu ? La seule reponse qui compte est datee." + - cible: remise-recleer + nature: ecriture + fixes: {CONFIRMER: "true"} + pourquoi: >- + Temps 2 : mesurer la revocation REELLE, puis estampiller. On ne coche pas cette + case, on la mesure. + + # ───────────────────────────────────────────────────────────────────────────── + - id: cles + titre: "Les cles hors du poste" + portee: poste + doc: docs/sortir-les-cles-du-poste.md + but: >- + Ce qui n'existe QUE sur le poste meurt avec lui. Les voutes sortent chiffrees, sur un + support qu'on relit — et on refait l'operation a chaque voute nouvelle. + etapes: + - cible: cles-recenser + nature: mesure + pourquoi: "Ce qui n'existe que sur ce poste, sans rien ecrire. La liste fait peur, c'est le but." + - cible: cles-exporter + nature: ecriture + variables: [VERS] + pourquoi: "Sortir les cles, chiffrees, et les RELIRE apres ecriture." + - cible: cles-compagnons + nature: ecriture + variables: [VERS] + pourquoi: >- + Deposer le script de restauration sur la cle. Une archive qu'on ne sait pas + rouvrir dans cinq ans n'est pas une sauvegarde. + - cible: cles-restaurer + nature: ecriture + variables: [ARCHIVE] + pourquoi: "Remettre les cles en place. A eprouver AVANT d'en avoir besoin." + - cible: depot-hors-site + nature: ecriture + variables: [VERS] + pourquoi: >- + Copier le depot de sauvegarde HORS du site. Le site heberge du chiffre et ne peut + pas le juger : la verification suit la cle. + + # ───────────────────────────────────────────────────────────────────────────── + - id: lire-le-plan + titre: "Lire le plan et ce qu'il derive" + portee: tenant + doc: docs/plan-et-generation.md + but: >- + Tout ce qui se regarde sans rien changer : les registres du plan, leur validation, et + l'inventaire qui en descend. Le premier geste devant un ecosysteme qu'on ne connait pas. + etapes: + - cible: serveurs + nature: mesure + pourquoi: "Les serveurs declares au plan." + - cible: serveurs-verifier + nature: mesure + pourquoi: "Le registre des serveurs tient-il ses regles ?" + - cible: applications + nature: mesure + pourquoi: "Les applications declarees au plan." + - cible: applications-verifier + nature: mesure + pourquoi: "Le registre des applications tient-il ses regles ?" + - cible: bases + nature: mesure + pourquoi: "Les bases de donnees declarees au plan." + - cible: bases-verifier + nature: mesure + pourquoi: "Le registre des bases tient-il ses regles ?" + - cible: domaines + nature: mesure + pourquoi: "Les domaines declares au plan." + - cible: domaines-verifier + nature: mesure + pourquoi: "Le registre des domaines tient-il ses regles ?" + - cible: inventaire-lister + nature: mesure + pourquoi: "L'inventaire complet, en JSON — la verite generee." + - cible: inventaire-graphe + nature: mesure + pourquoi: "Le graphe des groupes : qui herite de quoi." + - cible: inventaire-hote + nature: mesure + variables: [HOTE] + pourquoi: "Les variables derivees d'une machine, telles qu'Ansible les verra." + - cible: inventaire-lab + nature: mesure + pourquoi: "Le graphe de l'inventaire de laboratoire." + - cible: inventaire-production + nature: mesure + pourquoi: "Le graphe de l'inventaire de production." + - cible: config + nature: mesure + pourquoi: "La configuration Proxmox telle que le moteur la lit." + + # ───────────────────────────────────────────────────────────────────────────── + - id: depot-verifier + titre: "Verifier le depot avant de livrer" + portee: toute + doc: AGENTS.md + but: >- + Ce que le depot se doit a lui-meme : syntaxe, lint, tests, schema, fiches. Rien n'est + « pret » sans au moins la syntaxe du playbook touche et l'entree de CHANGELOG. + etapes: + - cible: syntaxe + nature: mesure + pourquoi: "La syntaxe de TOUS les playbooks. Jamais declarer pret si elle echoue." + - cible: lint + nature: mesure + pourquoi: "`ansible-lint` sur tout le depot." + - cible: test + nature: mesure + pourquoi: "Les tests unitaires de derivation — nomenclature et inventaire." + - cible: ci + nature: mesure + pourquoi: >- + Verifier le depot comme la CI, sur un modele public monte a l'ecart : sans jamais + toucher a l'ecosysteme monte. + - cible: schema + nature: ecriture + pourquoi: >- + Regenerer le schema du plan depuis les registres et les validateurs. C'est lui qui + construit les formulaires : un champ nouveau apparait sans toucher a l'interface. + - cible: fiches + nature: ecriture + variables: [ROLE] + pourquoi: "Regenerer la fiche de chaque role, depuis le role lui-meme." + - cible: plan-recette + nature: ecriture + pourquoi: "Regenerer le plan de recette depuis le wiki." + - cible: wiki-publier + nature: ecriture + pourquoi: "Publier le wiki vers la forge, une fois qu'il dit vrai." + +# CE QUE LA CONSOLE N'OFFRE PAS, ET POURQUOI. Une exemption muette serait un oubli +# deguise : chaque ligne porte son motif, et la garde refuse une exemption vide. +hors_assistant: + aide: >- + Aide en ligne de commande. La console porte la meme information autrement — chaque + etape affiche le libelle lu dans le Makefile. + ansible-runtime: >- + Prerequis interne, appele par les cibles qui deploient. L'offrir seul donnerait un + bouton qui ne fait rien de visible. + inventaire-ui: >- + C'est cette console elle-meme. Un bouton qui la relance depuis elle-meme n'a pas d'objet. + syntaxe-modele: "Couverte par `syntaxe`, qui passe tous les playbooks d'un coup." + syntaxe-verification-modele: "Couverte par `syntaxe`." + syntaxe-nettoyage: "Couverte par `syntaxe`." + syntaxe-verification-hote: "Couverte par `syntaxe`." + syntaxe-groupes: "Couverte par `syntaxe`." + syntaxe-proxmox: "Couverte par `syntaxe`." + model-creer: >- + Fabrique un MODELE d'ecosysteme — un geste de mainteneur du moteur, pas d'exploitant. + Il se fait au poste, en connaissance du catalogue des modeles. + serveurs-bootstrap: >- + Chemin de REPRISE : reconstitue le plan depuis un inventaire existant. Il ecrit par + dessus le plan, et ne doit pas etre a un clic d'un exploitant qui explore. + applications-bootstrap: >- + Meme raison que `serveurs-bootstrap` : amorcage de reprise, pas geste courant. + cacher-paquets: >- + Se fait EN LIGNE depuis le poste, avant de partir sur un site hors ligne. Le runner, + lui, est deja derriere la frontiere : le bouton serait au mauvais endroit. + ca-racine: >- + Recupere la racine de l'AC pour l'installer sur un poste. L'empreinte doit etre + verifiee A LA MAIN avant installation — un bouton encouragerait a sauter ce controle. + ca-empreinte: >- + Le temoin de comparaison de `ca-racine`. Meme raison : il se lit, il ne se clique pas. diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 2a5c7f7..235dd4d 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -272,9 +272,30 @@ POUVOIR_REQUIS = { "/api/deployer": "configurer", "/api/pousser": "configurer", "/api/creer": "materialiser", + # LA GARDE DE CETTE ROUTE EST PLUS FINE QUE LES AUTRES, ET C'EST VOULU. Un runbook de + # site et un runbook de locataire n'exigent pas le meme pouvoir : un seul mot ici + # serait soit trop laxiste (un site lancerait un geste de locataire), soit trop + # severe (un locataire ne pourrait plus rien). La portee est DECLAREE au registre, + # et c'est elle qu'on resout — jamais celle que le navigateur annonce. + "/api/runbook-etape": "selon-le-runbook", } +# CE QU'UNE PORTEE DE RUNBOOK EXIGE DE LA CONSOLE. Les deux pouvoirs de `contexte()`, et +# rien d'autre : `poste` porte les deux liens, il doit donc les deux. +POUVOIR_PAR_PORTEE = { + "tenant": ("configurer",), + "site": ("materialiser",), + "poste": ("configurer", "materialiser"), + "toute": (), +} + +# Ce qu'une valeur d'etape a le droit d'etre. Les commandes partent en LISTE (jamais par +# un shell), donc l'injection n'est pas le risque ; ce qui l'est, c'est qu'une valeur se +# fasse lire par `make` comme une option. On refuse donc ce qui commence par un tiret. +MOTIF_VALEUR = re.compile(r"^[^-\s][\w .@:/+=,-]{0,199}$", re.UNICODE) + + def _fichier_frontiere() -> Path: """Les intrants de la frontiere — a la RACINE du depot de site depuis le 2026-08-22. @@ -1727,7 +1748,29 @@ HTML = r""" .re-msg { background: var(--surface-3); border-radius: 6px; padding: 4px 8px; margin-top: 3px; font-family: ui-monospace, monospace; font-size: 11.5px; white-space: pre-wrap; word-break: break-word; } /* Devis réseau (config switch) */ .devis-reseau { margin: 8px 0 0; padding: 12px 14px; background: var(--surface-3); border-radius: 8px; font-family: ui-monospace, monospace; font-size: 11.5px; line-height: 1.5; white-space: pre; overflow: auto; max-height: calc(100vh - 210px); } - + +/* --- LES ASSISTANTS (runbooks) ------------------------------------------------ */ +.rb-liste{display:flex;flex-wrap:wrap;gap:8px;margin:2px 0 14px} +.rb-puce{border:1px solid var(--bord,#2a3340);background:#131a22;color:inherit;border-radius:999px; + padding:6px 13px;font-size:12.5px;font-weight:700;cursor:pointer;display:flex;align-items:center;gap:7px} +.rb-puce.on{background:#1d2a38;border-color:#3d6ea8} +.rb-puce.hors{opacity:.55} +.rb-etape{border:1px solid var(--bord,#2a3340);border-radius:10px;padding:11px 13px;margin-bottom:9px; + display:grid;grid-template-columns:30px 1fr auto;gap:11px;align-items:start;background:#10161d} +.rb-etape.faite{border-color:#2f6b45} +.rb-etape.ratee{border-color:#8a3b3b} +.rb-etape.bloquee{opacity:.5} +.rb-num{font-weight:800;opacity:.55;text-align:right;padding-top:2px} +.rb-cible{font-family:ui-monospace,Menlo,monospace;font-size:12.5px;font-weight:700} +.rb-libelle{font-size:12.5px;opacity:.9;margin-top:2px} +.rb-pourquoi{font-size:12px;opacity:.72;margin-top:5px;line-height:1.45} +.rb-nature{font-size:10.5px;font-weight:800;letter-spacing:.03em;padding:2px 7px;border-radius:999px;margin-left:7px} +.rb-nature.mesure{background:#1b2c3a;color:#8fc4f0} +.rb-nature.ecriture{background:#2e2716;color:#e6bf6a} +.rb-nature.destructif{background:#3a1c1c;color:#f08a8a} +.rb-meta{font-size:11.5px;opacity:.6;margin-top:4px} +.rb-but{font-size:12.5px;line-height:1.5;opacity:.88} +
@@ -1749,6 +1792,7 @@ HTML = r""" + @@ -1792,6 +1836,19 @@ HTML = r""" +python3 scripts/runbooks.py verifier' + echapper(rb.portee)
+ + ' · ' + compte + '/' + rb.etapes.length + ' étape(s) réussie(s) dans cette session.'
+ + echapper(rb.doc) + 'Makefile, '
+ + 'lus à chaque affichage ; l\'ordre et le « pourquoi » viennent de '
+ + 'docs/runbooks-construction.yml. Une étape d\'écriture attend que la '
+ + 'précédente ait réussi ; une mesure reste toujours offerte.