From 8b3648e9f9fd5f0899ecfcb50fa28c23453fe5c0 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Tue, 30 Jun 2026 15:59:37 -0400 Subject: [PATCH] =?UTF-8?q?Garde-fou=20de=20d=C3=A9ploiement=20par=20insta?= =?UTF-8?q?nce=20(setops=5Fproduction)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CHANGELOG.md | 9 +++++++++ scripts/inventory_gui.py | 25 ++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73e2b93..1b7ba5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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, diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index c47ab2d..05ebbf5 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -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 + `/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."})