GUI : état réel (sonde de vie) + auto-actif
Le badge planifié/actif = intention du plan, pas existence de la VM. - Sonde de vie : /api/sondes teste la joignabilité SSH de chaque hôte (parallèle, non bloquant) → point ● vivante/injoignable sur les tuiles et dans le détail (« Plan : … · Réel : … »). - Auto-actif : un hôte cloné (creer) ou déployé passe actif dans le plan automatiquement (matérialisé = actif). Répond au constat « serveur planifié alors qu'il est vivant ». Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7a98678bd9
commit
347e739818
2 changed files with 63 additions and 1 deletions
|
|
@ -3,6 +3,15 @@
|
|||
## 2026-07-01
|
||||
|
||||
### Ajouté
|
||||
- **État RÉEL vs plan dans le GUI (sonde de vie + auto-actif).** Le badge `planifié`/
|
||||
`actif` décrit l'*intention* du plan, pas l'existence de la VM — d'où la confusion
|
||||
« serveur planifié mais vivant ». Deux ajouts :
|
||||
- **Sonde de vie** : le GUI teste la joignabilité SSH de chaque hôte en arrière-plan
|
||||
(`/api/sondes`, en parallèle) et affiche un état réel — **● vivante** / **● injoignable**
|
||||
— sur les tuiles et dans le détail (« Plan : … · Réel : … »), distinct du plan.
|
||||
- **Auto-actif** : un hôte qu'on **matérialise** (clone `creer` réussi) ou qu'on
|
||||
**déploie** passe automatiquement `actif` dans le plan (matérialisé = actif).
|
||||
|
||||
- **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) :
|
||||
|
|
|
|||
|
|
@ -9,9 +9,11 @@ import json
|
|||
import os
|
||||
import re
|
||||
import secrets
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
|
@ -230,6 +232,33 @@ def liste_hotes(data: dict) -> list[dict]:
|
|||
return hotes
|
||||
|
||||
|
||||
def _joignable(ip: str, port: int = 22, timeout: float = 1.5) -> bool:
|
||||
if not ip:
|
||||
return False
|
||||
try:
|
||||
with socket.create_connection((ip, port), timeout=timeout):
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def sonder_hotes(path: Path) -> dict:
|
||||
"""Etat REEL (vs plan declaratif) : la VM de chaque hote repond-elle sur SSH ?"""
|
||||
cibles = [(h.get("nom"), h.get("adresse_ip")) for h in liste_hotes(charger_yaml(path))]
|
||||
with ThreadPoolExecutor(max_workers=16) as executeur:
|
||||
resultats = executeur.map(lambda c: (c[0], _joignable(c[1])), cibles)
|
||||
return {nom: ok for nom, ok in resultats if nom}
|
||||
|
||||
|
||||
def marquer_serveur_actif(hote: str) -> None:
|
||||
"""Auto-actif : un hote materialise (clone/deploye) passe 'actif' dans le plan."""
|
||||
registre = charger_serveurs(FICHIER_SERVEURS)
|
||||
serveurs = registre.get("serveurs") or {}
|
||||
if hote in serveurs and serveurs[hote].get("etat") != "actif":
|
||||
serveurs[hote]["etat"] = "actif"
|
||||
ecrire_serveurs(FICHIER_SERVEURS, registre)
|
||||
|
||||
|
||||
def serveurs_reconcilies(data: dict) -> list:
|
||||
"""Serveurs du plan + valeurs derivees + statut de reconciliation (lecture)."""
|
||||
registre = charger_serveurs(FICHIER_SERVEURS)
|
||||
|
|
@ -849,6 +878,7 @@ HTML = r"""<!doctype html>
|
|||
let modaleResolveur = null;
|
||||
let intrants = null;
|
||||
let integrationsDispo = [];
|
||||
let sondes = {}; // état RÉEL par hôte : joignable (SSH) ou non — distinct du plan
|
||||
let chronoConsole = null;
|
||||
let consoleDebut = 0;
|
||||
|
||||
|
|
@ -903,6 +933,14 @@ HTML = r"""<!doctype html>
|
|||
if (selServeur == null || selServeur >= serveurs.length) selServeur = serveurs.length ? 0 : null;
|
||||
dessiner();
|
||||
message('Inventaire chargé.', 'ok');
|
||||
sonder(); // état réel (joignabilité) en arrière-plan, non bloquant
|
||||
}
|
||||
|
||||
async function sonder() {
|
||||
try {
|
||||
const r = await fetch('/api/sondes');
|
||||
if (r.ok) { sondes = await r.json(); dessiner(); }
|
||||
} catch (e) { /* silencieux */ }
|
||||
}
|
||||
|
||||
function setOnglet(o) { ongletActif = o; dessinerDetail(); }
|
||||
|
|
@ -1271,6 +1309,11 @@ HTML = r"""<!doctype html>
|
|||
function ouvrirApp(id) { const i = applications.findIndex(a => a.id === id); selApp = i >= 0 ? i : null; setVue('applications'); }
|
||||
function ouvrirBaseServeur(nom) { const i = bdServeurs.findIndex(x => x.nom === nom); selBase = i >= 0 ? 'srv:' + i : null; setVue('bases'); }
|
||||
|
||||
function pointVie(nom) {
|
||||
const v = sondes[nom];
|
||||
return v === true ? '<span class="pt actif" title="VM joignable (SSH)"></span> '
|
||||
: v === false ? '<span class="pt bloq" title="Injoignable"></span> ' : '';
|
||||
}
|
||||
function carteServeur(s, i) {
|
||||
const actif = s.etat === 'actif';
|
||||
const recon = s.statut === 'divergence' ? '<span class="tag plus" title="Le plan diverge de l\'inventaire généré">⚠ divergent</span>'
|
||||
|
|
@ -1278,7 +1321,7 @@ HTML = r"""<!doctype html>
|
|||
return `<button type="button" class="carte ${actif ? 'actif' : 'attente'} ${i === selServeur ? 'selectionnee' : ''}" id="tuile-srv-${i}" onclick="choisirServeur(${i})">
|
||||
<div class="carte-haut"><span class="badge-statut ${actif ? 'ok' : 'attente'}">${actif ? 'actif' : 'planifié'}</span>${s.vmid ? `<span class="carte-vmid">#${echapper(s.vmid)}</span>` : ''}</div>
|
||||
<div class="carte-nom ${s.nom ? '' : 'sans'}">${echapper(s.nom || '(sans nom)')}</div>
|
||||
<div class="carte-ip">${echapper(s.adresse_ip || '—')}</div>
|
||||
<div class="carte-ip">${pointVie(s.nom)}${echapper(s.adresse_ip || '—')}</div>
|
||||
${recon ? `<div class="carte-grps">${recon}</div>` : ''}
|
||||
</button>`;
|
||||
}
|
||||
|
|
@ -1348,6 +1391,7 @@ HTML = r"""<!doctype html>
|
|||
<span class="grandit"></span>${statBadge}
|
||||
<button type="button" class="danger" title="Retirer ce serveur" onclick="retirerServeur()">✕</button></div>
|
||||
<div class="stats">${tuile(s.vmid, 'VMID')}${tuile(s.adresse_ip, 'IP')}${tuile(s.vlan, 'VLAN')}</div>
|
||||
<div class="dsn" style="padding:5px 2px"><b>Plan</b> : ${echapper(s.etat)} · <b>Réel</b> : ${sondes[s.nom] === true ? '<span class="pt actif"></span> vivante' + (s.adresse_ip ? ' (' + echapper(s.adresse_ip) + ')' : '') : sondes[s.nom] === false ? '<span class="pt bloq"></span> injoignable' : '<span class="pt plan"></span> non sondé'}</div>
|
||||
<div class="section-tete" style="margin:4px 2px 2px">Identité — saisie du plan (surcharge les défauts)</div>
|
||||
<div class="grille">
|
||||
<label class="champ"><span>Serveur (fonction-NN)</span><input data-v="nom" value="${echapper(s.nom)}" placeholder="web-frontal-03" oninput="definirServeur('nom', this.value)"></label>
|
||||
|
|
@ -1902,6 +1946,13 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
|||
VERIF_OK[hote] = True
|
||||
elif mode in ("deployer", "pousser"):
|
||||
VERIF_OK.pop(hote, None)
|
||||
# Auto-actif : un hote qu'on vient de materialiser (clone reussi) ou de
|
||||
# deployer passe 'actif' dans le plan. Reflet fidele : materialise = actif.
|
||||
if rc == 0 and mode in ("creer", "deployer", "pousser"):
|
||||
try:
|
||||
marquer_serveur_actif(hote)
|
||||
except Exception:
|
||||
pass
|
||||
self._ecrire_flux(f"\n__FIN__ rc={rc}\n")
|
||||
finally:
|
||||
if fichier_vault:
|
||||
|
|
@ -1921,6 +1972,8 @@ class Gestionnaire(BaseHTTPRequestHandler):
|
|||
self.repondre(204, b"", "image/x-icon")
|
||||
elif chemin == "/api/inventaire":
|
||||
self.repondre_json(200, inventaire_api(self.inventaire))
|
||||
elif chemin == "/api/sondes":
|
||||
self.repondre_json(200, sonder_hotes(self.inventaire))
|
||||
else:
|
||||
self.repondre_json(404, {"erreur": "Introuvable"})
|
||||
except Exception as exc:
|
||||
|
|
|
|||
Loading…
Reference in a new issue