Set-OPS-Public/scripts/devis_placement.py
Daniel Allaire 3af607e8b5 placement : un devis confronte les QUATRE objets au cluster — dont le gabarit
J'avais ecarte le gabarit de P37 (« objet du cluster, pas une liste declaree »).
L'exploitant a releve que ce n'etait pas une raison de ne pas le verifier : ca
deplace la question du STATIQUE vers le DEVIS.

devis_placement.py interroge le cluster et confronte les quatre valeurs : le noeud
existe ; le stockage existe ET accepte `images` ; le pont existe SUR LE NOEUD
retenu ; le gabarit existe ET porte template=1 — cloner une VM ordinaire
fonctionnerait, et produirait quatorze copies d'une machine vivante.

AU PREMIER PASSAGE il a trouve une declaration perimee : vmbr3 n'existe plus sur
aucun noeud (disparu au passage du transport VXLAN sur les interfaces VLAN), mais
proxmox-hebergeur.yml le declarait encore depuis le 3 aout — et P37 le validait,
puisqu'elle valide la DECLARATION, pas le cluster.

Invisible jusqu'ici : flotte-creer surcharge le pont avec le VNet derive de chaque
zone, donc le defaut n'aurait morde que sur un `make cloner-vm` manuel. Corrige
des deux cotes ; le defaut des trois tenants passe a vmbr1, que le cluster nomme
lui-meme « VM (5Gig) ».

P37 et ce devis ne se remplacent pas : l'un garde la coherence entre deux
declarations, l'autre confronte la declaration au reel. Il fallait les deux pour
voir un pont disparu depuis dix jours.

P31 a refuse le script tant qu'aucune cible make ne l'atteignait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 10:11:25 -04:00

152 lines
6.6 KiB
Python

#!/usr/bin/env python3
"""Devis du PLACEMENT — les quatre objets d'hebergeur que le tenant monte designe.
D-80 : un tenant est agnostique de son underlay ; tout son adressage derive du seed.
Ce qui le lie a une fabric tient en QUATRE valeurs, et elles nomment des objets qui
appartiennent a l'hebergeur :
proxmox_clone_noeud sur quel noeud cloner
proxmox_clone_stockage ou poser le disque
proxmox_clone_pont ou brancher la carte
proxmox_clone_vmid_modele quel gabarit cloner
**P37 en verifie trois**, statiquement, contre les listes que l'hebergeur DECLARE
(`proxmox-hebergeur.yml`). C'est utile et insuffisant : une liste declaree peut elle-meme
etre perimee, et le GABARIT n'y figure pas — c'est un objet du cluster, pas une
declaration.
Ce devis interroge donc le CLUSTER. Il ne verifie pas ce qui est ecrit : il verifie ce qui
existe. Sur un site inconnu, c'est la premiere chose a savoir — sans quoi l'ecart se
decouvre au premier clone, apres quarante minutes de deploiement.
N'ECRIT RIEN. Lecture seule, sans effet de bord.
python3 scripts/devis_placement.py # tableau lisible
python3 scripts/devis_placement.py --verifier # code de sortie (0 = tout existe)
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
import yaml
RACINE = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(RACINE / "scripts"))
from proxmox_api import Cluster # noqa: E402
INVENTAIRES = ("principal", "production", "lab")
def placement_du_tenant() -> tuple[dict, Path | None]:
"""Les valeurs de placement declarees par le tenant MONTE."""
for nom in INVENTAIRES:
f = RACINE / "instance" / "inventories" / nom / "group_vars" / "proxmox.yml"
if f.is_file():
return (yaml.safe_load(f.read_text(encoding="utf-8")) or {}), f
return {}, None
def constater(api: Cluster, place: dict) -> list[dict]:
"""Confronte chaque valeur au cluster. Renvoie une ligne par objet."""
lignes: list[dict] = []
noeuds = [n.get("node") for n in (api("/nodes") or [])]
voulu = place.get("proxmox_clone_noeud")
lignes.append({"objet": "noeud", "voulu": voulu,
"existe": voulu in noeuds, "offre": noeuds})
# Un stockage ne suffit pas : il doit accepter le contenu `images`, sinon on ne peut
# pas y poser un disque de VM — et l'erreur ne se lit qu'au clone.
stockages = {}
for s in (api("/storage") or []):
stockages[s.get("storage")] = str(s.get("content") or "")
voulu = place.get("proxmox_clone_stockage")
lignes.append({"objet": "stockage", "voulu": voulu,
"existe": voulu in stockages and "images" in stockages[voulu],
"offre": [k for k, v in stockages.items() if "images" in v],
"note": ("" if voulu not in stockages or "images" in stockages[voulu]
else f"present mais n'accepte pas `images` ({stockages[voulu]})")})
# Le pont doit exister SUR LE NOEUD retenu : un pont partiel est un piege, la VM ne
# demarre que sur certains noeuds et l'erreur ne le dit pas.
n = place.get("proxmox_clone_noeud")
ponts: list[str] = []
if n in noeuds:
ponts = [i.get("iface") for i in (api(f"/nodes/{n}/network") or [])
if str(i.get("type")) in ("bridge", "OVSBridge")]
voulu = place.get("proxmox_clone_pont")
lignes.append({"objet": "pont", "voulu": voulu,
"existe": voulu in ponts, "offre": ponts,
"note": "" if n in noeuds else "noeud introuvable : pont non verifiable"})
# Le gabarit : celui que P37 ne peut PAS voir. Presence, et surtout `template=1` —
# cloner une VM ordinaire marcherait, et produirait quatorze copies d'une machine
# vivante.
vmid = place.get("proxmox_clone_vmid_modele")
vms = {v.get("vmid"): v for v in (api("/cluster/resources?type=vm") or [])}
trouve = vms.get(int(vmid)) if vmid is not None else None
est_gabarit = bool(trouve and str(trouve.get("template", 0)) == "1")
lignes.append({"objet": "gabarit", "voulu": vmid,
"existe": est_gabarit,
"offre": [f"{v.get('vmid')} ({v.get('name')})" for v in vms.values()
if str(v.get("template", 0)) == "1"],
"note": ("" if est_gabarit else
(f"VMID {vmid} existe (« {trouve.get('name')} ») mais n'est PAS "
f"un gabarit" if trouve else f"aucune VM au VMID {vmid}"))})
return lignes
def afficher(lignes: list[dict], source: Path | None, depot: Path) -> bool:
print(f"Devis du placement — tenant « {source.parents[3].name if source else '?'} » "
f"sur l'hebergeur « {depot.name} »\n")
print(f" {'OBJET':<10} {'DEMANDE':<22} {'VERDICT'}")
ok = True
for l in lignes:
verdict = "existe" if l["existe"] else "INTROUVABLE"
ok = ok and l["existe"]
print(f" {l['objet']:<10} {str(l['voulu']):<22} {verdict}")
if l.get("note"):
print(f" {'':<10} {'':<22} -> {l['note']}")
if not l["existe"] and l["offre"]:
offert = ", ".join(str(x) for x in l["offre"][:6])
print(f" {'':<10} {'':<22} offert : {offert}")
print("\n" + ("CONFORME : les quatre objets de placement existent sur ce cluster."
if ok else
"ECART : le tenant designe des objets que ce cluster n'offre pas."))
return ok
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--verifier", action="store_true", help="code de sortie seulement")
a = ap.parse_args()
place, source = placement_du_tenant()
if not place:
print("Aucun placement declare par le tenant monte — rien a confronter.")
return 0
try:
api, depot = Cluster.depuis_hebergeur()
except Exception as e: # noqa: BLE001 — l'API est un tiers : on rend la cause lisible
print(f"Cluster injoignable : {e}", file=sys.stderr)
return 2
lignes = constater(api, place)
if a.verifier:
mauvais = [l for l in lignes if not l["existe"]]
if mauvais:
for l in mauvais:
print(f"placement : {l['objet']} « {l['voulu']} » introuvable"
+ (f" — {l['note']}" if l.get("note") else ""), file=sys.stderr)
return 1
print(f"Placement conforme : noeud, stockage, pont et gabarit existent sur "
f"« {depot.name} ».")
return 0
return 0 if afficher(lignes, source, depot) else 1
if __name__ == "__main__":
sys.exit(main())