devis_proxmox_pools et vpn_admin prennent machines, VMID, etat, pairs et index dans la face publiee. Sorties identiques a un worktree complet de la version precedente ; temoins d'alteration verts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
271 lines
12 KiB
Python
271 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
"""Devis des pools Proxmox — un pool par tenant.
|
|
|
|
POURQUOI. Onze des quatorze serveurs portent le MEME nom court chez deux tenants
|
|
(`infra-pki-01`, `backup-01`, `obs-01`...). Ce n'est pas un defaut : c'est la preuve
|
|
que la nomenclature est un vrai gabarit — meme fonction, meme nom, partout. Tout le
|
|
reste differe et derive du seed (IP, VMID, VLAN/VNI, FQDN), et rien dans Set-OPS
|
|
n'est indexe sur le nom court : les operations Proxmox portent toutes un `vmid`, les
|
|
certificats un FQDN, les depots de sauvegarde vivent chez le serveur du tenant.
|
|
|
|
Le seul endroit ou l'homonymie se paie est HUMAIN : la console Proxmox affiche le
|
|
NOM. Deux `infra-pki-01` y sont indiscernables a l'oeil, et c'est ainsi qu'on eteint
|
|
la mauvaise machine. Le VMID porte pourtant le tenant (117... contre 111...), mais
|
|
il faut connaitre le codage pour le lire.
|
|
|
|
Un pool par tenant restitue l'appartenance dans l'arbre du cluster, sans renommer
|
|
quoi que ce soit. Effet secondaire utile : un pool est aussi une PORTEE DE
|
|
PERMISSION — c'est l'objet auquel on attachera plus tard un acces par tenant.
|
|
|
|
NON destructif : ce script n'ecrit rien sur le cluster. Il derive du plan ce que
|
|
l'etat cible devrait etre, et le rend a relire. `--json` sert l'API.
|
|
|
|
LIMITE ASSUMEE. Le devis ne LIT PAS le cluster : il ne peut donc pas dire ce qui est
|
|
deja en place, seulement ce que le plan implique. Les commandes emises sont
|
|
idempotentes — reappliquer un membre deja present ne fait rien.
|
|
|
|
Usage :
|
|
python3 scripts/devis_proxmox_pools.py # devis lisible
|
|
python3 scripts/devis_proxmox_pools.py --json # meme contenu, pour l'API
|
|
python3 scripts/devis_proxmox_pools.py --verifier # garde : un pool unique par tenant
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
RACINE = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(RACINE / "scripts"))
|
|
|
|
from inventory_rules import (POOL_SITE,
|
|
instance_courante, # noqa: E402
|
|
charger_serveurs,
|
|
deriver_nomenclature,
|
|
fonction_seq,
|
|
pool_de,
|
|
)
|
|
from devis_reseau import DOSSIER_INSTANCES, decouvrir_du_site # noqa: E402
|
|
|
|
|
|
def _plan_de(nom_instance: str) -> dict:
|
|
p = DOSSIER_INSTANCES / nom_instance / "plan" / "serveurs.yml"
|
|
if not p.is_file():
|
|
return {}
|
|
return charger_serveurs(p).get("serveurs") or {}
|
|
|
|
|
|
def _face_publiee(nom: str) -> dict | None:
|
|
"""La face reseau que ce locataire a publiee, ou None (et on le dit)."""
|
|
try:
|
|
import contexte
|
|
face = contexte.Locataire(DOSSIER_INSTANCES / nom).face_publiee()
|
|
except Exception:
|
|
face = None
|
|
if face is None:
|
|
print(f"note : {nom} n'a pas publie sa face reseau — lecture de son plan (transition).",
|
|
file=sys.stderr)
|
|
return face
|
|
|
|
|
|
def pool_actif() -> str:
|
|
"""Pool du tenant ACTIF — ce que le playbook de clonage passe a la creation.
|
|
|
|
Meme derivation que le devis : `make creer-vm` et `make devis-proxmox-pools` ne
|
|
peuvent pas nommer le pool differemment. Chaine vide si aucune instance n'est
|
|
liee — l'appelant omet alors le parametre plutot que d'inventer un nom.
|
|
"""
|
|
instance = instance_courante()
|
|
nomenclature = instance / "plan" / "nomenclature.yml"
|
|
if not nomenclature.is_file():
|
|
return ""
|
|
n = yaml.safe_load(nomenclature.read_text(encoding="utf-8")) or {}
|
|
if n.get("index") is None:
|
|
return ""
|
|
return pool_de(instance.resolve().name, int(n["index"]))
|
|
|
|
|
|
def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|
"""{pools: [{pool, tenant, index, membres: [{nom, vmid, etat}], sans_vmid: []}]}"""
|
|
blocs = []
|
|
for nom_instance, _prefixe, nomenclature in tenants:
|
|
index = int(nomenclature["index"])
|
|
membres, sans_vmid = [], []
|
|
# LA FACE RESEAU D'ABORD (2026-10-05, docs/conception-contextes.md §6, etape 3) : le
|
|
# locataire publie ses machines, leur VMID et leur etat ; le devis ne lit plus son plan.
|
|
face = _face_publiee(nom_instance)
|
|
if face is not None:
|
|
for nom, m in sorted((face.get("machines") or {}).items()):
|
|
vmid = (m.get("materialisation") or {}).get("proxmox_vmid")
|
|
if vmid is None:
|
|
sans_vmid.append(nom)
|
|
continue
|
|
membres.append({"nom": nom, "vmid": int(vmid), "etat": str(m.get("etat", "planifie"))})
|
|
blocs.append({"pool": pool_de(nom_instance, index), "tenant": nom_instance,
|
|
"index": index, "membres": membres, "sans_vmid": sans_vmid})
|
|
continue
|
|
for nom, srv in sorted(_plan_de(nom_instance).items()):
|
|
_, seq = fonction_seq(nom)
|
|
derive = deriver_nomenclature(str(srv.get("fonction", "")), seq, nomenclature) or {}
|
|
vmid = derive.get("vmid")
|
|
if vmid is None:
|
|
# Une fonction absente de la nomenclature ne derive pas de VMID : on ne
|
|
# peut pas la placer. La taire ferait croire le pool complet.
|
|
sans_vmid.append(nom)
|
|
continue
|
|
membres.append({"nom": nom, "vmid": int(vmid),
|
|
"etat": str(srv.get("etat", "planifie"))})
|
|
blocs.append({
|
|
"pool": pool_de(nom_instance, index),
|
|
"tenant": nom_instance,
|
|
"index": index,
|
|
"membres": membres,
|
|
"sans_vmid": sans_vmid,
|
|
})
|
|
return {"pools": blocs}
|
|
|
|
|
|
def rendre(devis: dict) -> str:
|
|
out = [
|
|
"# Devis des pools Proxmox — un pool par tenant",
|
|
"#",
|
|
"# Ne renomme RIEN : les noms courts identiques d'un tenant a l'autre sont",
|
|
"# voulus (meme fonction, meme nom). Le pool restitue l'appartenance dans la",
|
|
"# console, la ou l'oeil ne voyait que deux `infra-pki-01`.",
|
|
"#",
|
|
"# Ce devis derive du PLAN et ne lit pas le cluster : il dit l'etat cible, pas",
|
|
"# l'ecart. Les commandes sont idempotentes.",
|
|
"",
|
|
]
|
|
for b in devis["pools"]:
|
|
actifs = [m for m in b["membres"] if m["etat"] == "actif"]
|
|
out += [
|
|
f"## {b['tenant']} — pool `{b['pool']}` (index {b['index']})",
|
|
f"# {len(b['membres'])} VM au plan, dont {len(actifs)} active(s).",
|
|
"# Nom derive du seed, comme tout le reste. Distinct des pools "
|
|
"`Env.Tenant` anterieurs a Set-OPS, qu'on ne touche pas.",
|
|
"",
|
|
"### 1. Creer le pool (sans effet s'il existe)",
|
|
f"pvesh create /pools --poolid {b['pool']} \\",
|
|
f" --comment 'Tenant {b['tenant']} (index {b['index']}) — genere par Set-OPS'",
|
|
"",
|
|
"### 2. Y placer les VM",
|
|
"# Un membre deja present est ignore par Proxmox.",
|
|
]
|
|
if actifs:
|
|
out.append(f"pvesh set /pools/{b['pool']} --vms "
|
|
+ ",".join(str(m['vmid']) for m in actifs))
|
|
else:
|
|
out.append("# (aucune VM active : rien a placer pour l'instant)")
|
|
out += ["", "# VMID serveur etat"]
|
|
for m in b["membres"]:
|
|
marque = "" if m["etat"] == "actif" else " (pas encore creee)"
|
|
out.append(f"# {m['vmid']:<11} {m['nom']:<22} {m['etat']}{marque}")
|
|
if b["sans_vmid"]:
|
|
out += ["",
|
|
"# /!\\ Sans VMID derivable (fonction absente de la nomenclature) :",
|
|
"# " + ", ".join(b["sans_vmid"]),
|
|
"# Ces serveurs ne peuvent pas etre places tant que leur fonction",
|
|
"# n'est pas declaree — les placer a la main recreerait un ecart."]
|
|
out.append("")
|
|
out += [
|
|
"## Ensuite",
|
|
"# Les VM CREEES PAR LA SUITE entrent d'elles-memes dans le pool : le playbook",
|
|
"# de clonage le derive et le passe a la creation. Ce devis ne sert donc qu'a",
|
|
"# rattraper la flotte deja en place — une fois.",
|
|
"#",
|
|
"# Note : l'API Proxmox ne permet pas de CHANGER le pool d'une VM existante par",
|
|
"# le meme appel que la creation. Deplacer une VM d'un pool a l'autre se fait",
|
|
"# par le membre, pas par la VM — ce qui compte le jour d'une migration.",
|
|
]
|
|
return "\n".join(out)
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("--json", action="store_true")
|
|
ap.add_argument("--verifier", action="store_true")
|
|
ap.add_argument("--pool-actif", action="store_true",
|
|
help="nom du pool du tenant actif (consomme par make creer-vm)")
|
|
ap.add_argument("--pool-site", action="store_true",
|
|
help="nom invariable du pool du genome (consomme par make site-creer)")
|
|
args = ap.parse_args(argv)
|
|
|
|
if args.pool_actif:
|
|
print(pool_actif())
|
|
return 0
|
|
|
|
# LE SITE NE DERIVE D'AUCUN INDEX, DONC SON POOL NE SE CALCULE PAS (2026-09-13).
|
|
#
|
|
# `--pool-actif` rend le pool du TENANT lie. `make site-creer` passait par le meme
|
|
# chemin : les sept machines du genome sont donc nees dans `OPS-Chezlepro`, aux cotes
|
|
# des quatorze du locataire — vingt et une VM dans un pool cense n'en contenir que
|
|
# quatorze. Range a la main, ca se serait defait au prochain `site-creer`.
|
|
#
|
|
# Une option distincte, et non un drapeau sur la premiere : le site et un tenant ne
|
|
# repondent pas a la meme question, et une fonction qui repond aux deux finit par
|
|
# rendre la mauvaise reponse a l'un des deux.
|
|
if args.pool_site:
|
|
print(POOL_SITE)
|
|
return 0
|
|
|
|
# Les tenants de CE SITE (voir `devis_proxmox_fw.main`, 2026-09-28).
|
|
devis = construire(decouvrir_du_site())
|
|
# LE SITE EST UN POOL AUSSI (2026-09-12). Les machines du genome — cache, forge, AC,
|
|
# noms, depot, supervision — ne derivent d'aucun index : elles sont l'infrastructure
|
|
# SUR laquelle les index vivent. D'ou un nom invariable, `Site-OPS`, identique d'un
|
|
# hebergeur a l'autre.
|
|
#
|
|
# Sans ce bloc, elles restaient hors de tout pool : dans la console, sept machines
|
|
# sans appartenance a cote de trois flottes rangees — exactement le desordre que le
|
|
# reste du devis supprime.
|
|
try:
|
|
import underlay as _u
|
|
_plan = (_u.lire_plan_site("serveurs.yml") or {}).get("serveurs") or {}
|
|
_membres = sorted(
|
|
({"nom": n, "vmid": int(v["vmid"]), "etat": str(v.get("etat", "actif"))}
|
|
for n, v in _plan.items() if str(v.get("vmid", "")).strip().isdigit()),
|
|
key=lambda m: m["vmid"])
|
|
if _membres:
|
|
devis["pools"].insert(0, {
|
|
"pool": POOL_SITE, "tenant": "(site)", "index": None,
|
|
"membres": _membres, "sans_vmid": [],
|
|
})
|
|
except Exception:
|
|
pass # aucun underlay monte : le site n'existe pas de ce point de vue
|
|
|
|
if args.verifier:
|
|
blocs = devis["pools"]
|
|
if not blocs:
|
|
print("erreur: aucun tenant federe decouvert.", file=sys.stderr)
|
|
return 2
|
|
noms = [b["pool"] for b in blocs]
|
|
if len(set(noms)) != len(noms):
|
|
doublons = sorted({n for n in noms if noms.count(n) > 1})
|
|
print(f"erreur: pool(s) en collision entre tenants : {', '.join(doublons)}",
|
|
file=sys.stderr)
|
|
return 2
|
|
# Un VMID dans deux pools serait pire qu'une homonymie : la machine
|
|
# appartiendrait a deux tenants. L'index les separe, mais on le prouve.
|
|
vus: dict[int, str] = {}
|
|
for b in blocs:
|
|
for m in b["membres"]:
|
|
if m["vmid"] in vus:
|
|
print(f"erreur: VMID {m['vmid']} revendique par {vus[m['vmid']]} "
|
|
f"et {b['pool']}.", file=sys.stderr)
|
|
return 2
|
|
vus[m["vmid"]] = b["pool"]
|
|
total = sum(len(b["membres"]) for b in blocs)
|
|
print(f"CONFORME : {len(blocs)} pool(s) Proxmox, {total} VM placee(s), "
|
|
f"aucun nom ni VMID en collision.")
|
|
return 0
|
|
print(json.dumps(devis, indent=2, ensure_ascii=False) if args.json else rendre(devis))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv[1:]))
|