patchcenter/app/routers/campaigns.py
Khalid MOUTAOUAKIL 8277653c43 PatchCenter v2.0 — Initial commit
Modules: Dashboard, Serveurs, Campagnes, Planning, Specifiques, Settings, Users
Stack: FastAPI + Jinja2 + HTMX + Alpine.js + TailwindCSS + PostgreSQL
Features: Qualys sync, prereqs auto, planning annuel, server specifics, role-based access

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:00:12 +02:00

213 lines
9.2 KiB
Python

"""Router campagnes — creation depuis planning + gestion exclusions"""
from datetime import datetime
from fastapi import APIRouter, Request, Depends, Query, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import text
from ..dependencies import get_db, get_current_user
from ..services.campaign_service import (
list_campaigns, get_campaign, get_campaign_sessions, get_campaign_stats,
create_campaign_from_planning, get_servers_for_planning,
update_campaign_status, exclude_session, restore_session,
validate_prereq, get_prereq_stats, can_plan_campaign,
bulk_auto_exclude_failed_prereqs,
)
from ..services.prereq_service import check_prereqs_campaign, check_single_prereq
from ..config import APP_NAME
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
EXCLUSION_REASONS = [
("eol", "Fin de vie (EOL)"),
("creneau_inadequat", "Creneau non adequat"),
("intervention_non_secops", "Intervention non-SecOps prevue"),
("report_cycle", "Report au cycle suivant"),
("non_patchable", "Serveur non patchable"),
("autre", "Autre"),
]
@router.get("/campaigns", response_class=HTMLResponse)
async def campaigns_list(request: Request, db=Depends(get_db),
year: int = Query(None), status: str = Query(None)):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
if not year:
year = datetime.now().year
campaigns = list_campaigns(db, year=year, status=status)
# Semaines planifiees pour cette annee (pour le formulaire de creation)
now = datetime.now()
current_week = now.isocalendar()[1]
planned_weeks = db.execute(text("""
SELECT DISTINCT pp.week_number, pp.week_code, pp.week_start, pp.week_end,
string_agg(DISTINCT d.name || ' (' || pp.env_scope || ')', ', ' ORDER BY d.name || ' (' || pp.env_scope || ')') as scope
FROM patch_planning pp
LEFT JOIN domains d ON pp.domain_code = d.code
WHERE pp.year = :y AND pp.status = 'open' AND pp.domain_code IS NOT NULL
AND pp.week_number >= :cw
GROUP BY pp.week_number, pp.week_code, pp.week_start, pp.week_end
ORDER BY pp.week_number
"""), {"y": year, "cw": current_week}).fetchall()
return templates.TemplateResponse("campaigns.html", {
"request": request, "user": user, "app_name": APP_NAME,
"campaigns": campaigns, "year": year, "status_filter": status,
"planned_weeks": planned_weeks,
})
@router.get("/campaigns/preview", response_class=HTMLResponse)
async def campaign_preview(request: Request, db=Depends(get_db),
year: int = Query(...), week: int = Query(...)):
"""HTMX: preview des serveurs pour une semaine du planning"""
user = get_current_user(request)
if not user:
return HTMLResponse("<p>Non autorise</p>")
servers, planning = get_servers_for_planning(db, year, week)
scope = ", ".join(set(f"{p.domain_name} ({p.env_scope})" for p in planning if p.domain_code))
return templates.TemplateResponse("partials/campaign_preview.html", {
"request": request, "servers": servers, "scope": scope,
"week": week, "year": year, "count": len(servers),
})
@router.post("/campaigns/create")
async def campaign_create(request: Request, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
form = await request.form()
year = int(form.get("year", datetime.now().year))
week = int(form.get("week_number", 0))
label = form.get("label", f"Patch S{week:02d} {year}")
# Serveurs exclus (checkboxes non cochees)
excluded = []
for key in form.keys():
if key.startswith("exclude_"):
sid = int(key.replace("exclude_", ""))
excluded.append(sid)
cid = create_campaign_from_planning(db, year, week, label, user.get("uid"), excluded)
if not cid:
return RedirectResponse(url=f"/campaigns?year={year}&msg=no_servers", status_code=303)
return RedirectResponse(url=f"/campaigns/{cid}", status_code=303)
@router.get("/campaigns/{campaign_id}", response_class=HTMLResponse)
async def campaign_detail(request: Request, campaign_id: int, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
campaign = get_campaign(db, campaign_id)
if not campaign:
return RedirectResponse(url="/campaigns")
sessions = get_campaign_sessions(db, campaign_id)
stats = get_campaign_stats(db, campaign_id)
prereq = get_prereq_stats(db, campaign_id)
can_plan = can_plan_campaign(db, campaign_id)
return templates.TemplateResponse("campaign_detail.html", {
"request": request, "user": user, "app_name": APP_NAME,
"c": campaign, "sessions": sessions, "stats": stats,
"prereq": prereq, "can_plan": can_plan,
"exclusion_reasons": EXCLUSION_REASONS,
"msg": request.query_params.get("msg"),
})
@router.post("/campaigns/{campaign_id}/status")
async def campaign_status_change(request: Request, campaign_id: int,
db=Depends(get_db), new_status: str = Form(...)):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
# Bloquer planned si prereqs non valides
if new_status == "planned" and not can_plan_campaign(db, campaign_id):
return RedirectResponse(url=f"/campaigns/{campaign_id}?msg=prereq_needed", status_code=303)
update_campaign_status(db, campaign_id, new_status)
return RedirectResponse(url=f"/campaigns/{campaign_id}", status_code=303)
@router.post("/campaigns/session/{session_id}/prereq")
async def session_prereq(request: Request, session_id: int, db=Depends(get_db),
prereq_ssh: str = Form(...), prereq_satellite: str = Form(...),
rollback_method: str = Form(""), rollback_justif: str = Form("")):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
validate_prereq(db, session_id, prereq_ssh, prereq_satellite,
rollback_method or None, rollback_justif, user.get("sub"))
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
{"id": session_id}).fetchone()
cid = row.campaign_id if row else 0
return RedirectResponse(url=f"/campaigns/{cid}?msg=prereq_saved#row-{session_id}", status_code=303)
@router.post("/campaigns/{campaign_id}/auto-exclude-failed")
async def auto_exclude_failed(request: Request, campaign_id: int, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
count = bulk_auto_exclude_failed_prereqs(db, campaign_id, user.get("sub"))
return RedirectResponse(url=f"/campaigns/{campaign_id}?msg=auto_excluded_{count}", status_code=303)
@router.post("/campaigns/{campaign_id}/check-prereqs")
async def campaign_check_prereqs(request: Request, campaign_id: int, db=Depends(get_db)):
"""Lance la verification automatique des prereqs pour toute la campagne"""
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
checked, auto_excluded = check_prereqs_campaign(db, campaign_id)
return RedirectResponse(
url=f"/campaigns/{campaign_id}?msg=checked_{checked}_{auto_excluded}",
status_code=303
)
@router.post("/campaigns/session/{session_id}/check-prereq")
async def session_check_prereq(request: Request, session_id: int, db=Depends(get_db)):
"""Lance la verification prereq pour un seul serveur"""
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
check_single_prereq(db, session_id)
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
{"id": session_id}).fetchone()
cid = row.campaign_id if row else 0
return RedirectResponse(url=f"/campaigns/{cid}?msg=prereq_checked#row-{session_id}", status_code=303)
@router.post("/campaigns/session/{session_id}/exclude")
async def session_exclude(request: Request, session_id: int, db=Depends(get_db),
reason: str = Form(...), detail: str = Form("")):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
exclude_session(db, session_id, reason, detail, user.get("sub"))
# Retrouver campaign_id
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
{"id": session_id}).fetchone()
cid = row.campaign_id if row else 0
return RedirectResponse(url=f"/campaigns/{cid}?msg=excluded#row-{session_id}", status_code=303)
@router.post("/campaigns/session/{session_id}/restore")
async def session_restore(request: Request, session_id: int, db=Depends(get_db)):
user = get_current_user(request)
if not user:
return RedirectResponse(url="/login")
restore_session(db, session_id)
row = db.execute(text("SELECT campaign_id FROM patch_sessions WHERE id = :id"),
{"id": session_id}).fetchone()
cid = row.campaign_id if row else 0
return RedirectResponse(url=f"/campaigns/{cid}?msg=restored#row-{session_id}", status_code=303)