setops_production : garde-fou de prudence, plus un blocage de déploiement
Le déploiement réel est permis sur TOUTE instance (un bac à sable déploie sur son infra lab : isolé ET fonctionnel). Le drapeau ne bloque plus ; il marque la PROD pour exiger une confirmation renforcée (badge/bouton rouge, re-saisie du nom d'hôte). executer_flux ne refuse plus selon le drapeau. Badge « PROD » / « bac à sable » dans l'en-tête. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e2b924cedc
commit
0e7cefbb2f
2 changed files with 31 additions and 21 deletions
16
CHANGELOG.md
16
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
`<inventaire>/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 `<inventaire>/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"""<!doctype html>
|
|||
.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"""<!doctype html>
|
|||
const actifs = hotes.filter(h => h.etat === 'actif').length;
|
||||
const bloques = hotes.filter(h => hoteBloque(h)).length;
|
||||
document.getElementById('resume').innerHTML = `
|
||||
${estProduction ? '<span class="badge-prod" title="Instance de PRODUCTION : déploiement sous confirmation renforcée">PROD</span>' : '<span class="badge-lab" title="Instance bac à sable (setops_production: false)">bac à sable</span>'}
|
||||
<span class="chip"><b>${hotes.length}</b> hôtes</span>
|
||||
<span class="chip"><span class="pt actif"></span><b>${actifs}</b> actifs</span>
|
||||
<span class="chip"><span class="pt plan"></span><b>${hotes.length - actifs}</b> planifiés</span>
|
||||
|
|
@ -1320,13 +1324,14 @@ HTML = r"""<!doctype html>
|
|||
: '<div class="ch-stub">aucun serveur de base ici</div>';
|
||||
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 = `<div class="pied"><span class="grandit"></span>
|
||||
const clsDep = estProduction ? 'danger' : 'primaire';
|
||||
pied = `<div class="pied">${estProduction ? '<span class="badge-prod">PROD</span>' : ''}<span class="grandit"></span>
|
||||
<button type="button" id="btn-verifier" onclick="lancer('verifier', ${hi})">Vérifier</button>
|
||||
<button type="button" id="btn-deployer" class="primaire" onclick="lancer('deployer', ${hi})">Déployer</button></div>`;
|
||||
<button type="button" id="btn-deployer" class="${clsDep}" onclick="lancer('deployer', ${hi})">Déployer</button></div>`;
|
||||
} 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 = `<div class="pied"><span class="hint-dep">${txt}</span></div>`;
|
||||
}
|
||||
const statBadge = s.statut === 'divergence' ? '<span class="badge-statut bloque">divergent</span>'
|
||||
|
|
@ -1661,8 +1666,14 @@ HTML = r"""<!doctype html>
|
|||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue