diff --git a/app/routers/campaigns.py b/app/routers/campaigns.py index 56af088..4e8f406 100644 --- a/app/routers/campaigns.py +++ b/app/routers/campaigns.py @@ -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"), diff --git a/app/routers/contacts.py b/app/routers/contacts.py index 76acfa1..63407cf 100644 --- a/app/routers/contacts.py +++ b/app/routers/contacts.py @@ -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) diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 665bd01..b4c8531 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -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 diff --git a/app/routers/planning.py b/app/routers/planning.py index 2face36..a1cdda3 100644 --- a/app/routers/planning.py +++ b/app/routers/planning.py @@ -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() diff --git a/app/routers/servers.py b/app/routers/servers.py index 2257b1c..6fccf21 100644 --- a/app/routers/servers.py +++ b/app/routers/servers.py @@ -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("

Serveur non trouve

") 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("

Non autorise

") + from ..dependencies import get_user_perms, can_edit + if not can_edit(get_user_perms(db, user), "servers"): + return HTMLResponse("

Permission refusee

", 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"), }) diff --git a/app/services/campaign_service.py b/app/services/campaign_service.py index aa4339f..77ecbfa 100644 --- a/app/services/campaign_service.py +++ b/app/services/campaign_service.py @@ -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)}) """ diff --git a/app/services/prereq_service.py b/app/services/prereq_service.py index 5d2a349..edad0f4 100644 --- a/app/services/prereq_service.py +++ b/app/services/prereq_service.py @@ -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" diff --git a/app/services/server_service.py b/app/services/server_service.py index 181e5d7..318d992 100644 --- a/app/services/server_service.py +++ b/app/services/server_service.py @@ -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' diff --git a/app/templates/campaign_detail.html b/app/templates/campaign_detail.html index f22b3ff..6cb2006 100644 --- a/app/templates/campaign_detail.html +++ b/app/templates/campaign_detail.html @@ -213,7 +213,7 @@ function showForm(id, type) { {{ s.status }} {% if s.exclusion_reason %}
- {% 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 %}({{ s.excluded_by }}){% endif %}
{% endif %} diff --git a/app/templates/contacts.html b/app/templates/contacts.html index d826069..3b27461 100644 --- a/app/templates/contacts.html +++ b/app/templates/contacts.html @@ -51,7 +51,7 @@ Rôle Scopes Actif - Actions + {% if can_edit_contacts %}Actions{% endif %} {% for c in contacts %} @@ -61,6 +61,7 @@ {{ c.role }} {{ (c.scopes_summary or '-')[:80] }} {{ 'Oui' if c.is_active else 'Non' }} + {% if can_edit_contacts %}
+ {% endif %} {% endfor %} +{% if can_edit_contacts %}

Ajouter un contact

@@ -98,4 +101,5 @@
+{% endif %} {% endblock %} diff --git a/app/templates/partials/campaign_preview.html b/app/templates/partials/campaign_preview.html index 79ebf6a..053be36 100644 --- a/app/templates/partials/campaign_preview.html +++ b/app/templates/partials/campaign_preview.html @@ -28,7 +28,7 @@ {% for s in servers %} - + {{ s.hostname }} {{ s.domaine or '-' }} @@ -36,7 +36,7 @@ {{ s.os_family or '-' }} {{ s.tier }} {{ s.ssh_method or '-' }} - {{ s.licence_support }} + {{ s.licence_support }} {% endfor %} diff --git a/app/templates/partials/server_detail.html b/app/templates/partials/server_detail.html index 0c87157..08e7fa2 100644 --- a/app/templates/partials/server_detail.html +++ b/app/templates/partials/server_detail.html @@ -51,7 +51,7 @@
Environnement{{ s.environnement }}
Zone{{ s.zone or 'LAN' }}
Tier{{ s.tier }}
-
Etat{{ s.etat }}
+
Etat{{ s.etat }}
@@ -61,7 +61,7 @@
OS{{ s.os_family or '-' }}
{{ s.os_version or '' }}
-
Licence{{ s.licence_support }}
+
Licence{{ s.licence_support }}
@@ -88,6 +88,46 @@ + + {% if links and (links.as_prod or links.as_nonprod) %} +
+

Correspondance Prod ↔ Hors-Prod

+ {% if links.as_prod %} +
+
Ce serveur est un PROD — hors-prod liés :
+ +
+ {% endif %} + {% if links.as_nonprod %} +
+
Ce serveur est un HORS-PROD — prod(s) lié(s) :
+ +
+ {% endif %} + Gérer dans le builder → +
+ {% else %} +
+

Correspondance

+

Aucune correspondance prod ↔ hors-prod définie. Créer →

+
+ {% endif %} + {% if s.mode_operatoire %}

Mode operatoire

diff --git a/app/templates/partials/server_edit.html b/app/templates/partials/server_edit.html index ed93a6e..c72ffbe 100644 --- a/app/templates/partials/server_edit.html +++ b/app/templates/partials/server_edit.html @@ -58,7 +58,7 @@
@@ -67,6 +67,13 @@ {% for o in ['secops','ipop','editeur','tiers','na','a_definir'] %}{% endfor %}
+
+ + +
diff --git a/app/templates/servers.html b/app/templates/servers.html index d1d5a3a..2faa236 100644 --- a/app/templates/servers.html +++ b/app/templates/servers.html @@ -36,7 +36,7 @@ {% for t in ['tier0','tier1','tier2','tier3'] %}{% endfor %} + Reset +{% if can_edit_servers %} +{% endif %}