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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-24 11:42:32 -04:00
parent 32e1f6fbcd
commit e6c5f9f539
5 changed files with 185 additions and 5 deletions

View file

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

View file

@ -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._

View file

@ -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: ""

View file

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

View file

@ -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": {}},