From 8294e5dc7134fb93c09b40d200e260d8ca749fa3 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 5 Oct 2026 17:08:21 -0400 Subject: [PATCH] contexte : materialisation M2, les commandes du site nomment leur locataire locataire-creer, locataire-raser et placement-plan TENANT= lisent la face reseau du locataire, sans monter son depot comme instance. flotte-creer, creer-vm et raser acceptent TENANT= ; sans lui, l'ancien chemin est inchange. P94 : creer, raser et placer par la face visent les memes machines, VMID, valeurs et ponts que l'ancien chemin, chacun mesure par sa commande lancee a part. 116 controles dans test_contexte.py, temoins compris. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- CHANGELOG.md | 36 ++++++++ Makefile | 36 +++++--- docs/devis-services.md | 2 +- docs/responsabilites-locataire-hebergeur.md | 2 +- docs/runbooks-construction.yml | 15 ++++ scripts/contexte.py | 95 +++++++++++++++++++++ scripts/devis_placement.py | 30 +++++-- scripts/prouver.py | 24 ++++++ scripts/raser.py | 24 +++++- scripts/tests/test_contexte.py | 29 +++++++ 11 files changed, 274 insertions(+), 21 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3d5ff2f..487a957 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 93 preuves de `make prouver` : elles lisent le dépôt, sans le +C'est aussi ce que font les 94 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 12b723f..c1da853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # CHANGELOG — Set-OPS +## 2026-10-05 (98) — Matérialisation, M2 : les commandes du site nomment leur locataire + +**Le chemin** (`docs/conception-contextes.md`, matérialisation M2) : le runner du site crée, +rase et confronte le placement d'un locataire **nommé**, d'après sa face réseau, sans monter +son dépôt comme instance. + +**Fait** : +- `make locataire-creer TENANT= CONFIRMER=true` remplace + `SETOPS_INSTANCE=… make flotte-creer`. `flotte-creer` et `creer-vm` acceptent `TENANT=` : + la liste des machines actives (`contexte.py --hotes-actifs`) et les paramètres de clonage + (`contexte.py --parametres-clonage … --hote`) viennent de la face. Sans `TENANT`, l'ancien + chemin est inchangé, et l'instance reste exigée. +- `make locataire-raser TENANT= CONFIRMER=true INSTANCE=` (et + `raser TENANT=`) : `raser.py --locataire` prend les couples (hôte, VMID) dans la face. Les + quatre verrous restent : liste, refus d'un VMID au nom étranger, `CONFIRMER`, nom tapé. +- `make placement-plan TENANT=` : `devis_placement.py --locataire` lit les valeurs + de placement (`placement`, désormais dans la face) et les ponts des machines publiées. +- Les deux cibles entrent au runbook du site (P83). + +**Éprouvé** : +- Lignes de paramètres de clonage **identiques à l'octet** aux appels de l'ancien chemin, pour + les 26 machines ; listes de machines actives identiques. +- **P94**, « Créer, raser et placer un locataire par sa face visent les mêmes machines » : + chaque intrant comparé à la commande de l'ancien chemin, lancée à part : `lister-actifs` + (création), `plan_derive` (le plan, via `serveurs.py lister`) pour raser, `proxmox.yml` et + les ponts dérivés de l'inventaire (placement). Aucun écart sur les deux locataires. +- `test_contexte.py`, 116 contrôles : un VMID, un état, un pont, une valeur de placement + altérés dans la face se voient, chacun du côté qu'il touche. +- `make -n` : `TENANT=` appelle les nouvelles options sans instance montée ; sans `TENANT` + et sans instance, refus. +- `make verifier` conforme, **94/94**. Les trois documents comptent 94 preuves. + +**Pas éprouvé ici** : la confrontation au cluster. Depuis le poste, l'API Proxmox refuse la +connexion (errno 111) aux **deux** chemins ; une sortie identique sur un refus ne prouve rien. +Elle se fera depuis le runner du site (M3), puis par reconstruction (M4). + ## 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 diff --git a/Makefile b/Makefile index b078b16..fbf0024 100644 --- a/Makefile +++ b/Makefile @@ -746,8 +746,8 @@ proxmox-fw-activer-vm: ansible-runtime ## Active le pare-feu d'UNE VM en le prou .PHONY: proxmox-fw-eprouver proxmox-fw-activer-vm .PHONY: placement-plan sdn-plan sdn-appliquer -placement-plan: ansible-runtime ## Le noeud, stockage, pont et gabarit du tenant existent-ils sur ce cluster ? (aucune ecriture) - python3 scripts/devis_placement.py +placement-plan: ansible-runtime ## Le noeud, stockage, pont et gabarit du tenant existent-ils sur ce cluster ? (aucune ecriture) [TENANT=] + python3 scripts/devis_placement.py $(if $(TENANT),--locataire $(TENANT)) sdn-plan: ansible-runtime ## Ecart entre le SDN EVPN (+ sortie des VRF) et son devis (aucune ecriture) python3 scripts/appliquer_sdn.py @@ -1138,14 +1138,23 @@ define VAULT_UNE_FOIS endef -flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le plan — PARALLELE=n +# LE SITE MATERIALISE UN LOCATAIRE NOMME, D'APRES SA FACE (2026-10-05, docs/conception-contextes.md, +# materialisation M2). Avec `TENANT=`, la liste des machines et leurs parametres de clonage +# viennent de `face-reseau.yml` que le locataire publie — rendus a l'octet pres comme +# `parametres-proxmox` (P93) — et aucune instance n'est montee. Sans `TENANT`, l'ancienne lecture +# de l'instance montee (le poste, le laboratoire). +LISTE_ACTIFS = $(if $(TENANT),python3 scripts/contexte.py --hotes-actifs $(TENANT),python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) lister-actifs) +PARAMS_CLONAGE = $(if $(TENANT),python3 scripts/contexte.py --parametres-clonage $(TENANT) --hote,python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote) +INSTANCE_OU_LOCATAIRE = $(if $(TENANT),,_instance-requise) + +flotte-creer: $(INSTANCE_OU_LOCATAIRE) ## Cree les VM manquantes de la flotte depuis le plan — PARALLELE=n @set -e; \ if [[ "$(CONFIRMER)" != "true" ]]; then \ printf '%s\n' 'Refus: creation de TOUTES les VM actives du plan (clone Proxmox).'; \ printf '%s\n' 'Relancer avec CONFIRMER=true.'; \ exit 2; \ fi; \ - hotes="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) lister-actifs)"; \ + hotes="$$($(LISTE_ACTIFS))"; \ if [[ -z "$$hotes" ]]; then printf '%s\n' 'Refus: aucun hote actif dans le plan.'; exit 2; fi; \ par="$(PARALLELE)"; par="$${par:-4}"; \ tmp="$$(mktemp -d)"; trap 'rm -rf "$$tmp"' EXIT; \ @@ -1154,7 +1163,7 @@ flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le n=0; \ for h in $$hotes; do \ printf '\n=== Creation VM: %s ===\n' "$$h"; \ - ( $(MAKE) creer-vm HOTE="$$h" > "$$tmp/$$h.log" 2>&1; printf '%s' "$$?" > "$$tmp/$$h.rc" ) & \ + ( $(MAKE) creer-vm HOTE="$$h" $(if $(TENANT),TENANT="$(TENANT)") > "$$tmp/$$h.log" 2>&1; printf '%s' "$$?" > "$$tmp/$$h.rc" ) & \ n=$$((n+1)); \ if (( n >= par )); then wait -n || true; n=$$((n-1)); fi; \ done; \ @@ -1169,6 +1178,13 @@ flotte-creer: _instance-requise ## Cree les VM manquantes de la flotte depuis le if (( echec )); then printf '\nAu moins une VM n a pas ete creee — rien ne continue.\n'; exit 2; fi; \ printf '\nToutes les VM actives sont creees.\n' +locataire-creer: ## Le SITE cree les VM d'un locataire d'apres sa face reseau — TENANT= CONFIRMER=true [PARALLELE=n] + @if [[ -z "$(TENANT)" ]]; then printf '%s\n' 'Refus: TENANT= requis.'; exit 2; fi + @$(MAKE) --no-print-directory flotte-creer TENANT="$(TENANT)" CONFIRMER="$(CONFIRMER)" PARALLELE="$(PARALLELE)" +locataire-raser: ## DESTRUCTIF : le SITE rase les VM d'un locataire d'apres sa face reseau — TENANT= CONFIRMER=true INSTANCE= [HOTE=h] + @if [[ -z "$(TENANT)" ]]; then printf '%s\n' 'Refus: TENANT= requis.'; exit 2; fi + @$(MAKE) --no-print-directory raser TENANT="$(TENANT)" CONFIRMER="$(CONFIRMER)" INSTANCE="$(INSTANCE)" HOTE="$(HOTE)" + # --- LES MACHINES DU SITE ----------------------------------------------------- # # UN SITE N'EST PAS UN PLAN. Un tenant se derive de son `index` ; un site n'en a pas et @@ -1434,8 +1450,8 @@ _attendre-flotte: ansible-runtime printf 'Flotte joignable.\n' .PHONY: reconstruire -raser: ansible-runtime _instance-requise ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE= - @python3 scripts/raser.py $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(HOTE),--hote $(HOTE)) $(if $(filter true,$(CONFIRMER)),--confirmer) +raser: ansible-runtime $(INSTANCE_OU_LOCATAIRE) ## DESTRUCTIF : detruit les VM derivees du plan — exige CONFIRMER=true ET INSTANCE= [TENANT=] + @python3 scripts/raser.py $(if $(TENANT),--locataire $(TENANT)) $(if $(INSTANCE),--instance $(INSTANCE)) $(if $(HOTE),--hote $(HOTE)) $(if $(filter true,$(CONFIRMER)),--confirmer) # `make flux` D'ABORD, et ce n'est pas une precaution : sans lui, `instance/flux-genere/` # est VIDE et le socle deploie nftables en `policy drop` SANS AUCUNE REGLE DERIVEE. La @@ -1660,13 +1676,13 @@ cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE= fi; \ ansible-playbook -i localhost, $(PLAYBOOK_PROXMOX_CLONER_VM) "$${vault_args[@]}" "$${extra_vars[@]}" -creer-vm: _instance-requise ## Cree une VM et attend qu'elle soit joignable — HOTE= +creer-vm: $(INSTANCE_OU_LOCATAIRE) ## Cree une VM et attend qu'elle soit joignable — HOTE= [TENANT=] @set -e; \ if [[ -z "$(HOTE)" ]]; then \ printf '%s\n' 'Refus: relancer avec HOTE=nom_hote (declare dans le plan).'; \ exit 2; \ fi; \ - params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \ + params="$$($(PARAMS_CLONAGE) $(HOTE))"; \ eval "$$params"; \ export SETOPS_CLES_AMORCAGE; \ $(MAKE) cloner-vm \ @@ -1707,7 +1723,7 @@ creer-vm: _instance-requise ## Cree une VM et attend qu'elle soit joignable — @# ATTENDRE LA DISPONIBILITE est un autre geste, deplace chez `deployer` : cloud-init @# et dpkg se guettent juste avant de configurer, par celui qui va configurer. @if [[ "$(ATTENDRE)" != "false" ]]; then \ - params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \ + params="$$($(PARAMS_CLONAGE) $(HOTE))"; \ eval "$$params"; \ python3 scripts/attendre_materialisation.py \ --vmid "$$SETOPS_VMID" --ip "$$SETOPS_IP" --hote "$(HOTE)" \ diff --git a/docs/devis-services.md b/docs/devis-services.md index ebdfa94..4be4454 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 93 preuves (dont une conditionnelle, sautée sans la clé de la voûte). Elles sont toutes **statiques** : elles lisent le +`scripts/prouver.py` porte 94 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 2f3675b..3dd3734 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 (93 preuves, zéro réseau) +make prouver le dépôt est-il cohérent avec lui-même (94 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/docs/runbooks-construction.yml b/docs/runbooks-construction.yml index 09bb0e0..a79c5d4 100644 --- a/docs/runbooks-construction.yml +++ b/docs/runbooks-construction.yml @@ -224,6 +224,21 @@ runbooks: - cible: routes-fabric-etat nature: mesure pourquoi: "Une route de zone manquante est invisible jusqu'au premier invite qui la traverse." + - cible: locataire-creer + nature: ecriture + variables: [TENANT, PARALLELE] + fixes: {CONFIRMER: "true"} + pourquoi: >- + Cloner les VM d'un locataire NOMME, d'apres la face reseau qu'il publie : le + runner du site materialise sans monter son depot. Memes machines, memes + parametres que `flotte-creer` sur l'instance montee (P93, P94). + - cible: locataire-raser + nature: destructif + variables: [TENANT, INSTANCE] + fixes: {CONFIRMER: "true"} + pourquoi: >- + Detruire les VM d'un locataire NOMME, d'apres sa face : les memes VMID que le + plan derive (P94). Son nom court s'ecrit en toutes lettres, comme pour `raser`. - cible: site-raser nature: destructif variables: [SITE] diff --git a/scripts/contexte.py b/scripts/contexte.py index abd2117..b53a424 100644 --- a/scripts/contexte.py +++ b/scripts/contexte.py @@ -575,6 +575,10 @@ class Locataire(Ecosysteme): if isinstance(c, dict)}}, "dns_public": dns_public, "sauvegarde": {"cle_publique": cle_sauvegarde}, + # Ses valeurs de placement (noeud, stockage, pont par defaut, gabarit, options de + # clone) : non sensibles, les jetons vivent dans sa voute. Le devis du placement les + # confronte au cluster. + "placement": _yaml(self.dossier_inventaire() / "group_vars" / "proxmox.yml"), "flux": flux, "conditions": conditions, } @@ -1125,6 +1129,24 @@ def verifier_face_publiee(locataire: "Locataire") -> list[str]: return [] +def lignes_clonage(face: dict, hote: str) -> list[str]: + """Les lignes shell que `parametres-proxmox` rend pour cet hote — depuis la face publiee. + Meme forme, a l'octet pres : `SETOPS_X='valeur'`, une valeur pouvant tenir sur plusieurs + lignes (les cles d'amorcage). `make creer-vm TENANT=` les evalue comme les autres.""" + m = (face.get("machines") or {}).get(hote) + if m is None: + raise SystemExit(f"Hote absent de la face reseau publiee : {hote}.") + c = m.get("clonage") or {} + if "erreur" in c: + raise SystemExit(c["erreur"]) + return [f"{k}='{v}'" for k, v in c.items()] + + +def hotes_actifs(face: dict) -> list[str]: + """Les machines actives que la face publie, dans l'ordre de `inventory_host lister-actifs`.""" + return [h for h, m in (face.get("machines") or {}).items() if m.get("etat") == "actif"] + + 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 @@ -1154,6 +1176,62 @@ def verifier_clonage(locataire: "Locataire", face: dict | None = None) -> list[s return ecarts +def couples_a_raser(face: dict) -> list[tuple[str, int]]: + """(nom d'hote, vmid) des machines que la face publie : ce que `raser --locataire` detruit.""" + return sorted((h, int(m["materialisation"]["proxmox_vmid"])) + for h, m in (face.get("machines") or {}).items() + if (m.get("materialisation") or {}).get("proxmox_vmid")) + + +def ponts_publies(face: dict) -> set[str]: + """Les ponts ou les machines publiees atterrissent : ce que `placement-plan` confronte.""" + return {str(m["materialisation"]["proxmox_pont"]) + for m in (face.get("machines") or {}).values() + if (m.get("materialisation") or {}).get("proxmox_pont")} + + +def verifier_materialisation(locataire: "Locataire", face: dict | None = None) -> list[str]: + """Les commandes du site qui NOMMENT leur locataire (materialisation M2) tirent-elles de sa + face les memes intrants que l'ancien chemin, l'instance montee ? + + Chaque cote est mesure par la commande meme de l'ancien chemin, a part : + - machines a creer : `inventory_host.py lister-actifs` (ce que lit `flotte-creer`) ; + - machines a raser : `raser.plan_derive` (le plan, via `serveurs.py lister`) ; + - placement et ponts : `devis_placement` sur l'instance (ses valeurs, ses ponts derives). + """ + import subprocess + import devis_placement + import raser + f = face if face is not None else (locataire.face_publiee() or {}) + ecarts: list[str] = [] + r = subprocess.run([sys.executable, "scripts/inventory_host.py", "--inventaire", + str(locataire.inventaire()), "lister-actifs"], + capture_output=True, text=True, cwd=RACINE) + if r.returncode != 0: + ecarts.append(f"lister-actifs refuse ({(r.stderr or '').strip().splitlines()[-1:]})") + elif sorted(hotes_actifs(f)) != sorted(r.stdout.split()): + diff = sorted(set(hotes_actifs(f)) ^ set(r.stdout.split())) + ecarts.append(f"machines a creer : la face differe de lister-actifs ({', '.join(diff)})") + # Le plan du locataire VISE, pas l'inventaire que `make` exporte pour l'instance montee : + # `serveurs.py` refuse, a raison, un SETOPS_INVENTAIRE hors de l'instance demandee. + garde = os.environ.pop("SETOPS_INVENTAIRE", None) + try: + plan = sorted(raser.plan_derive(locataire.depot)) + finally: + if garde is not None: + os.environ["SETOPS_INVENTAIRE"] = garde + if couples_a_raser(f) != plan: + diff = sorted(set(couples_a_raser(f)) ^ set(plan)) + ecarts.append(f"machines a raser : la face differe du plan ({diff[:4]})") + place = _yaml(locataire.dossier_inventaire() / "group_vars" / "proxmox.yml") + if dict(f.get("placement") or {}) != place: + ecarts.append("placement : la face differe de group_vars/proxmox.yml") + derives = devis_placement._ponts_derives(locataire.depot) + if ponts_publies(f) != derives: + ecarts.append(f"ponts : la face differe de l'inventaire ({sorted(ponts_publies(f) ^ derives)})") + 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 ? @@ -1258,6 +1336,11 @@ def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("--json", action="store_true") ap.add_argument("--fiche", metavar="LOCATAIRE", help="la fiche que son hebergeur lui destine") + ap.add_argument("--parametres-clonage", metavar="LOCATAIRE", + help="les parametres de clonage d'un hote (--hote), depuis sa face publiee") + ap.add_argument("--hote", help="avec --parametres-clonage") + ap.add_argument("--hotes-actifs", metavar="LOCATAIRE", + help="ses machines actives, depuis sa face publiee (une par ligne)") ap.add_argument("--publier-faces", action="store_true", help="chaque locataire ecrit sa face reseau dans son depot") ap.add_argument("--deposer-fiches", action="store_true", @@ -1298,6 +1381,18 @@ def main(argv: list[str] | None = None) -> int: return 1 print(yaml.safe_dump(site.fiche_pour(loc), allow_unicode=True, sort_keys=False), end="") return 0 + if args.parametres_clonage or args.hotes_actifs: + loc = Locataire.charger(args.parametres_clonage or args.hotes_actifs) + face = loc.face_publiee() + if face is None: + raise SystemExit(f"{loc.nom} n'a pas publie sa face reseau ({NOM_FICHIER_FACE_RESEAU}).") + if args.hotes_actifs: + print("\n".join(hotes_actifs(face))) + return 0 + if not args.hote: + raise SystemExit("--hote requis avec --parametres-clonage.") + print("\n".join(lignes_clonage(face, args.hote))) + return 0 if args.publier_faces: locataires = sorted({loc for _s, loc in couples_hebergeur()}, key=lambda l: l.nom) if not locataires: diff --git a/scripts/devis_placement.py b/scripts/devis_placement.py index 42643a2..3eb8f51 100644 --- a/scripts/devis_placement.py +++ b/scripts/devis_placement.py @@ -115,10 +115,13 @@ def _ponts_derives(base: Path | None) -> set[str]: def constater(api: Cluster, place: dict, base: Path | None = None, - routage: str = "sdn") -> list[dict]: - """Confronte chaque valeur au cluster. Renvoie une ligne par objet.""" + routage: str = "sdn", ponts: set[str] | None = None) -> list[dict]: + """Confronte chaque valeur au cluster. Renvoie une ligne par objet. + + `ponts` : ceux que la face reseau du locataire publie (`--locataire`) ; sans eux, ceux de + l'inventaire de l'instance montee.""" lignes: list[dict] = [] - place = {**place, "_instance": base} + place = {**place, "_instance": base, "_ponts": ponts} noeuds = [n.get("node") for n in _liste(api, "/nodes")] voulu = place.get("proxmox_clone_noeud") @@ -148,7 +151,8 @@ def constater(api: Cluster, place: dict, base: Path | None = None, # # D-80 corrigee le 2026-08-13 dit deja que la liaison de placement est nœud, stockage # et gabarit — le pont se derive. Le devis, lui, continuait de compter quatre objets. - ponts_derives = sorted(_ponts_derives(place.get("_instance"))) + ponts_derives = sorted(place["_ponts"] if place.get("_ponts") is not None + else _ponts_derives(place.get("_instance"))) if routage == "sdn": vnets = {v.get("vnet") for v in _liste(api, "/cluster/sdn/vnets")} manquants = [p for p in ponts_derives if p not in vnets] @@ -216,9 +220,23 @@ def afficher(lignes: list[dict], source: Path | None, depot: Path) -> bool: def main() -> int: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--verifier", action="store_true", help="code de sortie seulement") + ap.add_argument("--locataire", help="le locataire NOMME, d'apres sa face reseau publiee " + "(le site, sans monter son depot)") a = ap.parse_args() - place, source = placement_du_tenant() + ponts = None + if a.locataire: + # LE SITE CONFRONTE LE PLACEMENT D'UN LOCATAIRE NOMME (2026-10-05, materialisation M2) : + # ses valeurs de placement et les ponts de ses machines viennent de sa face publiee. + import contexte + face = contexte.Locataire.charger(a.locataire).face_publiee() + if face is None: + print(f"{a.locataire} n'a pas publie sa face reseau.", file=sys.stderr) + return 2 + place, source = dict(face.get("placement") or {}), None + ponts = contexte.ponts_publies(face) + else: + place, source = placement_du_tenant() if not place: print("Aucun placement declare par le tenant monte — rien a confronter.") return 0 @@ -230,7 +248,7 @@ def main() -> int: base = source.parents[3] if source else None routage = underlay_mod.routage_tenants(underlay_mod.charger()) - lignes = constater(api, place, base, routage) + lignes = constater(api, place, base, routage, ponts) if a.verifier: mauvais = [l for l in lignes if not l["existe"]] if mauvais: diff --git a/scripts/prouver.py b/scripts/prouver.py index 5ba3363..f81a74c 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -4895,6 +4895,28 @@ def preuve_clonage_publie() -> tuple[bool, str]: return True, f"{n} machine(s) : la face publie exactement les parametres de clonage de l'inventaire." +def preuve_materialisation_par_la_face() -> tuple[bool, str]: + """Les commandes du site qui nomment leur locataire tirent-elles de sa face les memes + intrants que l'ancien chemin ? + + POURQUOI (2026-10-05, `docs/conception-contextes.md`, materialisation M2). `locataire-creer`, + `locataire-raser` et `placement-plan TENANT=` ne montent plus le depot du locataire : ils + lisent sa face. Creer, raser et placer doivent donc viser exactement les memes machines, + VMID, valeurs et ponts que `flotte-creer`, `raser` et `placement-plan` sur l'instance montee + — chacun mesure par la commande de l'ancien chemin, 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 : aucune materialisation a confronter." + ecarts = [f"{l.nom} : {e}" for l in locataires for e in _ctx.verifier_materialisation(l)] + if ecarts: + return False, f"{len(ecarts)} ecart(s) : " + " | ".join(ecarts[:4]) + return True, (f"{len(locataires)} locataire(s) : creer, raser et placer visent par la face " + f"les memes machines que par l'instance montee.") + + PREUVES: list[dict] = [ {"id": "P01", "titre": "Lint (ansible-lint)", "refs": ["AFF-006"], "cmds": [["ansible-lint", "-q"]]}, @@ -5101,6 +5123,8 @@ PREUVES: list[dict] = [ "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}, + {"id": "P94", "titre": "Creer, raser et placer un locataire par sa face visent les memes machines", + "refs": [], "func": preuve_materialisation_par_la_face}, ] diff --git a/scripts/raser.py b/scripts/raser.py index 627b773..5bdce6d 100755 --- a/scripts/raser.py +++ b/scripts/raser.py @@ -59,6 +59,16 @@ def instance_active() -> tuple[Path, str]: return reel, nom_court(reel) +def couples_publies(locataire: str) -> tuple[Path, list[tuple[str, int]]]: + """(dossier, [(nom d'hote, vmid)]) d'apres la face reseau que le locataire publie.""" + import contexte + loc = contexte.Locataire.charger(locataire) + face = loc.face_publiee() + if face is None: + raise SystemExit(f"{locataire} n'a pas publie sa face reseau : rien a raser par elle.") + return loc.depot, contexte.couples_a_raser(face) + + def plan_derive(base: Path) -> list[tuple[str, int]]: """(nom d'hote, vmid) tels que le PLAN les derive — la seule source légitime.""" env = dict(os.environ, SETOPS_INSTANCE=str(base)) @@ -102,10 +112,19 @@ def main(argv: list[str] | None = None) -> int: ap.add_argument("--instance", help="nom de l'ecosysteme a raser (doit correspondre au monte)") ap.add_argument("--hote", help="ne raser QUE cet hote (les quatre verrous s'appliquent)") ap.add_argument("--confirmer", action="store_true") + ap.add_argument("--locataire", help="le locataire NOMME, d'apres sa face reseau publiee " + "(le site, sans monter son depot)") a = ap.parse_args(argv) - base, nom_actif = instance_active() - couples = plan_derive(base) + if a.locataire: + # LE SITE RASE UN LOCATAIRE NOMME (2026-10-05, materialisation M2) : ses machines et leurs + # VMID viennent de sa face publiee, sans monter son depot. Les quatre verrous restent : + # le nom court a taper en derive, comme pour l'instance montee. + base, couples = couples_publies(a.locataire) + nom_actif = nom_court(base) + else: + base, nom_actif = instance_active() + couples = plan_derive(base) if a.hote: # Les quatre verrous restent en vigueur : on ne fait que RESTREINDRE la liste # derivee du plan. Raser un seul hote sert a eprouver un gabarit ou a reprendre @@ -155,6 +174,7 @@ def main(argv: list[str] | None = None) -> int: if not a.confirmer: print(f"\n{len(presentes)} VM seraient DETRUITES, disques compris.\n" f"Relancer avec : make raser CONFIRMER=true INSTANCE={nom_actif}" + + (f" TENANT={a.locataire}" if a.locataire else "") + (f" HOTE={a.hote}" if a.hote else "")) return 0 diff --git a/scripts/tests/test_contexte.py b/scripts/tests/test_contexte.py index 3c60dfc..2c8bf57 100644 --- a/scripts/tests/test_contexte.py +++ b/scripts/tests/test_contexte.py @@ -655,6 +655,34 @@ def clonage_publie() -> None: verifier(bool(C.verifier_clonage(loc, f)), f"un parametre de clonage altere ({h}, memoire) se voit") +def materialisation_par_la_face() -> None: + """Creer, raser et placer par la face visent les memes intrants que par l'instance montee + — et une face alteree (VMID, etat, pont, placement) se voit, chacune a part.""" + import copy + couples = C.couples_hebergeur() + if not couples: + print("SAUTE aucun site frere : aucune materialisation a confronter") + return + for _s, loc in couples: + e = C.verifier_materialisation(loc) + verifier(not e, f"{loc.nom} : creer, raser et placer par la face = par l'instance" + + (f" ({e[0]})" if e else "")) + loc = couples[0][1] + h = sorted(loc.face_publiee()["machines"])[0] + def altere(fn): + f = copy.deepcopy(loc.face_publiee()) + fn(f) + return C.verifier_materialisation(loc, f) + e = altere(lambda f: f["machines"][h]["materialisation"].__setitem__("proxmox_vmid", 999999999)) + verifier(any("raser" in x for x in e), f"un VMID altere ({h}) se voit cote raser") + e = altere(lambda f: f["machines"][h].__setitem__("etat", "retire")) + verifier(any("creer" in x for x in e), f"un etat altere ({h}) se voit cote creation") + e = altere(lambda f: f["machines"][h]["materialisation"].__setitem__("proxmox_pont", "zzpont")) + verifier(any("ponts" in x for x in e), f"un pont altere ({h}) se voit cote placement") + e = altere(lambda f: f.__setitem__("placement", {**(f.get("placement") or {}), "proxmox_noeud": "zz"})) + verifier(any("placement" in x for x in e), "une valeur de placement alteree se voit") + + def main() -> int: with tempfile.TemporaryDirectory() as tmp: d = Path(tmp) @@ -678,6 +706,7 @@ def main() -> int: pools_complets() pools_et_tunnels_lisent_la_face() clonage_publie() + materialisation_par_la_face() if ECHECS: print(f"\n{len(ECHECS)} echec(s).") return 1