From 7a98678bd9ce859c745b9568bfacf8eb9d80f346 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Wed, 1 Jul 2026 15:58:32 -0400 Subject: [PATCH] =?UTF-8?q?GUI=20:=20bouton=20=C2=AB=20Pousser=20=C2=BB=20?= =?UTF-8?q?(flux=20100=20%=20cliquable)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chaque objet du détail se matérialise sur son hôte, en streaming console (vault + confirmation prod) : - Serveur → 🖥 Pousser clone la VM (make creer-vm), même sur un planifié — comble le trou : le GUI ne clonait pas. - Application → Pousser déploie l'hôte porteur. - Base → Pousser déploie l'hôte du serveur de BD (crée la base). Nouveaux modes creer/pousser dans executer_flux + routes /api/creer et /api/pousser. Flux complet : éditer → Appliquer → Pousser → Vérifier → Déployer, sans quitter le navigateur. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 17 ++++++++ scripts/inventory_gui.py | 86 +++++++++++++++++++++++++++++----------- 2 files changed, 79 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 988fa35..d3d3317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # CHANGELOG — Set-OPS +## 2026-07-01 + +### Ajouté +- **Bouton « Pousser » dans le GUI — le flux devient 100 % cliquable.** Chaque objet + du détail se matérialise sur son hôte, en streaming console (avec mot de passe vault + + confirmation renforcée en prod) : + - **Serveur** → `🖥 Pousser` clone la VM depuis le golden template (`make creer-vm`), + disponible même sur un hôte planifié — comble le trou : le GUI ne clonait pas. + - **Application** → `Pousser` déploie l'hôte porteur (`make deployer HOTE=`). + - **Base** → `Pousser` déploie l'hôte du serveur de BD (crée la base). + Nouveaux modes `creer` / `pousser` dans `executer_flux` + routes `/api/creer` et + `/api/pousser`. Flux complet depuis l'interface : éditer → ⚙ Appliquer → 🖥 Pousser + → Vérifier → Déployer. +- **Premier déploiement RÉEL validé de bout en bout** (cluster asgard) : flux canonique + plan-piloté clonant + durcissant + faisant tourner un PowerDNS qui résout. Voir les 3 + correctifs ci-dessous. + ## 2026-06-30 ### Ajouté diff --git a/scripts/inventory_gui.py b/scripts/inventory_gui.py index 22c2632..5ba4561 100644 --- a/scripts/inventory_gui.py +++ b/scripts/inventory_gui.py @@ -1084,7 +1084,12 @@ HTML = r"""
Bases hébergées ici
${iciHtml} -
Sauvegarde (Ctrl+S) puis « Appliquer le plan ».
+ ${(() => { + const hS = hotes.find(h => h.nom === s.hote); + if (hS && hS.etat === 'actif') { const hiS = hotes.indexOf(hS); + return `
Pousser déploie l'hôte « ${echapper(s.hote)} ».${estProduction ? 'PROD' : ''}
`; } + return `
${s.hote ? `Active + pousse d'abord l'hôte « ${echapper(s.hote)} ».` : 'Aucun hôte assigné.'}
`; + })()} `; } else { const a = bdApplis[info.i]; @@ -1106,7 +1111,13 @@ HTML = r"""
${echapper(dsnBase(a))}
-
Sauvegarde (Ctrl+S) puis « Appliquer le plan ».
+ ${(() => { + const srv = bdServeurs.find(x => x.nom === a.serveur); + const hB = srv ? hotes.find(h => h.nom === srv.hote) : null; + if (hB && hB.etat === 'actif') { const hiB = hotes.indexOf(hB); + return `
Pousser déploie « ${echapper(srv.hote)} » (le serveur de BD qui héberge cette base).${estProduction ? 'PROD' : ''}
`; } + return `
${(srv && srv.hote) ? `Active + pousse d'abord l'hôte « ${echapper(srv.hote)} ».` : 'Serveur de BD sans hôte assigné.'}
`; + })()} `; } } @@ -1192,7 +1203,14 @@ HTML = r"""
Bases consommées
${liens} -
Sauvegarde (Ctrl+S) puis « Appliquer le plan » pour régénérer.
+ ${(() => { + const hA = hotes.find(x => x.nom === a.hote); + if (hA && hA.etat === 'actif') { + const hiA = hotes.indexOf(hA); + return `
Pousser déploie l'hôte porteur « ${echapper(a.hote)} ».${estProduction ? 'PROD' : ''}
`; + } + return `
${a.hote ? `Active + pousse d'abord l'hôte « ${echapper(a.hote)} ».` : 'Aucun hôte assigné.'}
`; + })()} `; } @@ -1306,19 +1324,21 @@ HTML = r""" ? `
${bds.map(x => `
${echapper(x.type)} :${echapper(x.port)}
`).join('')}
` : '
aucun serveur de base ici
'; hoteOpCourant = s.nom; - let pied; + const hi = h ? hotes.indexOf(h) : -1; + let boutons = ''; + if (h) boutons += ``; if (h && h.etat === 'actif' && !hoteBloque(h)) { - const hi = hotes.indexOf(h); const clsDep = estProduction ? 'danger' : 'primaire'; - pied = `
${estProduction ? 'PROD' : ''} - -
`; + boutons += `` + + ``; + } + let pied; + if (boutons) { + const bloque = h && h.etat === 'actif' && hoteBloque(h); + const hint = bloque ? `Prérequis manquant : ${[...new Set((h.groupes || []).flatMap(g => manquantsPourGroupe(g)))].join(', ')}` : ''; + pied = `
${estProduction ? 'PROD' : ''}${hint}${boutons}
`; } else { - const manquants = h ? [...new Set((h.groupes || []).flatMap(g => manquantsPourGroupe(g)))] : []; - const txt = !h ? 'Applique le plan pour instancier cet hôte' - : (h.etat !== 'actif' ? 'Hôte actif requis pour déployer' - : ('Prérequis manquant' + (manquants.length ? ' : ' + manquants.join(', ') : ''))); - pied = `
${txt}
`; + pied = `
Applique le plan pour instancier cet hôte (⚙ Appliquer le plan)
`; } const statBadge = s.statut === 'divergence' ? 'divergent' : s.statut === 'absent' ? 'non instancié' @@ -1617,21 +1637,28 @@ HTML = r""" if (!hote) { message('Hôte introuvable.', 'erreur'); return; } if (occupe) return; 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' + if (mode === 'deployer' && !verifie[hote]) { message('Vérifiez (dry-run) d\'abord.', 'erreur'); return; } + if (mode === 'creer' || mode === 'deployer' || mode === 'pousser') { + if (estProduction && (mode === 'deployer' || mode === 'pousser')) { + const saisi = prompt('⚠️ PRODUCTION — action réelle 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, - (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.'); + const av = estProduction ? '⚠️ PRODUCTION. ' : ''; + const txt = mode === 'creer' + ? 'Clone la VM « ' + hote + ' » depuis le golden template sur Proxmox (création réelle).' + : mode === 'pousser' + ? av + 'Déploie l\'hôte « ' + hote + ' » (applique ses playbooks, en root). Non transactionnel.' + : av + 'Applique les playbooks de groupes sur la VM réelle, en root (sudo). Non transactionnel.'; + const titre = (mode === 'creer' ? 'Pousser — créer la VM ' : mode === 'pousser' ? 'Pousser — déployer ' : 'Déployer ') + hote; + vault = await ouvrirModale(titre, txt); if (vault === null) return; motVault = vault; } occupe = true; majBoutonsDeploiement(); - ouvrirConsole(mode === 'deployer' ? 'Déploiement' : 'Vérification (dry-run)', hote); + const titreConsole = {verifier: 'Vérification (dry-run)', deployer: 'Déploiement', creer: 'Création VM (clone)', pousser: 'Poussée (déploiement)'}[mode] || mode; + ouvrirConsole(titreConsole, hote); try { const rep = await fetch('/api/' + mode, { method: 'POST', headers: {'Content-Type': 'application/json', 'X-Jeton': JETON}, @@ -1656,7 +1683,8 @@ HTML = r""" if (rc === null) rc = 1; finirConsole(rc); if (mode === 'verifier' && rc === 0) { verifie[hote] = true; message('Vérification réussie — déploiement débloqué pour ' + hote + '.', 'ok'); } - else if (mode === 'deployer') { verifie[hote] = false; message(rc === 0 ? 'Déploiement terminé.' : 'Déploiement en échec.', rc === 0 ? 'ok' : 'erreur'); } + else if (mode === 'creer') { message(rc === 0 ? 'VM « ' + hote + ' » créée.' : 'Échec de la création.', rc === 0 ? 'ok' : 'erreur'); if (rc === 0) charger(); } + else if (mode === 'deployer' || mode === 'pousser') { verifie[hote] = false; message(rc === 0 ? 'Poussée terminée.' : 'Poussée en échec.', rc === 0 ? 'ok' : 'erreur'); } } catch (e) { ecrireConsole('Erreur client : ' + e.message + '\n'); finirConsole(1); } @@ -1828,7 +1856,12 @@ class Gestionnaire(BaseHTTPRequestHandler): if not MOTIF_HOTE.match(hote or ""): self.repondre_json(400, {"erreur": "Nom d'hote invalide."}) return - if hote not in hotes_actifs_fichier(self.inventaire): + tous_hotes = {h.get("nom") for h in liste_hotes(charger_yaml(self.inventaire))} + if hote not in tous_hotes: + self.repondre_json(409, {"erreur": f"{hote} est inconnu de l'inventaire (applique le plan d'abord)."}) + return + # 'creer' (clone) est permis meme sur un hote planifie ; les autres exigent actif. + if mode in ("verifier", "deployer", "pousser") and hote not in hotes_actifs_fichier(self.inventaire): self.repondre_json(409, {"erreur": f"{hote} n'est pas un hote actif sauvegarde."}) return if mode == "deployer" and not VERIF_OK.get(hote): @@ -1839,7 +1872,8 @@ class Gestionnaire(BaseHTTPRequestHandler): return fichier_vault = None try: - cible = "deployer" if mode == "deployer" else "verifier-deploiement" + cible = {"deployer": "deployer", "pousser": "deployer", + "creer": "creer-vm"}.get(mode, "verifier-deploiement") env = dict(os.environ, PYTHONUNBUFFERED="1", ANSIBLE_FORCE_COLOR="0") if vault: fd, fichier_vault = tempfile.mkstemp(prefix="setops-vault-") # 0600 par defaut @@ -1866,7 +1900,7 @@ class Gestionnaire(BaseHTTPRequestHandler): rc = proc.wait() if mode == "verifier" and rc == 0: VERIF_OK[hote] = True - elif mode == "deployer": + elif mode in ("deployer", "pousser"): VERIF_OK.pop(hote, None) self._ecrire_flux(f"\n__FIN__ rc={rc}\n") finally: @@ -1934,6 +1968,10 @@ class Gestionnaire(BaseHTTPRequestHandler): self.executer_flux(str(donnees.get("hote", "")), "verifier", donnees.get("vault")) elif chemin == "/api/deployer": self.executer_flux(str(donnees.get("hote", "")), "deployer", donnees.get("vault")) + elif chemin == "/api/creer": + self.executer_flux(str(donnees.get("hote", "")), "creer", donnees.get("vault")) + elif chemin == "/api/pousser": + self.executer_flux(str(donnees.get("hote", "")), "pousser", donnees.get("vault")) else: self.repondre_json(404, {"erreur": "Introuvable"}) except Exception as exc: