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 à
|
instances existantes (découpage lab/production) continuent de fonctionner à
|
||||||
l'identique ; les nouvelles peuvent adopter `inventories/principal/`. Deuxième pierre
|
l'identique ; les nouvelles peuvent adopter `inventories/principal/`. Deuxième pierre
|
||||||
de la séparation **par instance** (la 1re étant le drapeau `setops_production`).
|
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
|
- **Garde-fou de prudence par instance (`setops_production`).** Le déploiement réel
|
||||||
déployer pour de vrai (GUI Vérifier/Déployer, `executer_flux`) ne dépend plus du
|
est désormais possible sur **toute instance** (un bac à sable déploie sur *son*
|
||||||
*nom* de l'inventaire mais d'un drapeau explicite `setops_production` dans
|
infra lab — il est isolé **et** fonctionnel). Le drapeau `setops_production` dans
|
||||||
`group_vars/all/` de l'instance (true = prod, false = bac à sable). Rétro-compatible
|
`group_vars/all/` ne **bloque** plus rien : il marque la PRODUCTION pour exiger une
|
||||||
(à défaut de drapeau, ancien repère « inventaire production »). Pose la base de la
|
**confirmation renforcée** au déploiement (bannière/badge rouge « PROD », bouton
|
||||||
séparation **par instance** : un bac à sable garde `false` et ne peut rien déployer,
|
Déployer en rouge, re-saisie du nom d'hôte) ; un bac à sable (`false`) affiche
|
||||||
quel que soit le nom de ses dossiers.
|
« 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é
|
### Modifié
|
||||||
- **Voûte de secrets unique par environnement.** Fini les voûtes éparpillées : tous
|
- **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)
|
dependencies = charger_dependances(FICHIER_DEPENDANCES)
|
||||||
return {
|
return {
|
||||||
"inventaire": os.path.relpath(path, RACINE),
|
"inventaire": os.path.relpath(path, RACINE),
|
||||||
"production": instance_autorise_deploiement(path),
|
"production": instance_est_production(path),
|
||||||
"groupes": groupes_disponibles(data),
|
"groupes": groupes_disponibles(data),
|
||||||
"dependances": dependencies,
|
"dependances": dependencies,
|
||||||
"nomenclature": charger_nomenclature(FICHIER_NOMENCLATURE),
|
"nomenclature": charger_nomenclature(FICHIER_NOMENCLATURE),
|
||||||
|
|
@ -278,12 +278,13 @@ def _lire_yaml_dict(path: Path) -> dict:
|
||||||
return data if isinstance(data, dict) else {}
|
return data if isinstance(data, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
def instance_autorise_deploiement(path: Path) -> bool:
|
def instance_est_production(path: Path) -> bool:
|
||||||
"""Le déploiement réel est-il permis pour l'instance de cet inventaire ?
|
"""Cette instance est-elle la PRODUCTION ? (garde-fou de prudence, pas un blocage)
|
||||||
|
|
||||||
Repère explicite, par instance : `setops_production` dans
|
Le déploiement est permis sur toute instance (lab compris, vers son infra) ;
|
||||||
`<inventaire>/group_vars/all/`. Rétro-compatibilité : à défaut de drapeau,
|
ce drapeau ne sert qu'à exiger une confirmation renforcée côté GUI quand c'est
|
||||||
on retombe sur l'ancien repère (inventaire nommé « production »).
|
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"
|
base = path.parent / "group_vars" / "all"
|
||||||
fichiers = sorted(base.glob("*.yml")) if base.is_dir() else []
|
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.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.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-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-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 { 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); }
|
.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 actifs = hotes.filter(h => h.etat === 'actif').length;
|
||||||
const bloques = hotes.filter(h => hoteBloque(h)).length;
|
const bloques = hotes.filter(h => hoteBloque(h)).length;
|
||||||
document.getElementById('resume').innerHTML = `
|
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"><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 actif"></span><b>${actifs}</b> actifs</span>
|
||||||
<span class="chip"><span class="pt plan"></span><b>${hotes.length - actifs}</b> planifiés</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>';
|
: '<div class="ch-stub">aucun serveur de base ici</div>';
|
||||||
hoteOpCourant = s.nom;
|
hoteOpCourant = s.nom;
|
||||||
let pied;
|
let pied;
|
||||||
if (h && h.etat === 'actif' && !hoteBloque(h) && estProduction) {
|
if (h && h.etat === 'actif' && !hoteBloque(h)) {
|
||||||
const hi = hotes.indexOf(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-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 {
|
} 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>`;
|
pied = `<div class="pied"><span class="hint-dep">${txt}</span></div>`;
|
||||||
}
|
}
|
||||||
const statBadge = s.statut === 'divergence' ? '<span class="badge-statut bloque">divergent</span>'
|
const statBadge = s.statut === 'divergence' ? '<span class="badge-statut bloque">divergent</span>'
|
||||||
|
|
@ -1661,8 +1666,14 @@ HTML = r"""<!doctype html>
|
||||||
let vault = '';
|
let vault = '';
|
||||||
if (mode === 'deployer') {
|
if (mode === 'deployer') {
|
||||||
if (!verifie[hote]) { message('Vérifiez (dry-run) d\'abord.', 'erreur'); return; }
|
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,
|
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;
|
if (vault === null) return;
|
||||||
motVault = vault;
|
motVault = vault;
|
||||||
}
|
}
|
||||||
|
|
@ -1864,9 +1875,6 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
||||||
if not MOTIF_HOTE.match(hote or ""):
|
if not MOTIF_HOTE.match(hote or ""):
|
||||||
self.repondre_json(400, {"erreur": "Nom d'hote invalide."})
|
self.repondre_json(400, {"erreur": "Nom d'hote invalide."})
|
||||||
return
|
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):
|
if hote not in hotes_actifs_fichier(self.inventaire):
|
||||||
self.repondre_json(409, {"erreur": f"{hote} n'est pas un hote actif sauvegarde."})
|
self.repondre_json(409, {"erreur": f"{hote} n'est pas un hote actif sauvegarde."})
|
||||||
return
|
return
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue