""" Routes API pour l'exécution Ansible. """ import uuid import subprocess from datetime import datetime, timezone from time import perf_counter from typing import Optional, Dict, Any from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.dependencies import get_db, verify_api_key from app.crud.task import TaskRepository from app.schemas.ansible import AnsibleExecutionRequest, AdHocCommandRequest, AdHocCommandResult from app.schemas.task_api import Task from app.schemas.common import LogEntry from app.services import ( ansible_service, ws_manager, notification_service, adhoc_history_service, db, ) from app.services.task_log_service import TaskLogService from app.utils.ssh_utils import find_ssh_private_key router = APIRouter() # Instance du service de logs de tâches task_log_service = TaskLogService(settings.tasks_logs_dir) @router.get("/playbooks") async def get_ansible_playbooks( target: Optional[str] = None, api_key_valid: bool = Depends(verify_api_key) ): """Liste les playbooks Ansible disponibles.""" if target: playbooks = ansible_service.get_compatible_playbooks(target) else: playbooks = ansible_service.get_playbooks() return { "playbooks": playbooks, "categories": ansible_service.get_playbook_categories(), "ansible_dir": str(settings.ansible_dir), "filter": target } @router.get("/inventory") async def get_ansible_inventory( group: Optional[str] = None, api_key_valid: bool = Depends(verify_api_key) ): """Récupère l'inventaire Ansible.""" return { "hosts": [h.dict() for h in ansible_service.get_hosts_from_inventory(group_filter=group)], "groups": ansible_service.get_groups(), "inventory_path": str(ansible_service.inventory_path), "filter": group } @router.get("/groups") async def get_ansible_groups(api_key_valid: bool = Depends(verify_api_key)): """Récupère la liste des groupes Ansible.""" return {"groups": ansible_service.get_groups()} @router.get("/ssh-config") async def get_ssh_config(api_key_valid: bool = Depends(verify_api_key)): """Diagnostic de la configuration SSH.""" from pathlib import Path import shutil ssh_key_path = Path(settings.ssh_key_path) ssh_dir = ssh_key_path.parent available_files = [] if ssh_dir.exists(): available_files = [f.name for f in ssh_dir.iterdir()] private_key_exists = ssh_key_path.exists() public_key_exists = Path(settings.ssh_key_path + ".pub").exists() pub_keys_found = [] for key_type in ["id_rsa", "id_ed25519", "id_ecdsa", "id_dsa"]: key_path = ssh_dir / f"{key_type}.pub" if key_path.exists(): pub_keys_found.append(str(key_path)) active_private_key = find_ssh_private_key() return { "ssh_key_path": settings.ssh_key_path, "ssh_dir": str(ssh_dir), "ssh_dir_exists": ssh_dir.exists(), "private_key_exists": private_key_exists, "public_key_exists": public_key_exists, "available_files": available_files, "public_keys_found": pub_keys_found, "active_private_key": active_private_key, "ssh_user": settings.ssh_user, "sshpass_available": shutil.which("sshpass") is not None, } @router.post("/execute") async def execute_ansible_playbook( request: AnsibleExecutionRequest, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db) ): """Exécute un playbook Ansible.""" start_time_dt = datetime.now(timezone.utc) # Valider la compatibilité playbook-target playbooks = ansible_service.get_playbooks() playbook_info = next( (pb for pb in playbooks if pb['filename'] == request.playbook or pb['name'] == request.playbook.replace('.yml', '').replace('.yaml', '')), None ) 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}'." ) # Créer une tâche en BD task_repo = TaskRepository(db_session) task_id = f"pb_{uuid.uuid4().hex[:12]}" playbook_name = request.playbook.replace('.yml', '').replace('-', ' ').title() db_task = await task_repo.create( id=task_id, action=f"playbook:{request.playbook}", target=request.target, playbook=request.playbook, status="running", ) await task_repo.update(db_task, started_at=start_time_dt) await db_session.commit() # Créer aussi en mémoire task = Task( id=task_id, name=f"Playbook: {playbook_name}", host=request.target, status="running", progress=0, start_time=start_time_dt ) db.tasks.insert(0, task) try: result = await ansible_service.execute_playbook( playbook=request.playbook, target=request.target, extra_vars=request.extra_vars, check_mode=request.check_mode, verbose=request.verbose ) # Mettre à jour la tâche task.status = "completed" if result["success"] else "failed" task.progress = 100 task.end_time = datetime.now(timezone.utc) task.duration = f"{result.get('execution_time', 0):.1f}s" task.output = result.get("stdout", "") task.error = result.get("stderr", "") if not result["success"] else None # Ajouter un log log_entry = LogEntry( id=db.get_next_id("logs"), timestamp=datetime.now(timezone.utc), level="INFO" if result["success"] else "ERROR", message=f"Playbook {request.playbook} exécuté sur {request.target}: {'succès' if result['success'] else 'échec'}", source="ansible", host=request.target ) db.logs.insert(0, log_entry) # Sauvegarder le log markdown try: log_path = task_log_service.save_task_log( task=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}") await ws_manager.broadcast({ "type": "ansible_execution", "data": result }) # Mettre à jour la BD await task_repo.update( db_task, status=task.status, completed_at=task.end_time, error_message=task.error, result_data={"output": result.get("stdout", "")[:5000]} ) await db_session.commit() # Notification if result["success"]: await notification_service.notify_task_completed( task_name=task.name, target=request.target, duration=task.duration ) else: await notification_service.notify_task_failed( task_name=task.name, target=request.target, error=result.get("stderr", "Erreur inconnue")[:200] ) result["task_id"] = task_id return result except FileNotFoundError as e: task.status = "failed" task.end_time = datetime.now(timezone.utc) task.error = str(e) try: log_path = task_log_service.save_task_log(task=task, error=str(e)) 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 await task_repo.update(db_task, status="failed", completed_at=task.end_time, error_message=str(e)) await db_session.commit() await notification_service.notify_task_failed(task_name=task.name, target=request.target, error=str(e)[:200]) raise HTTPException(status_code=404, detail=str(e)) except Exception as e: task.status = "failed" task.end_time = datetime.now(timezone.utc) task.error = str(e) try: log_path = task_log_service.save_task_log(task=task, error=str(e)) 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 await task_repo.update(db_task, status="failed", completed_at=task.end_time, error_message=str(e)) await db_session.commit() await notification_service.notify_task_failed(task_name=task.name, target=request.target, error=str(e)[:200]) raise HTTPException(status_code=500, detail=str(e)) @router.post("/adhoc", response_model=AdHocCommandResult) async def execute_adhoc_command( request: AdHocCommandRequest, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db) ): """Exécute une commande ad-hoc Ansible.""" start_time_perf = perf_counter() start_time_dt = datetime.now(timezone.utc) # Créer une tâche en BD task_repo = TaskRepository(db_session) task_id = f"adhoc_{uuid.uuid4().hex[:12]}" task_name = f"Ad-hoc: {request.command[:40]}{'...' if len(request.command) > 40 else ''}" db_task = await task_repo.create( id=task_id, action=f"adhoc:{request.module}", target=request.target, playbook=None, status="running", ) await task_repo.update(db_task, started_at=start_time_dt) await db_session.commit() # Créer aussi en mémoire task = Task( id=task_id, name=task_name, host=request.target, status="running", progress=0, start_time=start_time_dt ) db.tasks.insert(0, task) # Construire la commande ansible ansible_cmd = [ "ansible", request.target, "-i", str(settings.ansible_dir / "inventory" / "hosts.yml"), "-m", request.module, "-a", request.command, "--timeout", str(request.timeout), ] if request.become: ansible_cmd.append("--become") private_key = find_ssh_private_key() if private_key: ansible_cmd.extend(["--private-key", private_key]) if settings.ssh_user: ansible_cmd.extend(["-u", settings.ssh_user]) try: result = subprocess.run( ansible_cmd, capture_output=True, text=True, timeout=request.timeout + 10, cwd=str(settings.ansible_dir) ) duration = perf_counter() - start_time_perf success = result.returncode == 0 task.status = "completed" if success else "failed" task.progress = 100 task.end_time = datetime.now(timezone.utc) task.duration = f"{round(duration, 2)}s" task.output = result.stdout task.error = result.stderr if result.stderr else None log_path = task_log_service.save_task_log(task, output=result.stdout, error=result.stderr or "", source_type='adhoc') 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() }) log_entry = LogEntry( id=db.get_next_id("logs"), timestamp=datetime.now(timezone.utc), level="INFO" if success else "WARN", message=f"Ad-hoc [{request.module}] sur {request.target}: {request.command[:50]}{'...' if len(request.command) > 50 else ''}", source="ansible-adhoc", host=request.target ) db.logs.insert(0, log_entry) await ws_manager.broadcast({ "type": "adhoc_executed", "data": { "target": request.target, "command": request.command, "success": success, "task_id": task_id } }) await adhoc_history_service.add_command( command=request.command, target=request.target, module=request.module, become=request.become, category=request.category or "default" ) await task_repo.update( db_task, status=task.status, completed_at=task.end_time, error_message=task.error, result_data={"output": result.stdout[:5000] if result.stdout else None} ) await db_session.commit() if success: await notification_service.notify_task_completed( task_name=task.name, target=request.target, duration=task.duration ) else: await notification_service.notify_task_failed( task_name=task.name, target=request.target, error=(result.stderr or "Erreur inconnue")[:200] ) return AdHocCommandResult( target=request.target, command=request.command, success=success, return_code=result.returncode, stdout=result.stdout, stderr=result.stderr if result.stderr else None, duration=round(duration, 2) ) except subprocess.TimeoutExpired: duration = perf_counter() - start_time_perf task.status = "failed" task.progress = 100 task.end_time = datetime.now(timezone.utc) task.duration = f"{round(duration, 2)}s" task.error = f"Timeout après {request.timeout} secondes" try: log_path = task_log_service.save_task_log(task, error=task.error, source_type='adhoc') 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 await task_repo.update(db_task, status="failed", completed_at=task.end_time, error_message=task.error) await db_session.commit() await notification_service.notify_task_failed(task_name=task.name, target=request.target, error=task.error[:200]) return AdHocCommandResult( target=request.target, command=request.command, success=False, return_code=-1, stdout="", stderr=f"Timeout après {request.timeout} secondes", duration=round(duration, 2) ) except FileNotFoundError: duration = perf_counter() - start_time_perf error_msg = "ansible non trouvé. Vérifiez que Ansible est installé." task.status = "failed" task.progress = 100 task.end_time = datetime.now(timezone.utc) task.duration = f"{round(duration, 2)}s" task.error = error_msg try: log_path = task_log_service.save_task_log(task, error=error_msg, source_type='adhoc') 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 await task_repo.update(db_task, status="failed", completed_at=task.end_time, error_message=error_msg) await db_session.commit() await notification_service.notify_task_failed(task_name=task.name, target=request.target, error=error_msg[:200]) return AdHocCommandResult( target=request.target, command=request.command, success=False, return_code=-1, stdout="", stderr=error_msg, duration=round(duration, 2) ) except Exception as e: duration = perf_counter() - start_time_perf error_msg = f"Erreur interne: {str(e)}" task.status = "failed" task.progress = 100 task.end_time = datetime.now(timezone.utc) task.duration = f"{round(duration, 2)}s" task.error = error_msg try: log_path = task_log_service.save_task_log(task, error=error_msg, source_type='adhoc') 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 await task_repo.update(db_task, status="failed", completed_at=task.end_time, error_message=error_msg) await db_session.commit() await notification_service.notify_task_failed(task_name=task.name, target=request.target, error=error_msg[:200]) return AdHocCommandResult( target=request.target, command=request.command, success=False, return_code=-1, stdout="", stderr=error_msg, duration=round(duration, 2) )