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:
Pierre & Lumière 2026-04-11 21:26:45 +02:00
parent b7b0965722
commit 3d053019e6
5 changed files with 602 additions and 2 deletions

View File

@ -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)

View 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

View File

@ -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 %}

View 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 %}

View 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 %}