""" Routes API pour la gestion des tâches. """ import uuid import asyncio from datetime import datetime, timezone from pathlib import Path from typing import Optional from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.constants import ACTION_PLAYBOOK_MAP, ACTION_DISPLAY_NAMES from app.core.dependencies import get_db, verify_api_key from app.crud.task import TaskRepository from app.crud.log import LogRepository from app.schemas.task_api import TaskRequest from app.services import ws_manager, db, ansible_service, notification_service from app.services.task_log_service import TaskLogService from app.models.database import async_session_maker from app.schemas.task_api import Task from app.schemas.common import LogEntry from time import perf_counter router = APIRouter() # Instance du service de logs de tâches task_log_service = TaskLogService(settings.tasks_logs_dir) # Dictionnaire des tâches en cours d'exécution running_task_handles = {} @router.get("") async def get_tasks( limit: int = 100, offset: int = 0, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Récupère la liste des tâches.""" repo = TaskRepository(db_session) tasks = await repo.list(limit=limit, offset=offset) return [ { "id": t.id, "name": t.action, "host": t.target, "status": t.status, "progress": 100 if t.status == "completed" else (50 if t.status == "running" else 0), "start_time": t.started_at, "end_time": t.completed_at, "duration": None, "output": t.result_data.get("output") if t.result_data else None, "error": t.error_message, } for t in tasks ] @router.get("/running") async def get_running_tasks( api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Récupère les tâches en cours d'exécution.""" repo = TaskRepository(db_session) tasks = await repo.list(limit=100, offset=0) running_tasks = [t for t in tasks if t.status in ("running", "pending")] return { "tasks": [ { "id": t.id, "name": t.action, "host": t.target, "status": t.status, "progress": 50 if t.status == "running" else 0, "start_time": t.started_at, "end_time": t.completed_at, } for t in running_tasks ], "count": len(running_tasks) } @router.get("/logs") async def get_task_logs( status: Optional[str] = None, year: Optional[str] = None, month: Optional[str] = None, day: Optional[str] = None, hour_start: Optional[str] = None, hour_end: Optional[str] = None, target: Optional[str] = None, category: Optional[str] = None, source_type: Optional[str] = None, limit: int = 50, offset: int = 0, api_key_valid: bool = Depends(verify_api_key) ): """Récupère les logs de tâches depuis les fichiers markdown.""" logs, total_count = task_log_service.get_task_logs( year=year, month=month, day=day, status=status, target=target, category=category, source_type=source_type, hour_start=hour_start, hour_end=hour_end, limit=limit, offset=offset ) return { "logs": [log.dict() for log in logs], "count": len(logs), "total_count": total_count, "has_more": offset + len(logs) < total_count, "filters": { "status": status, "year": year, "month": month, "day": day, "hour_start": hour_start, "hour_end": hour_end, "target": target, "source_type": source_type }, "pagination": { "limit": limit, "offset": offset } } @router.get("/logs/dates") async def get_task_logs_dates(api_key_valid: bool = Depends(verify_api_key)): """Récupère les dates disponibles pour le filtrage.""" return task_log_service.get_available_dates() @router.get("/logs/stats") async def get_task_logs_stats(api_key_valid: bool = Depends(verify_api_key)): """Récupère les statistiques des logs de tâches.""" return task_log_service.get_stats() @router.get("/logs/{log_id}") async def get_task_log_content(log_id: str, api_key_valid: bool = Depends(verify_api_key)): """Récupère le contenu d'un log de tâche spécifique.""" logs, _ = task_log_service.get_task_logs(limit=0) log = next((l for l in logs if l.id == log_id), None) if not log: raise HTTPException(status_code=404, detail="Log non trouvé") try: content = Path(log.path).read_text(encoding='utf-8') return { "log": log.dict(), "content": content } except Exception as e: raise HTTPException(status_code=500, detail=f"Erreur lecture du fichier: {str(e)}") @router.delete("/logs/{log_id}") async def delete_task_log(log_id: str, api_key_valid: bool = Depends(verify_api_key)): """Supprime un fichier de log de tâche.""" logs, _ = task_log_service.get_task_logs(limit=0) log = next((l for l in logs if l.id == log_id), None) if not log: raise HTTPException(status_code=404, detail="Log non trouvé") try: log_path = Path(log.path) if log_path.exists(): log_path.unlink() # Le TaskLogService met en cache l'index des fichiers pendant ~60s. # Invalider immédiatement pour que la suppression soit visible instantanément. task_log_service.invalidate_index() await ws_manager.broadcast({ "type": "task_log_deleted", "data": {"id": log_id} }) return {"message": "Log supprimé", "id": log_id} except Exception as e: raise HTTPException(status_code=500, detail=f"Erreur suppression: {str(e)}") @router.post("/{task_id}/cancel") async def cancel_task( task_id: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Annule une tâche en cours d'exécution.""" repo = TaskRepository(db_session) task = await repo.get(task_id) if not task: raise HTTPException(status_code=404, detail="Tâche non trouvée") if task.status not in ("running", "pending"): raise HTTPException(status_code=400, detail=f"La tâche n'est pas en cours (statut: {task.status})") # Marquer comme annulée if task_id in running_task_handles: running_task_handles[task_id]["cancelled"] = True async_task = running_task_handles[task_id].get("asyncio_task") if async_task and not async_task.done(): async_task.cancel() process = running_task_handles[task_id].get("process") if process: try: process.terminate() await asyncio.sleep(0.5) if process.returncode is None: process.kill() except Exception: pass del running_task_handles[task_id] await repo.update( task, status="cancelled", completed_at=datetime.now(timezone.utc), error_message="Tâche annulée par l'utilisateur" ) await db_session.commit() # Log log_repo = LogRepository(db_session) await log_repo.create( level="WARNING", message=f"Tâche '{task.action}' annulée manuellement", source="task", task_id=task_id, ) await db_session.commit() await ws_manager.broadcast({ "type": "task_cancelled", "data": { "id": task_id, "status": "cancelled", "message": "Tâche annulée par l'utilisateur" } }) return { "success": True, "message": f"Tâche {task_id} annulée avec succès", "task_id": task_id } @router.get("/{task_id}") async def get_task( task_id: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Récupère une tâche spécifique.""" repo = TaskRepository(db_session) task = await repo.get(task_id) if not task: raise HTTPException(status_code=404, detail="Tâche non trouvée") return { "id": task.id, "name": task.action, "host": task.target, "status": task.status, "progress": 100 if task.status == "completed" else (50 if task.status == "running" else 0), "start_time": task.started_at, "end_time": task.completed_at, "duration": None, "output": task.result_data.get("output") if task.result_data else None, "error": task.error_message, } @router.delete("/{task_id}") async def delete_task( task_id: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Supprime une tâche.""" repo = TaskRepository(db_session) task = await repo.get(task_id) if not task: raise HTTPException(status_code=404, detail="Tâche non trouvée") await db_session.delete(task) await db_session.commit() await ws_manager.broadcast({ "type": "task_deleted", "data": {"id": task_id} }) return {"message": "Tâche supprimée avec succès"} @router.post("") async def create_task( task_request: TaskRequest, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Crée une nouvelle tâche et exécute le playbook correspondant.""" repo = TaskRepository(db_session) task_id = uuid.uuid4().hex target = task_request.host or task_request.group or "all" playbook = ACTION_PLAYBOOK_MAP.get(task_request.action) task_obj = await repo.create( id=task_id, action=task_request.action, target=target, playbook=playbook, status="running", ) await repo.update(task_obj, started_at=datetime.now(timezone.utc)) await db_session.commit() task_name = ACTION_DISPLAY_NAMES.get(task_request.action, f"Tâche {task_request.action}") response_data = { "id": task_obj.id, "name": task_name, "host": target, "status": "running", "progress": 0, "start_time": task_obj.started_at, "end_time": None, "duration": None, "output": None, "error": None, } await ws_manager.broadcast({ "type": "task_created", "data": response_data }) # Exécuter le playbook en arrière-plan if playbook: asyncio.create_task(_execute_task_playbook( task_id=task_id, task_name=task_name, playbook=playbook, target=target, extra_vars=task_request.extra_vars, check_mode=task_request.dry_run if hasattr(task_request, 'dry_run') else False )) return response_data async def _execute_task_playbook( task_id: str, task_name: str, playbook: str, target: str, extra_vars: dict = None, check_mode: bool = False ): """Exécute un playbook Ansible en arrière-plan et met à jour le statut.""" start_time = perf_counter() # Créer une tâche en mémoire pour le service de logs mem_task = Task( id=task_id, name=task_name, host=target, status="running", progress=10, start_time=datetime.now(timezone.utc) ) # Notifier la progression await ws_manager.broadcast({ "type": "task_progress", "data": {"id": task_id, "progress": 10, "message": "Démarrage du playbook..."} }) try: # Exécuter le playbook result = await ansible_service.execute_playbook( playbook=playbook, target=target, extra_vars=extra_vars, check_mode=check_mode, verbose=True ) execution_time = perf_counter() - start_time success = result.get("success", False) # Mettre à jour la tâche en mémoire mem_task.status = "completed" if success else "failed" mem_task.progress = 100 mem_task.end_time = datetime.now(timezone.utc) mem_task.duration = f"{execution_time:.1f}s" mem_task.output = result.get("stdout", "") mem_task.error = result.get("stderr", "") if not success else None # Mettre à jour en BD async with async_session_maker() as session: repo = TaskRepository(session) db_task = await repo.get(task_id) if db_task: await repo.update( db_task, status=mem_task.status, completed_at=mem_task.end_time, error_message=mem_task.error, result_data={"output": result.get("stdout", "")[:5000]} ) await session.commit() # Sauvegarder le log markdown try: log_path = task_log_service.save_task_log( task=mem_task, output=result.get("stdout", ""), error=result.get("stderr", "") ) created_log = task_log_service.index_log_file(log_path) if created_log: await ws_manager.broadcast({ "type": "task_log_created", "data": created_log.dict() }) except Exception as log_error: print(f"Erreur sauvegarde log markdown: {log_error}") # Notifier la fin via WebSocket await ws_manager.broadcast({ "type": "task_completed", "data": { "id": task_id, "status": mem_task.status, "progress": 100, "duration": mem_task.duration, "success": success } }) # Notification ntfy if success: await notification_service.notify_task_completed( task_name=task_name, target=target, duration=mem_task.duration ) else: await notification_service.notify_task_failed( task_name=task_name, target=target, error=result.get("stderr", "Erreur inconnue")[:200] ) except Exception as e: execution_time = perf_counter() - start_time error_msg = str(e) mem_task.status = "failed" mem_task.end_time = datetime.now(timezone.utc) mem_task.duration = f"{execution_time:.1f}s" mem_task.error = error_msg # Mettre à jour en BD try: async with async_session_maker() as session: repo = TaskRepository(session) db_task = await repo.get(task_id) if db_task: await repo.update( db_task, status="failed", completed_at=mem_task.end_time, error_message=error_msg ) await session.commit() except Exception as db_error: print(f"Erreur mise à jour BD: {db_error}") # Sauvegarder le log markdown try: log_path = task_log_service.save_task_log(task=mem_task, error=error_msg) created_log = task_log_service.index_log_file(log_path) if created_log: await ws_manager.broadcast({ "type": "task_log_created", "data": created_log.dict() }) except Exception: pass # Notifier l'échec await ws_manager.broadcast({ "type": "task_failed", "data": { "id": task_id, "status": "failed", "error": error_msg } }) await notification_service.notify_task_failed( task_name=task_name, target=target, error=error_msg[:200] )