From 4532a98707c07dbf0da13c2431367f1ac85fbd5e Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 5 Oct 2026 15:22:43 -0400 Subject: [PATCH] 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 --- AGENTS.md | 2 +- CHANGELOG.md | 28 ++++++++++++ docs/devis-services.md | 2 +- docs/responsabilites-locataire-hebergeur.md | 2 +- scripts/contexte.py | 47 ++++++++++++++++++++- scripts/prouver.py | 23 ++++++++++ scripts/tests/test_contexte.py | 20 +++++++++ 7 files changed, 120 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index b23af30..3d5ff2f 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 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.** diff --git a/CHANGELOG.md b/CHANGELOG.md index 37a326d..12b723f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 : diff --git a/docs/devis-services.md b/docs/devis-services.md index 08c4730..ebdfa94 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 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. diff --git a/docs/responsabilites-locataire-hebergeur.md b/docs/responsabilites-locataire-hebergeur.md index 9f1877a..2f3675b 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 (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ù diff --git a/scripts/contexte.py b/scripts/contexte.py index 30879f2..abd2117 100644 --- a/scripts/contexte.py +++ b/scripts/contexte.py @@ -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 ? diff --git a/scripts/prouver.py b/scripts/prouver.py index 425b130..5ba3363 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -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}, ] diff --git a/scripts/tests/test_contexte.py b/scripts/tests/test_contexte.py index cac093b..3c60dfc 100644 --- a/scripts/tests/test_contexte.py +++ b/scripts/tests/test_contexte.py @@ -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