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:
parent
e7f1a74040
commit
a204ead8ab
5 changed files with 100 additions and 8 deletions
25
CHANGELOG.md
25
CHANGELOG.md
|
|
@ -1,5 +1,30 @@
|
||||||
# CHANGELOG — Set-OPS
|
# 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
|
## 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
|
**Un trou dans M2, trouvé avant tout usage.** `locataire-creer` comparait les paramètres de
|
||||||
|
|
|
||||||
|
|
@ -195,10 +195,19 @@ def constater(api: Cluster, place: dict, base: Path | None = None,
|
||||||
return lignes
|
return lignes
|
||||||
|
|
||||||
|
|
||||||
def afficher(lignes: list[dict], source: Path | None, depot: Path) -> bool:
|
def nom_du_tenant(source: Path | None, locataire: str | None = None) -> str:
|
||||||
# `.resolve()` : sans lui, un tenant monte par symlink s'annonce « instance » — le nom
|
"""Le nom que le devis annonce. Un devis doit nommer ce qu'il a mesure.
|
||||||
# du lien, pas celui du tenant. Un devis doit nommer ce qu'il a mesure.
|
|
||||||
tenant = source.parents[3].resolve().name if source else "?"
|
`.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"Devis du placement — tenant « {tenant} » sur l'hebergeur « {depot.name} »\n")
|
||||||
print(f" {'OBJET':<12} {'DEMANDE':<34} {'VERDICT'}")
|
print(f" {'OBJET':<12} {'DEMANDE':<34} {'VERDICT'}")
|
||||||
ok = True
|
ok = True
|
||||||
|
|
@ -237,8 +246,9 @@ def main() -> int:
|
||||||
ponts = contexte.ponts_publies(face)
|
ponts = contexte.ponts_publies(face)
|
||||||
else:
|
else:
|
||||||
place, source = placement_du_tenant()
|
place, source = placement_du_tenant()
|
||||||
|
tenant = nom_du_tenant(source, a.locataire)
|
||||||
if not place:
|
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
|
return 0
|
||||||
try:
|
try:
|
||||||
api, depot = Cluster.depuis_hebergeur()
|
api, depot = Cluster.depuis_hebergeur()
|
||||||
|
|
@ -259,7 +269,7 @@ def main() -> int:
|
||||||
print(f"Placement conforme : noeud, stockage, gabarit et reseaux des VM "
|
print(f"Placement conforme : noeud, stockage, gabarit et reseaux des VM "
|
||||||
f"existent sur « {depot.name} ».")
|
f"existent sur « {depot.name} ».")
|
||||||
return 0
|
return 0
|
||||||
return 0 if afficher(lignes, source, depot) else 1
|
return 0 if afficher(lignes, tenant, depot) else 1
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,18 @@ def couples_publies(locataire: str) -> tuple[Path, list[tuple[str, int]]]:
|
||||||
return loc.depot, contexte.couples_a_raser(face)
|
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]]:
|
def plan_derive(base: Path) -> list[tuple[str, int]]:
|
||||||
"""(nom d'hote, vmid) tels que le PLAN les derive — la seule source légitime."""
|
"""(nom d'hote, vmid) tels que le PLAN les derive — la seule source légitime."""
|
||||||
env = dict(os.environ, SETOPS_INSTANCE=str(base))
|
env = dict(os.environ, SETOPS_INSTANCE=str(base))
|
||||||
|
|
@ -153,7 +165,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
else:
|
else:
|
||||||
presentes.append((nom, vmid, v.get("node")))
|
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:
|
for nom, vmid, noeud in presentes:
|
||||||
print(f" À DÉTRUIRE {vmid} {nom:16} sur {noeud}")
|
print(f" À DÉTRUIRE {vmid} {nom:16} sur {noeud}")
|
||||||
for nom, vmid in absentes:
|
for nom, vmid in absentes:
|
||||||
|
|
@ -180,7 +192,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||||
|
|
||||||
if (a.instance or "").lower() != nom_actif:
|
if (a.instance or "").lower() != nom_actif:
|
||||||
print(f"\nREFUS : il faut NOMMER l'ecosysteme a raser.\n"
|
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.")
|
f"Taper le nom est ce qui distingue le POC de la production.")
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
"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__":
|
if __name__ == "__main__":
|
||||||
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")]
|
||||||
for t in tests:
|
for t in tests:
|
||||||
|
|
|
||||||
|
|
@ -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)")
|
print(f">>> code de sortie : {code} (2 = refus attendu)")
|
||||||
assert code == 2, "LE VERROU N'A PAS TENU"
|
assert code == 2, "LE VERROU N'A PAS TENU"
|
||||||
print(">>> le verrou tient : aucune VM n'aurait ete touchee")
|
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}")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue