homelab_automation/app/routes/builtin_playbooks.py
Bruno Charest 68a9b0f390
Some checks failed
Tests / Backend Tests (Python) (3.10) (push) Has been cancelled
Tests / Backend Tests (Python) (3.11) (push) Has been cancelled
Tests / Backend Tests (Python) (3.12) (push) Has been cancelled
Tests / Frontend Tests (JS) (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / All Tests Passed (push) Has been cancelled
Remove Node.js cache files containing npm vulnerability data for vitest and vite packages
2025-12-15 20:36:06 -05:00

255 lines
8.7 KiB
Python

"""
Routes API pour les builtin playbooks (collecte métriques).
"""
import asyncio
import uuid
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
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()
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)
@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}
async def collect_for_all():
service = _get_service()
results = []
async with async_session_maker() as session:
host_repo_bg = HostRepository(session)
metrics_repo = HostMetricsRepository(session)
hosts_bg = await host_repo_bg.list(limit=1000)
for host in hosts_bg:
try:
result = await service.execute_builtin("collect_system_info", host.name)
ok = bool(result.get("success", False))
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,
})
except Exception as e:
await session.rollback()
results.append({
"host": host.name,
"success": False,
"error": str(e)
})
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"))
}
})
# Lancer en arrière-plan via la boucle asyncio active (retour immédiat)
asyncio.create_task(collect_for_all())
return {
"success": True,
"message": f"Collecte des métriques lancée pour {len(hosts)} hôte(s)",
"hosts_count": len(hosts)
}