Garde-fou de déploiement par instance (setops_production)

Le droit de déployer ne dépend plus du nom de l'inventaire mais d'un
drapeau explicite setops_production dans group_vars/all/ de l'instance
(true = prod, false = bac à sable). Le GUI (champ "production" de l'API) et
executer_flux le lisent ; rétro-compatible (à défaut, ancien repère
"inventaire production"). Première pierre de la séparation par instance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-06-30 15:59:37 -04:00
parent c4e73f8712
commit 8b3648e9f9
2 changed files with 31 additions and 3 deletions

View file

@ -2,6 +2,15 @@
## 2026-06-30
### Ajouté
- **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
`group_vars/all/` de l'instance (true = prod, false = bac à sable). Rétro-compatible
(à défaut de drapeau, ancien repère « inventaire production »). Pose la base de la
séparation **par instance** : un bac à sable garde `false` et ne peut rien déployer,
quel que soit le nom de ses dossiers.
### Modifié
- **Voûte de secrets unique par environnement.** Fini les voûtes éparpillées : tous
les secrets de l'instance (token Proxmox + 17 `vault_*` pour PKI, LDAP/SSO, bases,

View file

@ -239,7 +239,7 @@ def inventaire_api(path: Path) -> dict:
dependencies = charger_dependances(FICHIER_DEPENDANCES)
return {
"inventaire": os.path.relpath(path, RACINE),
"production": path.resolve() == INVENTAIRE_PRODUCTION,
"production": instance_autorise_deploiement(path),
"groupes": groupes_disponibles(data),
"dependances": dependencies,
"nomenclature": charger_nomenclature(FICHIER_NOMENCLATURE),
@ -262,6 +262,25 @@ def _lire_yaml_dict(path: Path) -> dict:
return data if isinstance(data, dict) else {}
def instance_autorise_deploiement(path: Path) -> bool:
"""Le déploiement réel est-il permis pour l'instance de cet inventaire ?
Repère explicite, par instance : `setops_production` dans
`<inventaire>/group_vars/all/`. Rétro-compatibilité : à défaut de drapeau,
on retombe sur l'ancien repère (inventaire nommé « production »).
"""
base = path.parent / "group_vars" / "all"
fichiers = sorted(base.glob("*.yml")) if base.is_dir() else []
plat = path.parent / "group_vars" / "all.yml"
if plat.exists():
fichiers.append(plat)
for f in fichiers:
d = _lire_yaml_dict(f)
if isinstance(d, dict) and "setops_production" in d:
return bool(d["setops_production"])
return path.resolve() == INVENTAIRE_PRODUCTION
def intrants_courants() -> dict:
cache: dict = {}
valeurs: dict = {}
@ -1829,8 +1848,8 @@ class Gestionnaire(BaseHTTPRequestHandler):
if not MOTIF_HOTE.match(hote or ""):
self.repondre_json(400, {"erreur": "Nom d'hote invalide."})
return
if self.inventaire != INVENTAIRE_PRODUCTION:
self.repondre_json(409, {"erreur": "Deploiement reserve a l'inventaire production."})
if not instance_autorise_deploiement(self.inventaire):
self.repondre_json(409, {"erreur": "Deploiement non autorise pour cette instance (setops_production: false)."})
return
if hote not in hotes_actifs_fichier(self.inventaire):
self.repondre_json(409, {"erreur": f"{hote} n'est pas un hote actif sauvegarde."})