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 # 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 ## 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, **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_repertoires_partages.py
python3 scripts/tests/test_frontiere_refus.py python3 scripts/tests/test_frontiere_refus.py
python3 scripts/tests/test_contexte.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 # 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 # 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_nom="$(HOTE)" \
-e proxmox_clone_vmid="$(VMID)" \ -e proxmox_clone_vmid="$(VMID)" \
-e proxmox_clone_ipconfig0="$$ipconfig" \ -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 "$(VMID_MODELE)" ]] && extra_vars+=( -e proxmox_clone_vmid_modele="$(VMID_MODELE)" ); \
[[ -n "$(NOEUD_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_noeud="$(NOEUD_PROXMOX)" ); \ [[ -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'é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 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 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 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. 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"])) 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: def construire(tenants: list[tuple[str, str, dict]]) -> dict:
"""{pools: [{pool, tenant, index, membres: [{nom, vmid, etat}], sans_vmid: []}]}""" """{pools: [{pool, tenant, index, membres: [{nom, vmid, etat}], sans_vmid: []}]}"""
blocs = [] blocs = []
@ -191,6 +205,8 @@ def main(argv: list[str]) -> int:
ap.add_argument("--verifier", action="store_true") ap.add_argument("--verifier", action="store_true")
ap.add_argument("--pool-actif", action="store_true", ap.add_argument("--pool-actif", action="store_true",
help="nom du pool du tenant actif (consomme par make creer-vm)") 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", ap.add_argument("--pool-site", action="store_true",
help="nom invariable du pool du genome (consomme par make site-creer)") help="nom invariable du pool du genome (consomme par make site-creer)")
args = ap.parse_args(argv) args = ap.parse_args(argv)
@ -198,6 +214,9 @@ def main(argv: list[str]) -> int:
if args.pool_actif: if args.pool_actif:
print(pool_actif()) print(pool_actif())
return 0 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). # 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: 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 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 `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 `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. 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", rc = lancer(["ansible-playbook", "-i", str(inv), "playbooks/backup/restauration.yml",
"-e", "restauration_action=sauvegarder"], env=env, "-e", "restauration_action=sauvegarder"], env=env,
filtre=("fatal:", "PLAY RECAP", "failed=")) 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": 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)}", f"INSTANCE={shlex.quote(ecosysteme)}",
filtre=("detruite", "VM detruites", "Refus", "REFUS", "ERREUR")) filtre=("detruite", "VM detruites", "Refus", "REFUS", "ERREUR"))
elif nom == "creer": 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", f"CONFIRMER=true PARALLELE=4",
filtre=("=== Creation", "a ECHOUE", "Toutes les VM", "Refus")) filtre=("=== Creation", "a ECHOUE", "Toutes les VM", "Refus"))
elif nom == "inseminer": 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())