#!/usr/bin/env python3 """Les machines de l'HEBERGEUR, lues dans `underlay.yml` — pas dans un plan. POURQUOI UN SCRIPT SEPARE, ET PAS UNE OPTION DE `inventory_host.py`. Un tenant se DERIVE : de son seul `index` descendent son supernet, ses VLAN, ses VMID, ses VNet SDN. Un site ne derive de rien — il n'a pas d'index, il EST le terrain sur lequel les tenants derivent. J'avais d'abord fait entrer le site dans le generateur des tenants, avec une branche « si l'adresse est declaree, elle gagne » ; elle est retiree, et ce fichier existe pour qu'elle ne revienne pas. CE QU'IL NE FAIT PAS. Il ne clone rien, ne parle a aucune API. Il TRADUIT une declaration d'underlay en les memes `SETOPS_*` que `make cloner-vm` consomme deja. Le travail reel — le clone, l'attente, la cle SSH — reste au chemin eprouve, et il n'en existe qu'un. Les gardes sont dans `underlay.valider()`, pas ici : ce script suppose une carte deja validee, et `make prouver` s'en assure avant. """ from __future__ import annotations import argparse import shlex import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import underlay as U # noqa: E402 # Ce que vaut une machine du site quand son `gabarit:` ne le dit pas. Volontairement # modeste : une machine de l'hebergeur sert UN role, elle n'heberge pas de charge. GABARIT_DEFAUT = {"vcpu": 2, "memoire": 2048, "disque": "40G"} # Le plancher de memoire suit la MEME regle que pour un tenant — un site n'est pas un # genre different de machine, seulement un autre proprietaire. from inventory_rules import plancher_memoire as _plancher # noqa: E402 def _groupes_de(nom: str) -> list[str]: """Les groupes que cette machine du site porte, d'apres le plan.""" apps = U.lire_plan_site("applications.yml").get("applications") or {} return sorted({str(a.get("groupe")) for a in apps.values() if a.get("hote") == nom}) def _taille(v) -> str: """Une taille de disque PORTE TOUJOURS SON UNITE. `disque: 40` a produit un `resize` a « 40 » que Proxmox a lu comme un RETRECISSEMENT : « shrinking disks is not supported ». Le playbook tolere cette erreur — a raison, un disque deja assez grand n'est pas une panne — donc la machine est nee avec les 16 Go du gabarit au lieu de 40, SANS QUE RIEN NE LE DISE. Le plan des tenants ecrit `40G` depuis toujours ; le site ecrit pareil, et un entier nu se voit rattacher son unite plutot que de repartir en silence. """ s = str(v).strip() return f"{s}G" if s.isdigit() else s def materialisation() -> dict: """Gabarit, stockage et mode de clonage — le gabarit vient du PLAN du site. Sans declaration du site, le playbook de clonage prenait ces valeurs dans les group_vars du TENANT ACTIF : `make site-creer` aurait clone depuis un gabarit different selon le symlink `instance`, sans rien dire. LE REMEDE AVAIT CREE UNE SECONDE DECLARATION (corrige le 2026-09-13). Le VMID vivait a la fois dans `plan/10-intrants.yml` (`gabarit:`) et dans `underlay.yml` (`materialisation.vmid_modele`). Les deux ont diverge : le plan disait 9006, l'underlay 99998 — que le plan nomme justement `precedent`. Consequence, invisible parce que les deux gabarits clonent : les MACHINES DU SITE naissaient de l'ancien, les machines des LOCATAIRES du nouveau. Le Makefile, lui, derivait deja `VMID_MODELE` de `underlay.gabarit()`, c'est-a-dire du plan. On lit donc la meme source que tout le monde. Le motif d'origine est preserve : `underlay.gabarit()` lit le plan DU SITE, jamais celui du tenant actif — il ne depend d'aucun symlink `instance`. """ m = dict((U.charger() or {}).get("materialisation") or {}) g = U.gabarit() if not g.get("vmid"): raise SystemExit( "plan/10-intrants.yml : `gabarit.vmid` est requis — c'est de lui que descend " "chaque VM du site, et il ne peut pas etre decide par un tenant.") m["vmid_modele"] = g["vmid"] return m def _machines() -> dict[str, dict]: """Les machines du PLAN du site — plus de l'underlay (2026-08-25). Elles y avaient vecu quelques heures, faute d'avoir vu qu'un SITE A BIEN UN PLAN. Ce qui le distingue d'un tenant n'est pas l'absence de plan mais l'absence de DERIVATION. `underlay.yml` decrit ce qui est MESURE ; le plan, ce qui est VOULU. """ import site_inventaire as SI d = SI.plan_dir() f = (d / "serveurs.yml") if d else None if not f or not f.is_file(): raise SystemExit( "Aucun `plan/serveurs.yml` a cote de `underlay.yml` : le site n'a pas de plan, " "il n'y a donc aucune machine a materialiser.") import yaml srv = (yaml.safe_load(f.read_text(encoding="utf-8")) or {}).get("serveurs") or {} return {nom: {**s, "nom": nom} for nom, s in srv.items() if str(s.get("etat", "actif")) == "actif"} def _services_par_hote() -> dict[str, list[str]]: """Quels roles chaque machine porte — lu dans `plan/applications.yml`. Les services ne vivent plus sur la machine mais dans le registre des applications, comme chez un tenant : une machine dit OU elle est, une application dit CE QU'ELLE FAIT et sur quelle machine. La jointure se fait ici. """ import site_inventaire as SI import yaml d = SI.plan_dir() f = (d / "applications.yml") if d else None if not f or not f.is_file(): return {} apps = (yaml.safe_load(f.read_text(encoding="utf-8")) or {}).get("applications") or {} out: dict[str, list[str]] = {} for app in apps.values(): if app.get("hote") and app.get("groupe"): out.setdefault(str(app["hote"]), []).append(str(app["groupe"])) return {h: sorted(g) for h, g in out.items()} def _reseau_de(machine: dict) -> dict: for r in U.reseaux(U.charger()): if r.get("nom") == machine.get("reseau"): return r raise SystemExit(f"machine '{machine.get('nom')}': reseau " f"'{machine.get('reseau')}' introuvable dans l'underlay") def parametres(nom: str) -> dict[str, str]: m = _machines().get(nom) if not m: raise SystemExit(f"machine '{nom}' inconnue — `site-lister` donne les noms") r = _reseau_de(m) if not str(r.get("pont", "") or "").strip(): # La meme garde que le validateur, refaite au moment d'agir. Un reseau sans pont # est un reseau qu'aucun hyperviseur ne porte : la VM naitrait sourde, et le # decouvrir apres le clone coute une VM a detruire. raise SystemExit(f"machine '{nom}': le reseau '{r.get('nom')}' ne declare aucun " f"`pont:` — aucune VM ne peut y naitre") g = {**GABARIT_DEFAUT, **(m.get("gabarit") or {})} sous = str(r["sous_reseau"]) p = { "SETOPS_VMID": str(m["vmid"]), "SETOPS_IP": str(m["ip"]), "SETOPS_CIDR": sous.split("/")[1], "SETOPS_PASSERELLE": str(r.get("passerelle") or ""), "SETOPS_PONT": str(r["pont"]), "SETOPS_NOEUD": str(m["noeud"]), "SETOPS_COEURS": str(g["vcpu"]), "SETOPS_MEMOIRE": str(g["memoire"]), # LE PLANCHER — meme regle que pour un tenant, derivee de ce que la machine PORTE. # Un plancher ecrit au plan (`gabarit.memoire_min`) prime. "SETOPS_MEMOIRE_MIN": str(g.get("memoire_min") or _plancher( int(g["memoire"]), _groupes_de(nom))), "SETOPS_DISQUE": _taille(g["disque"]), } # UN SEGMENT NON ETIQUETE N'A PAS DE TAG, et lui en donner un le couperait du fil. # `segment_physique` le dit ; `vlan: 1` (le natif) ne s'etiquette pas davantage. vlan = r.get("vlan") if not r.get("segment_physique") and isinstance(vlan, int) and vlan > 1: p["SETOPS_VLAN"] = str(vlan) mat = materialisation() p["SETOPS_VMID_MODELE"] = str(mat["vmid_modele"]) # L'explicite de la machine gagne sur le defaut du site : une machine peut demander # un autre stockage sans que tout le site bascule. p["SETOPS_STOCKAGE"] = str(m.get("stockage") or mat.get("stockage") or "") p["SETOPS_CLONE_COMPLET"] = "true" if mat.get("clone_complet", True) else "false" return p def main() -> int: ap = argparse.ArgumentParser(description=__doc__) sp = ap.add_subparsers(dest="cmd", required=True) sp.add_parser("lister", help="noms des machines du site, un par ligne") sp.add_parser("decrire", help="tableau lisible : ou vit quoi, et pour quel role") q = sp.add_parser("parametres-proxmox", help="SETOPS_* evaluables par le shell") q.add_argument("--machine", required=True) a = ap.parse_args() if U.charger() is None: print("Aucun underlay monte (underlay.yml absent) — aucun site a materialiser.", file=sys.stderr) return 2 if a.cmd == "lister": for nom in _machines(): print(nom) elif a.cmd == "decrire": ms = _machines() if not ms: print("Aucune machine declaree dans l'underlay.") return 0 print("{:16s} {:18s} {:16s} {:8s} {:>7s} {}".format( "machine", "reseau", "adresse", "noeud", "vmid", "services")) services = _services_par_hote() for nom, m in ms.items(): print("{:16s} {:18s} {:16s} {:8s} {:>7s} {}".format( nom, str(m.get("reseau")), str(m.get("ip")), str(m.get("noeud")), str(m.get("vmid")), ", ".join(services.get(nom, [])))) else: for k, v in parametres(a.machine).items(): print(f"{k}={shlex.quote(v)}") return 0 if __name__ == "__main__": raise SystemExit(main())