Onze des quatorze serveurs portent le même nom court chez Chezlepro et chez
Technolibre. Vérifié un par un, ce n'est pas un problème technique : tout le
reste dérive du seed et diverge (10.27.19.21 contre 10.21.19.21, VMID 117402101
contre 111402101, VLAN 1174 contre 1114, deux domaines internes), et rien n'est
indexé sur le nom court — les opérations Proxmox portent toutes un vmid, les
certificats un FQDN, et client_backup_repo vise backup-01.{{ domaine_interne }}.
Le coût est humain : la console Proxmox affiche le nom, et deux infra-pki-01 y
sont indiscernables à l'œil. Le VMID porte le tenant, encore faut-il connaître le
codage.
Un pool par tenant, dérivé du dossier d'instance et de l'index — déjà unique par
P21, donc aucun registre de plus : Chezlepro-17, Technolibre-11.
make devis-proxmox-pools rattrape la flotte existante (création du pool, puis
affectation des VM actives). Les VM créées ensuite entrent d'elles-mêmes :
make creer-vm dérive le pool par la même fonction et le passe à la création. Le
playbook crée le pool au préalable — proxmox_kvm échoue sur un pool inconnu, et
l'API ne sait pas changer le pool d'une VM existante ; c'est aussi pourquoi le
rattrapage passe par les membres.
P28 garde deux collisions : même nom de pool entre tenants, et surtout même VMID
— une machine appartenant à deux tenants serait pire qu'une homonymie.
Rien n'est renommé : les homonymes sont la preuve que la nomenclature est un vrai
gabarit. Le devis ne lit pas le cluster, il dit l'état cible et non l'écart.
27 preuves OK, 0 échec. --syntax-check du playbook de clonage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
202 lines
8.5 KiB
Python
202 lines
8.5 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 ( # noqa: E402
|
|
charger_serveurs,
|
|
deriver_nomenclature,
|
|
fonction_seq,
|
|
pool_de,
|
|
)
|
|
from devis_reseau import DOSSIER_INSTANCES, decouvrir # 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 = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
|
|
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).",
|
|
"",
|
|
"### 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)")
|
|
args = ap.parse_args(argv)
|
|
|
|
if args.pool_actif:
|
|
print(pool_actif())
|
|
return 0
|
|
|
|
devis = construire(decouvrir())
|
|
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:]))
|