clonage : le plancher se perdait au troisieme maillon

instancier le derivait, le playbook savait le poser, et la table qui les relie
ne transmettait rien. Quatorze machines allaient naitre avec le ballooning
desactive sans qu une seule etape echoue : variable vide, extra-var non passee,
garde when: qui saute. Trois silences en file.

P75 lit la cible creer-vm, releve chaque $SETOPS_X qu elle consomme et exige
que la table qui les emet le declare. Eprouvee dans les deux sens.

74 OK, 0 echec, 1 saute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
This commit is contained in:
Daniel Allaire 2026-09-13 20:10:24 -04:00
parent 77cf63f1cf
commit 224ac2fe32
7 changed files with 96 additions and 4 deletions

View file

@ -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 74 preuves de `make prouver` : elles lisent le dépôt, sans le
C'est aussi ce que font les 75 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.**

View file

@ -1,5 +1,37 @@
# CHANGELOG — Set-OPS
## 2026-09-13 (8) — Le plancher traversait trois modules et se perdait au troisieme
Trouve en preparant la creation des quatorze machines de TechnoLibre, une commande avant
de les creer. `instancier` derivait bien `proxmox_memoire_min` dans l'inventaire ; le
playbook de clonage savait bien poser `balloon:` ; entre les deux, la table
`CHAMPS_PROXMOX` de `inventory_host.py` ne transmettait **rien**.
### Pourquoi rien n'aurait echoue
Trois silences qui s'enchainent :
1. `$SETOPS_MEMOIRE_MIN` non defini vaut la chaine vide
2. le Makefile ne passe alors pas `-e proxmox_clone_memoire_min`
3. la garde `when:` de la tache saute proprement
Quatorze machines seraient nees avec le ballooning **desactive**, sans une seule erreur.
C'est le patron d'une liste qui en suit une autre et prend du retard — deja rencontre
quatre fois.
### La garde, ecrite en meme temps que le remede
**P75** lit la cible `creer-vm` du Makefile, releve chaque `$SETOPS_X` qu'elle consomme, et
exige que la table qui les EMET le declare. Elle ne juge aucune valeur : elle refuse qu'un
maillon manque. Eprouvee dans les deux sens — elle mord quand on retire le maillon, elle
se tait quand il est la.
La regle qui en sort : **quand une variable traverse trois modules, le troisieme s'ecrit en
meme temps que le premier, pas quand on s'en sert.**
`test_inventory_host.py` a signale le changement de contrat de son cote, comme il l'avait
fait pour `SETOPS_DOMAINE` et `SETOPS_CLES_AMORCAGE`.
## 2026-09-13 (7) — Un hote retire du plan laissait son pare-feu derriere lui
En retirant `backup-01` du plan de TechnoLibre, `flux-genere/backup-01.nft` est reste sur

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance` — inventaire `/home/danallaire/Espace Chezlepro/DépôtsSurForge/Set-OPS-public/instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (73 OK · 0 echec · 1 saute)
- **Verdict** : ✅ CONFORME (74 OK · 0 echec · 1 saute)
## Preuves
@ -69,7 +69,7 @@
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 14 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 0. |
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (74 preuves, 68 roles, 41 groupes). |
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (75 preuves, 68 roles, 41 groupes). |
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `904ece3` (publie le 2026-09-12). |
@ -87,6 +87,7 @@
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 5 intrant(s) du locataire concordent avec ce que le site expose. |
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
## Couverture des affirmations ✅ du registre

View file

@ -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 74 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le
`scripts/prouver.py` porte 75 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.

View file

@ -167,6 +167,19 @@ CHAMPS_PROXMOX: tuple[tuple, ...] = (
("SETOPS_NOEUD", "proxmox_noeud", False),
("SETOPS_COEURS", "proxmox_coeurs", False),
("SETOPS_MEMOIRE", "proxmox_memoire", False),
# LE PLANCHER : ce que l'hote a le droit de REPRENDRE a cette machine (`balloon`).
#
# OUBLIE ICI LE 2026-09-13, ET C'EST LE TROISIEME MAILLON. Le moteur derivait bien
# `proxmox_memoire_min` dans l'inventaire, le playbook de clonage savait bien poser
# `balloon:` — et entre les deux, cette table ne transmettait rien. Quatorze machines
# allaient naitre sans plancher, donc avec le ballooning DESACTIVE, sans qu'aucune
# etape n'echoue : `proxmox_clone_memoire_min` serait simplement reste indefini et sa
# garde `when:` aurait saute la tache en silence.
#
# C'est le patron d'une liste qui en suit une autre et prend du retard. La regle qui en
# sort : quand une variable traverse trois modules, la troisieme s'ecrit EN MEME TEMPS
# que la premiere, pas quand on s'en sert.
("SETOPS_MEMOIRE_MIN", "proxmox_memoire_min", False),
# Resolveur d'AMORCAGE, pose par cloud-init. Il ne sert qu'une fois : `client_resolveur`
# bascule ensuite `/etc/resolv.conf` vers 127.0.0.1. Mais sans lui, la VM nait sans
# resolution de noms et `apt` ne peut meme pas installer Unbound — l'amorcage

View file

@ -2093,6 +2093,46 @@ def preuve_carte_dit_vrai() -> tuple[bool, str]:
f"{len(_carte_chiffres_mesures())} chiffres correspondent a la mesure.")
def preuve_parametres_clone_traversent() -> tuple[bool, str]:
"""Tout `SETOPS_*` que le Makefile transmet au clonage a bien une SOURCE.
LE PLANCHER DE MEMOIRE A TRAVERSE TROIS MODULES ET S'EST PERDU AU TROISIEME
(2026-09-13). `instancier` derivait `proxmox_memoire_min` dans l'inventaire ; le
playbook de clonage savait poser `balloon:` ; entre les deux, `inventory_host.py` ne
transmettait rien. Quatorze machines allaient naitre avec le ballooning DESACTIVE.
ET RIEN N'AURAIT ECHOUE. `$$SETOPS_MEMOIRE_MIN` non defini vaut la chaine vide ; le
Makefile ne passe alors pas `-e proxmox_clone_memoire_min` ; la garde `when:` de la
tache saute proprement. Trois silences qui s'enchainent et une flotte muette.
CE QUE CETTE GARDE LIT, ET C'EST DELIBEREMENT MECANIQUE : chaque `$$SETOPS_X` que la
cible `creer-vm` consomme doit figurer dans `CHAMPS_PROXMOX`, la table qui les EMET.
Elle ne juge pas la valeur — elle refuse qu'un maillon manque.
"""
mk = (RACINE / "Makefile").read_text(encoding="utf-8")
ih = (RACINE / "scripts" / "inventory_host.py").read_text(encoding="utf-8")
# Le bloc de la cible `creer-vm` : de son entete a la ligne vide qui la ferme.
m = re.search(r"^creer-vm:.*?(?=^\S|\Z)", mk, re.S | re.M)
if not m:
return False, "cible `creer-vm` introuvable dans le Makefile — la garde ne lit plus rien."
consommes = set(re.findall(r"\$\$\{?(SETOPS_[A-Z_]+)", m.group(0)))
if not consommes:
return False, "aucun `$SETOPS_*` lu dans `creer-vm` — la garde ne lit plus rien."
# La table qui les emet, plus les variables posees a la main autour d'elle.
emis = set(re.findall(r'\("(SETOPS_[A-Z_]+)"', ih))
emis |= set(re.findall(r"^\s*(SETOPS_[A-Z_]+)\s*=", ih, re.M))
emis |= set(re.findall(r'"(SETOPS_[A-Z_]+)"\s*:', ih))
manquants = sorted(consommes - emis)
if manquants:
return False, ("transmis au clonage sans source dans `CHAMPS_PROXMOX` : "
+ ", ".join(manquants)
+ " — la VM naitra sans, et aucune etape n'echouera.")
return True, f"{len(consommes)} parametre(s) de clonage, tous emis par l'inventaire."
def preuve_gabarit_minimal_et_repris() -> tuple[bool, str]:
"""Le gabarit ne porte que l'indispensable, et rien de retire n'est perdu.
@ -3730,6 +3770,8 @@ PREUVES: list[dict] = [
"refs": [], "func": preuve_locataire_suit_son_site},
{"id": "P74", "titre": "Gabarit dore : une seule declaration, au plan du site",
"refs": [], "func": preuve_gabarit_une_seule_declaration},
{"id": "P75", "titre": "Les parametres de clonage traversent les trois maillons",
"refs": [], "func": preuve_parametres_clone_traversent},
]

View file

@ -98,6 +98,10 @@ def test_hote_complet_avec_noeud() -> None:
"SETOPS_NOEUD='noeud-a'",
"SETOPS_COEURS='4'",
"SETOPS_MEMOIRE='2048'",
# Ajoute le 2026-09-13 : le PLANCHER de memoire (`balloon`), ce que l'hote a
# le droit de reprendre. Vide ici — la fixture ne le declare pas — et c'est le
# point : ce test epingle le CONTRAT, pas les valeurs. Il a signale l'ajout.
"SETOPS_MEMOIRE_MIN=''",
"SETOPS_DNS=''",
# Ajoute le 2026-08-09 : le domaine de recherche cloud-init se derive de
# `domaine_interne`, comme le DNS. Ce test epingle le CONTRAT de