pools Proxmox : un par tenant, dérivé de l'index (D-37, P28)

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>
This commit is contained in:
Daniel Allaire 2026-08-03 15:15:14 -04:00
parent b0e56cbfc0
commit 60a60b6fb1
10 changed files with 298 additions and 1 deletions

View file

@ -1,5 +1,48 @@
# CHANGELOG — Set-OPS
## 2026-08-03 (suite 12) — un pool Proxmox par tenant
Onze des quatorze serveurs portent le **même nom court** chez Chezlepro et chez
Technolibre : `infra-pki-01`, `backup-01`, `obs-01`
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, et deux domaines internes distincts. Et rien n'est indexé sur le
nom court — toutes les opérations Proxmox portent un `vmid` (le `name:` n'est qu'une
étiquette), les certificats un FQDN, et `client_backup_repo` vise
`backup-01.{{ domaine_interne }}`, donc le serveur du tenant.
Le coût est **humain**, et il est réel : la console Proxmox affiche le nom. Deux
`infra-pki-01` y sont indiscernables à l'œil, et c'est ainsi qu'on éteint la mauvaise
machine. Le VMID porte pourtant le tenant — encore faut-il connaître le codage.
### Ce qui a été fait
Un pool par tenant, **dérivé** : dossier d'instance + `index``Chezlepro-17`,
`Technolibre-11`. L'`index` étant déjà unique par P21, le nom l'est aussi — aucun
registre de plus.
`make devis-proxmox-pools` produit le rattrapage de la flotte existante (création du
pool, puis affectation des VM actives). Non destructif, à relire avant d'appliquer.
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 — deux
raisons : `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 : deux tenants ne peuvent pas revendiquer le même nom de
pool, ni le même VMID — une machine appartenant à deux tenants serait pire qu'une
homonymie. Décision **D-37**, affirmation **AFF-110**.
### Ce que je n'ai pas fait
**Renommer les VM par tenant.** Ça casserait ce que ces homonymes prouvent : même
fonction, même nom, partout — c'est ce qui rend un modèle réutilisable.
Le devis **ne lit pas le cluster** : il dit l'état cible, pas l'écart. Les commandes
sont idempotentes, donc rejouables sans risque, mais il ne saura pas dire ce qui est
déjà en place.
## 2026-08-03 (suite 11) — le cluster appartient à l'hébergeur
En ouvrant le panneau « Intrants de base », on trouvait côte à côte et sans distinction

View file

@ -355,6 +355,13 @@ devis-proxmox-fw: ansible-runtime ## Devis pare-feu Proxmox (est-ouest intra-ten
devis-proxmox-fw-verifier:
python3 scripts/devis_proxmox_fw.py --verifier
.PHONY: devis-proxmox-pools devis-proxmox-pools-verifier
devis-proxmox-pools: ansible-runtime ## Devis des pools Proxmox (un par tenant), derive du plan
python3 scripts/devis_proxmox_pools.py $(if $(JSON),--json,)
devis-proxmox-pools-verifier:
python3 scripts/devis_proxmox_pools.py --verifier
devis-opnsense-verifier:
python3 scripts/devis_opnsense.py --verifier
@ -546,6 +553,7 @@ cloner-vm: ansible-runtime
-e proxmox_clone_nom="$(HOTE)" \
-e proxmox_clone_vmid="$(VMID)" \
-e proxmox_clone_ipconfig0="$$ipconfig" \
-e proxmox_clone_pool="$$(python3 scripts/devis_proxmox_pools.py --pool-actif)" \
); \
[[ -n "$(VMID_MODELE)" ]] && extra_vars+=( -e proxmox_clone_vmid_modele="$(VMID_MODELE)" ); \
[[ -n "$(NOEUD_PROXMOX)" ]] && extra_vars+=( -e proxmox_clone_noeud="$(NOEUD_PROXMOX)" ); \

View file

@ -158,6 +158,7 @@ Légende du statut : ✅ prouvée · 🟡 partielle · ❌ fausse · ⚪ invéri
| AFF-107 | « Le filtrage est-ouest intra-tenant est dérivé du registre des flux, et redit la même politique que les nftables d'hôte » (`docs/sdn-evpn.md` §3) | structurelle | **P25** (`scripts/devis_proxmox_fw.py --verifier`) | ✅ | 34 groupes, 40 règles sur 2 tenants. Les deux barrières lisent le registre par les **mêmes fonctions** : elles ne peuvent pas diverger. |
| AFF-108 | « Une intégration universelle est déclarée par le rôle, pas recopiée par serveur : aucun hôte n'y échappe et le plan ne la redit pas » (`docs/decisions-architecture.md` D-33) | structurelle | **P26** (`scripts/prouver.py`) | ✅ | L'exemption se dérive du **service rendu** (`sauf_role`), jamais d'un nom d'hôte : l'AC ne s'enrôle pas auprès d'elle-même, et l'exemption suit step-ca si on le déplace. |
| AFF-109 | « Ce qui appartient à l'hébergeur (cluster, fabric, frontière) vit dans son dépôt, jamais recopié chez un tenant » (`docs/decisions-architecture.md` D-35) | structurelle | **P27** (`scripts/prouver.py`) | ✅ | Le chemin se dérive du symlink `underlay.yml` : l'hébergeur n'est toujours pas déclaré (D-17). Sautée si aucun underlay n'est monté. |
| AFF-110 | « Chaque tenant a son pool Proxmox, dérivé, sans collision de nom ni de VMID » (`docs/decisions-architecture.md` D-37) | structurelle | **P28** (`scripts/devis_proxmox_pools.py --verifier`) | ✅ | Le nom dérive du dossier d'instance et de l'`index`, déjà unique par P21 — aucun registre de plus. Le devis ne lit pas le cluster : il affirme la cohérence du **plan**, pas l'état appliqué. |
| AFF-106 | « Le panneau couvre le schéma du plan : aucun champ n'oblige à éditer le YAML » (`docs/intrants-base-gui-conception.md`) | fonctionnelle | **P19** (`scripts/couverture_gui.py`) | 🟡 | Vrai des registres du plan. **Exceptions assumées** : `reseaux` et `hotes` de l'underlay (listes de tables) restent hors du panneau ; les ports physiques et le nœud de sortie EVPN se nomment à la main. |
**Non enregistré volontairement.** Les devis eux-mêmes (`make devis-reseau`,
@ -397,6 +398,7 @@ Chaque affirmation ✅ est soit **rejouée** par une preuve de `make prouver`
| AFF-107 | **P25** (pare-feu Proxmox est-ouest) |
| AFF-108 | **P26** (intégrations universelles : aucun hôte laissé de côté) |
| AFF-109 | **P27** (propriété des intrants : hébergeur et tenant séparés) |
| AFF-110 | **P28** (pools Proxmox : un par tenant) |
**Structurelles/doctrinales, hors harnais récurrent** (vérifiées à l'audit) : AFF-005
(`make`=aide), AFF-014 (ciblage `groupe:&hotes_actifs`), AFF-024 (`instancier-appliquer`,

View file

@ -7,7 +7,7 @@
> [`docs/audit/affirmations.md`](affirmations.md).
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
- **Verdict** : ✅ CONFORME (26 OK · 0 echec · 1 saute)
- **Verdict** : ✅ CONFORME (27 OK · 0 echec · 1 saute)
## Preuves
@ -40,6 +40,7 @@
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 36 groupe(s), 56 regle(s). |
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 3 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). |
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 7 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. |
## Couverture des affirmations ✅ du registre

View file

@ -48,6 +48,7 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis
| Voûte au déploiement | secret jamais en clair | `ANSIBLE_VAULT_PASSWORD_FILE` / `~/.config/setops-vault-pass` ; déréférencé par `lookup('vars', <nom>)` | — |
| Multi-instance | un dépôt par écosystème ; l'active = symlink `instance/`, les autres **découvertes par convention** (dossiers frères, aucun registre) | active : symlink `instance/` ; découverte : `scripts/instances.py` / `devis_reseau.py` (glob `../*/plan/nomenclature.yml` avec `index`) ; garde-fou collision : preuve **P21** | `multi-instances.md` |
| Exposition → edge | app expose un FQDN public servi par un edge | `plan/domaines.yml` + `expose` (applications) | `bindings-conception.md` §4 |
| Pools Proxmox | un pool par tenant : les noms courts de VM sont **volontairement identiques** d'un tenant à l'autre (même fonction, même nom), et seule la console Proxmox en souffrait | `scripts/devis_proxmox_pools.py` (`make devis-proxmox-pools`) ; nom dérivé de l'`index` ; garde de collision = preuve **P28** | `decisions-architecture.md` D-37 |
| Frontière nord/sud | les flux `pair: externe`**sautés** par le pare-feu d'hôte — sont la politique de bordure | `scripts/devis_opnsense.py` (`make devis-opnsense`) ; garde d'accès admin = preuve **P24** | `frontiere-opnsense.md` |
> ⚠️ **Deux directions de binding, assumées** : `app→app` côté app (instancier),

View file

@ -52,6 +52,7 @@ sont les seules vérifiables.
| **D-17** | L'hébergeur **n'est pas déclaré** : le symlink le désigne | une seconde déclaration ouvrirait deux valeurs contradictoires | `frontiere-opnsense.md` §2 | — |
| **D-35** | Le **cluster Proxmox** appartient à l'hébergeur ; seuls le **golden template** et les **défauts de placement** restent au tenant | recopié chez chaque tenant, l'inventaire du cluster avait déjà divergé — deux listes de stockages contradictoires pour le même matériel | `config-proxmox.md` | P27 |
| **D-36** | Le panneau **nomme le propriétaire** de chaque section d'intrants | éditer une section « hébergeur » vaut pour tous ses tenants ; l'écran ne le disait pas | `scripts/inventory_gui.py` (`INTRANTS_SCHEMA`) | — |
| **D-37** | Chaque tenant a son **pool Proxmox** ; les noms courts de VM restent **identiques** d'un tenant à l'autre | 11 serveurs sur 14 sont homonymes — c'est la preuve que la nomenclature est un gabarit ; le coût est humain (la console affiche le nom), et le pool le corrige sans rien renommer | `devis_proxmox_pools.py` | P28 |
| **D-18** | Chaque tenant a un **responsable désigné** | sans lui, « qui peut décider de déménager cette organisation ? » se pose au pire moment | `migration-tenant.md` §3 | — |
## 3. Les secrets

View file

@ -131,6 +131,23 @@
l'environnement Python utilise par Ansible, puis relancer make creer-vm.
when: proxmoxer_verification.rc != 0
- name: S'assurer que le pool du tenant existe
# Un pool par tenant : la console Proxmox affiche le NOM, et onze serveurs
# portent le meme d'un tenant a l'autre. Le pool restitue l'appartenance sans
# rien renommer. Il doit exister AVANT le clone — `proxmox_kvm` echoue sur un
# pool inconnu, et l'API ne sait pas changer le pool d'une VM deja creee.
community.general.proxmox_pool:
api_host: "{{ proxmox_api_host_effectif }}"
api_port: "{{ proxmox_api_port_effectif | int if proxmox_api_port_effectif | length > 0 else omit }}"
api_user: "{{ proxmox_api_user_effectif }}"
api_token_id: "{{ proxmox_api_token_id_effectif }}"
api_token_secret: "{{ proxmox_api_token_secret_effectif }}"
validate_certs: "{{ proxmox_validate_certs | default(false) | bool }}"
poolid: "{{ proxmox_clone_pool }}"
comment: "Tenant Set-OPS — genere, ne pas renommer a la main"
state: present
when: proxmox_clone_pool | default('', true) | length > 0
- name: Cloner la VM depuis le modele
community.general.proxmox_kvm:
api_host: "{{ proxmox_api_host_effectif }}"
@ -149,6 +166,8 @@
storage: "{{ proxmox_clone_stockage | default(omit, true) }}"
format: "{{ proxmox_clone_format | default(omit, true) }}"
full: "{{ proxmox_clone_complet | default(true) | bool }}"
# A la CREATION seulement : l'API ne permet pas de le changer ensuite.
pool: "{{ proxmox_clone_pool | default(omit, true) }}"
timeout: "{{ proxmox_clone_timeout | default(600) | int }}"
- name: Ajuster le reseau de la VM clonee
@ -231,4 +250,5 @@
ansible.builtin.debug:
msg:
- "VM creee: {{ proxmox_clone_nom }} (VMID {{ proxmox_clone_vmid }})"
- "Pool: {{ proxmox_clone_pool | default('(aucun)', true) }}"
- "Cloud-Init ipconfig0: {{ proxmox_clone_ipconfig0 }}"

View file

@ -0,0 +1,202 @@
#!/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:]))

View file

@ -92,6 +92,23 @@ def integrations_de(srv: dict, services_hote: set[str] | None = None,
return effectives | set(srv.get("integrations") or [])
def pool_de(nom_instance: str, index: int) -> str:
"""Nom du pool Proxmox d'un tenant. DERIVE, jamais saisi.
Onze des quatorze serveurs portent le meme nom court chez deux tenants : c'est
la preuve que la nomenclature est un vrai gabarit, et il ne faut pas la casser.
Mais la console Proxmox, elle, affiche le NOM deux `infra-pki-01` y sont
indiscernables a l'oeil, alors que tout le reste (IP, VMID, VLAN, FQDN) differe.
Le pool restitue l'appartenance la ou elle manquait : dans l'arbre du cluster.
`index` est deja garanti unique dans la federation par P21 : le nom l'est donc
aussi, sans registre supplementaire. Forme : `Chezlepro-17`, `Technolibre-11`.
"""
base = re.sub(r"^OPS-", "", nom_instance)
base = re.sub(r"[^A-Za-z0-9._-]", "", base) or "tenant"
return f"{base}-{int(index)}"
def est_groupe_operationnel(groupe: str) -> bool:
return groupe.startswith(GROUPES_OPERATIONNELS_PREFIXES)

View file

@ -378,6 +378,8 @@ PREUVES: list[dict] = [
"func": preuve_integrations_universelles},
{"id": "P27", "titre": "Propriete des intrants : hebergeur et tenant separes", "refs": ["AFF-109"],
"func": preuve_propriete_des_intrants},
{"id": "P28", "titre": "Pools Proxmox : un par tenant, sans collision", "refs": ["AFF-110"],
"cmds": [[sys.executable, "scripts/devis_proxmox_pools.py", "--verifier"]]},
]