Qualys: deploy agent background jobs + upgrade/downgrade + AJAX overlays
- Background job system pour deploiement (threads paralleles, progression live) - Upgrade/downgrade: compare versions installee vs package, rpm -Uvh --oldpackage - Checkbox "Forcer le downgrade" dans UI - Choix auto DEB/RPM base sur os_version (centos/rhel/rocky/oracle -> RPM) - Check agent: rpm -q / dpkg -s (evite faux positifs "agent installe mais inactif") - Bouton "Rafraichir depuis Qualys" AJAX avec timer - Agents page: colonne version installee + statut Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
8479d7280e
commit
5ea4100f4c
@ -519,20 +519,20 @@ async def qualys_agents_page(request: Request, db=Depends(get_db)):
|
||||
|
||||
@router.post("/qualys/agents/refresh")
|
||||
async def qualys_agents_refresh(request: Request, db=Depends(get_db)):
|
||||
from fastapi.responses import JSONResponse
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login")
|
||||
return JSONResponse({"ok": False, "msg": "Non authentifié"}, status_code=401)
|
||||
perms = get_user_perms(db, user)
|
||||
if not can_edit(perms, "qualys"):
|
||||
return RedirectResponse(url="/qualys/agents")
|
||||
return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403)
|
||||
from ..services.qualys_service import refresh_all_agents
|
||||
try:
|
||||
stats = refresh_all_agents(db)
|
||||
msg = f"refresh_ok_{stats.get('created',0)}_{stats.get('updated',0)}"
|
||||
return JSONResponse({"ok": True, "msg": f"{stats.get('created',0)} créés, {stats.get('updated',0)} mis à jour", "stats": stats})
|
||||
except Exception as e:
|
||||
import traceback; traceback.print_exc()
|
||||
msg = "refresh_error"
|
||||
return RedirectResponse(url=f"/qualys/agents?msg={msg}", status_code=303)
|
||||
return JSONResponse({"ok": False, "msg": str(e)[:200]}, status_code=500)
|
||||
|
||||
|
||||
@router.get("/qualys/agents/export-no-agent")
|
||||
@ -872,15 +872,20 @@ async def qualys_deploy_page(request: Request, db=Depends(get_db)):
|
||||
|
||||
packages = list_packages()
|
||||
servers = db.execute(text("""
|
||||
SELECT s.id, s.hostname, s.os_family, s.etat, s.ssh_user, s.ssh_port, s.ssh_method,
|
||||
d.name as domain, e.name as env
|
||||
SELECT s.id, s.hostname, s.os_family, s.os_version, s.etat, s.ssh_user, s.ssh_port, s.ssh_method,
|
||||
d.name as domain, e.name as env,
|
||||
qa.agent_version, qa.agent_status, qa.last_checkin
|
||||
FROM servers s
|
||||
LEFT JOIN domain_environments de ON s.domain_env_id = de.id
|
||||
LEFT JOIN domains d ON de.domain_id = d.id
|
||||
LEFT JOIN environments e ON de.environment_id = e.id
|
||||
LEFT JOIN qualys_assets qa ON qa.server_id = s.id
|
||||
ORDER BY s.hostname
|
||||
""")).fetchall()
|
||||
servers = [dict(r._mapping) for r in servers]
|
||||
for s in servers:
|
||||
if s.get("last_checkin"):
|
||||
s["last_checkin"] = str(s["last_checkin"])[:19]
|
||||
|
||||
ctx = base_context(request, db, user)
|
||||
ctx.update({
|
||||
@ -896,106 +901,96 @@ async def qualys_deploy_page(request: Request, db=Depends(get_db)):
|
||||
|
||||
|
||||
@router.post("/qualys/deploy/run")
|
||||
async def qualys_deploy_run(request: Request, db=Depends(get_db),
|
||||
server_ids: str = Form(""),
|
||||
activation_id: str = Form(""),
|
||||
customer_id: str = Form(""),
|
||||
server_uri: str = Form(""),
|
||||
package_deb: str = Form(""),
|
||||
package_rpm: str = Form("")):
|
||||
async def qualys_deploy_run(request: Request, db=Depends(get_db)):
|
||||
from fastapi.responses import JSONResponse
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login")
|
||||
return JSONResponse({"ok": False, "msg": "Non authentifié"}, status_code=401)
|
||||
perms = get_user_perms(db, user)
|
||||
if not can_edit(perms, "qualys"):
|
||||
return RedirectResponse(url="/dashboard")
|
||||
return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403)
|
||||
|
||||
from ..services.agent_deploy_service import deploy_agent
|
||||
from ..services.agent_deploy_service import start_deploy_job
|
||||
from ..services.secrets_service import get_secret
|
||||
|
||||
ids = [int(x) for x in server_ids.split(",") if x.strip().isdigit()]
|
||||
body = await request.json()
|
||||
server_ids = body.get("server_ids", "")
|
||||
activation_id = body.get("activation_id", "")
|
||||
customer_id = body.get("customer_id", "")
|
||||
server_uri = body.get("server_uri", "")
|
||||
package_deb = body.get("package_deb", "")
|
||||
package_rpm = body.get("package_rpm", "")
|
||||
force_downgrade = body.get("force_downgrade", False)
|
||||
|
||||
ids = [int(x) for x in str(server_ids).split(",") if x.strip().isdigit()]
|
||||
if not ids:
|
||||
return RedirectResponse(url="/qualys/deploy?msg=no_servers", status_code=303)
|
||||
return JSONResponse({"ok": False, "msg": "Aucun serveur sélectionné"})
|
||||
|
||||
ssh_key = get_secret(db, "ssh_key_file") or "/opt/patchcenter/keys/id_ed25519"
|
||||
|
||||
# Get servers
|
||||
placeholders = ",".join(str(i) for i in ids)
|
||||
servers = db.execute(text(f"""
|
||||
SELECT id, hostname, os_family, ssh_user, ssh_port, ssh_method
|
||||
rows = db.execute(text(f"""
|
||||
SELECT id, hostname, os_family, os_version, ssh_user, ssh_port, ssh_method
|
||||
FROM servers WHERE id IN ({placeholders})
|
||||
""")).fetchall()
|
||||
servers = [{"hostname": r.hostname, "os_family": r.os_family,
|
||||
"os_version": r.os_version, "ssh_user": r.ssh_user, "ssh_port": r.ssh_port} for r in rows]
|
||||
|
||||
results = []
|
||||
log_lines = []
|
||||
job_id = start_deploy_job(servers, ssh_key, package_deb, package_rpm,
|
||||
activation_id, customer_id, server_uri, force_downgrade=force_downgrade)
|
||||
|
||||
for srv in servers:
|
||||
# Choose package based on OS
|
||||
if "debian" in (srv.os_family or "").lower() or srv.os_family == "linux":
|
||||
pkg = package_deb
|
||||
else:
|
||||
pkg = package_rpm
|
||||
|
||||
if not pkg or not os.path.exists(pkg):
|
||||
results.append({"hostname": srv.hostname, "status": "FAILED", "detail": f"Package introuvable: {pkg}"})
|
||||
continue
|
||||
|
||||
def on_line(msg, h=srv.hostname):
|
||||
log_lines.append(f"[{h}] {msg}")
|
||||
|
||||
result = deploy_agent(
|
||||
hostname=srv.hostname,
|
||||
ssh_user=srv.ssh_user or "root",
|
||||
ssh_key_path=ssh_key,
|
||||
ssh_port=srv.ssh_port or 22,
|
||||
os_family=srv.os_family,
|
||||
package_path=pkg,
|
||||
activation_id=activation_id,
|
||||
customer_id=customer_id,
|
||||
server_uri=server_uri,
|
||||
on_line=on_line,
|
||||
)
|
||||
results.append(result)
|
||||
|
||||
# Save to app state for display
|
||||
request.app.state.last_deploy_results = results
|
||||
request.app.state.last_deploy_log = log_lines
|
||||
|
||||
# Audit log
|
||||
from ..services.audit_service import log_action
|
||||
ok = sum(1 for r in results if r["status"] in ("SUCCESS", "ALREADY_INSTALLED"))
|
||||
fail = sum(1 for r in results if r["status"] not in ("SUCCESS", "ALREADY_INSTALLED"))
|
||||
log_action(db, request, user, "qualys_deploy", f"{ok} OK, {fail} fail sur {len(results)} serveurs")
|
||||
log_action(db, request, user, "qualys_deploy",
|
||||
entity_type="deploy_job",
|
||||
details={"job_id": job_id, "servers": len(servers)})
|
||||
db.commit()
|
||||
|
||||
ctx = base_context(request, db, user)
|
||||
ctx.update({
|
||||
"app_name": APP_NAME,
|
||||
"results": results,
|
||||
"log_lines": log_lines,
|
||||
"total": len(results),
|
||||
"ok": ok,
|
||||
"failed": fail,
|
||||
return JSONResponse({"ok": True, "job_id": job_id, "total": len(servers)})
|
||||
|
||||
|
||||
@router.get("/qualys/deploy/status/{job_id}")
|
||||
async def qualys_deploy_status(request: Request, job_id: str, db=Depends(get_db)):
|
||||
from fastapi.responses import JSONResponse
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return JSONResponse({"ok": False, "msg": "Non authentifié"}, status_code=401)
|
||||
|
||||
from ..services.agent_deploy_service import get_deploy_job
|
||||
job = get_deploy_job(job_id)
|
||||
if not job:
|
||||
return JSONResponse({"ok": False, "msg": "Job introuvable"}, status_code=404)
|
||||
|
||||
import time
|
||||
elapsed = int(time.time() - job["started_at"])
|
||||
return JSONResponse({
|
||||
"ok": True,
|
||||
"job_id": job_id,
|
||||
"total": job["total"],
|
||||
"done": job["done"],
|
||||
"finished": job["finished"],
|
||||
"elapsed": elapsed,
|
||||
"servers": job["servers"],
|
||||
"log": job["log"][-50:], # dernières 50 lignes
|
||||
})
|
||||
return templates.TemplateResponse("qualys_deploy_results.html", ctx)
|
||||
|
||||
|
||||
@router.post("/qualys/deploy/check")
|
||||
async def qualys_deploy_check(request: Request, db=Depends(get_db),
|
||||
server_ids: str = Form("")):
|
||||
async def qualys_deploy_check(request: Request, db=Depends(get_db)):
|
||||
from fastapi.responses import JSONResponse
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login")
|
||||
return JSONResponse({"ok": False, "msg": "Non authentifié"}, status_code=401)
|
||||
perms = get_user_perms(db, user)
|
||||
if not can_view(perms, "qualys"):
|
||||
return RedirectResponse(url="/dashboard")
|
||||
return JSONResponse({"ok": False, "msg": "Permission refusée"}, status_code=403)
|
||||
|
||||
from ..services.agent_deploy_service import check_agent
|
||||
from ..services.secrets_service import get_secret
|
||||
|
||||
ids = [int(x) for x in server_ids.split(",") if x.strip().isdigit()]
|
||||
body = await request.json()
|
||||
server_ids = body.get("server_ids", "")
|
||||
ids = [int(x) for x in str(server_ids).split(",") if x.strip().isdigit()]
|
||||
if not ids:
|
||||
return RedirectResponse(url="/qualys/deploy?msg=no_servers", status_code=303)
|
||||
return JSONResponse({"ok": False, "msg": "Aucun serveur sélectionné"})
|
||||
|
||||
ssh_key = get_secret(db, "ssh_key_file") or "/opt/patchcenter/keys/id_ed25519"
|
||||
placeholders = ",".join(str(i) for i in ids)
|
||||
@ -1006,13 +1001,7 @@ async def qualys_deploy_check(request: Request, db=Depends(get_db),
|
||||
r = check_agent(srv.hostname, srv.ssh_user or "root", ssh_key, srv.ssh_port or 22)
|
||||
results.append(r)
|
||||
|
||||
ctx = base_context(request, db, user)
|
||||
ctx.update({
|
||||
"app_name": APP_NAME,
|
||||
"results": results,
|
||||
"total": len(results),
|
||||
return JSONResponse({"ok": True, "results": results, "total": len(results),
|
||||
"active": sum(1 for r in results if r["status"] == "ACTIVE"),
|
||||
"not_installed": sum(1 for r in results if r["status"] == "NOT_INSTALLED"),
|
||||
"failed": sum(1 for r in results if r["status"] == "CONNECTION_FAILED"),
|
||||
})
|
||||
return templates.TemplateResponse("qualys_deploy_results.html", ctx)
|
||||
"failed": sum(1 for r in results if r["status"] == "CONNECTION_FAILED")})
|
||||
|
||||
@ -80,11 +80,21 @@ def check_agent(hostname, ssh_user, ssh_key_path, ssh_port=22):
|
||||
|
||||
result = {"hostname": hostname}
|
||||
|
||||
# Check if installed
|
||||
code, out, _ = _run_cmd(client, "which qualys-cloud-agent 2>/dev/null || rpm -q qualys-cloud-agent 2>/dev/null || dpkg -l qualys-cloud-agent 2>/dev/null | grep '^ii'")
|
||||
if code != 0 and not out.strip():
|
||||
# Check if installed (rpm -q returns 0 only if installed, dpkg -s returns 0 only if installed)
|
||||
installed = False
|
||||
code, out, _ = _run_cmd(client, "rpm -q qualys-cloud-agent 2>/dev/null")
|
||||
if code == 0 and "not installed" not in out.lower() and "n'est pas install" not in out.lower():
|
||||
installed = True
|
||||
else:
|
||||
code, out, _ = _run_cmd(client, "dpkg -s qualys-cloud-agent 2>/dev/null")
|
||||
if code == 0 and "install ok installed" in out.lower():
|
||||
installed = True
|
||||
|
||||
if not installed:
|
||||
result["status"] = "NOT_INSTALLED"
|
||||
result["detail"] = "Agent non installe"
|
||||
result["version"] = ""
|
||||
result["service_status"] = ""
|
||||
client.close()
|
||||
return result
|
||||
|
||||
@ -94,9 +104,8 @@ def check_agent(hostname, ssh_user, ssh_key_path, ssh_port=22):
|
||||
result["service_status"] = status
|
||||
|
||||
# Get version via package manager
|
||||
code, out, _ = _run_cmd(client, "rpm -q qualys-cloud-agent 2>/dev/null || dpkg -l qualys-cloud-agent 2>/dev/null | grep '^ii' | awk '{print $3}'")
|
||||
code, out, _ = _run_cmd(client, "rpm -q --qf '%{VERSION}-%{RELEASE}' qualys-cloud-agent 2>/dev/null || dpkg-query -W -f='${Version}' qualys-cloud-agent 2>/dev/null")
|
||||
version = out.strip()
|
||||
# Extract just version number
|
||||
m = re.search(r'(\d+\.\d+\.\d+[\.\-]\d+)', version)
|
||||
result["version"] = m.group(1) if m else version[:50]
|
||||
|
||||
@ -118,65 +127,129 @@ def check_agent(hostname, ssh_user, ssh_key_path, ssh_port=22):
|
||||
return result
|
||||
|
||||
|
||||
def _compare_versions(v1, v2):
|
||||
"""Compare deux versions. Retourne -1 (v1<v2), 0 (egales), 1 (v1>v2)"""
|
||||
def to_parts(v):
|
||||
return [int(x) for x in re.split(r'[.\-]', v) if x.isdigit()]
|
||||
p1, p2 = to_parts(v1 or ""), to_parts(v2 or "")
|
||||
for a, b in zip(p1, p2):
|
||||
if a < b: return -1
|
||||
if a > b: return 1
|
||||
if len(p1) < len(p2): return -1
|
||||
if len(p1) > len(p2): return 1
|
||||
return 0
|
||||
|
||||
|
||||
def deploy_agent(hostname, ssh_user, ssh_key_path, ssh_port, os_family,
|
||||
package_path, activation_id, customer_id, server_uri,
|
||||
on_line=None):
|
||||
"""Deploie l'agent Qualys sur un serveur"""
|
||||
on_line=None, on_stage=None, force_downgrade=False):
|
||||
"""Deploie l'agent Qualys sur un serveur (install, upgrade ou downgrade)"""
|
||||
|
||||
def emit(msg):
|
||||
if on_line:
|
||||
on_line(msg)
|
||||
log.info(f"[{hostname}] {msg}")
|
||||
|
||||
def stage(name, detail=""):
|
||||
if on_stage:
|
||||
on_stage(name, detail)
|
||||
|
||||
stage("connecting", f"Connexion SSH {ssh_user}@{hostname}:{ssh_port}")
|
||||
emit(f"Connexion SSH {ssh_user}@{hostname}:{ssh_port}...")
|
||||
client, error = _get_ssh_client(hostname, ssh_user, ssh_key_path, ssh_port)
|
||||
if not client:
|
||||
emit(f"ERREUR connexion: {error}")
|
||||
stage("failed", error)
|
||||
return {"hostname": hostname, "status": "FAILED", "detail": error}
|
||||
|
||||
result = {"hostname": hostname, "status": "PENDING"}
|
||||
pkg_name = os.path.basename(package_path)
|
||||
pkg_version = _extract_version(pkg_name)
|
||||
is_deb = pkg_name.endswith(".deb")
|
||||
|
||||
try:
|
||||
# 1. Detect OS if not provided
|
||||
if not os_family:
|
||||
code, out, _ = _run_cmd(client, "cat /etc/os-release 2>/dev/null | head -3")
|
||||
os_family = "linux" # default
|
||||
os_family = "linux"
|
||||
if "debian" in out.lower() or "ubuntu" in out.lower():
|
||||
os_family = "debian"
|
||||
elif "red hat" in out.lower() or "centos" in out.lower() or "rocky" in out.lower():
|
||||
os_family = "rhel"
|
||||
emit(f"OS detecte: {os_family}")
|
||||
|
||||
# 2. Check if already installed
|
||||
# 2. Check installed version
|
||||
stage("checking", "Verification agent existant")
|
||||
installed_version = None
|
||||
code, out, _ = _run_cmd(client, "rpm -q qualys-cloud-agent 2>/dev/null")
|
||||
if code == 0 and "not installed" not in out.lower() and "n'est pas install" not in out.lower():
|
||||
m = re.search(r'(\d+\.\d+\.\d+[\.\-]\d+)', out)
|
||||
installed_version = m.group(1) if m else None
|
||||
else:
|
||||
code, out, _ = _run_cmd(client, "dpkg-query -W -f='${Version}' qualys-cloud-agent 2>/dev/null")
|
||||
if code == 0 and out.strip():
|
||||
m = re.search(r'(\d+\.\d+\.\d+[\.\-]\d+)', out)
|
||||
installed_version = m.group(1) if m else None
|
||||
|
||||
if installed_version:
|
||||
cmp = _compare_versions(pkg_version, installed_version)
|
||||
emit(f"Version installee: {installed_version}, package: {pkg_version}")
|
||||
if cmp == 0:
|
||||
# Meme version
|
||||
code, out, _ = _run_cmd(client, "systemctl is-active qualys-cloud-agent 2>/dev/null")
|
||||
if out.strip() == "active":
|
||||
emit("Agent deja installe et actif - skip")
|
||||
emit(f"Meme version ({installed_version}) et agent actif - skip")
|
||||
stage("already_installed", f"v{installed_version} deja active")
|
||||
result["status"] = "ALREADY_INSTALLED"
|
||||
result["detail"] = "Agent deja actif"
|
||||
result["detail"] = f"v{installed_version} deja installee et active"
|
||||
client.close()
|
||||
return result
|
||||
else:
|
||||
emit(f"Meme version mais service {out.strip()} - reinstallation")
|
||||
elif cmp < 0:
|
||||
# Downgrade
|
||||
if not force_downgrade:
|
||||
emit(f"DOWNGRADE refuse: {installed_version} → {pkg_version}")
|
||||
stage("downgrade_refused", f"v{installed_version} > v{pkg_version}")
|
||||
result["status"] = "DOWNGRADE_REFUSED"
|
||||
result["detail"] = f"Version installee ({installed_version}) plus recente que le package ({pkg_version}). Cochez 'Forcer le downgrade'."
|
||||
client.close()
|
||||
return result
|
||||
else:
|
||||
emit(f"DOWNGRADE force: {installed_version} → {pkg_version}")
|
||||
else:
|
||||
emit(f"UPGRADE: {installed_version} → {pkg_version}")
|
||||
|
||||
# 3. Copy package
|
||||
pkg_name = os.path.basename(package_path)
|
||||
remote_path = f"/tmp/{pkg_name}"
|
||||
emit(f"Copie {pkg_name} ({os.path.getsize(package_path)//1024//1024} Mo)...")
|
||||
pkg_size = os.path.getsize(package_path) // 1024 // 1024
|
||||
stage("copying", f"Copie {pkg_name} ({pkg_size} Mo)")
|
||||
emit(f"Copie {pkg_name} ({pkg_size} Mo)...")
|
||||
|
||||
sftp = client.open_sftp()
|
||||
sftp.put(package_path, remote_path)
|
||||
sftp.put(package_path, remote_path=f"/tmp/{pkg_name}")
|
||||
sftp.close()
|
||||
emit("Copie terminee")
|
||||
|
||||
# 4. Install
|
||||
is_deb = pkg_name.endswith(".deb")
|
||||
remote_path = f"/tmp/{pkg_name}"
|
||||
|
||||
# 4. Install / Upgrade / Downgrade
|
||||
if is_deb:
|
||||
stage("installing", "Installation (dpkg)")
|
||||
emit("Installation (dpkg)...")
|
||||
code, out, err = _run_cmd(client, f"dpkg --install {remote_path}", sudo=True, timeout=120)
|
||||
else:
|
||||
emit("Installation (rpm)...")
|
||||
code, out, err = _run_cmd(client, f"rpm -ivh --nosignature {remote_path}", sudo=True, timeout=120)
|
||||
if force_downgrade and installed_version:
|
||||
stage("installing", f"Downgrade (rpm) {installed_version} → {pkg_version}")
|
||||
emit(f"Downgrade (rpm --oldpackage)...")
|
||||
code, out, err = _run_cmd(client, f"rpm -Uvh --nosignature --oldpackage {remote_path}", sudo=True, timeout=120)
|
||||
else:
|
||||
stage("installing", "Installation/Upgrade (rpm)")
|
||||
emit("Installation/Upgrade (rpm -Uvh)...")
|
||||
code, out, err = _run_cmd(client, f"rpm -Uvh --nosignature {remote_path}", sudo=True, timeout=120)
|
||||
|
||||
if code != 0 and "already installed" not in (out + err).lower():
|
||||
emit(f"ERREUR installation (code {code}): {err[:200]}")
|
||||
stage("failed", f"Installation echouee: {err[:100]}")
|
||||
result["status"] = "INSTALL_FAILED"
|
||||
result["detail"] = err[:200]
|
||||
client.close()
|
||||
@ -184,6 +257,7 @@ def deploy_agent(hostname, ssh_user, ssh_key_path, ssh_port, os_family,
|
||||
emit("Installation OK")
|
||||
|
||||
# 5. Activate
|
||||
stage("activating", "Activation de l'agent")
|
||||
emit("Activation de l'agent...")
|
||||
activate_cmd = (
|
||||
f"/usr/local/qualys/cloud-agent/bin/qualys-cloud-agent.sh "
|
||||
@ -195,6 +269,7 @@ def deploy_agent(hostname, ssh_user, ssh_key_path, ssh_port, os_family,
|
||||
code, out, err = _run_cmd(client, activate_cmd, sudo=True, timeout=60)
|
||||
if code != 0:
|
||||
emit(f"ERREUR activation (code {code}): {err[:200]}")
|
||||
stage("failed", f"Activation echouee: {err[:100]}")
|
||||
result["status"] = "ACTIVATE_FAILED"
|
||||
result["detail"] = err[:200]
|
||||
client.close()
|
||||
@ -202,17 +277,22 @@ def deploy_agent(hostname, ssh_user, ssh_key_path, ssh_port, os_family,
|
||||
emit("Activation OK")
|
||||
|
||||
# 6. Restart service
|
||||
stage("restarting", "Redemarrage du service")
|
||||
emit("Redemarrage du service...")
|
||||
_run_cmd(client, "systemctl restart qualys-cloud-agent", sudo=True)
|
||||
|
||||
# 7. Verify
|
||||
stage("verifying", "Verification du service")
|
||||
code, out, _ = _run_cmd(client, "systemctl is-active qualys-cloud-agent")
|
||||
action = "Upgrade" if installed_version else "Install"
|
||||
if out.strip() == "active":
|
||||
emit("Agent deploye et actif !")
|
||||
emit(f"Agent {action.lower()} OK et actif !")
|
||||
stage("success", f"{action} OK — v{pkg_version} active")
|
||||
result["status"] = "SUCCESS"
|
||||
result["detail"] = "Agent deploye avec succes"
|
||||
result["detail"] = f"{action} v{pkg_version} OK"
|
||||
else:
|
||||
emit(f"Agent installe mais statut: {out.strip()}")
|
||||
stage("partial", f"Service: {out.strip()}")
|
||||
result["status"] = "PARTIAL"
|
||||
result["detail"] = f"Installe, service: {out.strip()}"
|
||||
|
||||
@ -221,8 +301,108 @@ def deploy_agent(hostname, ssh_user, ssh_key_path, ssh_port, os_family,
|
||||
|
||||
except Exception as e:
|
||||
emit(f"ERREUR: {e}")
|
||||
stage("failed", str(e)[:100])
|
||||
result["status"] = "FAILED"
|
||||
result["detail"] = str(e)[:200]
|
||||
|
||||
client.close()
|
||||
return result
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# Background job manager
|
||||
# ═══════════════════════════════════════════════
|
||||
import threading
|
||||
import uuid
|
||||
import time
|
||||
|
||||
_deploy_jobs = {} # job_id -> job dict
|
||||
|
||||
|
||||
def start_deploy_job(servers, ssh_key, package_deb, package_rpm,
|
||||
activation_id, customer_id, server_uri, force_downgrade=False):
|
||||
"""Lance un job de deploiement en arriere-plan. Retourne le job_id."""
|
||||
job_id = str(uuid.uuid4())[:8]
|
||||
job = {
|
||||
"id": job_id,
|
||||
"started_at": time.time(),
|
||||
"total": len(servers),
|
||||
"done": 0,
|
||||
"servers": {},
|
||||
"log": [],
|
||||
"finished": False,
|
||||
}
|
||||
for srv in servers:
|
||||
job["servers"][srv["hostname"]] = {
|
||||
"hostname": srv["hostname"],
|
||||
"stage": "pending",
|
||||
"detail": "En attente",
|
||||
"status": None,
|
||||
"result": None,
|
||||
}
|
||||
_deploy_jobs[job_id] = job
|
||||
|
||||
def _run():
|
||||
threads = []
|
||||
for srv in servers:
|
||||
t = threading.Thread(target=_deploy_one, args=(job, srv, ssh_key,
|
||||
package_deb, package_rpm, activation_id, customer_id, server_uri, force_downgrade))
|
||||
t.daemon = True
|
||||
threads.append(t)
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
job["finished"] = True
|
||||
job["finished_at"] = time.time()
|
||||
|
||||
master = threading.Thread(target=_run, daemon=True)
|
||||
master.start()
|
||||
return job_id
|
||||
|
||||
|
||||
def _deploy_one(job, srv, ssh_key, package_deb, package_rpm,
|
||||
activation_id, customer_id, server_uri, force_downgrade=False):
|
||||
hostname = srv["hostname"]
|
||||
|
||||
def on_stage(stage, detail=""):
|
||||
job["servers"][hostname]["stage"] = stage
|
||||
job["servers"][hostname]["detail"] = detail
|
||||
|
||||
def on_line(msg):
|
||||
job["log"].append(f"[{hostname}] {msg}")
|
||||
|
||||
osv = (srv.get("os_version") or "").lower()
|
||||
if any(k in osv for k in ("centos", "red hat", "rhel", "rocky", "oracle", "fedora", "alma")):
|
||||
pkg = package_rpm
|
||||
else:
|
||||
pkg = package_deb
|
||||
|
||||
if not pkg or not os.path.exists(pkg):
|
||||
job["servers"][hostname]["stage"] = "failed"
|
||||
job["servers"][hostname]["detail"] = f"Package introuvable: {pkg}"
|
||||
job["servers"][hostname]["status"] = "FAILED"
|
||||
job["done"] += 1
|
||||
return
|
||||
|
||||
result = deploy_agent(
|
||||
hostname=hostname, ssh_user=srv.get("ssh_user") or "root",
|
||||
ssh_key_path=ssh_key, ssh_port=srv.get("ssh_port") or 22,
|
||||
os_family=srv.get("os_family"), package_path=pkg,
|
||||
activation_id=activation_id, customer_id=customer_id,
|
||||
server_uri=server_uri, on_line=on_line, on_stage=on_stage,
|
||||
force_downgrade=force_downgrade,
|
||||
)
|
||||
job["servers"][hostname]["status"] = result["status"]
|
||||
job["servers"][hostname]["result"] = result
|
||||
job["done"] += 1
|
||||
|
||||
|
||||
def get_deploy_job(job_id):
|
||||
"""Retourne l'etat d'un job de deploiement."""
|
||||
return _deploy_jobs.get(job_id)
|
||||
|
||||
|
||||
def list_deploy_jobs():
|
||||
"""Liste les jobs recents (< 1h)."""
|
||||
now = time.time()
|
||||
return {jid: j for jid, j in _deploy_jobs.items() if now - j["started_at"] < 3600}
|
||||
|
||||
@ -7,26 +7,71 @@
|
||||
<p class="text-xs text-gray-500 mt-1">Activation keys et versions des agents déployés</p>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px">
|
||||
<form method="POST" action="/qualys/agents/refresh" style="display:inline">
|
||||
<button type="submit" class="btn-primary px-4 py-2 text-sm"
|
||||
onclick="this.disabled=true;this.textContent='Rafraîchissement...'">
|
||||
<button id="btn-refresh" class="btn-primary px-4 py-2 text-sm" onclick="refreshAgents()">
|
||||
Rafraîchir depuis Qualys
|
||||
</button>
|
||||
</form>
|
||||
<a href="/qualys/deploy" class="btn-sm bg-cyber-border text-gray-300 px-4 py-2">Déployer</a>
|
||||
<a href="/qualys/search" class="btn-sm bg-cyber-border text-gray-300 px-4 py-2">Recherche</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if 'refresh_ok' in msg %}
|
||||
<div style="background:#1a5a2e;color:#8f8;padding:8px 16px;border-radius:6px;margin-bottom:12px;font-size:0.85rem">
|
||||
Données rafraîchies depuis Qualys.
|
||||
<!-- Overlay chargement -->
|
||||
<div id="refresh-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;justify-content:center;align-items:center">
|
||||
<div class="card p-6 text-center" style="min-width:320px">
|
||||
<div style="margin-bottom:12px">
|
||||
<svg style="display:inline;animation:spin 1s linear infinite;width:36px;height:36px" viewBox="0 0 24 24" fill="none" stroke="#00ffc8" stroke-width="2"><circle cx="12" cy="12" r="10" stroke-opacity="0.3"/><path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round"/></svg>
|
||||
</div>
|
||||
{% elif msg == 'refresh_error' %}
|
||||
<div style="background:#5a1a1a;color:#ff3366;padding:8px 16px;border-radius:6px;margin-bottom:12px;font-size:0.85rem">
|
||||
Erreur lors du rafraîchissement.
|
||||
<div class="text-cyber-accent font-bold text-sm" id="refresh-title">Rafraîchissement en cours...</div>
|
||||
<div class="text-gray-400 text-xs mt-2" id="refresh-detail">Synchronisation des agents depuis l'API Qualys</div>
|
||||
<div class="text-gray-500 text-xs mt-3" id="refresh-timer">0s</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<style>@keyframes spin{to{transform:rotate(360deg)}}</style>
|
||||
|
||||
<!-- Message résultat -->
|
||||
<div id="refresh-msg" style="display:none;padding:8px 16px;border-radius:6px;margin-bottom:12px;font-size:0.85rem"></div>
|
||||
|
||||
<script>
|
||||
function refreshAgents() {
|
||||
var btn = document.getElementById('btn-refresh');
|
||||
var overlay = document.getElementById('refresh-overlay');
|
||||
var timer = document.getElementById('refresh-timer');
|
||||
var msgDiv = document.getElementById('refresh-msg');
|
||||
btn.disabled = true;
|
||||
overlay.style.display = 'flex';
|
||||
msgDiv.style.display = 'none';
|
||||
var t0 = Date.now();
|
||||
var iv = setInterval(function(){ timer.textContent = Math.floor((Date.now()-t0)/1000) + 's'; }, 1000);
|
||||
fetch('/qualys/agents/refresh', {method:'POST', credentials:'same-origin'})
|
||||
.then(function(r){ return r.json().then(function(d){ return {ok:r.ok, data:d}; }); })
|
||||
.then(function(res){
|
||||
clearInterval(iv);
|
||||
overlay.style.display = 'none';
|
||||
btn.disabled = false;
|
||||
if(res.ok && res.data.ok){
|
||||
msgDiv.style.background = '#1a5a2e';
|
||||
msgDiv.style.color = '#8f8';
|
||||
msgDiv.textContent = 'Données rafraîchies : ' + res.data.msg;
|
||||
msgDiv.style.display = 'block';
|
||||
setTimeout(function(){ location.reload(); }, 1500);
|
||||
} else {
|
||||
msgDiv.style.background = '#5a1a1a';
|
||||
msgDiv.style.color = '#ff3366';
|
||||
msgDiv.textContent = 'Erreur : ' + (res.data.msg || 'Erreur inconnue');
|
||||
msgDiv.style.display = 'block';
|
||||
}
|
||||
})
|
||||
.catch(function(err){
|
||||
clearInterval(iv);
|
||||
overlay.style.display = 'none';
|
||||
btn.disabled = false;
|
||||
msgDiv.style.background = '#5a1a1a';
|
||||
msgDiv.style.color = '#ff3366';
|
||||
msgDiv.textContent = 'Erreur réseau : ' + err.message;
|
||||
msgDiv.style.display = 'block';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- KPIs agents -->
|
||||
<div style="display:flex;flex-wrap:nowrap;gap:8px;margin-bottom:16px;">
|
||||
@ -156,7 +201,7 @@
|
||||
<td class="p-2 text-center text-gray-400">{{ s.domain or '-' }}</td>
|
||||
<td class="p-2 text-center">{{ s.env or '-' }}</td>
|
||||
<td class="p-2 text-center">{% if s.zone == 'DMZ' %}<span class="badge badge-red">DMZ</span>{% else %}{{ s.zone or '-' }}{% endif %}</td>
|
||||
<td class="p-2 text-center" title="{{ s.etat or '' }}"><span class="badge {% if s.etat == 'en_production' %}badge-green{% elif s.etat == 'decommissionne' %}badge-red{% elif s.etat == 'eteint' %}badge-gray{% else %}badge-yellow{% endif %}">{{ (s.etat or '-')[:8] }}</span></td>
|
||||
<td class="p-2 text-center" title="{{ s.etat or '' }}"><span class="badge {% if s.etat == 'production' %}badge-green{% elif s.etat == 'obsolete' %}badge-red{% elif s.etat == 'stock' %}badge-gray{% else %}badge-yellow{% endif %}">{{ (s.etat or '-')[:8] }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@ -78,28 +78,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div style="display:flex;gap:8px">
|
||||
<form method="POST" action="/qualys/deploy/run" id="deployForm">
|
||||
<input type="hidden" name="server_ids" :value="selectedIds.join(',')">
|
||||
<input type="hidden" name="activation_id" :value="document.getElementById('activation_id').value">
|
||||
<input type="hidden" name="customer_id" :value="document.getElementById('customer_id').value">
|
||||
<input type="hidden" name="server_uri" :value="document.getElementById('server_uri').value">
|
||||
<input type="hidden" name="package_deb" :value="document.getElementById('package_deb').value">
|
||||
<input type="hidden" name="package_rpm" :value="document.getElementById('package_rpm').value">
|
||||
<button type="submit" class="btn-primary px-4 py-2 text-sm"
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<button id="btn-deploy" class="btn-primary px-4 py-2 text-sm"
|
||||
:disabled="selectedIds.length === 0"
|
||||
onclick="if(!confirm('Déployer l\'agent sur ' + selectedIds.length + ' serveur(s) ?')) return false; this.textContent='Déploiement en cours...'">
|
||||
@click="deployAgent(selectedIds)">
|
||||
Déployer l'agent
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action="/qualys/deploy/check">
|
||||
<input type="hidden" name="server_ids" :value="selectedIds.join(',')">
|
||||
<button type="submit" style="padding:8px 16px;font-size:0.85rem;background:#334155;color:#e2e8f0;border:1px solid #475569;border-radius:6px;cursor:pointer"
|
||||
<button id="btn-check" style="padding:8px 16px;font-size:0.85rem;background:#334155;color:#e2e8f0;border:1px solid #475569;border-radius:6px;cursor:pointer"
|
||||
:disabled="selectedIds.length === 0"
|
||||
onclick="this.textContent='Vérification...'">
|
||||
@click="checkAgent(selectedIds)">
|
||||
Vérifier l'agent
|
||||
</button>
|
||||
</form>
|
||||
<label class="text-xs text-gray-400 ml-4" style="display:flex;align-items:center;gap:4px">
|
||||
<input type="checkbox" id="force_downgrade"> Forcer le downgrade
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Serveurs -->
|
||||
@ -109,10 +101,11 @@
|
||||
<th class="p-2 w-8"><input type="checkbox" @change="selectAll = $event.target.checked; selectedIds = selectAll ? servers.map(s => s.id) : []"
|
||||
x-init="servers = {{ servers | tojson }}"></th>
|
||||
<th class="p-2 text-left">Hostname</th>
|
||||
<th class="p-2">OS</th>
|
||||
<th class="p-2">OS / Version</th>
|
||||
<th class="p-2">Domaine</th>
|
||||
<th class="p-2">Env</th>
|
||||
<th class="p-2">État</th>
|
||||
<th class="p-2">Agent installé</th>
|
||||
<th class="p-2">SSH</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@ -129,13 +122,20 @@
|
||||
<td class="p-2 text-center">
|
||||
{% if s.os_family == 'linux' %}<span class="badge badge-green">Linux</span>
|
||||
{% else %}<span class="badge badge-blue">{{ s.os_family or '?' }}</span>{% endif %}
|
||||
{% if s.os_version %}<div class="text-gray-400 mt-1" style="font-size:10px">{{ s.os_version }}</div>{% endif %}
|
||||
</td>
|
||||
<td class="p-2 text-center text-gray-400">{{ s.domain or '-' }}</td>
|
||||
<td class="p-2 text-center">{{ s.env or '-' }}</td>
|
||||
<td class="p-2 text-center">
|
||||
{% if s.etat == 'en_production' %}<span class="badge badge-green">Prod</span>
|
||||
{% if s.etat == 'production' %}<span class="badge badge-green">Prod</span>
|
||||
{% else %}{{ s.etat or '-' }}{% endif %}
|
||||
</td>
|
||||
<td class="p-2 text-center">
|
||||
{% if s.agent_version %}
|
||||
<span class="font-mono text-cyber-green">{{ s.agent_version }}</span>
|
||||
<span class="ml-1 badge {% if s.agent_status and 'ACTIVE' in (s.agent_status|upper) and 'INACTIVE' not in (s.agent_status|upper) %}badge-green{% elif s.agent_status and 'INACTIVE' in (s.agent_status|upper) %}badge-red{% else %}badge-gray{% endif %}" style="font-size:9px">{{ s.agent_status or '?' }}</span>
|
||||
{% else %}<span class="text-gray-600">—</span>{% endif %}
|
||||
</td>
|
||||
<td class="p-2 text-center text-gray-500">{{ s.ssh_user or 'root' }}:{{ s.ssh_port or 22 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
@ -143,4 +143,248 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Zone progression / résultats -->
|
||||
<div id="progress-zone" style="display:none" class="mt-4 space-y-4">
|
||||
<div class="card p-4">
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<h3 class="text-sm font-bold text-cyber-accent" id="progress-title">Déploiement en cours...</h3>
|
||||
<span class="text-xs text-gray-400 font-mono" id="progress-timer">0s</span>
|
||||
</div>
|
||||
<!-- Barre de progression globale -->
|
||||
<div style="background:#1e293b;border-radius:4px;height:8px;margin-bottom:16px;overflow:hidden">
|
||||
<div id="progress-bar" style="height:100%;background:#00ffc8;width:0%;transition:width 0.5s ease"></div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 mb-3" id="progress-summary"></div>
|
||||
<!-- Tableau progression par serveur -->
|
||||
<table class="w-full table-cyber text-xs">
|
||||
<thead><tr>
|
||||
<th class="p-2 text-left">Hostname</th>
|
||||
<th class="p-2">Étape</th>
|
||||
<th class="p-2 text-left">Détail</th>
|
||||
</tr></thead>
|
||||
<tbody id="progress-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!-- Log détaillé (affiché une fois terminé) -->
|
||||
<div id="progress-log" class="card p-4" style="display:none">
|
||||
<h3 class="text-sm font-bold text-cyber-accent mb-2">Log détaillé</h3>
|
||||
<div id="progress-log-content" style="background:#0a0a23;border-radius:6px;padding:12px;max-height:400px;overflow-y:auto;font-family:monospace;font-size:0.75rem;color:#00ff88;white-space:pre-wrap"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overlay vérification (check reste synchrone, c'est rapide) -->
|
||||
<div id="check-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.75);z-index:9999;justify-content:center;align-items:center">
|
||||
<div class="card p-6 text-center" style="min-width:360px">
|
||||
<div style="margin-bottom:12px">
|
||||
<svg style="display:inline;animation:opspin 1s linear infinite;width:40px;height:40px" viewBox="0 0 24 24" fill="none" stroke="#00ffc8" stroke-width="2"><circle cx="12" cy="12" r="10" stroke-opacity="0.3"/><path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round"/></svg>
|
||||
</div>
|
||||
<div class="text-cyber-accent font-bold text-sm" id="check-title">Vérification en cours...</div>
|
||||
<div class="text-gray-400 text-xs mt-2" id="check-detail"></div>
|
||||
<div class="text-gray-500 text-xs mt-3 font-mono" id="check-timer">0s</div>
|
||||
</div>
|
||||
</div>
|
||||
<style>@keyframes opspin{to{transform:rotate(360deg)}}</style>
|
||||
|
||||
<!-- Zone résultats check -->
|
||||
<div id="check-results" style="display:none" class="mt-4 space-y-4">
|
||||
<div id="check-kpis" style="display:flex;gap:8px;margin-bottom:16px"></div>
|
||||
<div id="check-table" class="card overflow-hidden"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var _pollTimer = null;
|
||||
var _checkTimer = null;
|
||||
|
||||
var STAGE_LABELS = {
|
||||
'pending': {label: 'En attente', cls: 'badge-gray', icon: '●'},
|
||||
'connecting': {label: 'Connexion SSH', cls: 'badge-yellow', icon: '↻'},
|
||||
'checking': {label: 'Vérification', cls: 'badge-yellow', icon: '↻'},
|
||||
'copying': {label: 'Copie package', cls: 'badge-yellow', icon: '↻'},
|
||||
'installing': {label: 'Installation', cls: 'badge-yellow', icon: '↻'},
|
||||
'activating': {label: 'Activation', cls: 'badge-yellow', icon: '↻'},
|
||||
'restarting': {label: 'Redémarrage', cls: 'badge-yellow', icon: '↻'},
|
||||
'verifying': {label: 'Vérification', cls: 'badge-yellow', icon: '↻'},
|
||||
'success': {label: 'Succès', cls: 'badge-green', icon: '✓'},
|
||||
'already_installed': {label: 'Déjà installé', cls: 'badge-blue', icon: '✓'},
|
||||
'downgrade_refused': {label: 'Downgrade refusé', cls: 'badge-yellow', icon: '⚠'},
|
||||
'partial': {label: 'Partiel', cls: 'badge-yellow', icon: '⚠'},
|
||||
'failed': {label: 'Échec', cls: 'badge-red', icon: '✗'},
|
||||
};
|
||||
|
||||
function _stageBadge(stage) {
|
||||
var s = STAGE_LABELS[stage] || {label: stage, cls: 'badge-gray', icon: '?'};
|
||||
var anim = (stage !== 'pending' && stage !== 'success' && stage !== 'failed' && stage !== 'partial' && stage !== 'already_installed')
|
||||
? ' style="animation:pulse 1.5s ease-in-out infinite"' : '';
|
||||
return '<span class="badge ' + s.cls + '"' + anim + '>' + s.icon + ' ' + s.label + '</span>';
|
||||
}
|
||||
|
||||
function deployAgent(ids) {
|
||||
if (!ids.length) return;
|
||||
if (!confirm('Déployer l\'agent sur ' + ids.length + ' serveur(s) ?\n\nLe déploiement se fait en arrière-plan.')) return;
|
||||
|
||||
document.getElementById('btn-deploy').disabled = true;
|
||||
|
||||
fetch('/qualys/deploy/run', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
server_ids: ids.join(','),
|
||||
activation_id: document.getElementById('activation_id').value,
|
||||
customer_id: document.getElementById('customer_id').value,
|
||||
server_uri: document.getElementById('server_uri').value,
|
||||
package_deb: document.getElementById('package_deb').value,
|
||||
package_rpm: document.getElementById('package_rpm').value,
|
||||
force_downgrade: document.getElementById('force_downgrade').checked,
|
||||
})
|
||||
})
|
||||
.then(function(r){
|
||||
var ct = r.headers.get('content-type') || '';
|
||||
if (ct.indexOf('json') === -1) throw new Error('Erreur serveur (HTTP ' + r.status + ')');
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data){
|
||||
if (data.ok && data.job_id) {
|
||||
startPolling(data.job_id, data.total);
|
||||
} else {
|
||||
alert('Erreur: ' + (data.msg || 'Erreur inconnue'));
|
||||
document.getElementById('btn-deploy').disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(function(err){
|
||||
alert('Erreur: ' + err.message);
|
||||
document.getElementById('btn-deploy').disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function startPolling(jobId, total) {
|
||||
var zone = document.getElementById('progress-zone');
|
||||
var title = document.getElementById('progress-title');
|
||||
var timer = document.getElementById('progress-timer');
|
||||
var bar = document.getElementById('progress-bar');
|
||||
var summary = document.getElementById('progress-summary');
|
||||
var tbody = document.getElementById('progress-body');
|
||||
|
||||
zone.style.display = 'block';
|
||||
zone.scrollIntoView({behavior: 'smooth'});
|
||||
title.textContent = 'Déploiement en cours... (' + total + ' serveur(s))';
|
||||
|
||||
function poll() {
|
||||
fetch('/qualys/deploy/status/' + jobId, {credentials: 'same-origin'})
|
||||
.then(function(r){ return r.json(); })
|
||||
.then(function(data){
|
||||
if (!data.ok) return;
|
||||
|
||||
timer.textContent = data.elapsed + 's';
|
||||
var pct = data.total > 0 ? Math.round((data.done / data.total) * 100) : 0;
|
||||
bar.style.width = pct + '%';
|
||||
summary.textContent = data.done + ' / ' + data.total + ' terminé(s)';
|
||||
|
||||
// Build rows
|
||||
var rows = '';
|
||||
var hostnames = Object.keys(data.servers).sort();
|
||||
hostnames.forEach(function(hn){
|
||||
var s = data.servers[hn];
|
||||
rows += '<tr class="border-t border-cyber-border/30">';
|
||||
rows += '<td class="p-2 font-mono">' + hn + '</td>';
|
||||
rows += '<td class="p-2 text-center">' + _stageBadge(s.stage) + '</td>';
|
||||
rows += '<td class="p-2 text-gray-400 text-xs">' + (s.detail || '') + '</td>';
|
||||
rows += '</tr>';
|
||||
});
|
||||
tbody.innerHTML = rows;
|
||||
|
||||
if (data.finished) {
|
||||
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
|
||||
var ok = 0, fail = 0;
|
||||
hostnames.forEach(function(hn){
|
||||
var st = data.servers[hn].stage;
|
||||
if (st === 'success' || st === 'already_installed') ok++;
|
||||
else if (st === 'failed') fail++;
|
||||
});
|
||||
title.innerHTML = 'Déploiement terminé — <span class="text-cyber-green">' + ok + ' OK</span> / <span class="text-cyber-red">' + fail + ' échec(s)</span>';
|
||||
bar.style.background = fail > 0 ? '#ff3366' : '#00ffc8';
|
||||
document.getElementById('btn-deploy').disabled = false;
|
||||
|
||||
// Show log
|
||||
if (data.log && data.log.length > 0) {
|
||||
document.getElementById('progress-log-content').textContent = data.log.join('\n');
|
||||
document.getElementById('progress-log').style.display = 'block';
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function(){});
|
||||
}
|
||||
|
||||
poll();
|
||||
_pollTimer = setInterval(poll, 2000);
|
||||
}
|
||||
|
||||
// === Check (reste synchrone, c'est rapide ~5-15s par serveur) ===
|
||||
|
||||
function checkAgent(ids) {
|
||||
if (!ids.length) return;
|
||||
var ov = document.getElementById('check-overlay');
|
||||
var timer = document.getElementById('check-timer');
|
||||
document.getElementById('check-title').textContent = 'Vérification de ' + ids.length + ' serveur(s)...';
|
||||
document.getElementById('check-detail').textContent = 'Connexion SSH et vérification du statut';
|
||||
timer.textContent = '0s';
|
||||
ov.style.display = 'flex';
|
||||
var t0 = Date.now();
|
||||
if (_checkTimer) clearInterval(_checkTimer);
|
||||
_checkTimer = setInterval(function(){ timer.textContent = Math.floor((Date.now()-t0)/1000) + 's'; }, 1000);
|
||||
|
||||
fetch('/qualys/deploy/check', {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({server_ids: ids.join(',')})
|
||||
})
|
||||
.then(function(r){
|
||||
var ct = r.headers.get('content-type') || '';
|
||||
if (ct.indexOf('json') === -1) throw new Error('Erreur serveur (HTTP ' + r.status + ')');
|
||||
return r.json();
|
||||
})
|
||||
.then(function(data){
|
||||
clearInterval(_checkTimer);
|
||||
ov.style.display = 'none';
|
||||
if (data.ok) showCheckResults(data);
|
||||
else alert('Erreur: ' + (data.msg || ''));
|
||||
})
|
||||
.catch(function(err){
|
||||
clearInterval(_checkTimer);
|
||||
ov.style.display = 'none';
|
||||
alert('Erreur: ' + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
function showCheckResults(data) {
|
||||
var zone = document.getElementById('check-results');
|
||||
var kpis = document.getElementById('check-kpis');
|
||||
var tbl = document.getElementById('check-table');
|
||||
|
||||
kpis.innerHTML =
|
||||
'<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-accent">' + data.total + '</div><div class="text-xs text-gray-500">Total</div></div>' +
|
||||
'<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-green">' + (data.active||0) + '</div><div class="text-xs text-gray-500">Actifs</div></div>' +
|
||||
'<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-yellow">' + (data.not_installed||0) + '</div><div class="text-xs text-gray-500">Non installés</div></div>' +
|
||||
'<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-red">' + (data.failed||0) + '</div><div class="text-xs text-gray-500">Connexion échouée</div></div>';
|
||||
|
||||
var rows = '';
|
||||
data.results.forEach(function(r){
|
||||
var cls = r.status==='ACTIVE'?'badge-green': r.status==='NOT_INSTALLED'?'badge-yellow': r.status==='INACTIVE'?'badge-yellow': 'badge-red';
|
||||
rows += '<tr class="border-t border-cyber-border/30">';
|
||||
rows += '<td class="p-2 font-mono">' + r.hostname + '</td>';
|
||||
rows += '<td class="p-2 text-center"><span class="badge ' + cls + '">' + r.status + '</span></td>';
|
||||
rows += '<td class="p-2 text-gray-400">' + (r.detail||'') + '</td>';
|
||||
rows += '<td class="p-2 text-center font-mono text-gray-400">' + (r.version||'-') + '</td>';
|
||||
rows += '<td class="p-2 text-center">' + (r.service_status||'-') + '</td>';
|
||||
rows += '</tr>';
|
||||
});
|
||||
tbl.innerHTML = '<table class="w-full table-cyber text-xs"><thead><tr><th class="p-2 text-left">Hostname</th><th class="p-2">Statut</th><th class="p-2 text-left">Détail</th><th class="p-2">Version</th><th class="p-2">Service</th></tr></thead><tbody>' + rows + '</tbody></table>';
|
||||
|
||||
zone.style.display = 'block';
|
||||
zone.scrollIntoView({behavior:'smooth'});
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.5} }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user