contexte : materialisation M1, la face publie les parametres de clonage (P93)

Chaque machine porte ce que parametres-proxmox rend, par la meme fonction.
P93 compare a la commande lancee a part pour les 26 machines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-10-05 15:22:43 -04:00
parent 40af2d1ec6
commit 4532a98707
7 changed files with 120 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 92 preuves de `make prouver` : elles lisent le dépôt, sans le
C'est aussi ce que font les 93 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,33 @@
# CHANGELOG — Set-OPS
## 2026-10-05 (97) — Matérialisation, M1 : la face publie les paramètres de clonage
**La décision de l'exploitant** : la seconde voie, fidèle au modèle — le **site** matérialise
les VM d'un locataire depuis sa face réseau, sans monter son dépôt. Plan validé avec lui :
M1 la face publie les paramètres de clonage ; M2 des commandes du site qui nomment leur
locataire (`locataire-creer`, `locataire-raser`, `placement-plan TENANT=`), à appels
identiques ; M3 la reconstruction les emploie ; M4 la preuve par reconstruction.
**Le constat** : la matérialisation tourne **déjà** sur le runner du site, mais elle y monte le
dépôt du locataire comme instance (`SETOPS_INSTANCE=/opt/setops/OPS-x make flotte-creer`), et
`make creer-vm` lit son inventaire par `inventory_host.py parametres-proxmox`.
**Fait (M1)** : chaque machine de la face porte `clonage`, exactement ce que `parametres-proxmox`
rend, calculé par la **même** fonction : VMID, adresse, masque, passerelle, VLAN, pont, stockage,
disque, nœud, cœurs, mémoire, DNS d'amorçage, domaine, et les clés d'amorçage — **publiques**
(vérifié : `ssh-ed25519` du runner du site et du runner du locataire, aucun bloc privé). Faces
republiées.
**Éprouvé** :
- **P93**, « La face publie, machine par machine, les paramètres de clonage de l'inventaire » :
la face publiée comparée à la commande `parametres-proxmox` elle-même, lancée à part pour
chacune des 26 machines. Aucun écart.
- Le témoin a d'abord signalé un écart sur `ops-01` : la seule machine à deux clés d'amorçage,
sur deux lignes, que ma lecture de la commande coupait au premier retour à la ligne. Le
défaut était dans le témoin ; il lit désormais la sortie comme le shell (`shlex`).
- `test_contexte.py`, 110 contrôles : un paramètre altéré dans la face se voit.
- `make verifier` conforme, **93/93**. Les trois documents comptent 93 preuves.
## 2026-10-05 (96) — Étape 3 : les pools et les tunnels d'administration lisent la face réseau
**Le chemin** (`docs/conception-contextes.md` §6), étape 3, matérialisation, premiers temps :

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

@ -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 (92 preuves, zéro réseau)
make prouver le dépôt est-il cohérent avec lui-même (93 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ù

View file

@ -450,12 +450,28 @@ class Locataire(Ecosysteme):
groupes = {g: sorted(h for h in _hotes_du_groupe(data, g) if h in ips)
for g in sorted(_enfants(data)) if est_groupe_operationnel(g)} if data else {}
actifs = set(_hotes_du_groupe(data, "hotes_actifs")) if data else set()
# LES PARAMETRES DE CLONAGE de chaque machine (M1 de la materialisation, 2026-10-05) :
# exactement ce que `make creer-vm` evalue aujourd'hui (`parametres-proxmox`), calcule
# ici par la MEME fonction. Le runner du site clonera d'apres eux, sans monter ce depot.
from inventory_host import parametres_proxmox_hote
def clonage(h: str) -> dict:
try:
lignes = parametres_proxmox_hote(data, h, self.inventaire())
except ValueError as e:
return {"erreur": str(e)}
out = {}
for ligne in lignes:
cle, _, valeur = ligne.partition("=")
out[cle] = valeur[1:-1] if valeur.startswith("'") and valeur.endswith("'") else valeur
return out
machines = {
h: {"ip": ip,
"etat": "actif" if h in actifs else "planifie",
"groupes": sorted(g for g, membres in groupes.items() if h in membres),
"materialisation": {k: v for k, v in sorted((hv.get(h) or {}).items())
if k.startswith("proxmox_")}}
if k.startswith("proxmox_")},
"clonage": clonage(h)}
for h, ip in sorted(ips.items())}
# Les reseaux d'administration que le locataire DECLARE (l'intrant), et son tunnel.
intrant: list[str] = []
@ -1109,6 +1125,35 @@ def verifier_face_publiee(locataire: "Locataire") -> list[str]:
return []
def verifier_clonage(locataire: "Locataire", face: dict | None = None) -> list[str]:
"""Machine par machine, la face publiee porte-t-elle les parametres de clonage que rend
`inventory_host.py parametres-proxmox` sur l'inventaire du locataire — la commande meme que
`make creer-vm` evalue ? Lancee a part, pour chaque machine : un temoin independant."""
import subprocess
f = face if face is not None else (locataire.face_publiee() or {})
ecarts: list[str] = []
for h, m in sorted((f.get("machines") or {}).items()):
r = subprocess.run([sys.executable, "scripts/inventory_host.py", "--inventaire",
str(locataire.inventaire()), "parametres-proxmox", "--hote", h],
capture_output=True, text=True, cwd=RACINE)
if r.returncode != 0:
ecarts.append(f"{h} : parametres-proxmox refuse ({(r.stderr or '').strip().splitlines()[-1:]})")
continue
# Lu COMME LE SHELL le lit (`eval` dans `make creer-vm`) : une valeur entre apostrophes
# peut tenir sur plusieurs lignes (deux cles d'amorcage sur le runner du locataire).
import shlex
attendu = {}
for jeton in shlex.split(r.stdout):
cle, _, valeur = jeton.partition("=")
if cle.startswith("SETOPS_"):
attendu[cle] = valeur
publie = m.get("clonage") or {}
if publie != attendu:
diff = sorted(k for k in set(publie) | set(attendu) if publie.get(k) != attendu.get(k))
ecarts.append(f"{h} : la face differe de parametres-proxmox ({', '.join(diff)})")
return ecarts
def verifier_inventaire_sans_site(locataire: "Locataire") -> list[str]:
"""L'inventaire du locataire se genere-t-il SANS son site, a l'octet pres ?

View file

@ -4874,6 +4874,27 @@ def preuve_face_publiee() -> tuple[bool, str]:
return True, f"{len(locataires)} locataire(s) : face reseau publiee, a jour."
def preuve_clonage_publie() -> tuple[bool, str]:
"""Chaque machine publiee porte-t-elle les parametres de clonage que rend
`parametres-proxmox` sur l'inventaire du locataire ?
POURQUOI (2026-10-05, `docs/conception-contextes.md`, materialisation M1). Le runner du site
va cloner les VM d'un locataire d'apres sa face, sans monter son depot. Elle doit donc porter
exactement ce que `make creer-vm` evalue aujourd'hui : VMID, adresse, pont, ressources, noeud,
DNS, domaine, cles d'amorcage (publiques). Compare a la commande meme, lancee a part.
"""
sys.path.insert(0, str(RACINE / "scripts"))
import contexte as _ctx
locataires = sorted({l for _s, l in _ctx.couples_hebergeur()}, key=lambda l: l.nom)
if not locataires:
return True, "Aucun locataire frere : aucun parametre de clonage a confronter."
ecarts = [f"{l.nom} : {e}" for l in locataires for e in _ctx.verifier_clonage(l)]
if ecarts:
return False, f"{len(ecarts)} ecart(s) : " + " | ".join(ecarts[:4])
n = sum(len((l.face_publiee() or {}).get("machines") or {}) for l in locataires)
return True, f"{n} machine(s) : la face publie exactement les parametres de clonage de l'inventaire."
PREUVES: list[dict] = [
{"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"],
"cmds": [["ansible-lint", "-q"]]},
@ -5078,6 +5099,8 @@ PREUVES: list[dict] = [
"refs": [], "func": preuve_inventaire_portable},
{"id": "P92", "titre": "Chaque locataire a publie sa face reseau a jour",
"refs": [], "func": preuve_face_publiee},
{"id": "P93", "titre": "La face publie, machine par machine, les parametres de clonage de l'inventaire",
"refs": [], "func": preuve_clonage_publie},
]

View file

@ -636,6 +636,25 @@ def pools_et_tunnels_lisent_la_face() -> None:
f"les tunnels d'administration lisent la face publiee ({len(pairs)} pair(s), puis 0)")
def clonage_publie() -> None:
"""La face publie, machine par machine, les parametres de clonage de `parametres-proxmox`
— et une valeur alteree se voit."""
import copy
couples = C.couples_hebergeur()
if not couples:
print("SAUTE aucun site frere : aucun parametre de clonage a confronter")
return
for _s, loc in couples:
e = C.verifier_clonage(loc)
verifier(not e, f"{loc.nom} : la face porte les parametres de clonage de chaque machine"
+ (f" ({e[0]})" if e else ""))
loc = couples[0][1]
f = copy.deepcopy(loc.face_publiee())
h = sorted(f["machines"])[0]
f["machines"][h]["clonage"]["SETOPS_MEMOIRE"] = "1"
verifier(bool(C.verifier_clonage(loc, f)), f"un parametre de clonage altere ({h}, memoire) se voit")
def main() -> int:
with tempfile.TemporaryDirectory() as tmp:
d = Path(tmp)
@ -658,6 +677,7 @@ def main() -> int:
decouverte_lit_la_face()
pools_complets()
pools_et_tunnels_lisent_la_face()
clonage_publie()
if ECHECS:
print(f"\n{len(ECHECS)} echec(s).")
return 1