""" Routes API pour les builtin playbooks (collecte métriques). """ import asyncio import uuid from typing import Optional from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks import logging from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import OperationalError from app.core.config import settings from app.core.dependencies import get_db, verify_api_key from app.crud.host import HostRepository from app.crud.host_metrics import HostMetricsRepository from app.models.database import async_session_maker from app.services import ansible_service, ws_manager from app.services.builtin_playbooks import ( BuiltinPlaybookService, BUILTIN_PLAYBOOKS, get_builtin_playbook_service, init_builtin_playbook_service, ) router = APIRouter() logger = logging.getLogger("homelab.metrics") async def _sync_hosts_from_inventory(db_session: AsyncSession) -> int: repo = HostRepository(db_session) inventory_hosts = ansible_service.get_hosts_from_inventory() created_or_updated = 0 for inv_host in inventory_hosts: existing = await repo.get_by_name(inv_host.name) ip_address = inv_host.ansible_host or inv_host.name ansible_group = getattr(inv_host, "group", None) inv_groups = getattr(inv_host, "groups", None) or [] docker_enabled = "role_docker" in inv_groups if existing: await repo.update( existing, ip_address=ip_address, ansible_group=ansible_group, docker_enabled=docker_enabled or existing.docker_enabled, ) created_or_updated += 1 else: created = await repo.create( id=uuid.uuid4().hex, name=inv_host.name, ip_address=ip_address, ansible_group=ansible_group, status="unknown", reachable=False, last_seen=None, ) if docker_enabled: await repo.update(created, docker_enabled=True) created_or_updated += 1 if created_or_updated: await db_session.commit() return created_or_updated def _get_service() -> BuiltinPlaybookService: """Récupère ou initialise le service builtin playbooks.""" try: return get_builtin_playbook_service() except RuntimeError: return init_builtin_playbook_service(settings.ansible_dir, ansible_service) async def collect_all_metrics_job() -> None: logger.info("[METRICS_JOB] Starting scheduled metrics collection") service = _get_service() results = [] async with async_session_maker() as session: host_repo_bg = HostRepository(session) metrics_repo = HostMetricsRepository(session) try: hosts_bg = await host_repo_bg.list(limit=1000) except OperationalError: return if not hosts_bg: await _sync_hosts_from_inventory(session) hosts_bg = await host_repo_bg.list(limit=1000) inventory_hosts = ansible_service.get_hosts_from_inventory() inventory_names = {h.name for h in (inventory_hosts or []) if getattr(h, "name", None)} skipped = 0 if inventory_names: filtered = [h for h in hosts_bg if h.name in inventory_names] skipped = len(hosts_bg) - len(filtered) hosts_bg = filtered for host in hosts_bg: try: result = await service.execute_builtin("collect_system_info", host.name) ok = bool(result.get("success", False)) stderr = (result.get("stderr") or "").strip() stdout = (result.get("stdout") or "").strip() return_code = result.get("return_code") error_detail = None if not ok: if stderr: error_detail = stderr[:400] elif stdout: error_detail = stdout[:400] else: error_detail = result.get("error") or "Unknown error" logger.warning( "[METRICS] Collection failed for host=%s return_code=%s error=%s", host.name, return_code, error_detail, ) if ok: for hostname, metrics_data in result.get("parsed_metrics", {}).items(): target_host = await host_repo_bg.get_by_name(hostname) if not target_host: continue metrics_create = service.create_metrics_from_parsed( host_id=target_host.id, parsed_data=metrics_data, builtin_id="collect_system_info", execution_time_ms=result.get("execution_time_ms", 0), ) await metrics_repo.create(**metrics_create.model_dump()) await session.commit() results.append({ "host": host.name, "success": ok, "return_code": return_code, "error": error_detail, }) except Exception as e: await session.rollback() logger.exception("[METRICS] Unhandled exception during collection for host=%s", host.name) results.append({ "host": host.name, "success": False, "error": str(e) }) logger.info( "[METRICS_JOB] Collection complete: total=%d success=%d failed=%d skipped=%d", len(results), sum(1 for r in results if r.get("success")), sum(1 for r in results if not r.get("success")), skipped, ) await ws_manager.broadcast({ "type": "metrics_collection_complete", "data": { "total": len(results), "success": sum(1 for r in results if r.get("success")), "failed": sum(1 for r in results if not r.get("success")), "skipped": skipped, "results": results, } }) @router.get("") async def list_builtin_playbooks(api_key_valid: bool = Depends(verify_api_key)): """Liste tous les builtin playbooks disponibles.""" return [pb.model_dump() for pb in BUILTIN_PLAYBOOKS.values()] @router.get("/{builtin_id}") async def get_builtin_playbook( builtin_id: str, api_key_valid: bool = Depends(verify_api_key) ): """Récupère les détails d'un builtin playbook.""" if builtin_id not in BUILTIN_PLAYBOOKS: raise HTTPException(status_code=404, detail=f"Builtin playbook '{builtin_id}' non trouvé") return BUILTIN_PLAYBOOKS[builtin_id].model_dump() @router.post("/execute") async def execute_builtin_playbook( builtin_id: str, target: str, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db) ): """Exécute un builtin playbook sur une cible.""" if builtin_id not in BUILTIN_PLAYBOOKS: raise HTTPException(status_code=404, detail=f"Builtin playbook '{builtin_id}' non trouvé") service = _get_service() result = await service.execute_builtin(builtin_id, target) # Si collecte de métriques, sauvegarder en BD if BUILTIN_PLAYBOOKS[builtin_id].collect_metrics and result.get("success"): metrics_repo = HostMetricsRepository(db_session) host_repo = HostRepository(db_session) for hostname, metrics_data in result.get("parsed_metrics", {}).items(): # Trouver l'host_id host = await host_repo.get_by_name(hostname) if host: metrics_create = service.create_metrics_from_parsed( host_id=host.id, parsed_data=metrics_data, builtin_id=builtin_id, execution_time_ms=result.get("execution_time_ms", 0) ) await metrics_repo.create(**metrics_create.model_dump()) await db_session.commit() await ws_manager.broadcast({ "type": "builtin_executed", "data": { "builtin_id": builtin_id, "target": target, "success": result.get("success", False) } }) return result @router.post("/execute-background") async def execute_builtin_playbook_background( builtin_id: str, target: str, background_tasks: BackgroundTasks, api_key_valid: bool = Depends(verify_api_key) ): """Exécute un builtin playbook en arrière-plan.""" if builtin_id not in BUILTIN_PLAYBOOKS: raise HTTPException(status_code=404, detail=f"Builtin playbook '{builtin_id}' non trouvé") async def run_in_background(): service = _get_service() result = await service.execute_builtin(builtin_id, target) await ws_manager.broadcast({ "type": "builtin_executed", "data": { "builtin_id": builtin_id, "target": target, "success": result.get("success", False) } }) # Lancer en arrière-plan via la boucle asyncio active (retour immédiat) asyncio.create_task(run_in_background()) return { "message": f"Builtin playbook '{builtin_id}' planifié pour exécution sur {target}", "builtin_id": builtin_id, "target": target } @router.post("/collect-all") async def collect_all_metrics( background_tasks: BackgroundTasks, api_key_valid: bool = Depends(verify_api_key), db_session: AsyncSession = Depends(get_db) ): """Collecte les métriques de tous les hôtes.""" host_repo = HostRepository(db_session) try: hosts = await host_repo.list(limit=1000) except OperationalError as e: raise HTTPException( status_code=500, detail=f"Base de données non initialisée/migrations manquantes (hosts): {str(e)}", ) if not hosts: await _sync_hosts_from_inventory(db_session) hosts = await host_repo.list(limit=1000) if not hosts: return {"success": True, "message": "Aucun hôte trouvé", "hosts_count": 0} # Lancer en arrière-plan via la boucle asyncio active (retour immédiat) asyncio.create_task(collect_all_metrics_job()) return { "success": True, "message": f"Collecte des métriques lancée pour {len(hosts)} hôte(s)", "hosts_count": len(hosts) }