Deploiement Agent Qualys complet
- Page /qualys/deploy: selection serveurs, config agent, choix package - Deploiement SSH: copie package, install rpm/dpkg, activation, verification - Verification agent: check status/version sur serveurs selectionnes - Auto-detect OS (deb vs rpm) - Packages stockes dans /opt/patchcenter/agents/ - Filtres: hostname, domaine, environnement, OS - Log detaille du deploiement - Menu "Deployer Agent" dans la navigation Qualys
This commit is contained in:
parent
b7b0965722
commit
3d053019e6
@ -3,7 +3,7 @@ from fastapi import APIRouter, Request, Depends, Query, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, StreamingResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlalchemy import text
|
||||
import csv, io, re
|
||||
import csv, io, re, os
|
||||
from ..dependencies import get_db, get_current_user, get_user_perms, can_view, can_edit, can_admin, base_context
|
||||
from ..services.qualys_service import (
|
||||
sync_server_qualys, search_assets_api, get_all_tags_api,
|
||||
@ -833,3 +833,167 @@ async def qualys_decoder(request: Request, db=Depends(get_db),
|
||||
"current_tags": current_tags, "auto_prefixes": AUTO_PREFIXES,
|
||||
})
|
||||
return templates.TemplateResponse("qualys_decoder.html", ctx)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# DEPLOIEMENT AGENT QUALYS
|
||||
# ═══════════════════════════════════════════════
|
||||
|
||||
@router.get("/qualys/deploy", response_class=HTMLResponse)
|
||||
async def qualys_deploy_page(request: Request, db=Depends(get_db)):
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login")
|
||||
perms = get_user_perms(db, user)
|
||||
if not can_edit(perms, "qualys"):
|
||||
return RedirectResponse(url="/dashboard")
|
||||
|
||||
from ..services.agent_deploy_service import list_packages
|
||||
from ..services.secrets_service import get_secret
|
||||
|
||||
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
|
||||
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
|
||||
ORDER BY s.hostname
|
||||
""")).fetchall()
|
||||
servers = [dict(r._mapping) for r in servers]
|
||||
|
||||
ctx = base_context(request, db, user)
|
||||
ctx.update({
|
||||
"app_name": APP_NAME,
|
||||
"packages": packages,
|
||||
"servers": servers,
|
||||
"activation_id": get_secret(db, "qualys_activation_id") or "081a9a12-ca97-4de1-828c-c6ad918ce77e",
|
||||
"customer_id": get_secret(db, "qualys_customer_id") or "a2e3271b-c2a1-ec6b-8324-8f51948783d4",
|
||||
"server_uri": get_secret(db, "qualys_server_uri") or "https://qagpublic.qg2.apps.qualys.eu/CloudAgent/",
|
||||
"msg": request.query_params.get("msg", ""),
|
||||
})
|
||||
return templates.TemplateResponse("qualys_deploy.html", ctx)
|
||||
|
||||
|
||||
@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("")):
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login")
|
||||
perms = get_user_perms(db, user)
|
||||
if not can_edit(perms, "qualys"):
|
||||
return RedirectResponse(url="/dashboard")
|
||||
|
||||
from ..services.agent_deploy_service import deploy_agent
|
||||
from ..services.secrets_service import get_secret
|
||||
|
||||
ids = [int(x) for x in server_ids.split(",") if x.strip().isdigit()]
|
||||
if not ids:
|
||||
return RedirectResponse(url="/qualys/deploy?msg=no_servers", status_code=303)
|
||||
|
||||
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
|
||||
FROM servers WHERE id IN ({placeholders})
|
||||
""")).fetchall()
|
||||
|
||||
results = []
|
||||
log_lines = []
|
||||
|
||||
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")
|
||||
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 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("")):
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login")
|
||||
perms = get_user_perms(db, user)
|
||||
if not can_view(perms, "qualys"):
|
||||
return RedirectResponse(url="/dashboard")
|
||||
|
||||
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()]
|
||||
if not ids:
|
||||
return RedirectResponse(url="/qualys/deploy?msg=no_servers", status_code=303)
|
||||
|
||||
ssh_key = get_secret(db, "ssh_key_file") or "/opt/patchcenter/keys/id_ed25519"
|
||||
placeholders = ",".join(str(i) for i in ids)
|
||||
servers = db.execute(text(f"SELECT id, hostname, ssh_user, ssh_port FROM servers WHERE id IN ({placeholders})")).fetchall()
|
||||
|
||||
results = []
|
||||
for srv in servers:
|
||||
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),
|
||||
"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)
|
||||
|
||||
217
app/services/agent_deploy_service.py
Normal file
217
app/services/agent_deploy_service.py
Normal file
@ -0,0 +1,217 @@
|
||||
"""Service de deploiement Qualys Cloud Agent via SSH"""
|
||||
import os
|
||||
import logging
|
||||
import glob
|
||||
from datetime import datetime
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
AGENTS_DIR = "/opt/patchcenter/agents"
|
||||
|
||||
try:
|
||||
import paramiko
|
||||
PARAMIKO_OK = True
|
||||
except ImportError:
|
||||
PARAMIKO_OK = False
|
||||
|
||||
|
||||
def list_packages():
|
||||
"""Liste les packages disponibles dans /opt/patchcenter/agents/"""
|
||||
packages = {"deb": [], "rpm": []}
|
||||
if not os.path.isdir(AGENTS_DIR):
|
||||
return packages
|
||||
for f in sorted(os.listdir(AGENTS_DIR)):
|
||||
path = os.path.join(AGENTS_DIR, f)
|
||||
size_mb = round(os.path.getsize(path) / 1024 / 1024, 1)
|
||||
if f.endswith(".deb"):
|
||||
packages["deb"].append({"name": f, "path": path, "size": size_mb})
|
||||
elif f.endswith(".rpm"):
|
||||
packages["rpm"].append({"name": f, "path": path, "size": size_mb})
|
||||
return packages
|
||||
|
||||
|
||||
def _get_ssh_client(hostname, ssh_user, ssh_key_path, ssh_port=22):
|
||||
"""Crée un client SSH paramiko"""
|
||||
if not PARAMIKO_OK:
|
||||
return None, "paramiko non disponible"
|
||||
client = paramiko.SSHClient()
|
||||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
for cls in [paramiko.Ed25519Key, paramiko.RSAKey, paramiko.ECDSAKey]:
|
||||
try:
|
||||
key = cls.from_private_key_file(ssh_key_path)
|
||||
client.connect(hostname, port=ssh_port, username=ssh_user, pkey=key,
|
||||
timeout=15, look_for_keys=False, allow_agent=False)
|
||||
return client, None
|
||||
except Exception:
|
||||
continue
|
||||
return None, f"Impossible de charger la cle {ssh_key_path}"
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
|
||||
def _run_cmd(client, cmd, sudo=False, timeout=120):
|
||||
"""Execute une commande SSH"""
|
||||
if sudo:
|
||||
_, stdout_id, _ = client.exec_command("id -u", timeout=5)
|
||||
uid = stdout_id.read().decode().strip()
|
||||
if uid != "0":
|
||||
cmd = f"sudo {cmd}"
|
||||
_, stdout, stderr = client.exec_command(cmd, timeout=timeout)
|
||||
exit_code = stdout.channel.recv_exit_status()
|
||||
out = stdout.read().decode("utf-8", errors="replace")
|
||||
err = stderr.read().decode("utf-8", errors="replace")
|
||||
return exit_code, out, err
|
||||
|
||||
|
||||
def check_agent(hostname, ssh_user, ssh_key_path, ssh_port=22):
|
||||
"""Vérifie le statut de l'agent Qualys sur un serveur"""
|
||||
client, error = _get_ssh_client(hostname, ssh_user, ssh_key_path, ssh_port)
|
||||
if not client:
|
||||
return {"hostname": hostname, "status": "CONNECTION_FAILED", "detail": error}
|
||||
|
||||
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():
|
||||
result["status"] = "NOT_INSTALLED"
|
||||
result["detail"] = "Agent non installe"
|
||||
client.close()
|
||||
return result
|
||||
|
||||
# Check if running
|
||||
code, out, _ = _run_cmd(client, "systemctl is-active qualys-cloud-agent 2>/dev/null")
|
||||
status = out.strip()
|
||||
result["service_status"] = status
|
||||
|
||||
# Get version
|
||||
code, out, _ = _run_cmd(client, "qualys-cloud-agent --version 2>/dev/null || cat /etc/qualys/cloud-agent/qualys-cloud-agent.conf 2>/dev/null | grep -i version | head -1")
|
||||
result["version"] = out.strip()[:50]
|
||||
|
||||
# Get last checkin from log
|
||||
code, out, _ = _run_cmd(client, "tail -5 /var/log/qualys/qualys-cloud-agent.log 2>/dev/null | grep 'HTTP response code: 200' | tail -1 | awk '{print $1, $2}'")
|
||||
result["last_checkin"] = out.strip()[:25]
|
||||
|
||||
if status == "active":
|
||||
result["status"] = "ACTIVE"
|
||||
result["detail"] = "Agent actif"
|
||||
elif status == "inactive" or status == "dead":
|
||||
result["status"] = "INACTIVE"
|
||||
result["detail"] = "Agent installe mais inactif"
|
||||
else:
|
||||
result["status"] = "UNKNOWN"
|
||||
result["detail"] = f"Statut: {status}"
|
||||
|
||||
client.close()
|
||||
return result
|
||||
|
||||
|
||||
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"""
|
||||
|
||||
def emit(msg):
|
||||
if on_line:
|
||||
on_line(msg)
|
||||
log.info(f"[{hostname}] {msg}")
|
||||
|
||||
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}")
|
||||
return {"hostname": hostname, "status": "FAILED", "detail": error}
|
||||
|
||||
result = {"hostname": hostname, "status": "PENDING"}
|
||||
|
||||
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
|
||||
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
|
||||
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")
|
||||
result["status"] = "ALREADY_INSTALLED"
|
||||
result["detail"] = "Agent deja actif"
|
||||
client.close()
|
||||
return result
|
||||
|
||||
# 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)...")
|
||||
|
||||
sftp = client.open_sftp()
|
||||
sftp.put(package_path, remote_path)
|
||||
sftp.close()
|
||||
emit("Copie terminee")
|
||||
|
||||
# 4. Install
|
||||
is_deb = pkg_name.endswith(".deb")
|
||||
if is_deb:
|
||||
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 code != 0 and "already installed" not in (out + err).lower():
|
||||
emit(f"ERREUR installation (code {code}): {err[:200]}")
|
||||
result["status"] = "INSTALL_FAILED"
|
||||
result["detail"] = err[:200]
|
||||
client.close()
|
||||
return result
|
||||
emit("Installation OK")
|
||||
|
||||
# 5. Activate
|
||||
emit("Activation de l'agent...")
|
||||
activate_cmd = (
|
||||
f"/usr/local/qualys/cloud-agent/bin/qualys-cloud-agent.sh "
|
||||
f"ActivationId={activation_id} "
|
||||
f"CustomerId={customer_id} "
|
||||
f"ServerUri={server_uri} "
|
||||
f"ProviderName=NONE"
|
||||
)
|
||||
code, out, err = _run_cmd(client, activate_cmd, sudo=True, timeout=60)
|
||||
if code != 0:
|
||||
emit(f"ERREUR activation (code {code}): {err[:200]}")
|
||||
result["status"] = "ACTIVATE_FAILED"
|
||||
result["detail"] = err[:200]
|
||||
client.close()
|
||||
return result
|
||||
emit("Activation OK")
|
||||
|
||||
# 6. Restart service
|
||||
emit("Redemarrage du service...")
|
||||
_run_cmd(client, "systemctl restart qualys-cloud-agent", sudo=True)
|
||||
|
||||
# 7. Verify
|
||||
code, out, _ = _run_cmd(client, "systemctl is-active qualys-cloud-agent")
|
||||
if out.strip() == "active":
|
||||
emit("Agent deploye et actif !")
|
||||
result["status"] = "SUCCESS"
|
||||
result["detail"] = "Agent deploye avec succes"
|
||||
else:
|
||||
emit(f"Agent installe mais statut: {out.strip()}")
|
||||
result["status"] = "PARTIAL"
|
||||
result["detail"] = f"Installe, service: {out.strip()}"
|
||||
|
||||
# 8. Cleanup
|
||||
_run_cmd(client, f"rm -f {remote_path}")
|
||||
|
||||
except Exception as e:
|
||||
emit(f"ERREUR: {e}")
|
||||
result["status"] = "FAILED"
|
||||
result["detail"] = str(e)[:200]
|
||||
|
||||
client.close()
|
||||
return result
|
||||
@ -59,7 +59,8 @@
|
||||
{% if p.qualys %}<a href="/qualys/search" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if '/qualys/' in request.url.path and 'tags' not in request.url.path and 'decoder' not in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Qualys</a>{% endif %}
|
||||
{% if p.qualys %}<a href="/qualys/tags" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if '/qualys/tags' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Tags</a>{% endif %}
|
||||
{% if p.qualys %}<a href="/qualys/decoder" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'decoder' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Décodeur</a>{% endif %}
|
||||
{% if p.qualys %}<a href="/qualys/agents" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'agents' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Agents</a>{% endif %}
|
||||
{% if p.qualys %}<a href="/qualys/agents" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'agents' in request.url.path and 'deploy' not in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Agents</a>{% endif %}
|
||||
{% if p.qualys in ('edit', 'admin') %}<a href="/qualys/deploy" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'deploy' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Déployer Agent</a>{% endif %}
|
||||
{% if p.campaigns %}<a href="/safe-patching" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'safe-patching' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">Safe Patching</a>{% endif %}
|
||||
{% if p.campaigns or p.quickwin %}<a href="/quickwin" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if 'quickwin' in request.url.path and 'safe' not in request.url.path and 'config' not in request.url.path and 'correspondance' not in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %}">QuickWin</a>{% endif %}
|
||||
{% if p.campaigns or p.quickwin %}<a href="/quickwin/config" class="block px-3 py-2 rounded-md text-sm hover:bg-cyber-border/30 {% if '/quickwin/config' in request.url.path %}bg-cyber-border/30 text-cyber-accent{% else %}text-gray-400{% endif %} pl-6 text-xs">Config exclusions</a>{% endif %}
|
||||
|
||||
146
app/templates/qualys_deploy.html
Normal file
146
app/templates/qualys_deploy.html
Normal file
@ -0,0 +1,146 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Déploiement Agent Qualys{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-cyber-accent">Déploiement Agent Qualys</h2>
|
||||
<p class="text-xs text-gray-500 mt-1">Installer et vérifier l'agent Qualys Cloud sur les serveurs</p>
|
||||
</div>
|
||||
<a href="/qualys/agents" class="btn-sm bg-cyber-border text-cyber-accent px-4 py-2">Agents</a>
|
||||
</div>
|
||||
|
||||
{% if msg == 'no_servers' %}
|
||||
<div style="background:#5a1a1a;color:#ff3366;padding:8px 16px;border-radius:6px;margin-bottom:12px;font-size:0.85rem">
|
||||
Sélectionner au moins un serveur.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div x-data="{selectedIds: [], selectAll: false, filter: '', filterDom: '', filterEnv: '', filterOs: ''}" class="space-y-4">
|
||||
|
||||
<!-- Configuration -->
|
||||
<div class="card p-4">
|
||||
<h3 class="text-sm font-bold text-cyber-accent mb-3">Configuration de l'agent</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 block mb-1">ActivationId</label>
|
||||
<input type="text" id="activation_id" value="{{ activation_id }}" class="w-full text-xs" style="font-family:monospace">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 block mb-1">CustomerId</label>
|
||||
<input type="text" id="customer_id" value="{{ customer_id }}" class="w-full text-xs" style="font-family:monospace">
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 block mb-1">ServerUri</label>
|
||||
<input type="text" id="server_uri" value="{{ server_uri }}" class="w-full text-xs" style="font-family:monospace">
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 block mb-1">Package DEB (Debian/Ubuntu)</label>
|
||||
<select id="package_deb" class="w-full text-xs">
|
||||
{% for p in packages.deb %}
|
||||
<option value="{{ p.path }}">{{ p.name }} ({{ p.size }} Mo)</option>
|
||||
{% endfor %}
|
||||
{% if not packages.deb %}<option value="">Aucun package .deb</option>{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-500 block mb-1">Package RPM (RHEL/CentOS)</label>
|
||||
<select id="package_rpm" class="w-full text-xs">
|
||||
{% for p in packages.rpm %}
|
||||
<option value="{{ p.path }}">{{ p.name }} ({{ p.size }} Mo)</option>
|
||||
{% endfor %}
|
||||
{% if not packages.rpm %}<option value="">Aucun package .rpm</option>{% endif %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filtres -->
|
||||
<div class="card p-3" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
||||
<input type="text" x-model="filter" placeholder="Rechercher hostname..." class="text-xs" style="width:200px">
|
||||
<select x-model="filterDom" class="text-xs" style="width:150px">
|
||||
<option value="">Tous domaines</option>
|
||||
{% set doms = servers|map(attribute='domain')|unique|sort %}
|
||||
{% for d in doms %}{% if d %}<option>{{ d }}</option>{% endif %}{% endfor %}
|
||||
</select>
|
||||
<select x-model="filterEnv" class="text-xs" style="width:150px">
|
||||
<option value="">Tous envs</option>
|
||||
{% set envs = servers|map(attribute='env')|unique|sort %}
|
||||
{% for e in envs %}{% if e %}<option>{{ e }}</option>{% endif %}{% endfor %}
|
||||
</select>
|
||||
<select x-model="filterOs" class="text-xs" style="width:120px">
|
||||
<option value="">Tous OS</option>
|
||||
<option value="linux">Linux</option>
|
||||
<option value="windows">Windows</option>
|
||||
</select>
|
||||
<span class="text-xs text-gray-500" x-text="selectedIds.length + ' sélectionné(s)'"></span>
|
||||
</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"
|
||||
:disabled="selectedIds.length === 0"
|
||||
onclick="if(!confirm('Déployer l\'agent sur ' + selectedIds.length + ' serveur(s) ?')) return false; this.textContent='Déploiement en cours...'">
|
||||
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"
|
||||
:disabled="selectedIds.length === 0"
|
||||
onclick="this.textContent='Vérification...'">
|
||||
Vérifier l'agent
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Serveurs -->
|
||||
<div class="card overflow-hidden">
|
||||
<table class="w-full table-cyber text-xs">
|
||||
<thead><tr>
|
||||
<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">Domaine</th>
|
||||
<th class="p-2">Env</th>
|
||||
<th class="p-2">État</th>
|
||||
<th class="p-2">SSH</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for s in servers %}
|
||||
<tr x-show="
|
||||
(filter === '' || '{{ s.hostname }}'.toLowerCase().includes(filter.toLowerCase()))
|
||||
&& (filterDom === '' || '{{ s.domain or '' }}' === filterDom)
|
||||
&& (filterEnv === '' || '{{ s.env or '' }}' === filterEnv)
|
||||
&& (filterOs === '' || '{{ s.os_family or '' }}' === filterOs)
|
||||
" class="border-t border-cyber-border/30 hover:bg-cyber-hover">
|
||||
<td class="p-2 text-center"><input type="checkbox" :value="{{ s.id }}"
|
||||
@change="$event.target.checked ? selectedIds.push({{ s.id }}) : selectedIds = selectedIds.filter(x => x !== {{ s.id }})"></td>
|
||||
<td class="p-2 font-mono">{{ s.hostname }}</td>
|
||||
<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 %}
|
||||
</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>
|
||||
{% else %}{{ s.etat or '-' }}{% endif %}
|
||||
</td>
|
||||
<td class="p-2 text-center text-gray-500">{{ s.ssh_user or 'root' }}:{{ s.ssh_port or 22 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
72
app/templates/qualys_deploy_results.html
Normal file
72
app/templates/qualys_deploy_results.html
Normal file
@ -0,0 +1,72 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block title %}Résultats Déploiement{% endblock %}
|
||||
{% block content %}
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<h2 class="text-xl font-bold text-cyber-accent">Résultats du déploiement</h2>
|
||||
<p class="text-xs text-gray-500 mt-1">{{ total }} serveur(s) traité(s)</p>
|
||||
</div>
|
||||
<a href="/qualys/deploy" class="btn-sm bg-cyber-border text-cyber-accent px-4 py-2">Retour</a>
|
||||
</div>
|
||||
|
||||
<!-- KPIs -->
|
||||
<div style="display:flex;gap:8px;margin-bottom:16px">
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-accent">{{ total }}</div><div class="text-xs text-gray-500">Total</div></div>
|
||||
{% if active is defined %}
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-green">{{ active }}</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-red">{{ not_installed }}</div><div class="text-xs text-gray-500">Non installés</div></div>
|
||||
{% else %}
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-green">{{ ok }}</div><div class="text-xs text-gray-500">Succès</div></div>
|
||||
<div class="card p-3 text-center" style="flex:1"><div class="text-2xl font-bold text-cyber-red">{{ failed }}</div><div class="text-xs text-gray-500">Échecs</div></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Résultats -->
|
||||
<div class="card overflow-hidden mb-4">
|
||||
<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>
|
||||
{% if results and results[0].version is defined %}
|
||||
<th class="p-2">Version</th>
|
||||
<th class="p-2">Service</th>
|
||||
{% endif %}
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for r in results %}
|
||||
<tr class="border-t border-cyber-border/30">
|
||||
<td class="p-2 font-mono">{{ r.hostname }}</td>
|
||||
<td class="p-2 text-center">
|
||||
{% if r.status == 'SUCCESS' or r.status == 'ACTIVE' %}
|
||||
<span class="badge badge-green">{{ r.status }}</span>
|
||||
{% elif r.status == 'ALREADY_INSTALLED' %}
|
||||
<span class="badge badge-blue">DÉJÀ INSTALLÉ</span>
|
||||
{% elif r.status == 'NOT_INSTALLED' %}
|
||||
<span class="badge badge-yellow">NON INSTALLÉ</span>
|
||||
{% elif r.status == 'INACTIVE' %}
|
||||
<span class="badge badge-yellow">INACTIF</span>
|
||||
{% else %}
|
||||
<span class="badge badge-red">{{ r.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="p-2 text-gray-400">{{ r.detail or r.get('detail', '') }}</td>
|
||||
{% if r.version is defined %}
|
||||
<td class="p-2 text-center text-gray-400">{{ r.version or '-' }}</td>
|
||||
<td class="p-2 text-center">{{ r.service_status or '-' }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Log détaillé -->
|
||||
{% if log_lines %}
|
||||
<div class="card p-4">
|
||||
<h3 class="text-sm font-bold text-cyber-accent mb-2">Log détaillé</h3>
|
||||
<div 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">{% for line in log_lines %}{{ line }}
|
||||
{% endfor %}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
Loading…
Reference in New Issue
Block a user