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
306 lines
10 KiB
Python
306 lines
10 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
|
|
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)
|
|
}
|