Set-OPS-Public/scripts/site_machines.py
Daniel Allaire bd0e40753b memoire : ce que l hote a le droit de reprendre, et pourquoi il ne le pouvait pas
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
2026-09-13 19:57:11 -04:00

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())