From e6c5f9f539ef96e2eed4940f447cce55cb77908f Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 24 Aug 2026 11:42:32 -0400 Subject: [PATCH] instancier : refuser une machine dont la fonction n'est pas declaree deriver_nomenclature(...) or {} avalait l'echec : une fonction absente rendait un dictionnaire vide, et la machine entrait dans l'inventaire avec ansible_host: None. La generation se declarait reussie ; la panne serait apparue au deploiement, sous une forme incomprehensible. Revele en portant serveur_ops dans les modeles : presence-web range son socle en zone 1 et n'a pas de categorie 4. Le defaut n'est pas apparu en ecrivant le role ni en le deployant chez patient 0 -- il a fallu le porter ailleurs. serveur_ops ne nomme plus patient 0 dans ses defauts : un ecosysteme distrait aurait clone le genome d'un autre, en silence. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 54 ++++++++++++++++++++ docs/audit/preuve-2026-08-24.md | 78 +++++++++++++++++++++++++++++ roles/serveur_ops/defaults/main.yml | 17 ++++++- roles/serveur_ops/tasks/main.yml | 14 ++++-- scripts/instancier.py | 27 ++++++++++ 5 files changed, 185 insertions(+), 5 deletions(-) create mode 100644 docs/audit/preuve-2026-08-24.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 294e206..ce34210 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,59 @@ # CHANGELOG — Set-OPS +## 2026-08-24 — Le poste d'exploitation entre dans les modèles, et un silence de plus est fermé + +`serveur_ops` n'existait que chez patient 0. Aucun modèle ne le portait — donc **aucun +écosystème livré n'aurait su se relire lui-même** : il aurait détenu son génome en +dépendant, pour l'exécuter, de la machine de quelqu'un d'autre. Les six modèles déclarent +désormais la fonction `ops`, la machine `ops-01` et son application. + +### Un défaut du rôle, corrigé avant qu'il ne serve + +Les valeurs par défaut de `serveur_ops` nommaient **patient 0** : + +```yaml +serveur_ops_depots: + - { depot: "ops-patient0", dest: "OPS-Patient0", role: "instance" } +``` + +Tout écosystème déployant un poste sans déclarer ses propres dépôts aurait donc cloné le +génome de patient 0 — silencieusement, en croyant piloter le sien. Le défaut ne nomme plus +que le moteur, qui est le même pour tous ; le dépôt d'instance **doit** être déclaré, et +la tâche d'exigence refuse de poursuivre sans lui. + +### Le silence que `presence-web` a révélé + +Ce modèle range tout son socle en zone 1 (« Fondations ») et n'a pas de catégorie 4. La +machine `ops-01` y a donc été posée sans que sa fonction existe — et le moteur a produit +un inventaire **en se déclarant réussi** : + +``` +ops-01 -> adresse=None vlan=None +``` + +`deriver_nomenclature(...) or {}` avalait l'échec : une fonction absente rendait un +dictionnaire vide, et la machine entrait dans l'inventaire sans adresse. La panne ne +serait apparue qu'au déploiement, sous une forme incompréhensible — Ansible tentant de +joindre une adresse qui n'existe pas. + +`instancier.py` refuse maintenant, en bloc et en nommant ce qu'il faut corriger : + +``` +Machines sans adresse derivable — leur `fonction` n'est pas declaree : + - ops-01 : fonction « ops » +Fonctions connues de ce plan : data, infra-dns, infra-edge, infra-mail, infra-pki, … +``` + +Contrôle négatif fait, puis contrôle positif sur les six modèles. + +### Ce que ça dit de la méthode + +Le défaut ne s'est pas montré pendant qu'on écrivait le rôle, ni pendant qu'on le +déployait chez patient 0 — où la catégorie 4 existe. Il a fallu **porter la pièce dans un +contexte différent** pour qu'il apparaisse. C'est la même leçon que Technolibre avait +donnée en révélant six défauts moteur invisibles avec un seul écosystème : un moteur ne +se prouve qu'au pluriel. + ## 2026-08-23 — La source d'artefacts : la forge sert le code, il manquait qui sert les binaires Pour poser une seule machine, un écosystème allait chercher chez **six serveurs diff --git a/docs/audit/preuve-2026-08-24.md b/docs/audit/preuve-2026-08-24.md new file mode 100644 index 0000000..b523768 --- /dev/null +++ b/docs/audit/preuve-2026-08-24.md @@ -0,0 +1,78 @@ +# Preuve de conformite — Set-OPS — 2026-08-24 + +> 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 (42 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 : 33 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 31 rôles, 82 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 | playbook: playbooks/proxmox/cloner_vm_debian.yml | +| 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 | 14 hotes, 31 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 : 24 secret(s) exige(s), tous presents. Voute reelle : 25 cle(s), aucun manque. | +| 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 : 7 reseau(x), aucune collision avec la plage tenant. | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 52 regles, 15 routes, admin=10.0.0.0/24,10.17.0.0/24,192.168.254.2/32,192.168.255.2/32. | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 48 groupe(s), 77 regle(s). | +| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (1 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, 33 VM placee(s), aucun nom ni VMID en collision. | +| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 25 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 14, 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 | 47 scripts expliques et atteignables, 98 cibles make documentees, 57 roles avec README. | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 34 exigence(s) de role, toutes satisfaites (118 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 (33 groupes). | +| P34 | Chaque document declare son lecteur | — | ✅ OK | 41 document(s) declarent leur lecteur (22 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 : 32 role(s) serveur/client tous nommes, 33 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 : 43 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 | + +## 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-08-24._ diff --git a/roles/serveur_ops/defaults/main.yml b/roles/serveur_ops/defaults/main.yml index e21211e..dfdc409 100644 --- a/roles/serveur_ops/defaults/main.yml +++ b/roles/serveur_ops/defaults/main.yml @@ -51,9 +51,19 @@ serveur_ops_forge_organisation: "genome" # (minuscules) ; les destinations reprennent la disposition du poste du mainteneur, où # le moteur et les instances sont des dossiers frères — c'est cette fraternité que # `scripts/instances.py` découvre. +# +# AUCUN DÉFAUT NE NOMME UN ÉCOSYSTÈME PARTICULIER (2026-08-24). +# +# La première version listait ici `ops-patient0`. Tout écosystème qui aurait déployé un +# poste sans déclarer ses propres dépôts aurait donc cloné le génome de **patient 0** — +# silencieusement, et en croyant piloter le sien. C'est la faute que ce dépôt combat sous +# tous ses déguisements : un défaut plausible qui rend un résultat faux sans rien dire. +# +# Le moteur est le seul défaut légitime : il est le même pour tous. Le dépôt d'instance, +# lui, est propre à l'écosystème et DOIT être déclaré — la tâche d'exigence refuse de +# poursuivre sans lui. serveur_ops_depots: - { depot: "set-ops-public", dest: "Set-OPS-public", role: "moteur" } - - { depot: "ops-patient0", dest: "OPS-Patient0", role: "instance" } # --- LES DEUX SYMLINKS (D-80) ------------------------------------------------ # @@ -68,7 +78,10 @@ serveur_ops_depots: # CE QUE LE POSTE N'AURA PAS POUR AUTANT : `underlay.vault.yml`, qui porte les secrets du # monde physique. Il est hors dépôt (gitignore), donc absent du génome. Le poste lit la # CARTE de la fabric, pas ses clés. -serveur_ops_instance: "OPS-Patient0" + +# Le dossier de l'instance pilotée, cible du lien `instance`. Sans valeur, le rôle +# refuse : un poste qui ne sait pas quel tenant il pilote n'a rien à piloter. +serveur_ops_instance: "" # Vide = le poste ne pose pas de lien `underlay.yml` (il configure, il n'engendre pas). serveur_ops_underlay: "" diff --git a/roles/serveur_ops/tasks/main.yml b/roles/serveur_ops/tasks/main.yml index 5328946..b0d786e 100644 --- a/roles/serveur_ops/tasks/main.yml +++ b/roles/serveur_ops/tasks/main.yml @@ -1,12 +1,20 @@ --- +# UN POSTE QUI NE SAIT PAS QUEL ÉCOSYSTÈME IL PILOTE N'A RIEN À PILOTER. +# +# On exige explicitement un dépôt d'instance, plutôt que de retomber sur un défaut : le +# défaut nommait patient 0, et un écosystème distrait aurait cloné le génome d'un autre +# sans qu'aucune erreur ne le signale. - name: Exiger les intrants du poste d'exploitation ansible.builtin.assert: that: - domaine_interne | default('') | length > 0 - - serveur_ops_depots | length > 0 + - (serveur_ops_depots | selectattr('role', 'eq', 'moteur') | list | length) == 1 + - (serveur_ops_depots | selectattr('role', 'eq', 'instance') | list | length) >= 1 + - serveur_ops_instance | default('') | length > 0 fail_msg: >- - serveur_ops exige `domaine_interne` et au moins un dépôt dans - `serveur_ops_depots` (le moteur au minimum). + serveur_ops exige `domaine_interne`, exactement un dépôt `role: moteur`, au moins + un dépôt `role: instance`, et `serveur_ops_instance` (le dossier que le lien + `instance` doit désigner). Déclarer le tout dans group_vars/serveur_ops.yml. - name: Installer l'outillage du poste ansible.builtin.apt: diff --git a/scripts/instancier.py b/scripts/instancier.py index d963f0a..3c00ad2 100644 --- a/scripts/instancier.py +++ b/scripts/instancier.py @@ -172,6 +172,33 @@ def generer() -> dict: sdn = underlay_mod.routage_tenants(underlay_mod.charger()) == "sdn" index_tenant = nomenclature.get("index") + # UNE MACHINE SANS FONCTION DECLAREE N'A PAS D'ADRESSE — ET LE DISAIT PAS (2026-08-24). + # + # `deriver_nomenclature(...) or {}` avale l'echec : une fonction absente de + # `nomenclature.fonctions` rend un dictionnaire vide, et la machine entre dans + # l'inventaire avec `ansible_host: None` et `vlan: None`. La generation se declare + # REUSSIE. La panne n'apparait qu'au deploiement, sous une forme incomprehensible — + # Ansible tentant de joindre une adresse qui n'existe pas. + # + # Constate en ajoutant `ops-01` aux modeles : `presence-web` range son socle en zone 1 + # (« Fondations ») et n'a pas de categorie 4. La machine a ete posee, la fonction non, + # et rien ne l'a signale. + # + # On refuse en BLOC plutot qu'a la premiere fautive : l'exploitant corrige son plan + # une fois, pas une machine a la fois. + sans_fonction = [ + nom for nom, srv in serveurs.items() + if not (deriver_nomenclature(str(srv.get("fonction", "")), + fonction_seq(nom)[1], nomenclature) or {}).get("adresse_ip") + ] + if sans_fonction: + connues = ", ".join(sorted((nomenclature.get("fonctions") or {}))) or "(aucune)" + raise SystemExit( + "Machines sans adresse derivable — leur `fonction` n'est pas declaree dans " + "`nomenclature.fonctions` :\n" + + "".join(f" - {n} : fonction « {serveurs[n].get('fonction', '')} »\n" for n in sans_fonction) + + f"Fonctions connues de ce plan : {connues}") + children: dict = { "modeles_vm": {"hosts": {}}, "hotes_actifs": {"hosts": {}},