Inventaire d'instance neutre et configurable (principal / SETOPS_INVENTAIRE)

Le moteur ne code plus en dur inventories/lab|production : un inventaire
par instance, détecté de façon rétro-compatible (principal > production >
lab) et surchargeable par SETOPS_INVENTAIRE. Makefile (définitions seules,
pas les 47 usages), inventory_gui, instancier, config_proxmox, serveurs,
applications. Les instances lab/production existantes marchent à
l'identique ; les nouvelles adoptent inventories/principal/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-06-30 16:08:25 -04:00
parent 8b3648e9f9
commit e2b924cedc
7 changed files with 98 additions and 18 deletions

View file

@ -3,6 +3,14 @@
## 2026-06-30
### Ajouté
- **Inventaire d'instance neutre et configurable (`principal` / `SETOPS_INVENTAIRE`).**
Le moteur (Makefile + `inventory_gui`, `instancier`, `config_proxmox`, `serveurs`,
`applications`) ne code plus en dur `inventories/lab` / `inventories/production` :
il vise **un inventaire par instance**, détecté de façon **rétro-compatible**
(`principal` > `production` > `lab`) et surchargeable par `SETOPS_INVENTAIRE`. Les
instances existantes (découpage lab/production) continuent de fonctionner à
l'identique ; les nouvelles peuvent adopter `inventories/principal/`. Deuxième pierre
de la séparation **par instance** (la 1re étant le drapeau `setops_production`).
- **Garde-fou de déploiement par instance (`setops_production`).** Le droit de
déployer pour de vrai (GUI Vérifier/Déployer, `executer_flux`) ne dépend plus du
*nom* de l'inventaire mais d'un drapeau explicite `setops_production` dans

View file

@ -6,9 +6,15 @@ export ANSIBLE_SSH_CONTROL_PATH_DIR ?= $(CURDIR)/.ansible/cp
export ANSIBLE_SSH_ARGS ?= -F /dev/null -o ControlMaster=no
export SETOPS_INSTANCE ?= instance
INVENTAIRE_LAB ?= $(SETOPS_INSTANCE)/inventories/lab/hosts.yml
INVENTAIRE_PRODUCTION ?= $(SETOPS_INSTANCE)/inventories/production/hosts.yml
FICHIER_INVENTAIRE ?= $(INVENTAIRE_PRODUCTION)
# Inventaire de l'instance : un seul par instance dans le modèle « séparation par
# instance ». Détection rétro-compatible : principal > production > lab.
# Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml
SETOPS_INVENTAIRE ?= $(firstword $(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) $(wildcard $(SETOPS_INSTANCE)/inventories/production/hosts.yml) $(SETOPS_INSTANCE)/inventories/principal/hosts.yml)
export SETOPS_INVENTAIRE
# Inventaire « modèle » (construction du golden template) : lab > principal > production.
INVENTAIRE_LAB ?= $(firstword $(wildcard $(SETOPS_INSTANCE)/inventories/lab/hosts.yml) $(wildcard $(SETOPS_INSTANCE)/inventories/principal/hosts.yml) $(SETOPS_INSTANCE)/inventories/production/hosts.yml)
INVENTAIRE_PRODUCTION ?= $(SETOPS_INVENTAIRE)
FICHIER_INVENTAIRE ?= $(SETOPS_INVENTAIRE)
FICHIER_DEPENDANCES ?= docs/dependances-groupes.yml
GROUPE_MODELE ?= modeles_vm
GROUPE_DEBIAN ?= serveur_debian

View file

@ -26,7 +26,18 @@ INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
FICHIER = INSTANCE / "plan/applications.yml"
FICHIER_BASES = INSTANCE / "plan/bases-donnees.yml"
FICHIER_DOMAINES = INSTANCE / "plan/domaines.yml"
INVENTAIRE = INSTANCE / "inventories/production/hosts.yml"
def _inventaire(instance: Path, *noms: str) -> Path:
forced = os.environ.get("SETOPS_INVENTAIRE")
if forced:
return Path(forced)
for nom in noms:
p = instance / "inventories" / nom / "hosts.yml"
if p.exists():
return p
return instance / "inventories" / noms[0] / "hosts.yml"
INVENTAIRE = _inventaire(INSTANCE, "principal", "production")
# Groupes 'serveurs_*' qui sont des capacites de SOCLE (sur toutes les VM),
# pas des applications a part entiere.

View file

@ -14,10 +14,25 @@ import yaml
RACINE = Path(__file__).resolve().parents[1]
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
FICHIER_PROXMOX = INSTANCE / "inventories/lab/group_vars/proxmox.yml"
# Voûte UNIQUE de l'environnement : tous les secrets (token Proxmox + vault_*) au
# même endroit, un seul mot de passe. Gabarit : exemples/vault.exemple.yml.
FICHIER_VAULT = INSTANCE / "inventories/lab/group_vars/all/vault.yml"
def _inventaire_dir(instance: Path, *noms: str) -> Path:
"""Répertoire de l'inventaire modèle (porte la config Proxmox / le clonage)."""
forced = os.environ.get("SETOPS_INVENTAIRE")
if forced:
return Path(forced).parent
for nom in noms:
d = instance / "inventories" / nom
if (d / "hosts.yml").exists():
return d
return instance / "inventories" / noms[0]
_INV_DIR = _inventaire_dir(INSTANCE, "lab", "principal", "production")
FICHIER_PROXMOX = _INV_DIR / "group_vars/proxmox.yml"
# Voûte UNIQUE de l'instance : tous les secrets (token Proxmox + vault_*) au même
# endroit, un seul mot de passe. Gabarit : exemples/vault.exemple.yml.
FICHIER_VAULT = _INV_DIR / "group_vars/all/vault.yml"
GABARIT_VAULT = RACINE / "exemples/vault.exemple.yml"

View file

@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""Generateur d'inventaire Set-OPS (Phase 3) : plan -> hosts.yml.
NON destructif : ecrit dans instance/inventories/production/hosts.genere.yml et compare
NON destructif : ecrit hosts.genere.yml a cote de l'inventaire de l'instance et compare
SEMANTIQUEMENT (via ansible-inventory --list) avec l'inventaire actuel. Aucune
bascule tant que la comparaison n'est pas vide et validee.
@ -37,8 +37,21 @@ from inventory_rules import (
RACINE = Path(__file__).resolve().parents[1]
ROLES = RACINE / "roles"
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
INVENTAIRE = INSTANCE / "inventories/production/hosts.yml"
GENERE = INSTANCE / "inventories/production/hosts.genere.yml"
def _inventaire(instance: Path, *noms: str) -> Path:
for nom in noms:
p = instance / "inventories" / nom / "hosts.yml"
if p.exists():
return p
return instance / "inventories" / noms[0] / "hosts.yml"
# Inventaire unique de l'instance (rétro-compat : principal > production).
# SETOPS_INVENTAIRE force la cible.
_force = os.environ.get("SETOPS_INVENTAIRE")
INVENTAIRE = Path(_force) if _force else _inventaire(INSTANCE, "principal", "production")
GENERE = INVENTAIRE.with_name("hosts.genere.yml")
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
FICHIER_APPLICATIONS = INSTANCE / "plan/applications.yml"
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"

View file

@ -38,8 +38,24 @@ from inventory_rules import (
RACINE = Path(__file__).resolve().parents[1]
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
INVENTAIRE_DEFAUT = INSTANCE / "inventories/production/hosts.yml"
INVENTAIRE_PRODUCTION = (INSTANCE / "inventories/production/hosts.yml").resolve()
def _inventaire(instance: Path, *noms: str) -> Path:
"""Inventaire de l'instance, détection rétro-compatible (premier existant)."""
for nom in noms:
p = instance / "inventories" / nom / "hosts.yml"
if p.exists():
return p
return instance / "inventories" / noms[0] / "hosts.yml"
# Un seul inventaire par instance (modèle « séparation par instance ») ; on accepte
# l'ancien découpage lab/production en rétro-compatibilité. SETOPS_INVENTAIRE force.
_force = os.environ.get("SETOPS_INVENTAIRE")
INVENTAIRE_DEFAUT = Path(_force) if _force else _inventaire(INSTANCE, "principal", "production")
INVENTAIRE_PRODUCTION = INVENTAIRE_DEFAUT.resolve()
# Inventaire qui porte la config Proxmox / le clonage (modèle) : lab > principal > production.
INVENTAIRE_MODELE = _inventaire(INSTANCE, "lab", "principal", "production")
DOSSIER_PLAYBOOKS_GROUPES = RACINE / "playbooks/groupes"
FICHIER_DEPENDANCES = RACINE / "docs/dependances-groupes.yml"
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"
@ -49,11 +65,11 @@ FICHIER_DOMAINES = INSTANCE / "plan/domaines.yml"
FICHIER_SERVEURS = INSTANCE / "plan/serveurs.yml"
# Intrants de base (panneau GUI). Cibles d'ecriture possedees par le GUI.
# Identite : source UNIQUE partagee par tous les envs (symlink depuis chaque
# group_vars/all/). Proxmox : fichier lab (le clonage le lit en dur), cles gerees
# mises a jour, les autres cles preservees. Les SECRETS n'y entrent jamais.
# Identite : source UNIQUE partagee (symlink depuis group_vars/all/). Proxmox :
# group_vars de l'inventaire modele, cles gerees mises a jour, autres preservees.
# Les SECRETS n'y entrent jamais.
INTRANTS_IDENTITE = INSTANCE / "inventories/partage/intrants-identite.yml"
INTRANTS_PROXMOX = INSTANCE / "inventories/lab/group_vars/proxmox.yml"
INTRANTS_PROXMOX = INVENTAIRE_MODELE.parent / "group_vars/proxmox.yml"
FICHIERS_INTRANTS = {"identite": INTRANTS_IDENTITE, "proxmox": INTRANTS_PROXMOX}
# (variable, fichier, classe, section, libelle, type)

View file

@ -29,7 +29,18 @@ RACINE = Path(__file__).resolve().parents[1]
INSTANCE = Path(os.environ.get("SETOPS_INSTANCE") or (RACINE / "instance"))
FICHIER = INSTANCE / "plan/serveurs.yml"
FICHIER_NOMENCLATURE = INSTANCE / "plan/nomenclature.yml"
INVENTAIRE = INSTANCE / "inventories/production/hosts.yml"
def _inventaire(instance: Path, *noms: str) -> Path:
forced = os.environ.get("SETOPS_INVENTAIRE")
if forced:
return Path(forced)
for nom in noms:
p = instance / "inventories" / nom / "hosts.yml"
if p.exists():
return p
return instance / "inventories" / noms[0] / "hosts.yml"
INVENTAIRE = _inventaire(INSTANCE, "principal", "production")
# Champs de placement / dimensionnement NON derivables (proviennent du plan).
CHAMPS_PLAN = [("noeud", "noeud"), ("stockage", "stockage"),