Toutes les VM naissaient sur le noeud du gabarit puis migraient. site-cache-01 est la
premiere a etre placee ailleurs, et elle a fait tomber quatre defauts enchaines :
1. l'URL de clonage visait le noeud de DESTINATION, alors que l'API veut celui qui DETIENT
le gabarit ; la destination se dit par `target`. D'ou un 500 sur tout clonage
inter-noeuds. Le noeud du gabarit se decouvre dans l'inventaire du cluster.
2. ce 500 etait avale par failed_when: false + no_log: true. Une assertion le releve
desormais, message de l'API compris.
3. l'attente interrogeait nodes/<destination>/tasks/<UPID> ; la tache vit sur le noeud du
gabarit. 60 tentatives x 10 s pour un clonage termine en 87 s, puis la suite qui reprend
sans un mot. Le noeud se lit dans l'UPID lui-meme.
4. la garde d'apres-attente retombait sur `exitstatus | default('OK')` : une attente qui
n'avait rien observe passait pour un succes. Elle exige d'avoir VU la tache s'arreter.
Et une taille de disque porte toujours son unite : `disque: 40` etait lu comme un
retrecissement, erreur toleree a raison — la machine naissait donc avec les 16 Go du
gabarit au lieu de 40, sans que rien ne le dise.
Le site declare enfin ce qu'il materialise (materialisation: gabarit, stockage,
clone_complet) au lieu de l'heriter du tenant actif.
Chrono : clonage reel 59 s et 87 s pour 3,3 Gio alloues.
42 preuves vertes, ansible-lint profil production.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
6.2 KiB
Python
Executable file
149 lines
6.2 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"}
|
|
|
|
|
|
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 — DECLARES par le site.
|
|
|
|
Sans ce bloc, 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. Un accident silencieux vaut moins qu'un refus.
|
|
"""
|
|
m = (U.charger() or {}).get("materialisation") or {}
|
|
if not m.get("vmid_modele"):
|
|
raise SystemExit(
|
|
"underlay.yml : `materialisation.vmid_modele` est requis — sans lui, le site "
|
|
"clonerait le gabarit du tenant actif, ce qui depend du symlink `instance`.")
|
|
return m
|
|
|
|
|
|
def _machines() -> dict[str, dict]:
|
|
return {m["nom"]: m for m in U.machines(U.charger())}
|
|
|
|
|
|
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"]),
|
|
"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"))
|
|
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(m.get("services") or [])))
|
|
else:
|
|
for k, v in parametres(a.machine).items():
|
|
print(f"{k}={shlex.quote(v)}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|