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>
213 lines
9.2 KiB
Python
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)
|