diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e69fdc..a33e1d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,15 @@ 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 - `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. +- **Garde-fou de prudence par instance (`setops_production`).** Le déploiement réel + est désormais possible sur **toute instance** (un bac à sable déploie sur *son* + infra lab — il est isolé **et** fonctionnel). Le drapeau `setops_production` dans + `group_vars/all/` ne **bloque** plus rien : il marque la PRODUCTION pour exiger une + **confirmation renforcée** au déploiement (bannière/badge rouge « PROD », bouton + Déployer en rouge, re-saisie du nom d'hôte) ; un bac à sable (`false`) affiche + « bac à sable » et déploie sans cette étape. Rétro-compatible (à défaut de drapeau, + ancien repère « inventaire production »). Le GUI affiche toujours quel type + d'instance est monté. ### Modifié - **Voûte de secrets unique par environnement.** Fini les voûtes éparpillées : tous diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 74e3c5d..1139f6b 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -255,7 +255,7 @@ def inventaire_api(path: Path) -> dict: dependencies = charger_dependances(FICHIER_DEPENDANCES) return { "inventaire": os.path.relpath(path, RACINE), - "production": instance_autorise_deploiement(path), + "production": instance_est_production(path), "groupes": groupes_disponibles(data), "dependances": dependencies, "nomenclature": charger_nomenclature(FICHIER_NOMENCLATURE), @@ -278,12 +278,13 @@ 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 ? +def instance_est_production(path: Path) -> bool: + """Cette instance est-elle la PRODUCTION ? (garde-fou de prudence, pas un blocage) - 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 »). + Le déploiement est permis sur toute instance (lab compris, vers son infra) ; + ce drapeau ne sert qu'à exiger une confirmation renforcée côté GUI quand c'est + la prod. Repère explicite : `setops_production` dans `/group_vars/all/`. + Rétro-compatibilité : à défaut, ancien repère (inventaire nommé « production »). """ base = path.parent / "group_vars" / "all" fichiers = sorted(base.glob("*.yml")) if base.is_dir() else [] @@ -737,6 +738,8 @@ HTML = r""" .badge-classe.const { background: color-mix(in srgb, var(--ambre) 20%, transparent); color: var(--ambre); } .badge-classe.def { background: color-mix(in srgb, var(--bleu) 18%, transparent); color: var(--bleu); } .badge-classe.cat { background: color-mix(in srgb, var(--teal) 20%, transparent); color: var(--teal); } + .badge-prod { display: inline-flex; align-items: center; font-size: 11px; font-weight: 800; letter-spacing: .06em; padding: 3px 9px; border-radius: 999px; background: var(--rouge); color: #fff; } + .badge-lab { display: inline-flex; align-items: center; font-size: 11px; font-weight: 800; letter-spacing: .04em; padding: 3px 9px; border-radius: 999px; background: color-mix(in srgb, var(--teal) 22%, transparent); color: var(--teal); } .integ-cases { display: flex; flex-wrap: wrap; gap: 6px 12px; padding: 4px 2px; } .integ-case { display: inline-flex; align-items: center; gap: 5px; font-size: 12px; font-weight: 600; color: var(--texte); cursor: pointer; } .integ-case input { width: 14px; height: 14px; flex: none; accent-color: var(--teal); } @@ -946,6 +949,7 @@ HTML = r""" const actifs = hotes.filter(h => h.etat === 'actif').length; const bloques = hotes.filter(h => hoteBloque(h)).length; document.getElementById('resume').innerHTML = ` + ${estProduction ? 'PROD' : 'bac à sable'} ${hotes.length} hôtes ${actifs} actifs ${hotes.length - actifs} planifiés @@ -1320,13 +1324,14 @@ HTML = r""" : '
aucun serveur de base ici
'; hoteOpCourant = s.nom; let pied; - if (h && h.etat === 'actif' && !hoteBloque(h) && estProduction) { + if (h && h.etat === 'actif' && !hoteBloque(h)) { const hi = hotes.indexOf(h); - pied = `
+ const clsDep = estProduction ? 'danger' : 'primaire'; + pied = `
${estProduction ? 'PROD' : ''} -
`; +
`; } else { - const txt = !h ? 'Applique le plan pour instancier cet hôte' : (h.etat !== 'actif' ? 'Hôte actif requis pour déployer' : (!estProduction ? 'Déploiement : inventaire production requis' : 'Prérequis manquant')); + const txt = !h ? 'Applique le plan pour instancier cet hôte' : (h.etat !== 'actif' ? 'Hôte actif requis pour déployer' : 'Prérequis manquant'); pied = `
${txt}
`; } const statBadge = s.statut === 'divergence' ? 'divergent' @@ -1661,8 +1666,14 @@ HTML = r""" let vault = ''; if (mode === 'deployer') { if (!verifie[hote]) { message('Vérifiez (dry-run) d\'abord.', 'erreur'); return; } + if (estProduction) { + const saisi = prompt('⚠️ PRODUCTION — déploiement réel sur « ' + hote + ' ».\n' + + 'Tape le nom exact de l\'hôte pour confirmer :'); + if (saisi === null) return; + if (saisi.trim() !== hote) { message('Confirmation annulée : le nom ne concorde pas.', 'erreur'); return; } + } vault = await ouvrirModale('Déployer ' + hote, - 'Applique les playbooks de groupes sur la VM réelle, en root (sudo). Action non transactionnelle : pas de rollback automatique en cas d\'échec.'); + (estProduction ? '⚠️ PRODUCTION. ' : '') + 'Applique les playbooks de groupes sur la VM réelle, en root (sudo). Action non transactionnelle : pas de rollback automatique en cas d\'échec.'); if (vault === null) return; motVault = vault; } @@ -1864,9 +1875,6 @@ class Gestionnaire(BaseHTTPRequestHandler): if not MOTIF_HOTE.match(hote or ""): self.repondre_json(400, {"erreur": "Nom d'hote invalide."}) return - 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."}) return