Set-OPS-Public/scripts/devis_proxmox_pools.py
Daniel Allaire fde604c2a5 pare-feu et pools Proxmox : les tenants du site, pas toute la federation
Les deux devis balayaient tous les dossiers freres. Le runner du site, qui
gardait un clone de patient 0, proposait de recreer ses groupes t29 ; le poste
comptait un dossier de CI comme tenant. La frontiere et le SDN filtraient deja
par underlay.tenants : ces deux-la suivent maintenant la meme liste.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 09:57:06 -04:00

245 lines
11 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 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 = [], []
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:]))