61 Go declares a 21 machines, 12 reellement utilises. balloon valait 0 partout, ce qui ne desactive pas le ballooning : cela RETIRE le peripherique de la ligne de commande QEMU. Le moniteur repond "No balloon device has been activated". Le moteur derive desormais un plancher par machine sur les deux chemins de creation, et le playbook de clonage le pose a cote de memory. La table des planchers a ete corrigee deux fois par la mesure : shared_buffers vaut 128 Mo et non une part de la RAM, une JVM tient 605 Mo, Redis 16 Mo. Ces machines tiennent du cache de pages, pas un jeu de donnees. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
216 lines
9.5 KiB
Python
Executable file
216 lines
9.5 KiB
Python
Executable file
#!/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())
|