From e7f1a7404013ae71e150bbc6f6ffd9a964092578 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 5 Oct 2026 17:27:25 -0400 Subject: [PATCH] contexte : materialisation M3, la reconstruction nomme son locataire reconstruire_locataire lance locataire-raser et locataire-creer TENANT= sur le runner du site, sans monter le depot du locataire comme instance. Trou de M2 corrige avant tout usage : le sous-make cloner-vm nommait le pool par l'instance montee (hors de tout pool sur le site, pool de l'autre locataire sur le poste). devis_proxmox_pools.py --pool-du-locataire le derive de la face. test_appels_locataire.py : un faux ansible-playbook consigne la ligne qui part ; 26 machines, ancien chemin, site et poste identiques a l'argument pres. Temoin : l'ancienne ligne de pool fait echouer le test. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 34 ++++++++++ Makefile | 3 +- docs/conception-contextes.md | 6 +- scripts/devis_proxmox_pools.py | 19 ++++++ scripts/reconstruire_locataire.py | 11 +++- scripts/tests/test_appels_locataire.py | 87 ++++++++++++++++++++++++++ 6 files changed, 155 insertions(+), 5 deletions(-) create mode 100644 scripts/tests/test_appels_locataire.py diff --git a/CHANGELOG.md b/CHANGELOG.md index c1da853..c550666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # CHANGELOG — Set-OPS +## 2026-10-05 (99) — Matérialisation, M3 : la reconstruction nomme son locataire, et le pool suit + +**Un trou dans M2, trouvé avant tout usage.** `locataire-creer` comparait les paramètres de +clonage, pas la ligne qui part. Le sous-make `cloner-vm` nommait encore le pool par l'instance +**montée** (`devis_proxmox_pools.py --pool-actif`). Sur le runner du site, sans instance, la VM +serait née hors de tout pool. Sur le poste, avec Chezlepro monté, +`locataire-creer TENANT=OPS-Technolibre` aurait rangé les VM dans le pool de Chezlepro. Mon +`make -n` ne descendait pas dans le sous-make : il ne pouvait pas le voir. Personne n'a lancé +`locataire-creer` entre-temps. + +**Fait** : +- `devis_proxmox_pools.py --pool-du-locataire ` : même dérivation (`pool_de`), à partir + de l'index de la face. `cloner-vm` l'emploie quand `TENANT` est donné. +- `reconstruire_locataire.py` : les étapes `raser` et `creer` lancent sur le runner du site + `make locataire-raser TENANT=…` et `make locataire-creer TENANT=…` ; plus de + `SETOPS_INSTANCE=/opt/setops/OPS-x`. Le dépôt du locataire y reste tiré : sa face y est publiée. +- `docs/conception-contextes.md` : l'état de l'étape 3. + +**Éprouvé** : +- `scripts/tests/test_appels_locataire.py` (dans `make test`) : un faux `ansible-playbook` + consigne la ligne qui part réellement. Pour les 26 machines, trois appels : l'ancien chemin + (instance montée), le site (aucune instance, `TENANT=`), le poste (l'autre locataire monté, + `TENANT=`). Identiques **à l'argument près**. +- Témoin : l'ancienne ligne de pool remise en place, le test échoue (pool vide côté site, pool + de l'autre locataire côté poste). +- Le test désigne les dépôts par des liens sans espace : le Makefile prend `SETOPS_INSTANCE` + dans des `$(wildcard …)` qu'une espace coupe en deux (`Espace Chezlepro` sur le poste ; le + runner du site vit sous `/opt/setops`). Premier essai faussé par là, les 26 « écarts » + venaient de l'instrument. + +**Pas encore** : la conformité horaire du site (`conformite-fabric.sh`) et `eprouver_parefeu.py` +montent encore un locataire comme instance pour leurs devis — reste de l'étape 3, à part de M3. +M4, la preuve par reconstruction, reste à faire et relève de l'exploitant. + ## 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, diff --git a/Makefile b/Makefile index fbf0024..997e639 100644 --- a/Makefile +++ b/Makefile @@ -334,6 +334,7 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire) python3 scripts/tests/test_repertoires_partages.py python3 scripts/tests/test_frontiere_refus.py python3 scripts/tests/test_contexte.py + python3 scripts/tests/test_appels_locataire.py # LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une # journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine @@ -1633,7 +1634,7 @@ cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE= -e proxmox_clone_nom="$(HOTE)" \ -e proxmox_clone_vmid="$(VMID)" \ -e proxmox_clone_ipconfig0="$$ipconfig" \ - -e proxmox_clone_pool="$(if $(POOL),$(POOL),$$(python3 scripts/devis_proxmox_pools.py --pool-actif))" \ + -e proxmox_clone_pool="$(if $(POOL),$(POOL),$$(python3 scripts/devis_proxmox_pools.py $(if $(TENANT),--pool-du-locataire $(TENANT),--pool-actif)))" \ ); \ [[ -n "$(VMID_MODELE)" ]] && extra_vars+=( -e proxmox_clone_vmid_modele="$(VMID_MODELE)" ); \ [[ -n "$(NOEUD_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_noeud="$(NOEUD_PROXMOX)" ); \ diff --git a/docs/conception-contextes.md b/docs/conception-contextes.md index b093e2d..2ebee46 100644 --- a/docs/conception-contextes.md +++ b/docs/conception-contextes.md @@ -183,7 +183,11 @@ les publie désormais (`sources_declarees`). La frontière, en trois temps : les **L'étape 2 est terminée** (2026-10-05). Étape 3 : l'instancier lit la fiche déposée par le site, et l'inventaire d'un locataire se génère sans son site, à l'octet près (P91). Le locataire publie sa face réseau (P92) ; les comptes de sauvegarde, le DNS public, le pare-feu Proxmox, la frontière et la -découverte des locataires du site la lisent. +découverte des locataires du site la lisent. La matérialisation aussi : la face publie les +paramètres de clonage (P93) ; `locataire-creer`, `locataire-raser` et `placement-plan TENANT=` +nomment leur locataire au lieu de le monter, et visent les mêmes machines, à l'argument près de +la ligne `ansible-playbook` (P94, `test_appels_locataire.py`) ; `reconstruire-locataire` les +emploie. Reste : la preuve par reconstruction. Méthodes du contrat : `site.fiche_pour(locataire)`, `locataire.face_reseau()`. Une classe n'ouvre jamais les fichiers de l'autre ; une preuve vérifiera la règle. diff --git a/scripts/devis_proxmox_pools.py b/scripts/devis_proxmox_pools.py index b7d8bb3..390d889 100644 --- a/scripts/devis_proxmox_pools.py +++ b/scripts/devis_proxmox_pools.py @@ -89,6 +89,20 @@ def pool_actif() -> str: return pool_de(instance.resolve().name, int(n["index"])) +def pool_du_locataire(nom: str) -> str: + """Pool d'un locataire NOMME, d'apres l'index de sa face publiee (materialisation M2). + + `pool_actif` lit l'instance MONTEE : appele par `make locataire-creer`, il rangeait les VM + hors de tout pool sur le runner du site (aucune instance), et dans le pool d'un AUTRE + locataire sur le poste (l'instance liee). Meme derivation (`pool_de`), autre source. + """ + import contexte + face = contexte.Locataire.charger(nom).face_publiee() + if face is None or face.get("index") is None: + raise SystemExit(f"{nom} n'a pas publie sa face reseau (ou elle ne porte pas d'index).") + return pool_de(contexte.Locataire.charger(nom).depot.resolve().name, int(face["index"])) + + def construire(tenants: list[tuple[str, str, dict]]) -> dict: """{pools: [{pool, tenant, index, membres: [{nom, vmid, etat}], sans_vmid: []}]}""" blocs = [] @@ -191,6 +205,8 @@ def main(argv: list[str]) -> int: ap.add_argument("--verifier", action="store_true") ap.add_argument("--pool-actif", action="store_true", help="nom du pool du tenant actif (consomme par make creer-vm)") + ap.add_argument("--pool-du-locataire", metavar="DEPOT", + help="nom du pool d'un locataire nomme, d'apres sa face (make locataire-creer)") ap.add_argument("--pool-site", action="store_true", help="nom invariable du pool du genome (consomme par make site-creer)") args = ap.parse_args(argv) @@ -198,6 +214,9 @@ def main(argv: list[str]) -> int: if args.pool_actif: print(pool_actif()) return 0 + if args.pool_du_locataire: + print(pool_du_locataire(args.pool_du_locataire)) + return 0 # LE SITE NE DERIVE D'AUCUN INDEX, DONC SON POOL NE SE CALCULE PAS (2026-09-13). # diff --git a/scripts/reconstruire_locataire.py b/scripts/reconstruire_locataire.py index 8f04dde..153bedb 100644 --- a/scripts/reconstruire_locataire.py +++ b/scripts/reconstruire_locataire.py @@ -100,7 +100,7 @@ def sur_site(args, commande: str, filtre: tuple[str, ...] = ()) -> int: def site_a_jour(args, tenant: str) -> int: """Le runner du SITE tire les trois depots qu'il va utiliser, AVANT sa premiere etape. - Le moteur (Set-OPS-public), le plan du locataire, et le depot du SITE — deduit du lien + Le moteur (Set-OPS-public), le depot du locataire (sa face reseau publiee), et le depot du SITE — deduit du lien `underlay.yml`, pas nomme : un autre site s'appelle autrement. Jusqu'au 2026-09-30 seul `raser` tirait, et seulement les deux premiers : une reprise `DEPUIS=creer` partait de ce que le runner avait sous la main, et `flotte-creer` lisait un plan du site jamais tire. @@ -200,12 +200,17 @@ def main() -> int: rc = lancer(["ansible-playbook", "-i", str(inv), "playbooks/backup/restauration.yml", "-e", "restauration_action=sauvegarder"], env=env, filtre=("fatal:", "PLAY RECAP", "failed=")) + # LE SITE NOMME SON LOCATAIRE, IL NE LE MONTE PLUS (2026-10-05, materialisation M3). + # Raser et creer lisaient le depot du locataire monte comme instance sur le runner du + # site (`SETOPS_INSTANCE=…/OPS-x`). Ils lisent desormais sa face reseau publiee : memes + # VMID, memes parametres de clonage, meme pool, a l'argument pres (P93, P94, + # `test_appels_locataire.py`). Le depot du locataire reste tire : sa face y est publiee. elif nom == "raser": - rc = sur_site(args, f"SETOPS_INSTANCE={args.racine_runner}/{T} make raser CONFIRMER=true " + rc = sur_site(args, f"make locataire-raser TENANT={shlex.quote(T)} CONFIRMER=true " f"INSTANCE={shlex.quote(ecosysteme)}", filtre=("detruite", "VM detruites", "Refus", "REFUS", "ERREUR")) elif nom == "creer": - rc = sur_site(args, f"SETOPS_INSTANCE={args.racine_runner}/{T} make flotte-creer " + rc = sur_site(args, f"make locataire-creer TENANT={shlex.quote(T)} " f"CONFIRMER=true PARALLELE=4", filtre=("=== Creation", "a ECHOUE", "Toutes les VM", "Refus")) elif nom == "inseminer": diff --git a/scripts/tests/test_appels_locataire.py b/scripts/tests/test_appels_locataire.py new file mode 100644 index 0000000..58b8b1a --- /dev/null +++ b/scripts/tests/test_appels_locataire.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""`make creer-vm TENANT=` lance-t-il le MEME clonage que l'ancien chemin, l'instance montee ? + +Compare, machine par machine, la ligne `ansible-playbook` qui part reellement — un faux +`ansible-playbook` en tete du PATH la consigne au lieu de cloner. Trois appels par machine : + - l'ancien chemin : `SETOPS_INSTANCE= make creer-vm HOTE=h` ; + - le runner du SITE : aucune instance, `TENANT=` ; + - le POSTE : l'AUTRE locataire monte, `TENANT=`. + +POURQUOI (2026-10-05, materialisation M2). Comparer les parametres de clonage ne suffisait pas : +le sous-make `cloner-vm` nommait encore le pool par l'instance MONTEE (`--pool-actif`). Sur le +site, la VM serait nee hors de tout pool ; sur le poste, dans le pool de l'autre locataire. Seule +la ligne finale montre tout ce qui part, y compris ce qu'aucune liste de parametres ne nomme. + +Les dossiers sont designes par des liens SANS ESPACE : le Makefile prend `SETOPS_INSTANCE` +dans des `$(wildcard …)`, qu'une espace coupe en deux (le runner du site vit sous `/opt/setops`). +""" +from __future__ import annotations + +import os +import subprocess +import sys +import tempfile +from pathlib import Path + +RACINE = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(RACINE / "scripts")) + +import contexte as C # noqa: E402 + +FAUX = "#!/bin/bash\nfor a in \"$@\"; do printf '%s\\n' \"$a\"; done >> \"$CAPTURE\"\n" + + +def appel(tmp: Path, instance: str, hote: str, tenant: str | None) -> tuple[int, str]: + capture = tmp / "capture" + capture.write_text("") + env = dict(os.environ, CAPTURE=str(capture), SETOPS_INSTANCE=instance, + PATH=f"{tmp / 'faux'}:{os.environ['PATH']}") + env.pop("SETOPS_INVENTAIRE", None) + cmd = ["make", "--no-print-directory", "creer-vm", "ATTENDRE=false", f"HOTE={hote}"] + if tenant: + cmd.append(f"TENANT={tenant}") + r = subprocess.run(cmd, cwd=RACINE, env=env, capture_output=True, text=True, + stdin=subprocess.DEVNULL) + return r.returncode, capture.read_text() + + +def main() -> int: + couples = C.couples_hebergeur() + locataires = sorted({l for _s, l in couples}, key=lambda l: l.nom) + if len(locataires) < 2: + print("SAUTE il faut deux locataires freres (le cas du poste monte l'autre)") + return 0 + echecs, n = [], 0 + with tempfile.TemporaryDirectory() as t: + tmp = Path(t) + (tmp / "faux").mkdir() + faux = tmp / "faux" / "ansible-playbook" + faux.write_text(FAUX) + faux.chmod(0o755) + liens = {} + for l in locataires: + liens[l.nom] = tmp / l.nom + liens[l.nom].symlink_to(l.depot.resolve()) + for l in locataires: + autre = next(x for x in locataires if x.nom != l.nom) + for h in C.hotes_actifs(l.face_publiee() or {}): + rc0, ancien = appel(tmp, str(liens[l.nom]), h, None) + rc1, site = appel(tmp, "/nulle/part", h, l.nom) + rc2, poste = appel(tmp, str(liens[autre.nom]), h, l.nom) + n += 1 + if rc0 or not ancien: + echecs.append(f"{l.nom} {h} : l'ancien chemin n'a rien lance (code {rc0})") + elif (rc1, site) != (0, ancien): + echecs.append(f"{l.nom} {h} : le site lancerait autre chose (code {rc1})") + elif (rc2, poste) != (0, ancien): + echecs.append(f"{l.nom} {h} : le poste ({autre.nom} monte) lancerait autre chose") + for e in echecs: + print("ECHEC " + e) + if echecs: + return 1 + print(f"OK {n} machine(s) : site et poste lancent le clonage de l'ancien chemin, a l'argument pres") + return 0 + + +if __name__ == "__main__": + sys.exit(main())