diff --git a/app/main.py b/app/main.py index 01fc31f..b683e56 100644 --- a/app/main.py +++ b/app/main.py @@ -6,7 +6,7 @@ from starlette.middleware.base import BaseHTTPMiddleware from .config import APP_NAME, APP_VERSION from .dependencies import get_current_user, get_user_perms from .database import SessionLocal -from .routers import auth, dashboard, servers, settings, users, campaigns, planning, specifics, audit, contacts, qualys, safe_patching, audit_full +from .routers import auth, dashboard, servers, settings, users, campaigns, planning, specifics, audit, contacts, qualys, safe_patching, audit_full, quickwin class PermissionsMiddleware(BaseHTTPMiddleware): @@ -43,6 +43,7 @@ app.include_router(contacts.router) app.include_router(qualys.router) app.include_router(safe_patching.router) app.include_router(audit_full.router) +app.include_router(quickwin.router) @app.get("/") diff --git a/app/routers/auth.py b/app/routers/auth.py index 2934fc5..07f5ca0 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -40,7 +40,14 @@ async def login(request: Request, username: str = Form(...), password: str = For user = {"sub": row.username, "role": row.role, "uid": row.id} log_login(db, request, user) db.commit() - response = RedirectResponse(url="/dashboard", status_code=303) + # Redirect qw_only users to quickwin + perms = db.execute(text("SELECT module FROM user_permissions WHERE user_id = :uid"), {"uid": row.id}).fetchall() + modules = {r.module for r in perms} + if modules == {"quickwin"}: + redirect_url = "/quickwin" + else: + redirect_url = "/dashboard" + response = RedirectResponse(url=redirect_url, status_code=303) response.set_cookie(key="access_token", value=token, httponly=True, samesite="lax", max_age=3600) return response diff --git a/app/routers/quickwin.py b/app/routers/quickwin.py new file mode 100644 index 0000000..6b527d8 --- /dev/null +++ b/app/routers/quickwin.py @@ -0,0 +1,297 @@ +"""Router QuickWin — Campagnes patching rapide avec exclusions par serveur""" +import json +from datetime import datetime +from fastapi import APIRouter, Request, Depends, Query, Form +from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse +from fastapi.templating import Jinja2Templates +from ..dependencies import get_db, get_current_user, get_user_perms, can_view, can_edit, base_context +from ..services.quickwin_service import ( + get_server_configs, upsert_server_config, delete_server_config, + get_eligible_servers, list_runs, get_run, get_run_entries, + create_run, delete_run, update_entry_field, + can_start_prod, get_run_stats, inject_yum_history, + DEFAULT_GENERAL_EXCLUDES, +) +from ..config import APP_NAME + +router = APIRouter() +templates = Jinja2Templates(directory="app/templates") + + +@router.get("/quickwin", response_class=HTMLResponse) +async def quickwin_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_view(perms, "campaigns") and not can_view(perms, "quickwin"): + return RedirectResponse(url="/dashboard") + + runs = list_runs(db) + configs = get_server_configs(db) + now = datetime.now() + + ctx = base_context(request, db, user) + ctx.update({ + "app_name": APP_NAME, + "runs": runs, + "configs": configs, + "config_count": len(configs), + "current_week": now.isocalendar()[1], + "current_year": now.isocalendar()[0], + "can_create": can_edit(perms, "campaigns"), + "msg": request.query_params.get("msg"), + }) + return templates.TemplateResponse("quickwin.html", ctx) + + +# -- Config exclusions par serveur -- + +@router.get("/quickwin/config", response_class=HTMLResponse) +async def quickwin_config_page(request: Request, db=Depends(get_db), + page: int = Query(1), + per_page: int = Query(14), + search: str = Query(""), + env: str = Query(""), + domain: str = Query(""), + zone: str = Query("")): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + perms = get_user_perms(db, user) + if not can_view(perms, "campaigns") and not can_view(perms, "quickwin"): + return RedirectResponse(url="/dashboard") + + configs = get_server_configs(db) + + # Filtres + filtered = configs + if search: + filtered = [s for s in filtered if search.lower() in s.hostname.lower()] + if env: + filtered = [s for s in filtered if s.environnement == env] + if domain: + filtered = [s for s in filtered if s.domaine == domain] + if zone: + filtered = [s for s in filtered if (s.zone or '') == zone] + + # Pagination + per_page = max(5, min(per_page, 100)) + total = len(filtered) + total_pages = max(1, (total + per_page - 1) // per_page) + page = max(1, min(page, total_pages)) + start = (page - 1) * per_page + page_servers = filtered[start:start + per_page] + + ctx = base_context(request, db, user) + ctx.update({ + "app_name": APP_NAME, + "all_servers": page_servers, + "all_configs": configs, + "default_excludes": DEFAULT_GENERAL_EXCLUDES, + "total_count": total, + "page": page, + "per_page": per_page, + "total_pages": total_pages, + "filters": {"search": search, "env": env, "domain": domain, "zone": zone}, + "msg": request.query_params.get("msg"), + }) + return templates.TemplateResponse("quickwin_config.html", ctx) + + +@router.post("/quickwin/config/save") +async def quickwin_config_save(request: Request, db=Depends(get_db), + server_id: int = Form(0), + general_excludes: str = Form(""), + specific_excludes: str = Form(""), + notes: str = Form("")): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + if server_id: + upsert_server_config(db, server_id, general_excludes.strip(), + specific_excludes.strip(), notes.strip()) + return RedirectResponse(url="/quickwin/config?msg=saved", status_code=303) + + +@router.post("/quickwin/config/delete") +async def quickwin_config_delete(request: Request, db=Depends(get_db), + config_id: int = Form(0)): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + if config_id: + delete_server_config(db, config_id) + return RedirectResponse(url="/quickwin/config?msg=deleted", status_code=303) + + +@router.post("/quickwin/config/bulk-add") +async def quickwin_config_bulk_add(request: Request, db=Depends(get_db), + server_ids: str = Form(""), + general_excludes: str = Form("")): + """Ajouter plusieurs serveurs d'un coup avec les memes exclusions generales""" + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + ids = [int(x) for x in server_ids.split(",") if x.strip().isdigit()] + for sid in ids: + upsert_server_config(db, sid, general_excludes.strip(), "", "") + return RedirectResponse(url=f"/quickwin/config?msg=added_{len(ids)}", status_code=303) + + +# -- Runs QuickWin -- + +@router.post("/quickwin/create") +async def quickwin_create(request: Request, db=Depends(get_db), + label: str = Form(""), + week_number: int = Form(0), + year: int = Form(0), + server_ids: str = Form(""), + notes: 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, "campaigns"): + return RedirectResponse(url="/quickwin") + + if not label: + label = f"Quick Win S{week_number:02d} {year}" + + ids = [int(x) for x in server_ids.split(",") if x.strip().isdigit()] + if not ids: + # Prendre tous les serveurs configures, sinon tous les eligibles + configs = get_server_configs(db) + ids = [c.server_id for c in configs] + if not ids: + eligible = get_eligible_servers(db) + ids = [s.id for s in eligible] + + if not ids: + return RedirectResponse(url="/quickwin?msg=no_servers", status_code=303) + + try: + run_id = create_run(db, year, week_number, label, user.get("uid"), ids, notes) + return RedirectResponse(url=f"/quickwin/{run_id}", status_code=303) + except Exception as e: + db.rollback() + return RedirectResponse(url=f"/quickwin?msg=error", status_code=303) + + +@router.get("/quickwin/{run_id}", response_class=HTMLResponse) +async def quickwin_detail(request: Request, run_id: int, db=Depends(get_db), + search: str = Query(""), + status: str = Query(""), + domain: str = Query(""), + hp_page: int = Query(1), + p_page: int = Query(1), + per_page: int = Query(14)): + user = get_current_user(request) + if not user: + return RedirectResponse(url="/login") + + run = get_run(db, run_id) + if not run: + return RedirectResponse(url="/quickwin") + + entries = get_run_entries(db, run_id) + stats = get_run_stats(db, run_id) + prod_ok = can_start_prod(db, run_id) + + hprod_all = [e for e in entries if e.branch == "hprod"] + prod_all = [e for e in entries if e.branch == "prod"] + + # Filtres + def apply_filters(lst): + filtered = lst + if search: + filtered = [e for e in filtered if search.lower() in e.hostname.lower()] + if status: + filtered = [e for e in filtered if e.status == status] + if domain: + filtered = [e for e in filtered if e.domaine == domain] + return filtered + + hprod = apply_filters(hprod_all) + prod = apply_filters(prod_all) + + # Pagination + per_page = max(5, min(per_page, 100)) + + hp_total = len(hprod) + hp_total_pages = max(1, (hp_total + per_page - 1) // per_page) + hp_page = max(1, min(hp_page, hp_total_pages)) + hp_start = (hp_page - 1) * per_page + hprod_page = hprod[hp_start:hp_start + per_page] + + p_total = len(prod) + p_total_pages = max(1, (p_total + per_page - 1) // per_page) + p_page = max(1, min(p_page, p_total_pages)) + p_start = (p_page - 1) * per_page + prod_page = prod[p_start:p_start + per_page] + + ctx = base_context(request, db, user) + ctx.update({ + "app_name": APP_NAME, + "run": run, "entries": entries, "stats": stats, + "hprod": hprod_page, "prod": prod_page, + "hprod_total": hp_total, "prod_total": p_total, + "hp_page": hp_page, "hp_total_pages": hp_total_pages, + "p_page": p_page, "p_total_pages": p_total_pages, + "per_page": per_page, + "prod_ok": prod_ok, + "filters": {"search": search, "status": status, "domain": domain}, + "msg": request.query_params.get("msg"), + }) + return templates.TemplateResponse("quickwin_detail.html", ctx) + + +@router.post("/quickwin/{run_id}/delete") +async def quickwin_delete(request: Request, run_id: int, 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, "campaigns"): + return RedirectResponse(url="/quickwin") + delete_run(db, run_id) + return RedirectResponse(url="/quickwin?msg=deleted", status_code=303) + + +# -- API JSON -- + +@router.post("/api/quickwin/entry/update") +async def quickwin_entry_update(request: Request, db=Depends(get_db)): + user = get_current_user(request) + if not user: + return JSONResponse({"error": "unauthorized"}, 401) + body = await request.json() + entry_id = body.get("id") + field = body.get("field") + value = body.get("value") + if not entry_id or not field: + return JSONResponse({"error": "id and field required"}, 400) + ok = update_entry_field(db, entry_id, field, value) + return JSONResponse({"ok": ok}) + + +@router.post("/api/quickwin/inject-yum-history") +async def quickwin_inject_yum(request: Request, db=Depends(get_db)): + user = get_current_user(request) + if not user: + return JSONResponse({"error": "unauthorized"}, 401) + body = await request.json() + if not isinstance(body, list): + return JSONResponse({"error": "expected list"}, 400) + updated, inserted = inject_yum_history(db, body) + return JSONResponse({"ok": True, "updated": updated, "inserted": inserted}) + + +@router.get("/api/quickwin/prod-check/{run_id}") +async def quickwin_prod_check(request: Request, run_id: int, db=Depends(get_db)): + """Verifie si le prod peut demarrer (tous hprod termines)""" + user = get_current_user(request) + if not user: + return JSONResponse({"error": "unauthorized"}, 401) + ok = can_start_prod(db, run_id) + return JSONResponse({"can_start_prod": ok}) diff --git a/app/routers/servers.py b/app/routers/servers.py index 64ca80e..e68e381 100644 --- a/app/routers/servers.py +++ b/app/routers/servers.py @@ -18,13 +18,14 @@ templates = Jinja2Templates(directory="app/templates") 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), 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, "search": search} + filters = {"domain": domain, "env": env, "tier": tier, "etat": etat, "os": os, "owner": owner, "search": search} servers, total = list_servers(db, filters, page, sort=sort, sort_dir=sort_dir) domains_list, envs_list = get_reference_data(db) @@ -41,12 +42,13 @@ async def servers_list(request: Request, db=Depends(get_db), async def servers_export_csv(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), search: str = Query(None)): user = get_current_user(request) if not user: return RedirectResponse(url="/login") import io, csv - filters = {"domain": domain, "env": env, "tier": tier, "etat": etat, "search": search} + filters = {"domain": domain, "env": env, "tier": tier, "etat": etat, "os": os, "owner": owner, "search": search} servers, total = list_servers(db, filters, page=1, per_page=99999, sort="hostname", sort_dir="asc") output = io.StringIO() w = csv.writer(output, delimiter=";") diff --git a/app/services/quickwin_service.py b/app/services/quickwin_service.py new file mode 100644 index 0000000..115e5d0 --- /dev/null +++ b/app/services/quickwin_service.py @@ -0,0 +1,254 @@ +"""Service QuickWin — gestion des campagnes + exclusions par serveur""" +import json +from sqlalchemy import text + +# Exclusions generales par defaut (reboot packages + middleware/apps) +DEFAULT_GENERAL_EXCLUDES = ( + "dbus* dracut* glibc* grub2* kernel* kexec-tools* " + "libselinux* linux-firmware* microcode_ctl* mokutil* " + "net-snmp* NetworkManager* network-scripts* nss* openssl-libs* " + "polkit* selinux-policy* shim* systemd* tuned*" +) + + +def get_server_configs(db, server_ids=None): + """Retourne les configs QuickWin pour les serveurs (ou tous)""" + if server_ids: + rows = db.execute(text(""" + SELECT qc.*, s.hostname, s.os_family, s.tier, + d.name as domaine, e.name as environnement, + z.name as zone + FROM quickwin_server_config qc + JOIN servers s ON qc.server_id = s.id + LEFT JOIN domain_environments de ON s.domain_env_id = de.id + LEFT JOIN domains d ON de.domain_id = d.id + LEFT JOIN environments e ON de.environment_id = e.id + LEFT JOIN zones z ON s.zone_id = z.id + WHERE qc.server_id = ANY(:ids) + ORDER BY s.hostname + """), {"ids": server_ids}).fetchall() + else: + rows = db.execute(text(""" + SELECT qc.*, s.hostname, s.os_family, s.tier, + d.name as domaine, e.name as environnement, + z.name as zone + FROM quickwin_server_config qc + JOIN servers s ON qc.server_id = s.id + LEFT JOIN domain_environments de ON s.domain_env_id = de.id + LEFT JOIN domains d ON de.domain_id = d.id + LEFT JOIN environments e ON de.environment_id = e.id + LEFT JOIN zones z ON s.zone_id = z.id + ORDER BY s.hostname + """)).fetchall() + return rows + + +def upsert_server_config(db, server_id, general_excludes=None, specific_excludes="", notes=""): + """Cree ou met a jour la config QuickWin d'un serveur. + Si general_excludes est vide lors de la creation, applique DEFAULT_GENERAL_EXCLUDES.""" + existing = db.execute(text( + "SELECT id FROM quickwin_server_config WHERE server_id = :sid" + ), {"sid": server_id}).fetchone() + if existing: + ge = general_excludes if general_excludes is not None else DEFAULT_GENERAL_EXCLUDES + db.execute(text(""" + UPDATE quickwin_server_config + SET general_excludes = :ge, specific_excludes = :se, notes = :n, updated_at = now() + WHERE server_id = :sid + """), {"sid": server_id, "ge": ge, "se": specific_excludes, "n": notes}) + else: + ge = general_excludes if general_excludes else DEFAULT_GENERAL_EXCLUDES + db.execute(text(""" + INSERT INTO quickwin_server_config (server_id, general_excludes, specific_excludes, notes) + VALUES (:sid, :ge, :se, :n) + """), {"sid": server_id, "ge": ge, "se": specific_excludes, "n": notes}) + db.commit() + + +def delete_server_config(db, config_id): + db.execute(text("DELETE FROM quickwin_server_config WHERE id = :id"), {"id": config_id}) + db.commit() + + +def get_eligible_servers(db): + """Serveurs Linux en_production, patch_os_owner=secops""" + return db.execute(text(""" + SELECT s.id, s.hostname, s.os_family, s.os_version, s.machine_type, + s.tier, s.etat, s.patch_excludes, s.is_flux_libre, s.is_podman, + d.name as domaine, d.code as domain_code, + e.name as environnement, e.code as env_code, + COALESCE(qc.general_excludes, '') as qw_general_excludes, + COALESCE(qc.specific_excludes, '') as qw_specific_excludes + FROM servers s + LEFT JOIN domain_environments de ON s.domain_env_id = de.id + LEFT JOIN domains d ON de.domain_id = d.id + LEFT JOIN environments e ON de.environment_id = e.id + LEFT JOIN quickwin_server_config qc ON qc.server_id = s.id + WHERE s.os_family = 'linux' + AND s.etat = 'en_production' + AND s.patch_os_owner = 'secops' + ORDER BY e.display_order, d.display_order, s.hostname + """)).fetchall() + + +# -- Runs -- + +def list_runs(db): + return db.execute(text(""" + SELECT r.*, + u.display_name as created_by_name, + (SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id) as total_entries, + (SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.status = 'patched') as patched_count, + (SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.status = 'failed') as failed_count, + (SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.branch = 'hprod') as hprod_count, + (SELECT COUNT(*) FROM quickwin_entries e WHERE e.run_id = r.id AND e.branch = 'prod') as prod_count + FROM quickwin_runs r + LEFT JOIN users u ON r.created_by = u.id + ORDER BY r.year DESC, r.week_number DESC, r.id DESC + """)).fetchall() + + +def get_run(db, run_id): + return db.execute(text(""" + SELECT r.*, u.display_name as created_by_name + FROM quickwin_runs r LEFT JOIN users u ON r.created_by = u.id + WHERE r.id = :id + """), {"id": run_id}).fetchone() + + +def get_run_entries(db, run_id): + return db.execute(text(""" + SELECT qe.*, s.hostname, s.os_family, s.machine_type, + d.name as domaine, e.name as environnement + FROM quickwin_entries qe + JOIN servers s ON qe.server_id = s.id + 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 + WHERE qe.run_id = :rid + ORDER BY qe.branch, s.hostname + """), {"rid": run_id}).fetchall() + + +def create_run(db, year, week_number, label, user_id, server_ids, notes=""): + """Cree un run QuickWin avec les serveurs selectionnes. + Classe auto en hprod/prod selon l'environnement du serveur.""" + row = db.execute(text(""" + INSERT INTO quickwin_runs (year, week_number, label, created_by, notes) + VALUES (:y, :w, :l, :uid, :n) RETURNING id + """), {"y": year, "w": week_number, "l": label, "uid": user_id, "n": notes}).fetchone() + run_id = row.id + + for sid in server_ids: + srv = db.execute(text(""" + SELECT s.id, e.name as env_name, + COALESCE(qc.general_excludes, '') as ge, + COALESCE(qc.specific_excludes, '') as se + FROM servers s + LEFT JOIN domain_environments de ON s.domain_env_id = de.id + LEFT JOIN environments e ON de.environment_id = e.id + LEFT JOIN quickwin_server_config qc ON qc.server_id = s.id + WHERE s.id = :sid + """), {"sid": sid}).fetchone() + if not srv: + continue + branch = "prod" if srv.env_name and "production" in srv.env_name.lower() else "hprod" + ge = srv.ge if srv.ge else DEFAULT_GENERAL_EXCLUDES + db.execute(text(""" + INSERT INTO quickwin_entries (run_id, server_id, branch, general_excludes, specific_excludes) + VALUES (:rid, :sid, :br, :ge, :se) + """), {"rid": run_id, "sid": sid, "br": branch, "ge": ge, "se": srv.se}) + + db.commit() + return run_id + + +def delete_run(db, run_id): + db.execute(text("DELETE FROM quickwin_entries WHERE run_id = :rid"), {"rid": run_id}) + db.execute(text("DELETE FROM quickwin_runs WHERE id = :rid"), {"rid": run_id}) + db.commit() + + +def update_entry_status(db, entry_id, status, patch_output="", packages_count=0, + packages="", reboot_required=False, notes=""): + db.execute(text(""" + UPDATE quickwin_entries SET + status = :st, patch_output = :po, patch_packages_count = :pc, + patch_packages = :pp, reboot_required = :rb, notes = :n, + patch_date = CASE WHEN :st IN ('patched','failed') THEN now() ELSE patch_date END, + updated_at = now() + WHERE id = :id + """), {"id": entry_id, "st": status, "po": patch_output, "pc": packages_count, + "pp": packages, "rb": reboot_required, "n": notes}) + db.commit() + + +def update_entry_field(db, entry_id, field, value): + """Mise a jour d'un champ unique (pour inline edit)""" + allowed = ("general_excludes", "specific_excludes", "notes", "status", + "snap_done", "prereq_ok", "prereq_detail", "dryrun_output") + if field not in allowed: + return False + db.execute(text(f"UPDATE quickwin_entries SET {field} = :val, updated_at = now() WHERE id = :id"), + {"val": value, "id": entry_id}) + db.commit() + return True + + +def can_start_prod(db, run_id): + """Verifie que tous les hprod sont termines avant d'autoriser le prod""" + pending = db.execute(text(""" + SELECT COUNT(*) as cnt FROM quickwin_entries + WHERE run_id = :rid AND branch = 'hprod' AND status IN ('pending', 'in_progress') + """), {"rid": run_id}).fetchone() + return pending.cnt == 0 + + +def get_run_stats(db, run_id): + return db.execute(text(""" + SELECT + COUNT(*) as total, + COUNT(*) FILTER (WHERE branch = 'hprod') as hprod_total, + COUNT(*) FILTER (WHERE branch = 'prod') as prod_total, + COUNT(*) FILTER (WHERE status = 'patched') as patched, + COUNT(*) FILTER (WHERE status = 'failed') as failed, + COUNT(*) FILTER (WHERE status = 'pending') as pending, + COUNT(*) FILTER (WHERE status = 'excluded') as excluded, + COUNT(*) FILTER (WHERE status = 'skipped') as skipped, + COUNT(*) FILTER (WHERE branch = 'hprod' AND status = 'patched') as hprod_patched, + COUNT(*) FILTER (WHERE branch = 'prod' AND status = 'patched') as prod_patched, + COUNT(*) FILTER (WHERE reboot_required) as reboot_count + FROM quickwin_entries WHERE run_id = :rid + """), {"rid": run_id}).fetchone() + + +def inject_yum_history(db, data): + """Injecte l'historique yum dans quickwin_server_config. + data = [{"server": "hostname", "yum_commands": [...]}]""" + updated = 0 + inserted = 0 + for item in data: + hostname = item.get("server", item.get("server_name", "")).strip() + if not hostname: + continue + srv = db.execute(text("SELECT id FROM servers WHERE hostname = :h"), {"h": hostname}).fetchone() + if not srv: + continue + cmds = json.dumps(item.get("yum_commands", item.get("last_yum_commands", [])), ensure_ascii=False) + existing = db.execute(text( + "SELECT id FROM quickwin_server_config WHERE server_id = :sid" + ), {"sid": srv.id}).fetchone() + if existing: + db.execute(text(""" + UPDATE quickwin_server_config SET last_yum_commands = :cmds::jsonb, updated_at = now() + WHERE server_id = :sid + """), {"sid": srv.id, "cmds": cmds}) + updated += 1 + else: + db.execute(text(""" + INSERT INTO quickwin_server_config (server_id, last_yum_commands) + VALUES (:sid, :cmds::jsonb) + """), {"sid": srv.id, "cmds": cmds}) + inserted += 1 + db.commit() + return updated, inserted diff --git a/app/services/server_service.py b/app/services/server_service.py index e09b3c2..ba8f2d6 100644 --- a/app/services/server_service.py +++ b/app/services/server_service.py @@ -119,6 +119,11 @@ def list_servers(db, filters, page=1, per_page=50, sort="hostname", sort_dir="as where.append("s.licence_support = 'eol'") else: where.append("s.etat = :etat"); params["etat"] = filters["etat"] + where.append("COALESCE(s.licence_support, '') != 'eol'") + 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("search"): where.append("s.hostname ILIKE :search"); params["search"] = f"%{filters['search']}%" diff --git a/app/templates/base.html b/app/templates/base.html index bbe27f1..d1191b3 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -51,7 +51,7 @@