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>
152 lines
6.6 KiB
Python
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())
|