From 60a60b6fb1926b1d13c0b2309490158ef8ce3d04 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Mon, 3 Aug 2026 15:15:14 -0400 Subject: [PATCH] =?UTF-8?q?pools=20Proxmox=20:=20un=20par=20tenant,=20d?= =?UTF-8?q?=C3=A9riv=C3=A9=20de=20l'index=20(D-37,=20P28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 43 ++++++ Makefile | 8 + docs/audit/affirmations.md | 2 + docs/audit/preuve-2026-08-03.md | 3 +- docs/carte-set-ops.md | 1 + docs/decisions-architecture.md | 1 + playbooks/proxmox/cloner_vm_debian.yml | 20 +++ scripts/devis_proxmox_pools.py | 202 +++++++++++++++++++++++++ scripts/inventory_rules.py | 17 +++ scripts/prouver.py | 2 + 10 files changed, 298 insertions(+), 1 deletion(-) create mode 100644 scripts/devis_proxmox_pools.py diff --git a/CHANGELOG.md b/CHANGELOG.md index ea572a1..aae38be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile index 025d513..9fd5160 100644 --- a/Makefile +++ b/Makefile @@ -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)" ); \ diff --git a/docs/audit/affirmations.md b/docs/audit/affirmations.md index 3758bd8..be521af 100644 --- a/docs/audit/affirmations.md +++ b/docs/audit/affirmations.md @@ -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`, diff --git a/docs/audit/preuve-2026-08-03.md b/docs/audit/preuve-2026-08-03.md index f7b5163..b568d46 100644 --- a/docs/audit/preuve-2026-08-03.md +++ b/docs/audit/preuve-2026-08-03.md @@ -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 diff --git a/docs/carte-set-ops.md b/docs/carte-set-ops.md index 0de25d6..14eb9be 100644 --- a/docs/carte-set-ops.md +++ b/docs/carte-set-ops.md @@ -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', )` | — | | 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), diff --git a/docs/decisions-architecture.md b/docs/decisions-architecture.md index 99e83ce..9154747 100644 --- a/docs/decisions-architecture.md +++ b/docs/decisions-architecture.md @@ -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 diff --git a/playbooks/proxmox/cloner_vm_debian.yml b/playbooks/proxmox/cloner_vm_debian.yml index 3d52f25..82a10cb 100644 --- a/playbooks/proxmox/cloner_vm_debian.yml +++ b/playbooks/proxmox/cloner_vm_debian.yml @@ -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 }}" diff --git a/scripts/devis_proxmox_pools.py b/scripts/devis_proxmox_pools.py new file mode 100644 index 0000000..54d2ffd --- /dev/null +++ b/scripts/devis_proxmox_pools.py @@ -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:])) diff --git a/scripts/inventory_rules.py b/scripts/inventory_rules.py index 093c2d1..8f08a33 100644 --- a/scripts/inventory_rules.py +++ b/scripts/inventory_rules.py @@ -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) diff --git a/scripts/prouver.py b/scripts/prouver.py index 32a3a5a..993a233 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -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"]]}, ]