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 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-10-05 17:27:25 -04:00
parent 8294e5dc71
commit e7f1a74040
6 changed files with 155 additions and 5 deletions

View file

@ -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 <dépôt>` : 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,

View file

@ -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=<nom>
-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)" ); \

View file

@ -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.

View file

@ -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).
#

View file

@ -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":

View file

@ -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=<locataire> make creer-vm HOTE=h` ;
- le runner du SITE : aucune instance, `TENANT=<locataire>` ;
- le POSTE : l'AUTRE locataire monte, `TENANT=<locataire>`.
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())