Misc: servers page (application + equivalent), campagne tweaks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a706e240ca
commit
caa2be71a4
@ -27,7 +27,7 @@ router = APIRouter()
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
EXCLUSION_REASONS = [
|
||||
("eol", "Fin de vie (EOL)"),
|
||||
("obsolete", "Fin de vie (EOL)"),
|
||||
("creneau_inadequat", "Creneau non adequat"),
|
||||
("intervention_non_secops", "Intervention non-SecOps prevue"),
|
||||
("report_cycle", "Report au cycle suivant"),
|
||||
|
||||
@ -107,6 +107,7 @@ async def contacts_page(request: Request, db=Depends(get_db),
|
||||
"SELECT DISTINCT app_type FROM server_specifics WHERE app_type IS NOT NULL ORDER BY app_type"
|
||||
)).fetchall()
|
||||
|
||||
perms = get_user_perms(db, user)
|
||||
ctx = base_context(request, db, user)
|
||||
ctx.update({
|
||||
"app_name": APP_NAME, "contacts": contacts,
|
||||
@ -115,6 +116,7 @@ async def contacts_page(request: Request, db=Depends(get_db),
|
||||
"domains": domains, "app_types": [r.app_type for r in app_types],
|
||||
"search": search, "role_filter": role, "server": server, "server_info": server_info,
|
||||
"msg": request.query_params.get("msg"),
|
||||
"can_edit_contacts": can_edit(perms, "servers") or can_edit(perms, "contacts"),
|
||||
})
|
||||
return templates.TemplateResponse("contacts.html", ctx)
|
||||
|
||||
|
||||
@ -18,21 +18,21 @@ async def dashboard(request: Request, db=Depends(get_db)):
|
||||
# Stats generales
|
||||
stats = {}
|
||||
stats["total_servers"] = db.execute(text("SELECT COUNT(*) FROM servers")).scalar()
|
||||
stats["patchable"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE patch_os_owner='secops' AND etat='en_production'")).scalar()
|
||||
stats["patchable"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE patch_os_owner='secops' AND etat='production'")).scalar()
|
||||
stats["linux"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE os_family='linux'")).scalar()
|
||||
stats["windows"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE os_family='windows'")).scalar()
|
||||
stats["decom"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE etat='decommissionne'")).scalar()
|
||||
stats["eol"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE licence_support='eol'")).scalar()
|
||||
stats["decom"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE etat='obsolete'")).scalar()
|
||||
stats["obsolete"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE licence_support='obsolete'")).scalar()
|
||||
stats["qualys_assets"] = db.execute(text("SELECT COUNT(*) FROM qualys_assets")).scalar()
|
||||
stats["qualys_tags"] = db.execute(text("SELECT COUNT(*) FROM qualys_tags")).scalar()
|
||||
stats["qualys_active"] = db.execute(text("SELECT COUNT(*) FROM qualys_assets WHERE agent_status ILIKE '%active%' AND agent_status NOT ILIKE '%inactive%'")).scalar()
|
||||
stats["qualys_inactive"] = db.execute(text("SELECT COUNT(*) FROM qualys_assets WHERE agent_status ILIKE '%inactive%'")).scalar()
|
||||
stats["qualys_no_agent"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE etat='en_production' AND NOT EXISTS (SELECT 1 FROM qualys_assets qa WHERE LOWER(qa.hostname) = LOWER(servers.hostname))")).scalar()
|
||||
stats["qualys_no_agent"] = db.execute(text("SELECT COUNT(*) FROM servers WHERE etat='production' AND NOT EXISTS (SELECT 1 FROM qualys_assets qa WHERE LOWER(qa.hostname) = LOWER(servers.hostname))")).scalar()
|
||||
|
||||
# Par domaine
|
||||
domains = db.execute(text("""
|
||||
SELECT d.name, d.code, COUNT(s.id) as total,
|
||||
COUNT(*) FILTER (WHERE s.etat='en_production') as actifs,
|
||||
COUNT(*) FILTER (WHERE s.etat='production') as actifs,
|
||||
COUNT(*) FILTER (WHERE s.os_family='linux') as linux,
|
||||
COUNT(*) FILTER (WHERE s.os_family='windows') as windows
|
||||
FROM servers s
|
||||
|
||||
@ -40,7 +40,7 @@ def _get_planning_data(db, year):
|
||||
SELECT d.code, d.name, COUNT(s.id) as srv_count
|
||||
FROM domains d
|
||||
LEFT JOIN domain_environments de ON de.domain_id = d.id
|
||||
LEFT JOIN servers s ON s.domain_env_id = de.id AND s.etat = 'en_production'
|
||||
LEFT JOIN servers s ON s.domain_env_id = de.id AND s.etat = 'production'
|
||||
GROUP BY d.code, d.name, d.display_order
|
||||
ORDER BY d.display_order
|
||||
""")).fetchall()
|
||||
|
||||
@ -2,6 +2,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
|
||||
from ..dependencies import get_db, get_current_user
|
||||
from ..services.server_service import (
|
||||
get_server_full, get_server_tags, get_server_ips,
|
||||
@ -19,21 +20,38 @@ async def servers_list(request: Request, db=Depends(get_db),
|
||||
domain: str = Query(None), env: str = Query(None),
|
||||
tier: str = Query(None), etat: str = Query(None),
|
||||
os: str = Query(None), owner: str = Query(None),
|
||||
application: str = Query(None),
|
||||
search: str = Query(None), page: int = Query(1),
|
||||
sort: str = Query("hostname"), sort_dir: str = Query("asc")):
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login")
|
||||
|
||||
filters = {"domain": domain, "env": env, "tier": tier, "etat": etat, "os": os, "owner": owner, "search": search}
|
||||
filters = {"domain": domain, "env": env, "tier": tier, "etat": etat, "os": os,
|
||||
"owner": owner, "application": application, "search": search}
|
||||
servers, total = list_servers(db, filters, page, sort=sort, sort_dir=sort_dir)
|
||||
domains_list, envs_list = get_reference_data(db)
|
||||
|
||||
applications_list = db.execute(text("""SELECT application_name, COUNT(*) as c FROM servers
|
||||
WHERE application_name IS NOT NULL AND application_name != ''
|
||||
GROUP BY application_name ORDER BY application_name""")).fetchall()
|
||||
|
||||
from ..dependencies import get_user_perms, can_edit
|
||||
perms = get_user_perms(db, user)
|
||||
can_edit_servers = can_edit(perms, "servers")
|
||||
|
||||
# Correspondances en bulk pour la page en cours
|
||||
from ..services.correspondance_service import get_links_bulk
|
||||
links = get_links_bulk(db, [s.id for s in servers])
|
||||
|
||||
return templates.TemplateResponse("servers.html", {
|
||||
"request": request, "user": user, "app_name": APP_NAME,
|
||||
"servers": servers, "total": total, "page": page, "per_page": 50,
|
||||
"domains_list": domains_list, "envs_list": envs_list, "filters": filters,
|
||||
"domains_list": domains_list, "envs_list": envs_list,
|
||||
"applications_list": applications_list, "filters": filters,
|
||||
"sort": sort, "sort_dir": sort_dir,
|
||||
"perms": perms, "can_edit_servers": can_edit_servers,
|
||||
"links": links,
|
||||
})
|
||||
|
||||
|
||||
@ -78,8 +96,10 @@ async def server_detail(request: Request, server_id: int, db=Depends(get_db)):
|
||||
return HTMLResponse("<p>Serveur non trouve</p>")
|
||||
tags = get_server_tags(db, s.qid)
|
||||
ips = get_server_ips(db, server_id)
|
||||
from ..services.correspondance_service import get_server_links
|
||||
links = get_server_links(db, server_id)
|
||||
return templates.TemplateResponse("partials/server_detail.html", {
|
||||
"request": request, "s": s, "tags": tags, "ips": ips
|
||||
"request": request, "s": s, "tags": tags, "ips": ips, "links": links
|
||||
})
|
||||
|
||||
|
||||
@ -100,10 +120,14 @@ async def server_edit(request: Request, server_id: int, db=Depends(get_db)):
|
||||
zones_list = db.execute(sqlt(
|
||||
"SELECT name FROM zones ORDER BY name"
|
||||
)).fetchall()
|
||||
applications = db.execute(sqlt(
|
||||
"SELECT id, nom_court FROM applications WHERE itop_id IS NOT NULL ORDER BY nom_court"
|
||||
)).fetchall()
|
||||
return templates.TemplateResponse("partials/server_edit.html", {
|
||||
"request": request, "s": s, "domains": domains, "envs": envs, "ips": ips,
|
||||
"dns_list": [r.name for r in dns_list],
|
||||
"zones_list": [r.name for r in zones_list],
|
||||
"applications": applications,
|
||||
})
|
||||
|
||||
|
||||
@ -116,11 +140,15 @@ async def server_update(request: Request, server_id: int, db=Depends(get_db),
|
||||
commentaire: str = Form(None),
|
||||
ip_reelle: str = Form(None), ip_connexion: str = Form(None),
|
||||
ssh_method: str = Form(None), domain_ltd: str = Form(None),
|
||||
pref_patch_jour: str = Form(None), pref_patch_heure: str = Form(None)):
|
||||
pref_patch_jour: str = Form(None), pref_patch_heure: str = Form(None),
|
||||
application_id: str = Form(None)):
|
||||
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return HTMLResponse("<p>Non autorise</p>")
|
||||
from ..dependencies import get_user_perms, can_edit
|
||||
if not can_edit(get_user_perms(db, user), "servers"):
|
||||
return HTMLResponse("<p>Permission refusee</p>", status_code=403)
|
||||
|
||||
data = {
|
||||
"domain_code": domain_code, "env_code": env_code, "zone": zone,
|
||||
@ -133,11 +161,47 @@ async def server_update(request: Request, server_id: int, db=Depends(get_db),
|
||||
}
|
||||
update_server(db, server_id, data, user.get("sub"))
|
||||
|
||||
# Application (changement manuel SecOps) — update + push iTop
|
||||
if application_id is not None:
|
||||
app_id_val = int(application_id) if application_id and application_id.strip().isdigit() else None
|
||||
app_itop_id = None
|
||||
app_name = None
|
||||
if app_id_val:
|
||||
row = db.execute(text("SELECT itop_id, nom_court FROM applications WHERE id=:id"),
|
||||
{"id": app_id_val}).fetchone()
|
||||
if row:
|
||||
app_itop_id = row.itop_id
|
||||
app_name = row.nom_court
|
||||
db.execute(text("""UPDATE servers SET application_id=:aid, application_name=:an, updated_at=NOW()
|
||||
WHERE id=:sid"""), {"aid": app_id_val, "an": app_name, "sid": server_id})
|
||||
db.commit()
|
||||
# Push iTop (best effort)
|
||||
try:
|
||||
from ..services.itop_service import ITopClient
|
||||
from ..services.secrets_service import get_secret
|
||||
srv_row = db.execute(text("SELECT hostname FROM servers WHERE id=:id"), {"id": server_id}).fetchone()
|
||||
if srv_row:
|
||||
url = get_secret(db, "itop_url")
|
||||
u = get_secret(db, "itop_user")
|
||||
p = get_secret(db, "itop_pass")
|
||||
if url and u and p:
|
||||
client = ITopClient(url, u, p)
|
||||
r = client._call("core/get", **{"class": "VirtualMachine",
|
||||
"key": f'SELECT VirtualMachine WHERE name = "{srv_row.hostname}"', "output_fields": "name"})
|
||||
if r.get("objects"):
|
||||
vm_id = list(r["objects"].values())[0]["key"]
|
||||
new_list = [{"applicationsolution_id": int(app_itop_id)}] if app_itop_id else []
|
||||
client.update("VirtualMachine", vm_id, {"applicationsolution_list": new_list})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
s = get_server_full(db, server_id)
|
||||
tags = get_server_tags(db, s.qid)
|
||||
ips = get_server_ips(db, server_id)
|
||||
from ..services.correspondance_service import get_server_links
|
||||
links = get_server_links(db, server_id)
|
||||
return templates.TemplateResponse("partials/server_detail.html", {
|
||||
"request": request, "s": s, "tags": tags, "ips": ips
|
||||
"request": request, "s": s, "tags": tags, "ips": ips, "links": links
|
||||
})
|
||||
|
||||
|
||||
@ -148,6 +212,9 @@ async def servers_bulk(request: Request, db=Depends(get_db),
|
||||
user = get_current_user(request)
|
||||
if not user:
|
||||
return RedirectResponse(url="/login")
|
||||
from ..dependencies import get_user_perms, can_edit
|
||||
if not can_edit(get_user_perms(db, user), "servers"):
|
||||
return RedirectResponse(url="/servers?msg=forbidden", status_code=303)
|
||||
if not server_ids or not bulk_field or not bulk_value:
|
||||
return RedirectResponse(url="/servers", status_code=303)
|
||||
|
||||
@ -203,7 +270,9 @@ async def server_sync_qualys(request: Request, server_id: int, db=Depends(get_db
|
||||
s = get_server_full(db, server_id)
|
||||
tags = get_server_tags(db, s.qid) if s else []
|
||||
ips = get_server_ips(db, server_id)
|
||||
from ..services.correspondance_service import get_server_links
|
||||
links = get_server_links(db, server_id) if s else {"as_prod": [], "as_nonprod": []}
|
||||
return templates.TemplateResponse("partials/server_detail.html", {
|
||||
"request": request, "s": s, "tags": tags, "ips": ips,
|
||||
"request": request, "s": s, "tags": tags, "ips": ips, "links": links,
|
||||
"sync_msg": result.get("msg"), "sync_ok": result.get("ok"),
|
||||
})
|
||||
|
||||
@ -124,7 +124,7 @@ def get_servers_for_planning(db, year, week_number):
|
||||
or_clauses.append("z.name = 'DMZ'")
|
||||
|
||||
where = f"""
|
||||
s.etat = 'en_production' AND s.patch_os_owner = 'secops'
|
||||
s.etat = 'production' AND s.patch_os_owner = 'secops'
|
||||
AND s.licence_support IN ('active', 'els') AND s.os_family = 'linux'
|
||||
AND ({' OR '.join(or_clauses)})
|
||||
"""
|
||||
|
||||
@ -142,24 +142,24 @@ def _check_server(s):
|
||||
result["disk_var_mb"] = 3000
|
||||
result["disk_ok"] = True
|
||||
# Quand meme exclure les EOL et decom
|
||||
if s.licence_support == 'eol':
|
||||
if s.licence_support == 'obsolete':
|
||||
result["eligible"] = False
|
||||
result["exclude_reason"] = "eol"
|
||||
result["exclude_reason"] = "obsolete"
|
||||
result["exclude_detail"] = "Licence EOL"
|
||||
elif s.etat != 'en_production':
|
||||
elif s.etat != 'production':
|
||||
result["eligible"] = False
|
||||
result["exclude_reason"] = "non_patchable"
|
||||
result["exclude_detail"] = f"Etat: {s.etat}"
|
||||
return result
|
||||
|
||||
# 1. Eligibilite de base
|
||||
if s.licence_support == 'eol':
|
||||
if s.licence_support == 'obsolete':
|
||||
result["eligible"] = False
|
||||
result["exclude_reason"] = "eol"
|
||||
result["exclude_reason"] = "obsolete"
|
||||
result["exclude_detail"] = "Licence EOL — serveur non supporte"
|
||||
return result
|
||||
|
||||
if s.etat != 'en_production':
|
||||
if s.etat != 'production':
|
||||
result["eligible"] = False
|
||||
result["exclude_reason"] = "non_patchable"
|
||||
result["exclude_detail"] = f"Etat: {s.etat}"
|
||||
@ -255,17 +255,17 @@ def _auto_exclude(db, campaign_id):
|
||||
FROM patch_sessions ps
|
||||
JOIN servers s ON ps.server_id = s.id
|
||||
WHERE ps.campaign_id = :cid AND ps.status = 'pending'
|
||||
AND (s.licence_support = 'eol'
|
||||
OR s.etat != 'en_production'
|
||||
AND (s.licence_support = 'obsolete'
|
||||
OR s.etat != 'production'
|
||||
OR ps.prereq_ssh = 'ko'
|
||||
OR ps.prereq_disk_ok = false)
|
||||
"""), {"cid": campaign_id}).fetchall()
|
||||
|
||||
count = 0
|
||||
for s in non_eligible:
|
||||
if s.licence_support == 'eol':
|
||||
reason, detail = "eol", "Licence EOL — auto-exclu"
|
||||
elif s.etat != 'en_production':
|
||||
if s.licence_support == 'obsolete':
|
||||
reason, detail = "obsolete", "Licence EOL — auto-exclu"
|
||||
elif s.etat != 'production':
|
||||
reason, detail = "non_patchable", f"Etat {s.etat} — auto-exclu"
|
||||
elif s.prereq_disk_ok is False:
|
||||
reason, detail = "creneau_inadequat", "Espace disque insuffisant — auto-exclu"
|
||||
|
||||
@ -115,15 +115,17 @@ def list_servers(db, filters, page=1, per_page=50, sort="hostname", sort_dir="as
|
||||
if filters.get("tier"):
|
||||
where.append("s.tier = :tier"); params["tier"] = filters["tier"]
|
||||
if filters.get("etat"):
|
||||
if filters["etat"] == "eol":
|
||||
where.append("s.licence_support = 'eol'")
|
||||
if filters["etat"] == "obsolete":
|
||||
where.append("s.licence_support = 'obsolete'")
|
||||
else:
|
||||
where.append("s.etat = :etat"); params["etat"] = filters["etat"]
|
||||
where.append("COALESCE(s.licence_support, '') != 'eol'")
|
||||
where.append("COALESCE(s.licence_support, '') != 'obsolete'")
|
||||
if filters.get("os"):
|
||||
where.append("s.os_family = :os"); params["os"] = filters["os"]
|
||||
if filters.get("owner"):
|
||||
where.append("s.patch_os_owner = :owner"); params["owner"] = filters["owner"]
|
||||
if filters.get("application"):
|
||||
where.append("s.application_name = :application"); params["application"] = filters["application"]
|
||||
if filters.get("search"):
|
||||
where.append("s.hostname ILIKE :search"); params["search"] = f"%{filters['search']}%"
|
||||
|
||||
@ -136,14 +138,20 @@ def list_servers(db, filters, page=1, per_page=50, sort="hostname", sort_dir="as
|
||||
SELECT s.id, s.hostname, s.fqdn, d.name as domaine, e.name as environnement,
|
||||
z.name as zone, s.os_family, s.os_version, s.tier, s.etat,
|
||||
s.licence_support, s.patch_os_owner, s.responsable_nom, s.machine_type,
|
||||
s.application_name,
|
||||
CASE
|
||||
WHEN s.os_version ILIKE '%Red Hat%' THEN
|
||||
'Red Hat ' || COALESCE((regexp_match(s.os_version, '(\d+\.\d+)'))[1], '')
|
||||
WHEN s.os_version ILIKE '%Oracle%Linux%' THEN
|
||||
'Oracle ' || COALESCE((regexp_match(s.os_version, '(\d+\.\d+)'))[1], '')
|
||||
WHEN s.os_version ILIKE '%CentOS Stream%' THEN
|
||||
'CentOS Stream ' || COALESCE((regexp_match(s.os_version, '(\d+[\.\d]*)'))[1], '')
|
||||
WHEN s.os_version ILIKE '%CentOS%' THEN
|
||||
'CentOS ' || COALESCE((regexp_match(s.os_version, '(\d+\.\d[\d.]*)'))[1], '')
|
||||
WHEN s.os_version ILIKE '%Ubuntu%' THEN 'Ubuntu'
|
||||
'CentOS ' || COALESCE((regexp_match(s.os_version, '(\d+[\.\d]*)'))[1], '')
|
||||
WHEN s.os_version ILIKE '%Debian%' THEN
|
||||
'Debian ' || COALESCE((regexp_match(s.os_version, '(\d+)'))[1], '')
|
||||
WHEN s.os_version ILIKE '%Ubuntu%' THEN
|
||||
'Ubuntu ' || COALESCE((regexp_match(s.os_version, '(\d+\.\d+)'))[1], '')
|
||||
WHEN s.os_version ILIKE '%Windows Server 2022 Standard%' THEN '2022 Standard'
|
||||
WHEN s.os_version ILIKE '%Windows Server 2022 Datacenter%' THEN '2022 Datacenter'
|
||||
WHEN s.os_version ILIKE '%Windows Server 2019 Standard%' THEN '2019 Standard'
|
||||
|
||||
@ -213,7 +213,7 @@ function showForm(id, type) {
|
||||
<span class="badge {% if s.status == 'patched' %}badge-green{% elif s.status == 'failed' %}badge-red{% elif s.status == 'excluded' %}badge-gray{% elif s.status == 'in_progress' %}badge-yellow{% else %}badge-gray{% endif %}">{{ s.status }}</span>
|
||||
{% if s.exclusion_reason %}
|
||||
<div class="text-[9px] text-gray-500" title="{{ s.exclusion_detail or '' }}">
|
||||
{% if s.exclusion_reason == 'eol' %}EOL{% elif s.exclusion_reason == 'creneau_inadequat' %}Prereq KO{% elif s.exclusion_reason == 'non_patchable' %}Non patchable{% else %}{{ s.exclusion_reason }}{% endif %}
|
||||
{% if s.exclusion_reason == 'obsolete' %}EOL{% elif s.exclusion_reason == 'creneau_inadequat' %}Prereq KO{% elif s.exclusion_reason == 'non_patchable' %}Non patchable{% else %}{{ s.exclusion_reason }}{% endif %}
|
||||
{% if s.excluded_by %}<span class="text-gray-600">({{ s.excluded_by }})</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
<th class="p-2">Rôle</th>
|
||||
<th class="text-left p-2">Scopes</th>
|
||||
<th class="p-2">Actif</th>
|
||||
<th class="p-2">Actions</th>
|
||||
{% if can_edit_contacts %}<th class="p-2">Actions</th>{% endif %}
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for c in contacts %}
|
||||
@ -61,6 +61,7 @@
|
||||
<td class="p-2 text-center"><span class="badge {% if 'responsable' in c.role %}badge-blue{% elif 'referent' in c.role %}badge-yellow{% elif 'ra_' in c.role %}badge-green{% else %}badge-gray{% endif %}">{{ c.role }}</span></td>
|
||||
<td class="p-2 text-xs text-gray-400" style="max-width:300px">{{ (c.scopes_summary or '-')[:80] }}</td>
|
||||
<td class="p-2 text-center"><span class="badge {% if c.is_active %}badge-green{% else %}badge-red{% endif %}">{{ 'Oui' if c.is_active else 'Non' }}</span></td>
|
||||
{% if can_edit_contacts %}
|
||||
<td class="p-2 text-center">
|
||||
<div class="flex gap-1 justify-center">
|
||||
<button class="btn-sm bg-cyber-border text-cyber-accent"
|
||||
@ -71,12 +72,14 @@
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if can_edit_contacts %}
|
||||
<!-- Ajouter -->
|
||||
<div class="card p-4 mt-4">
|
||||
<h4 class="text-sm font-bold text-cyber-accent mb-3">Ajouter un contact</h4>
|
||||
@ -98,4 +101,5 @@
|
||||
<button type="submit" class="btn-primary px-4 py-1 text-sm">Ajouter</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for s in servers %}
|
||||
<tr class="{% if s.licence_support == 'eol' %}bg-red-900/10{% elif s.licence_support == 'els' %}bg-yellow-900/10{% endif %}">
|
||||
<tr class="{% if s.licence_support == 'obsolete' %}bg-red-900/10{% elif s.licence_support == 'els' %}bg-yellow-900/10{% endif %}">
|
||||
<td class="p-1 text-center"><input type="checkbox" name="include_{{ s.id }}" checked></td>
|
||||
<td class="p-1 font-mono text-cyber-accent">{{ s.hostname }}</td>
|
||||
<td class="p-1 text-center">{{ s.domaine or '-' }}</td>
|
||||
@ -36,7 +36,7 @@
|
||||
<td class="p-1 text-center">{{ s.os_family or '-' }}</td>
|
||||
<td class="p-1 text-center"><span class="badge {% if s.tier == 'tier0' %}badge-red{% elif s.tier == 'tier1' %}badge-yellow{% else %}badge-blue{% endif %}">{{ s.tier }}</span></td>
|
||||
<td class="p-1 text-center">{{ s.ssh_method or '-' }}</td>
|
||||
<td class="p-1 text-center"><span class="badge {% if s.licence_support == 'active' %}badge-green{% elif s.licence_support == 'eol' %}badge-red{% else %}badge-yellow{% endif %}">{{ s.licence_support }}</span></td>
|
||||
<td class="p-1 text-center"><span class="badge {% if s.licence_support == 'active' %}badge-green{% elif s.licence_support == 'obsolete' %}badge-red{% else %}badge-yellow{% endif %}">{{ s.licence_support }}</span></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@ -51,7 +51,7 @@
|
||||
<div class="flex justify-between"><span class="text-gray-500">Environnement</span><span class="badge {% if s.environnement == 'Production' %}badge-green{% else %}badge-yellow{% endif %}">{{ s.environnement }}</span></div>
|
||||
<div class="flex justify-between"><span class="text-gray-500">Zone</span><span class="badge {% if s.zone == 'DMZ' %}badge-red{% else %}badge-blue{% endif %}">{{ s.zone or 'LAN' }}</span></div>
|
||||
<div class="flex justify-between"><span class="text-gray-500">Tier</span><span class="badge {% if s.tier == 'tier0' %}badge-red{% elif s.tier == 'tier1' %}badge-yellow{% else %}badge-blue{% endif %}">{{ s.tier }}</span></div>
|
||||
<div class="flex justify-between"><span class="text-gray-500">Etat</span><span class="badge {% if s.etat == 'en_production' %}badge-green{% elif s.etat == 'decommissionne' %}badge-red{% else %}badge-yellow{% endif %}">{{ s.etat }}</span></div>
|
||||
<div class="flex justify-between"><span class="text-gray-500">Etat</span><span class="badge {% if s.etat == 'production' %}badge-green{% elif s.etat == 'obsolete' %}badge-red{% else %}badge-yellow{% endif %}">{{ s.etat }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -61,7 +61,7 @@
|
||||
<div class="space-y-1 text-sm">
|
||||
<div class="flex justify-between"><span class="text-gray-500">OS</span><span>{{ s.os_family or '-' }}</span></div>
|
||||
<div class="text-xs text-gray-400 mt-1">{{ s.os_version or '' }}</div>
|
||||
<div class="flex justify-between"><span class="text-gray-500">Licence</span><span class="badge {% if s.licence_support == 'active' %}badge-green{% elif s.licence_support == 'eol' %}badge-red{% else %}badge-yellow{% endif %}">{{ s.licence_support }}</span></div>
|
||||
<div class="flex justify-between"><span class="text-gray-500">Licence</span><span class="badge {% if s.licence_support == 'active' %}badge-green{% elif s.licence_support == 'obsolete' %}badge-red{% else %}badge-yellow{% endif %}">{{ s.licence_support }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -88,6 +88,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Correspondance prod ↔ hors-prod -->
|
||||
{% if links and (links.as_prod or links.as_nonprod) %}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Correspondance Prod ↔ Hors-Prod</h4>
|
||||
{% if links.as_prod %}
|
||||
<div class="text-xs mb-2">
|
||||
<div class="text-cyber-green font-bold mb-1">Ce serveur est un PROD — hors-prod liés :</div>
|
||||
<ul class="ml-2 space-y-1">
|
||||
{% for l in links.as_prod %}
|
||||
<li class="flex gap-2 items-center">
|
||||
<span class="font-mono text-cyber-accent">{{ l.hostname }}</span>
|
||||
<span class="badge badge-yellow" style="font-size:9px">{{ l.env_name or l.environment_code or '?' }}</span>
|
||||
<span class="text-gray-600" style="font-size:9px">{{ l.source }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if links.as_nonprod %}
|
||||
<div class="text-xs mb-2">
|
||||
<div class="text-cyber-yellow font-bold mb-1">Ce serveur est un HORS-PROD — prod(s) lié(s) :</div>
|
||||
<ul class="ml-2 space-y-1">
|
||||
{% for l in links.as_nonprod %}
|
||||
<li class="flex gap-2 items-center">
|
||||
<span class="font-mono text-cyber-accent">{{ l.hostname }}</span>
|
||||
<span class="badge badge-green" style="font-size:9px">{{ l.env_name or 'Production' }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="/patching/correspondance?search={{ s.hostname }}" class="text-xs text-cyber-accent hover:underline">Gérer dans le builder →</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Correspondance</h4>
|
||||
<p class="text-xs text-gray-500">Aucune correspondance prod ↔ hors-prod définie. <a href="/patching/correspondance?search={{ s.hostname }}" class="text-cyber-accent hover:underline">Créer →</a></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if s.mode_operatoire %}
|
||||
<div class="mb-4">
|
||||
<h4 class="text-xs text-cyber-accent font-bold uppercase mb-2 border-b border-cyber-border pb-1">Mode operatoire</h4>
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
<div>
|
||||
<label class="text-xs text-gray-500">Etat</label>
|
||||
<select name="etat" class="w-full">
|
||||
{% for e in ['en_production','en_implementation','en_decommissionnement','decommissionne'] %}<option value="{{ e }}" {% if e == s.etat %}selected{% endif %}>{{ e }}</option>{% endfor %}
|
||||
{% for e in ['production','implementation','stock','obsolete'] %}<option value="{{ e }}" {% if e == s.etat %}selected{% endif %}>{{ e }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@ -67,6 +67,13 @@
|
||||
{% for o in ['secops','ipop','editeur','tiers','na','a_definir'] %}<option value="{{ o }}" {% if o == s.patch_os_owner %}selected{% endif %}>{{ o }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-500">Solution applicative (iTop) <span class="text-gray-600">— modifie aussi iTop</span></label>
|
||||
<select name="application_id" class="w-full">
|
||||
<option value="">-- Aucune --</option>
|
||||
{% for a in applications %}<option value="{{ a.id }}" {% if a.id == s.application_id %}selected{% endif %}>{{ a.nom_court }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-gray-500">Responsable</label>
|
||||
<input type="text" name="responsable_nom" value="{{ s.responsable_nom or '' }}" class="w-full">
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
{% for t in ['tier0','tier1','tier2','tier3'] %}<option value="{{ t }}" {% if filters.tier == t %}selected{% endif %}>{{ t }}</option>{% endfor %}
|
||||
</select>
|
||||
<select name="etat" onchange="this.form.submit()"><option value="">Etat</option>
|
||||
{% for e in ['en_production','en_implementation','en_decommissionnement','decommissionne','eteint','eol'] %}<option value="{{ e }}" {% if filters.etat == e %}selected{% endif %}>{{ e.replace("en_","En ").replace("_"," ").title() }}</option>{% endfor %}
|
||||
{% for e,l in [('production','Production'),('implementation','Implementation'),('stock','Stock'),('obsolete','Obsolete')] %}<option value="{{ e }}" {% if filters.etat == e %}selected{% endif %}>{{ l }}</option>{% endfor %}
|
||||
</select>
|
||||
<select name="os" onchange="this.form.submit()"><option value="">OS</option>
|
||||
<option value="linux" {% if filters.os == 'linux' %}selected{% endif %}>Linux</option>
|
||||
@ -47,10 +47,14 @@
|
||||
<option value="ipop" {% if filters.owner == 'ipop' %}selected{% endif %}>ipop</option>
|
||||
<option value="na" {% if filters.owner == 'na' %}selected{% endif %}>na</option>
|
||||
</select>
|
||||
<select name="application" onchange="this.form.submit()" style="max-width:200px"><option value="">Solution app.</option>
|
||||
{% for a in applications_list %}<option value="{{ a.application_name }}" {% if filters.application == a.application_name %}selected{% endif %}>{{ a.application_name }} ({{ a.c }})</option>{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn-primary px-3 py-1 text-sm">Filtrer</button>
|
||||
<a href="/servers" class="text-xs text-gray-500 hover:text-gray-300">Reset</a>
|
||||
</form>
|
||||
|
||||
{% if can_edit_servers %}
|
||||
<!-- Actions groupées -->
|
||||
<div id="bulk-bar" class="card p-3 mb-2 flex gap-3 items-center flex-wrap" style="display:none">
|
||||
<span class="text-xs text-gray-400" id="bulk-count">0 sélectionné(s)</span>
|
||||
@ -71,15 +75,16 @@
|
||||
<button type="submit" class="btn-primary px-3 py-1 text-xs">Appliquer</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
const bulkValues = {
|
||||
domain_code: [{% for d in domains_list %}{v:"{{ d.code }}", l:"{{ d.name }}"},{% endfor %}],
|
||||
env_code: [{% for e in envs_list %}{v:"{{ e.code }}", l:"{{ e.name }}"},{% endfor %}],
|
||||
tier: [{v:"tier0",l:"tier0"},{v:"tier1",l:"tier1"},{v:"tier2",l:"tier2"},{v:"tier3",l:"tier3"}],
|
||||
etat: [{v:"en_production",l:"En production"},{v:"en_implementation",l:"En implémentation"},{v:"en_decommissionnement",l:"En décommissionnement"},{v:"decommissionne",l:"Décommissionné"},{v:"eteint",l:"Éteint"},{v:"eol",l:"EOL"}],
|
||||
etat: [{v:"production",l:"Production"},{v:"implementation",l:"Implementation"},{v:"stock",l:"Stock"},{v:"obsolete",l:"Obsolete"}],
|
||||
patch_os_owner: [{v:"secops",l:"secops"},{v:"ipop",l:"ipop"},{v:"na",l:"na"}],
|
||||
licence_support: [{v:"active",l:"active"},{v:"eol",l:"eol"},{v:"els",l:"els"}],
|
||||
licence_support: [{v:"active",l:"active"},{v:"obsolete",l:"obsolete"},{v:"els",l:"els"}],
|
||||
};
|
||||
document.getElementById('bulk-field').addEventListener('change', function() {
|
||||
const sel = document.getElementById('bulk-value');
|
||||
@ -103,7 +108,7 @@ function updateBulk() {
|
||||
<div id="server-table" class="card overflow-x-auto">
|
||||
<table class="w-full table-cyber">
|
||||
<thead><tr>
|
||||
<th class="p-2 w-8"><input type="checkbox" id="check-all"></th>
|
||||
{% if can_edit_servers %}<th class="p-2 w-8"><input type="checkbox" id="check-all"></th>{% endif %}
|
||||
<th class="text-left p-2"><a href="{{ sort_url('hostname') }}" class="hover:text-cyber-accent">Hostname {{ sort_icon('hostname') }}</a></th>
|
||||
<th class="p-2"><a href="{{ sort_url('domaine') }}" class="hover:text-cyber-accent">Domaine {{ sort_icon('domaine') }}</a></th>
|
||||
<th class="p-2"><a href="{{ sort_url('env') }}" class="hover:text-cyber-accent">Env {{ sort_icon('env') }}</a></th>
|
||||
@ -114,24 +119,43 @@ function updateBulk() {
|
||||
<th class="p-2"><a href="{{ sort_url('tier') }}" class="hover:text-cyber-accent">Tier {{ sort_icon('tier') }}</a></th>
|
||||
<th class="p-2"><a href="{{ sort_url('etat') }}" class="hover:text-cyber-accent">Etat {{ sort_icon('etat') }}</a></th>
|
||||
<th class="p-2"><a href="{{ sort_url('owner') }}" class="hover:text-cyber-accent">Owner {{ sort_icon('owner') }}</a></th>
|
||||
<th class="p-2 text-left">Solution applicative</th>
|
||||
<th class="p-2 text-left">Équivalent(s)</th>
|
||||
<th class="p-2">Actions</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
{% for s in servers %}
|
||||
<tr id="row-{{ s.id }}" class="group" hx-get="/servers/{{ s.id }}/detail" hx-target="#detail-panel" hx-swap="innerHTML" onclick="openPanel()">
|
||||
<td class="p-2" onclick="event.stopPropagation()"><input type="checkbox" name="srv" value="{{ s.id }}" onchange="updateBulk()"></td>
|
||||
{% if can_edit_servers %}<td class="p-2" onclick="event.stopPropagation()"><input type="checkbox" name="srv" value="{{ s.id }}" onchange="updateBulk()"></td>{% endif %}
|
||||
<td class="p-2 font-mono text-sm text-cyber-accent">{{ s.hostname }}</td>
|
||||
<td class="p-2 text-center text-xs">{{ s.domaine or '-' }}</td>
|
||||
<td class="p-2 text-center"><span class="badge {% if s.environnement == 'Production' %}badge-green{% elif s.environnement == 'Recette' %}badge-yellow{% else %}badge-gray{% endif %}"title="{{ s.environnement or '' }}">{{ (s.environnement or '-')[:6] }}</span></td>
|
||||
<td class="p-2 text-center"><span class="badge {% if s.zone == 'DMZ' %}badge-red{% elif s.zone == 'EMV' %}badge-yellow{% else %}badge-blue{% endif %}">{{ s.zone or 'LAN' }}</span></td>
|
||||
<td class="p-2 text-center text-xs">{{ s.os_family or '-' }}</td>
|
||||
<td class="p-2 text-center text-xs text-gray-400" title="{{ s.os_version or '' }}">{{ s.os_short or '-' }}</td>
|
||||
<td class="p-2 text-center"><span class="badge {% if s.licence_support == 'active' %}badge-green{% elif s.licence_support == 'eol' %}badge-red{% elif s.licence_support == 'els' %}badge-yellow{% else %}badge-gray{% endif %}">{{ s.licence_support }}</span></td>
|
||||
<td class="p-2 text-center"><span class="badge {% if s.licence_support == 'active' %}badge-green{% elif s.licence_support == 'obsolete' %}badge-red{% elif s.licence_support == 'els' %}badge-yellow{% else %}badge-gray{% endif %}">{{ s.licence_support }}</span></td>
|
||||
<td class="p-2 text-center"><span class="badge {% if s.tier == 'tier0' %}badge-red{% elif s.tier == 'tier1' %}badge-yellow{% elif s.tier == 'tier2' %}badge-blue{% else %}badge-green{% endif %}">{{ s.tier }}</span></td>
|
||||
<td class="p-2 text-center"><span class="badge {% if s.etat == 'en_production' %}badge-green{% elif s.etat == 'decommissionne' %}badge-red{% else %}badge-yellow{% endif %}"title="{{ s.etat or '' }}">{{ (s.etat or '')[:8] }}</span></td>
|
||||
<td class="p-2 text-center"><span class="badge {% if s.etat == 'production' %}badge-green{% elif s.etat == 'obsolete' %}badge-red{% else %}badge-yellow{% endif %}"title="{{ s.etat or '' }}">{{ (s.etat or '')[:8] }}</span></td>
|
||||
<td class="p-2 text-center text-xs">{{ s.patch_os_owner or '-' }}</td>
|
||||
<td class="p-2 text-xs text-gray-300" title="{{ s.application_name or '' }}">{{ (s.application_name or '-')[:35] }}</td>
|
||||
<td class="p-2 text-xs" onclick="event.stopPropagation()" style="max-width:220px">
|
||||
{% set link = links.get(s.id, {}) %}
|
||||
{% if link.as_prod %}
|
||||
<span class="text-cyber-green" style="font-size:10px">→ non-prod :</span>
|
||||
{% for l in link.as_prod %}<span class="font-mono text-gray-300" title="{{ l.env_name or '' }}">{{ l.hostname }}{% if not loop.last %}, {% endif %}</span>{% endfor %}
|
||||
{% elif link.as_nonprod %}
|
||||
<span class="text-cyber-yellow" style="font-size:10px">→ prod :</span>
|
||||
{% for l in link.as_nonprod %}<span class="font-mono text-gray-300">{{ l.hostname }}{% if not loop.last %}, {% endif %}</span>{% endfor %}
|
||||
{% else %}
|
||||
<span class="text-gray-600">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="p-2 text-center" onclick="event.stopPropagation()">
|
||||
{% if can_edit_servers %}
|
||||
<button class="btn-sm bg-cyber-border text-cyber-accent" hx-get="/servers/{{ s.id }}/edit" hx-target="#detail-panel" hx-swap="innerHTML" onclick="openPanel()">Edit</button>
|
||||
{% else %}
|
||||
<span class="text-xs text-gray-600">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user