""" Routes API pour la gestion des schedules. """ import json import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.core.dependencies import get_db, verify_api_key from app.crud.schedule import ScheduleRepository from app.crud.schedule_run import ScheduleRunRepository from app.crud.log import LogRepository from app.schemas.schedule_api import ( Schedule, ScheduleCreateRequest, ScheduleUpdateRequest, ) from app.services import ansible_service, ws_manager, scheduler_service router = APIRouter() @router.get("") async def get_schedules( enabled: Optional[bool] = None, playbook: Optional[str] = None, tag: Optional[str] = None, limit: int = 100, offset: int = 0, api_key_valid: bool = Depends(verify_api_key), ): """Liste tous les schedules.""" schedules = scheduler_service.get_all_schedules( enabled=enabled, playbook=playbook, tag=tag, ) paginated = schedules[offset:offset + limit] results = [] for s in paginated: rec = s.recurrence results.append({ "id": s.id, "name": s.name, "playbook": s.playbook, "target": s.target, "schedule_type": s.schedule_type, "recurrence": rec.model_dump() if rec else None, "enabled": s.enabled, "notification_type": getattr(s, 'notification_type', 'all'), "tags": s.tags, "next_run_at": s.next_run_at, "last_run_at": s.last_run_at, "last_status": s.last_status, "run_count": s.run_count, "success_count": s.success_count, "failure_count": s.failure_count, "created_at": s.created_at, "updated_at": s.updated_at, }) return {"schedules": results, "count": len(schedules)} @router.get("/stats") async def get_schedules_stats(api_key_valid: bool = Depends(verify_api_key)): """Récupère les statistiques des schedules.""" stats = scheduler_service.get_stats() upcoming = scheduler_service.get_upcoming_executions(limit=5) return { "stats": stats.dict(), "upcoming": upcoming } @router.get("/upcoming") async def get_upcoming_schedules( limit: int = 10, api_key_valid: bool = Depends(verify_api_key) ): """Récupère les prochaines exécutions planifiées.""" upcoming = scheduler_service.get_upcoming_executions(limit=limit) return { "upcoming": upcoming, "count": len(upcoming) } @router.get("/validate-cron") async def validate_cron_expression( expression: str, api_key_valid: bool = Depends(verify_api_key) ): """Valide une expression cron.""" result = scheduler_service.validate_cron_expression(expression) return result @router.get("/{schedule_id}") async def get_schedule( schedule_id: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Récupère les détails d'un schedule.""" repo = ScheduleRepository(db_session) schedule = await repo.get(schedule_id) if not schedule: raise HTTPException(status_code=404, detail=f"Schedule '{schedule_id}' non trouvé") return { "id": schedule.id, "name": schedule.name, "playbook": schedule.playbook, "target": schedule.target, "schedule_type": schedule.schedule_type, "recurrence_type": schedule.recurrence_type, "recurrence_time": schedule.recurrence_time, "recurrence_days": json.loads(schedule.recurrence_days) if schedule.recurrence_days else None, "cron_expression": schedule.cron_expression, "enabled": schedule.enabled, "notification_type": schedule.notification_type or "all", "tags": json.loads(schedule.tags) if schedule.tags else [], "next_run": schedule.next_run, "last_run": schedule.last_run, "created_at": schedule.created_at, "updated_at": schedule.updated_at, } @router.post("") async def create_schedule( request: ScheduleCreateRequest, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Crée un nouveau schedule.""" # Vérifier que le playbook existe playbooks = ansible_service.get_playbooks() playbook_names = [p['filename'] for p in playbooks] + [p['name'] for p in playbooks] playbook_file = request.playbook if not playbook_file.endswith(('.yml', '.yaml')): playbook_file = f"{playbook_file}.yml" if playbook_file not in playbook_names and request.playbook not in playbook_names: raise HTTPException(status_code=400, detail=f"Playbook '{request.playbook}' non trouvé") # Récupérer les infos du playbook pour validation playbook_info = next((pb for pb in playbooks if pb['filename'] == playbook_file or pb['name'] == request.playbook), None) # Vérifier la cible if request.target_type == "group": groups = ansible_service.get_groups() if request.target not in groups and request.target != "all": raise HTTPException(status_code=400, detail=f"Groupe '{request.target}' non trouvé") else: if not ansible_service.host_exists(request.target): raise HTTPException(status_code=400, detail=f"Hôte '{request.target}' non trouvé") # Valider la compatibilité playbook-target if playbook_info: playbook_hosts = playbook_info.get('hosts', 'all') if not ansible_service.is_target_compatible_with_playbook(request.target, playbook_hosts): raise HTTPException( status_code=400, detail=f"Le playbook '{request.playbook}' (hosts: {playbook_hosts}) n'est pas compatible avec la cible '{request.target}'." ) # Valider la récurrence if request.schedule_type == "recurring" and not request.recurrence: raise HTTPException(status_code=400, detail="La récurrence est requise pour un schedule récurrent") if request.recurrence and request.recurrence.type == "custom": if not request.recurrence.cron_expression: raise HTTPException(status_code=400, detail="Expression cron requise pour le type 'custom'") validation = scheduler_service.validate_cron_expression(request.recurrence.cron_expression) if not validation["valid"]: raise HTTPException(status_code=400, detail=f"Expression cron invalide: {validation.get('error')}") # Créer en DB repo = ScheduleRepository(db_session) schedule_id = f"sched_{uuid.uuid4().hex[:12]}" recurrence = request.recurrence schedule_obj = await repo.create( id=schedule_id, name=request.name, description=request.description, playbook=playbook_file, target_type=request.target_type, target=request.target, extra_vars=request.extra_vars, schedule_type=request.schedule_type, schedule_time=request.start_at, recurrence_type=recurrence.type if recurrence else None, recurrence_time=recurrence.time if recurrence else None, recurrence_days=json.dumps(recurrence.days) if recurrence and recurrence.days else None, cron_expression=recurrence.cron_expression if recurrence else None, timezone=request.timezone, start_at=request.start_at, end_at=request.end_at, enabled=request.enabled, retry_on_failure=request.retry_on_failure, timeout=request.timeout, notification_type=request.notification_type, tags=json.dumps(request.tags) if request.tags else None, ) await db_session.commit() # Créer le schedule Pydantic et l'ajouter au cache du scheduler pydantic_schedule = Schedule( id=schedule_id, name=request.name, description=request.description, playbook=playbook_file, target_type=request.target_type, target=request.target, extra_vars=request.extra_vars, schedule_type=request.schedule_type, recurrence=request.recurrence, timezone=request.timezone, start_at=request.start_at, end_at=request.end_at, enabled=request.enabled, retry_on_failure=request.retry_on_failure, timeout=request.timeout, notification_type=request.notification_type, tags=request.tags or [], ) scheduler_service.add_schedule_to_cache(pydantic_schedule) # Log en DB log_repo = LogRepository(db_session) await log_repo.create( level="INFO", message=f"Schedule '{request.name}' créé pour {playbook_file} sur {request.target}", source="scheduler", ) await db_session.commit() # Notifier via WebSocket await ws_manager.broadcast({ "type": "schedule_created", "data": { "id": schedule_obj.id, "name": schedule_obj.name, "playbook": schedule_obj.playbook, "target": schedule_obj.target, } }) return { "success": True, "message": f"Schedule '{request.name}' créé avec succès", "schedule": { "id": schedule_obj.id, "name": schedule_obj.name, "playbook": schedule_obj.playbook, "target": schedule_obj.target, "enabled": schedule_obj.enabled, } } @router.put("/{schedule_id}") async def update_schedule( schedule_id: str, request: ScheduleUpdateRequest, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Met à jour un schedule existant.""" sched = scheduler_service.get_schedule(schedule_id) repo = ScheduleRepository(db_session) schedule = await repo.get(schedule_id) if not sched and not schedule: raise HTTPException(status_code=404, detail=f"Schedule '{schedule_id}' non trouvé") schedule_name = sched.name if sched else schedule.name # Valider le playbook si modifié if request.playbook: playbooks = ansible_service.get_playbooks() playbook_names = [p['filename'] for p in playbooks] + [p['name'] for p in playbooks] playbook_file = request.playbook if not playbook_file.endswith(('.yml', '.yaml')): playbook_file = f"{playbook_file}.yml" if playbook_file not in playbook_names and request.playbook not in playbook_names: raise HTTPException(status_code=400, detail=f"Playbook '{request.playbook}' non trouvé") # Valider l'expression cron si modifiée if request.recurrence and request.recurrence.type == "custom": if request.recurrence.cron_expression: validation = scheduler_service.validate_cron_expression(request.recurrence.cron_expression) if not validation["valid"]: raise HTTPException(status_code=400, detail=f"Expression cron invalide: {validation.get('error')}") # Mettre à jour en DB update_fields = {} if request.name: update_fields["name"] = request.name if request.description: update_fields["description"] = request.description if request.playbook: update_fields["playbook"] = request.playbook if request.target: update_fields["target"] = request.target if request.schedule_type: update_fields["schedule_type"] = request.schedule_type if request.timezone: update_fields["timezone"] = request.timezone if request.enabled is not None: update_fields["enabled"] = request.enabled if request.retry_on_failure is not None: update_fields["retry_on_failure"] = request.retry_on_failure if request.timeout is not None: update_fields["timeout"] = request.timeout if request.notification_type: update_fields["notification_type"] = request.notification_type if request.tags: update_fields["tags"] = json.dumps(request.tags) if request.recurrence: update_fields["recurrence_type"] = request.recurrence.type update_fields["recurrence_time"] = request.recurrence.time update_fields["recurrence_days"] = json.dumps(request.recurrence.days) if request.recurrence.days else None update_fields["cron_expression"] = request.recurrence.cron_expression if schedule: await repo.update(schedule, **update_fields) await db_session.commit() scheduler_service.update_schedule(schedule_id, request) # Log en DB log_repo = LogRepository(db_session) await log_repo.create( level="INFO", message=f"Schedule '{schedule_name}' mis à jour", source="scheduler", ) await db_session.commit() await ws_manager.broadcast({ "type": "schedule_updated", "data": {"id": schedule_id, "name": schedule_name} }) return { "success": True, "message": f"Schedule '{schedule_name}' mis à jour", "schedule": {"id": schedule_id, "name": schedule_name} } @router.delete("/{schedule_id}") async def delete_schedule( schedule_id: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Supprime un schedule.""" repo = ScheduleRepository(db_session) schedule = await repo.get(schedule_id) if not schedule: try: scheduler_service.delete_schedule(schedule_id) except Exception: pass return { "success": True, "message": f"Schedule '{schedule_id}' déjà supprimé ou inexistant." } schedule_name = schedule.name await repo.soft_delete(schedule_id) await db_session.commit() scheduler_service.delete_schedule(schedule_id) log_repo = LogRepository(db_session) await log_repo.create( level="WARN", message=f"Schedule '{schedule_name}' supprimé", source="scheduler", ) await db_session.commit() await ws_manager.broadcast({ "type": "schedule_deleted", "data": {"id": schedule_id, "name": schedule_name} }) return { "success": True, "message": f"Schedule '{schedule_name}' supprimé" } @router.post("/{schedule_id}/run") async def run_schedule_now( schedule_id: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Exécute immédiatement un schedule.""" sched = scheduler_service.get_schedule(schedule_id) if not sched: repo = ScheduleRepository(db_session) schedule = await repo.get(schedule_id) if not schedule: raise HTTPException(status_code=404, detail=f"Schedule '{schedule_id}' non trouvé") schedule_name = schedule.name else: schedule_name = sched.name run = await scheduler_service.run_now(schedule_id) return { "success": True, "message": f"Schedule '{schedule_name}' lancé", "run": run.dict() if run else None } @router.post("/{schedule_id}/pause") async def pause_schedule( schedule_id: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Met en pause un schedule.""" sched = scheduler_service.get_schedule(schedule_id) repo = ScheduleRepository(db_session) schedule = await repo.get(schedule_id) if not sched and not schedule: raise HTTPException(status_code=404, detail=f"Schedule '{schedule_id}' non trouvé") schedule_name = sched.name if sched else schedule.name if schedule: await repo.update(schedule, enabled=False) await db_session.commit() scheduler_service.pause_schedule(schedule_id) log_repo = LogRepository(db_session) await log_repo.create( level="INFO", message=f"Schedule '{schedule_name}' mis en pause", source="scheduler", ) await db_session.commit() await ws_manager.broadcast({ "type": "schedule_updated", "data": {"id": schedule_id, "name": schedule_name, "enabled": False} }) return { "success": True, "message": f"Schedule '{schedule_name}' mis en pause", "schedule": {"id": schedule_id, "name": schedule_name, "enabled": False} } @router.post("/{schedule_id}/resume") async def resume_schedule( schedule_id: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Reprend un schedule en pause.""" sched = scheduler_service.get_schedule(schedule_id) repo = ScheduleRepository(db_session) schedule = await repo.get(schedule_id) if not sched and not schedule: raise HTTPException(status_code=404, detail=f"Schedule '{schedule_id}' non trouvé") schedule_name = sched.name if sched else schedule.name if schedule: await repo.update(schedule, enabled=True) await db_session.commit() scheduler_service.resume_schedule(schedule_id) log_repo = LogRepository(db_session) await log_repo.create( level="INFO", message=f"Schedule '{schedule_name}' repris", source="scheduler", ) await db_session.commit() await ws_manager.broadcast({ "type": "schedule_updated", "data": {"id": schedule_id, "name": schedule_name, "enabled": True} }) return { "success": True, "message": f"Schedule '{schedule_name}' repris", "schedule": {"id": schedule_id, "name": schedule_name, "enabled": True} } @router.get("/{schedule_id}/runs") async def get_schedule_runs( schedule_id: str, limit: int = 50, offset: int = 0, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db), ): """Récupère l'historique des exécutions d'un schedule.""" sched = scheduler_service.get_schedule(schedule_id) repo = ScheduleRepository(db_session) schedule = await repo.get(schedule_id) if not sched and not schedule: raise HTTPException(status_code=404, detail=f"Schedule '{schedule_id}' non trouvé") schedule_name = sched.name if sched else schedule.name run_repo = ScheduleRunRepository(db_session) runs = await run_repo.list_for_schedule(schedule_id, limit=limit, offset=offset) return { "schedule_id": schedule_id, "schedule_name": schedule_name, "runs": [ { "id": r.id, "status": r.status, "started_at": r.started_at, "finished_at": r.completed_at, "duration_seconds": r.duration, "error_message": r.error_message, } for r in runs ], "count": len(runs) }