pre-vol M4 : raser et le devis de placement nomment le locataire designe

Sur le chemin TENANT=, raser annoncait un ecosysteme monte (rien n'est
monte sur le runner du site) et le devis de placement un tenant « ? ».
Les deux en-tetes disent desormais ce qui est vise et d'ou vient la liste.
Le pre-vol lui-meme est consigne : 13 VM, aucun conflit, placement conforme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-10-07 10:16:04 -04:00
parent e7f1a74040
commit a204ead8ab
5 changed files with 100 additions and 8 deletions

View file

@ -1,5 +1,30 @@
# CHANGELOG — Set-OPS
## 2026-10-07 (100) — Pré-vol de M4 : la face tient devant le cluster, deux en-têtes disaient faux
**Le pré-vol**, lancé par l'exploitant depuis le runner du site, en lecture seule :
`make locataire-raser TENANT=OPS-Technolibre` (sans `CONFIRMER`) et
`make placement-plan TENANT=OPS-Technolibre`. C'est la **première confrontation de la face au
vrai cluster**, celle que M2 n'avait pas pu faire depuis le poste (errno 111). Résultat :
13 VM, chacune sous son nom, **aucun conflit** ; placement conforme (`asgard`, `TrueNAS`,
gabarit `99998`, les six réseaux `t23*`). Rien n'a été touché.
**Deux en-têtes disaient faux** sur le chemin `TENANT=`, sans rien changer à ce qui est visé :
- `raser.py` annonçait « Écosystème **monté** » sur le runner du site, où rien n'est monté. Il
dit désormais « Locataire **nommé** … d'après sa face publiée » (`entete()`), et le refus
faute de nom dit « Nomme » au lieu de « Monte ».
- `devis_placement.py` annonçait « tenant « **?** » » : avec `--locataire`, aucun fichier
source d'où tirer le nom. `nom_du_tenant()` rend celui qu'on a désigné.
Corrigés **avant** M4, pour que la reconstruction couvre le commit qu'on étiquettera.
**Éprouvé** :
- `test_raser.py` juge l'en-tête sur la **sortie réelle** de `raser.main`, aux deux chemins,
face à un faux cluster qui rend exactement les machines publiées.
- `test_devis_placement.py` : le nom désigné, le nom tiré de la source, et l'en-tête affiché.
- Témoins : les anciens libellés remis en place, les deux tests échouent (« EN-TETE TROMPEUR »,
puis l'assertion du devis).
## 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

View file

@ -195,10 +195,19 @@ def constater(api: Cluster, place: dict, base: Path | None = None,
return lignes
def afficher(lignes: list[dict], source: Path | None, depot: Path) -> bool:
# `.resolve()` : sans lui, un tenant monte par symlink s'annonce « instance » — le nom
# du lien, pas celui du tenant. Un devis doit nommer ce qu'il a mesure.
tenant = source.parents[3].resolve().name if source else "?"
def nom_du_tenant(source: Path | None, locataire: str | None = None) -> str:
"""Le nom que le devis annonce. Un devis doit nommer ce qu'il a mesure.
`.resolve()` : sans lui, un tenant monte par symlink s'annonce « instance » — le nom du
lien, pas celui du tenant. NOMME (`--locataire`, 2026-10-07) : il n'y a pas de fichier
source d'ou tirer le nom, le devis annoncait « ? » ; le nom est celui qu'on a donne.
"""
if locataire:
return locataire
return source.parents[3].resolve().name if source else "?"
def afficher(lignes: list[dict], tenant: str, depot: Path) -> bool:
print(f"Devis du placement — tenant « {tenant} » sur l'hebergeur « {depot.name} »\n")
print(f" {'OBJET':<12} {'DEMANDE':<34} {'VERDICT'}")
ok = True
@ -237,8 +246,9 @@ def main() -> int:
ponts = contexte.ponts_publies(face)
else:
place, source = placement_du_tenant()
tenant = nom_du_tenant(source, a.locataire)
if not place:
print("Aucun placement declare par le tenant monte — rien a confronter.")
print(f"Aucun placement declare par « {tenant} » — rien a confronter.")
return 0
try:
api, depot = Cluster.depuis_hebergeur()
@ -259,7 +269,7 @@ def main() -> int:
print(f"Placement conforme : noeud, stockage, gabarit et reseaux des VM "
f"existent sur « {depot.name} ».")
return 0
return 0 if afficher(lignes, source, depot) else 1
return 0 if afficher(lignes, tenant, depot) else 1
if __name__ == "__main__":

View file

@ -69,6 +69,18 @@ def couples_publies(locataire: str) -> tuple[Path, list[tuple[str, int]]]:
return loc.depot, contexte.couples_a_raser(face)
def entete(nom: str, base: Path, nomme: bool) -> str:
"""Ce que le devis vise, et COMMENT il le designe (2026-10-07).
Le chemin `--locataire` annoncait « Ecosysteme monte » sur le runner du site, ou rien
n'est monte : le locataire y est NOMME, et sa face publiee fait foi. Un devis qui raconte
une autre provenance que la sienne fait douter de tout le reste de la liste.
"""
if nomme:
return f"Locataire nommé : « {nom} » ({base}), d'après sa face publiée"
return f"Écosystème monté : « {nom} » ({base})"
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))
@ -153,7 +165,7 @@ def main(argv: list[str] | None = None) -> int:
else:
presentes.append((nom, vmid, v.get("node")))
print(f"Écosystème monté : « {nom_actif} » ({base})\n")
print(entete(nom_actif, base, bool(a.locataire)) + "\n")
for nom, vmid, noeud in presentes:
print(f" À DÉTRUIRE {vmid} {nom:16} sur {noeud}")
for nom, vmid in absentes:
@ -180,7 +192,7 @@ def main(argv: list[str] | None = None) -> int:
if (a.instance or "").lower() != nom_actif:
print(f"\nREFUS : il faut NOMMER l'ecosysteme a raser.\n"
f"Monte : « {nom_actif} » ; recu : « {a.instance or '(rien)'} ».\n"
f"{'Nomme' if a.locataire else 'Monte'} : « {nom_actif} » ; recu : « {a.instance or '(rien)'} ».\n"
f"Taper le nom est ce qui distingue le POC de la production.")
return 2

View file

@ -102,6 +102,21 @@ def test_le_reseau_mesure_est_celui_ou_les_VM_atterrissent(tmp=None):
"le devis ne doit plus presenter le pont des clones manuels"
def test_le_devis_nomme_le_locataire_designe():
"""Par `--locataire`, aucun fichier source d'ou tirer le nom : le devis annoncait
« tenant « ? » » au pre-vol de M4 (2026-10-07). Il nomme celui qu'on lui a designe."""
import contextlib
import io
assert dp.nom_du_tenant(None, "OPS-Exemple") == "OPS-Exemple"
source = Path("/d/OPS-Monte/inventories/principal/group_vars/proxmox.yml")
assert dp.nom_du_tenant(source) == "OPS-Monte", "le chemin monte nomme toujours son tenant"
sortie = io.StringIO()
with contextlib.redirect_stdout(sortie):
dp.afficher([], dp.nom_du_tenant(None, "OPS-Exemple"), Path("/d/SITE-Exemple"))
entete = sortie.getvalue().splitlines()[0]
assert "« OPS-Exemple »" in entete and "?" not in entete, f"en-tete : {entete!r}"
if __name__ == "__main__":
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
for t in tests:

View file

@ -55,3 +55,33 @@ print(f"\n>>> instance montee : {NOM} | collision fabriquee sur {VMID_1} ({NOM
print(f">>> code de sortie : {code} (2 = refus attendu)")
assert code == 2, "LE VERROU N'A PAS TENU"
print(">>> le verrou tient : aucune VM n'aurait ete touchee")
# L'EN-TETE DIT D'OU VIENT LA LISTE (2026-10-07). Sur le runner du site, `--locataire`
# annoncait « Ecosysteme monte » — rien n'y est monte, le locataire est NOMME et sa face fait
# foi. Lu au pre-vol de M4, avant de raser : un devis qui se trompe de provenance fait douter
# de la liste. Le cluster rend ici exactement les machines publiees : rien n'est refuse, et
# sans `--confirmer` rien n'est fait — seul l'en-tete est juge, sur la sortie reelle.
import contextlib # noqa: E402
import io # noqa: E402
try:
_, COUPLES_FACE = raser.couples_publies(BASE.name)
except (SystemExit, Exception) as e: # noqa: BLE001 — une instance sans face publiee
COUPLES_FACE = []
print(f">>> SAUTE l'en-tete du chemin nomme : {e}")
if COUPLES_FACE:
class ClusterConforme(FauxCluster):
def __call__(self, chemin, methode="GET", corps=None):
return [{"type": "qemu", "vmid": v, "name": n, "node": "asgard"}
for n, v in COUPLES_FACE]
raser.Cluster = ClusterConforme
for args, attendu, banni in ((["--locataire", BASE.name], "Locataire nommé", "monté"),
([], "Écosystème monté", "nommé")):
sortie = io.StringIO()
with contextlib.redirect_stdout(sortie):
code = raser.main(args)
entete = sortie.getvalue().splitlines()[0]
assert code == 0, f"code {code} sur un cluster conforme ({' '.join(args) or 'instance'})"
assert attendu in entete and banni not in entete, f"EN-TETE TROMPEUR : {entete!r}"
print(f">>> {' '.join(args) or 'instance montee'} : {entete}")