Remove Node.js cache files containing npm vulnerability data for vitest and vite packages
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

This commit is contained in:
2025-12-15 20:36:06 -05:00
parent 27eed55c9b
commit 68a9b0f390
123 changed files with 13216 additions and 157 deletions
+2
View File
@@ -24,6 +24,7 @@ from app.routes.metrics import router as metrics_router
from app.routes.builtin_playbooks import router as builtin_playbooks_router
from app.routes.server import router as server_router
from app.routes.alerts import router as alerts_router
from app.routes.docker import router as docker_router
# Router principal qui agrège tous les sous-routers
api_router = APIRouter()
@@ -46,6 +47,7 @@ api_router.include_router(metrics_router, prefix="/metrics", tags=["Metrics"])
api_router.include_router(builtin_playbooks_router, prefix="/builtin-playbooks", tags=["Builtin Playbooks"])
api_router.include_router(server_router, prefix="/server", tags=["Server"])
api_router.include_router(alerts_router, prefix="/alerts", tags=["Alerts"])
api_router.include_router(docker_router, prefix="/docker", tags=["Docker"])
__all__ = [
"api_router",
+111 -2
View File
@@ -14,8 +14,9 @@ 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.ansible import AnsibleExecutionRequest, AdHocCommandRequest, AdHocCommandResult, BootstrapRequest
from app.schemas.task_api import Task
from app.schemas.common import CommandResult
from app.schemas.common import LogEntry
from app.services import (
ansible_service,
@@ -23,9 +24,10 @@ from app.services import (
notification_service,
adhoc_history_service,
db,
bootstrap_status_service,
)
from app.services.task_log_service import TaskLogService
from app.utils.ssh_utils import find_ssh_private_key
from app.utils.ssh_utils import find_ssh_private_key, bootstrap_host
router = APIRouter()
@@ -110,6 +112,113 @@ async def get_ssh_config(api_key_valid: bool = Depends(verify_api_key)):
}
@router.post("/bootstrap", response_model=CommandResult)
async def bootstrap_ansible_host_compat(
request: BootstrapRequest,
api_key_valid: bool = Depends(verify_api_key),
):
"""Compat: Bootstrap un hôte via /api/ansible/bootstrap (historique UI).
La route canonical est /api/bootstrap, mais l'UI historique appelle /api/ansible/bootstrap.
"""
import asyncio
import logging
import traceback
logger = logging.getLogger("bootstrap_endpoint")
try:
logger.info(f"[BOOTSTRAP] Bootstrap request for host={request.host}, user={request.automation_user}")
result = bootstrap_host(
host=request.host,
root_password=request.root_password,
automation_user=request.automation_user,
)
logger.info(f"[BOOTSTRAP] Bootstrap result: status={result.status}, return_code={result.return_code}")
if result.return_code != 0:
raise HTTPException(
status_code=500,
detail={
"status": result.status,
"return_code": result.return_code,
"stdout": result.stdout,
"stderr": result.stderr,
},
)
host_name = request.host
for h in db.hosts:
if h.ip == request.host or h.name == request.host:
host_name = h.name
break
bootstrap_status_service.set_bootstrap_status(
host_name=host_name,
success=True,
details=f"Bootstrap réussi via API (user: {request.automation_user})",
)
db.invalidate_hosts_cache()
log_entry = LogEntry(
id=db.get_next_id("logs"),
timestamp=datetime.now(timezone.utc),
level="INFO",
message=f"Bootstrap réussi pour {host_name} (user: {request.automation_user})",
source="bootstrap",
host=host_name,
)
db.logs.insert(0, log_entry)
await ws_manager.broadcast(
{
"type": "bootstrap_success",
"data": {
"host": host_name,
"user": request.automation_user,
"status": "ok",
"bootstrap_ok": True,
},
}
)
asyncio.create_task(notification_service.notify_bootstrap_success(host_name))
return result
except HTTPException as http_exc:
error_detail = str(http_exc.detail) if http_exc.detail else "Erreur inconnue"
asyncio.create_task(
notification_service.notify_bootstrap_failed(
hostname=request.host,
error=error_detail[:200],
)
)
raise
except Exception as e:
logger.error(f"[BOOTSTRAP] Bootstrap exception: {e}")
logger.error(traceback.format_exc())
log_entry = LogEntry(
id=db.get_next_id("logs"),
timestamp=datetime.now(timezone.utc),
level="ERROR",
message=f"Échec bootstrap pour {request.host}: {str(e)}",
source="bootstrap",
host=request.host,
)
db.logs.insert(0, log_entry)
asyncio.create_task(
notification_service.notify_bootstrap_failed(
hostname=request.host,
error=str(e)[:200],
)
)
raise HTTPException(status_code=500, detail=str(e))
@router.post("/execute")
async def execute_ansible_playbook(
request: AnsibleExecutionRequest,
+101 -21
View File
@@ -3,15 +3,18 @@ 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,
@@ -23,6 +26,46 @@ from app.services.builtin_playbooks import (
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:
@@ -116,8 +159,8 @@ async def execute_builtin_playbook_background(
}
})
# Planifier l'exécution en arrière-plan
background_tasks.add_task(asyncio.create_task, run_in_background())
# 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}",
@@ -134,41 +177,78 @@ async def collect_all_metrics(
):
"""Collecte les métriques de tous les hôtes."""
host_repo = HostRepository(db_session)
hosts = await host_repo.list(limit=1000)
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 {"message": "Aucun hôte trouvé", "hosts_count": 0}
return {"success": True, "message": "Aucun hôte trouvé", "hosts_count": 0}
async def collect_for_all():
service = _get_service()
results = []
for host in hosts:
try:
result = await service.execute_builtin("collect_system_info", host.name)
results.append({
"host": host.name,
"success": result.get("success", False)
})
except Exception as e:
results.append({
"host": host.name,
"success": False,
"error": str(e)
})
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(hosts),
"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"))
}
})
background_tasks.add_task(asyncio.create_task, collect_for_all())
# 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)
}
+425
View File
@@ -0,0 +1,425 @@
"""Docker management API routes."""
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.dependencies import get_db, verify_api_key, get_current_user, require_admin
from app.crud.docker_container import DockerContainerRepository
from app.crud.docker_image import DockerImageRepository
from app.crud.docker_volume import DockerVolumeRepository
from app.crud.docker_alert import DockerAlertRepository
from app.crud.host import HostRepository
from app.schemas.docker import (
DockerHostListResponse,
DockerHostInfo,
EnableDockerRequest,
DockerContainerListResponse,
DockerContainerResponse,
DockerImageListResponse,
DockerImageExtendedListResponse,
DockerImageExtendedResponse,
DockerVolumeListResponse,
DockerAlertListResponse,
DockerAlertAcknowledgeRequest,
ContainerActionResponse,
ContainerLogsRequest,
ContainerLogsResponse,
ContainerInspectResponse,
DockerCollectResponse,
DockerCollectAllResponse,
DockerStatsResponse,
ImageActionResponse,
)
from app.services.docker_service import docker_service
from app.services.docker_actions import docker_actions
from app.services.docker_alerts import docker_alerts_service
router = APIRouter()
# === Docker Hosts ===
@router.get("/hosts", response_model=DockerHostListResponse)
async def list_docker_hosts(
api_key_valid: bool = Depends(verify_api_key),
):
"""List all hosts with Docker information."""
hosts = await docker_service.get_docker_hosts()
enabled = sum(1 for h in hosts if h.get("docker_enabled"))
return DockerHostListResponse(
hosts=[DockerHostInfo(**h) for h in hosts],
total=len(hosts),
enabled=enabled
)
@router.post("/hosts/{host_id}/enable")
async def enable_docker_monitoring(
host_id: str,
request: EnableDockerRequest = None,
api_key_valid: bool = Depends(verify_api_key),
):
"""Enable or disable Docker monitoring on a host."""
enabled = request.enabled if request else True
success = await docker_service.enable_docker_monitoring(host_id, enabled)
if not success:
raise HTTPException(status_code=404, detail="Host not found")
action = "enabled" if enabled else "disabled"
return {"message": f"Docker monitoring {action} for host", "host_id": host_id}
@router.post("/hosts/{host_id}/collect", response_model=DockerCollectResponse)
async def collect_docker_now(
host_id: str,
api_key_valid: bool = Depends(verify_api_key),
):
"""Force an immediate Docker collection on a host."""
result = await docker_service.collect_docker_host(host_id)
return DockerCollectResponse(
success=result.get("success", False),
host_id=host_id,
host_name=result.get("host_name", ""),
message="Collection completed" if result.get("success") else "Collection failed",
docker_version=result.get("docker_version"),
containers_count=result.get("containers_count", 0),
images_count=result.get("images_count", 0),
volumes_count=result.get("volumes_count", 0),
duration_ms=result.get("duration_ms", 0),
error=result.get("error")
)
@router.post("/collect-all", response_model=DockerCollectAllResponse)
async def collect_all_docker_hosts(
api_key_valid: bool = Depends(verify_api_key),
):
"""Collect Docker info from all enabled hosts."""
result = await docker_service.collect_all_hosts()
normalized_results = []
for r in result.get("results", []):
if not isinstance(r, dict):
continue
normalized_results.append({
"success": bool(r.get("success", False)),
"host_id": r.get("host_id", ""),
"host_name": r.get("host_name", ""),
"message": r.get("message") or ("Collection completed" if r.get("success") else "Collection failed"),
"docker_version": r.get("docker_version"),
"containers_count": r.get("containers_count", 0),
"images_count": r.get("images_count", 0),
"volumes_count": r.get("volumes_count", 0),
"duration_ms": r.get("duration_ms", 0),
"error": r.get("error"),
})
return DockerCollectAllResponse(
success=result.get("success", False),
message=f"Collected from {result.get('successful', 0)}/{result.get('total_hosts', 0)} hosts",
total_hosts=result.get("total_hosts", 0),
successful=result.get("successful", 0),
failed=result.get("failed", 0),
results=[DockerCollectResponse(**r) for r in normalized_results]
)
# === Containers ===
@router.get("/hosts/{host_id}/containers", response_model=DockerContainerListResponse)
async def get_containers(
host_id: str,
state: Optional[str] = Query(None, description="Filter by state"),
compose_project: Optional[str] = Query(None, description="Filter by compose project"),
api_key_valid: bool = Depends(verify_api_key),
db_session: AsyncSession = Depends(get_db),
):
"""List containers for a host."""
container_repo = DockerContainerRepository(db_session)
containers = await container_repo.list_by_host(host_id, state=state, compose_project=compose_project)
counts = await container_repo.count_by_host(host_id)
return DockerContainerListResponse(
containers=[DockerContainerResponse(
id=c.id,
host_id=c.host_id,
container_id=c.container_id,
name=c.name,
image=c.image,
state=c.state,
status=c.status,
health=c.health,
ports=c.ports,
labels=c.labels,
compose_project=c.compose_project,
created_at=c.created_at,
last_update_at=c.last_update_at
) for c in containers],
total=counts.get("total", 0),
running=counts.get("running", 0),
stopped=counts.get("total", 0) - counts.get("running", 0)
)
@router.post("/containers/{host_id}/{container_id}/start", response_model=ContainerActionResponse)
async def start_container(
host_id: str,
container_id: str,
user: dict = Depends(get_current_user),
):
"""Start a stopped container."""
result = await docker_actions.start_container(host_id, container_id)
return ContainerActionResponse(**result)
@router.post("/containers/{host_id}/{container_id}/stop", response_model=ContainerActionResponse)
async def stop_container(
host_id: str,
container_id: str,
timeout: int = Query(10, ge=1, le=300),
user: dict = Depends(get_current_user),
):
"""Stop a running container."""
result = await docker_actions.stop_container(host_id, container_id, timeout=timeout)
return ContainerActionResponse(**result)
@router.post("/containers/{host_id}/{container_id}/restart", response_model=ContainerActionResponse)
async def restart_container(
host_id: str,
container_id: str,
timeout: int = Query(10, ge=1, le=300),
user: dict = Depends(get_current_user),
):
"""Restart a container."""
result = await docker_actions.restart_container(host_id, container_id, timeout=timeout)
return ContainerActionResponse(**result)
@router.post("/containers/{host_id}/{container_id}/remove", response_model=ContainerActionResponse)
async def remove_container(
host_id: str,
container_id: str,
force: bool = Query(False, description="Force remove running container"),
remove_volumes: bool = Query(False, description="Remove associated volumes"),
user: dict = Depends(require_admin),
):
"""Remove a container (admin only)."""
result = await docker_actions.remove_container(
host_id, container_id, force=force, remove_volumes=remove_volumes
)
return ContainerActionResponse(**result)
@router.post("/containers/{host_id}/{container_id}/redeploy", response_model=ContainerActionResponse)
async def redeploy_container(
host_id: str,
container_id: str,
user: dict = Depends(get_current_user),
):
"""Redeploy a container by pulling latest image."""
result = await docker_actions.redeploy_container(host_id, container_id)
return ContainerActionResponse(**result)
@router.get("/containers/{host_id}/{container_id}/logs", response_model=ContainerLogsResponse)
async def get_container_logs(
host_id: str,
container_id: str,
tail: int = Query(200, ge=1, le=5000),
timestamps: bool = Query(False),
since: Optional[str] = Query(None),
api_key_valid: bool = Depends(verify_api_key),
):
"""Get container logs."""
result = await docker_actions.get_container_logs(
host_id, container_id, tail=tail, timestamps=timestamps, since=since
)
return ContainerLogsResponse(**result)
@router.get("/containers/{host_id}/{container_id}/inspect", response_model=ContainerInspectResponse)
async def inspect_container(
host_id: str,
container_id: str,
api_key_valid: bool = Depends(verify_api_key),
):
"""Get detailed container information."""
result = await docker_actions.inspect_container(host_id, container_id)
return ContainerInspectResponse(**result)
# === Images ===
@router.get("/hosts/{host_id}/images", response_model=DockerImageExtendedListResponse)
async def get_images(
host_id: str,
api_key_valid: bool = Depends(verify_api_key),
db_session: AsyncSession = Depends(get_db),
):
"""List images for a host with usage information."""
image_repo = DockerImageRepository(db_session)
container_repo = DockerContainerRepository(db_session)
images = await image_repo.list_by_host(host_id)
counts = await image_repo.count_by_host(host_id)
# Get all container images to determine which images are in use
containers = await container_repo.list_by_host(host_id)
used_images = set()
for c in containers:
if c.image:
used_images.add(c.image)
# Also add the image without tag for matching
if ':' in c.image:
used_images.add(c.image.split(':')[0])
unused_count = 0
images_with_usage = []
for img in images:
# Check if image is in use by comparing repo_tags
in_use = False
if img.repo_tags:
for tag in img.repo_tags:
if tag in used_images:
in_use = True
break
# Check without tag
if ':' in tag and tag.split(':')[0] in used_images:
in_use = True
break
if not in_use:
unused_count += 1
images_with_usage.append({
"id": img.id,
"host_id": img.host_id,
"image_id": img.image_id,
"repo_tags": img.repo_tags,
"size": img.size,
"created": img.created,
"last_update_at": img.last_update_at,
"in_use": in_use
})
return DockerImageExtendedListResponse(
images=[DockerImageExtendedResponse(**img) for img in images_with_usage],
total=counts.get("total", 0),
total_size=counts.get("total_size", 0),
unused_count=unused_count
)
@router.delete("/images/{host_id}/{image_id}", response_model=ImageActionResponse)
async def remove_image(
host_id: str,
image_id: str,
force: bool = Query(False, description="Force remove even if image is in use"),
user: dict = Depends(require_admin),
):
"""Remove a Docker image (admin only)."""
result = await docker_actions.remove_image(host_id, image_id, force=force)
return ImageActionResponse(**result)
# === Volumes ===
@router.get("/hosts/{host_id}/volumes", response_model=DockerVolumeListResponse)
async def get_volumes(
host_id: str,
api_key_valid: bool = Depends(verify_api_key),
db_session: AsyncSession = Depends(get_db),
):
"""List volumes for a host."""
volume_repo = DockerVolumeRepository(db_session)
volumes = await volume_repo.list_by_host(host_id)
total = await volume_repo.count_by_host(host_id)
return DockerVolumeListResponse(
volumes=[{
"id": vol.id,
"host_id": vol.host_id,
"name": vol.name,
"driver": vol.driver,
"mountpoint": vol.mountpoint,
"scope": vol.scope,
"last_update_at": vol.last_update_at
} for vol in volumes],
total=total
)
# === Alerts ===
@router.get("/alerts", response_model=DockerAlertListResponse)
async def list_alerts(
host_id: Optional[str] = Query(None),
state: Optional[str] = Query("open", description="Filter by state: open, closed, acknowledged"),
severity: Optional[str] = Query(None, description="Filter by severity"),
limit: int = Query(100, ge=1, le=500),
offset: int = Query(0, ge=0),
api_key_valid: bool = Depends(verify_api_key),
):
"""List Docker alerts."""
result = await docker_alerts_service.get_alerts(
host_id=host_id,
state=state,
severity=severity,
limit=limit,
offset=offset
)
return DockerAlertListResponse(
alerts=result.get("alerts", []),
total=result.get("total", 0),
open_count=result.get("open_count", 0),
acknowledged_count=result.get("acknowledged_count", 0)
)
@router.post("/alerts/{alert_id}/acknowledge")
async def acknowledge_alert(
alert_id: int,
request: DockerAlertAcknowledgeRequest = None,
user: dict = Depends(get_current_user),
):
"""Acknowledge an alert."""
username = user.get("username", "unknown")
note = request.note if request else None
result = await docker_alerts_service.acknowledge_alert(alert_id, username, note)
if not result:
raise HTTPException(status_code=404, detail="Alert not found or already acknowledged")
return {"message": "Alert acknowledged", "alert": result}
@router.post("/alerts/{alert_id}/close")
async def close_alert(
alert_id: int,
user: dict = Depends(get_current_user),
):
"""Close an alert manually."""
result = await docker_alerts_service.close_alert(alert_id)
if not result:
raise HTTPException(status_code=404, detail="Alert not found or already closed")
return {"message": "Alert closed", "alert": result}
# === Stats ===
@router.get("/stats", response_model=DockerStatsResponse)
async def get_docker_stats(
api_key_valid: bool = Depends(verify_api_key),
):
"""Get global Docker statistics."""
stats = await docker_alerts_service.get_stats()
return DockerStatsResponse(**stats)
+24
View File
@@ -3,6 +3,7 @@ Routes API pour les health checks.
"""
from datetime import datetime, timezone
import sys
from fastapi import APIRouter, Depends, HTTPException
@@ -34,6 +35,29 @@ async def global_health_check():
}
@router.get("/runtime")
async def runtime_info(api_key_valid: bool = Depends(verify_api_key)):
"""Runtime diagnostics (python executable, asyncssh availability)."""
try:
import asyncssh # type: ignore
asyncssh_ok = True
asyncssh_version = getattr(asyncssh, "__version__", None)
except Exception as e:
asyncssh_ok = False
asyncssh_version = None
asyncssh_error = str(e)
else:
asyncssh_error = None
return {
"python_executable": sys.executable,
"python_version": sys.version,
"asyncssh_available": asyncssh_ok,
"asyncssh_version": asyncssh_version,
"asyncssh_error": asyncssh_error,
}
@router.get("/{host_name}", response_model=HealthCheck)
async def check_host_health(host_name: str, api_key_valid: bool = Depends(verify_api_key)):
"""Effectue un health check sur un hôte spécifique."""
+19 -2
View File
@@ -2,13 +2,16 @@
Routes API pour les métriques système et des hôtes.
"""
from datetime import timezone
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
import pytz
from app.core.dependencies import get_db, verify_api_key
from app.core.config import settings
from app.crud.host_metrics import HostMetricsRepository
@@ -22,6 +25,15 @@ class CollectionScheduleRequest(BaseModel):
router = APIRouter()
def _to_app_timezone(dt):
if dt is None:
return None
if getattr(dt, "tzinfo", None) is None:
dt = dt.replace(tzinfo=timezone.utc)
tz = pytz.timezone(settings.scheduler_timezone)
return dt.astimezone(tz)
@router.get("")
async def get_metrics(api_key_valid: bool = Depends(verify_api_key)):
"""Récupère les métriques système globales."""
@@ -41,6 +53,7 @@ async def get_all_hosts_metrics(
# Convertir en dict host_id -> metrics
result = {}
for host_id, m in metrics_dict.items():
collected_at = _to_app_timezone(m.collected_at)
result[m.host_id] = {
"host_id": m.host_id,
"metric_type": m.metric_type,
@@ -56,7 +69,8 @@ async def get_all_hosts_metrics(
"disk_root_used_gb": m.disk_root_used_gb,
"os_name": m.os_name,
"uptime_human": m.uptime_human,
"collected_at": m.collected_at,
"last_collected": collected_at,
"collected_at": collected_at,
"collection_status": "success" if not m.error_message else "failed",
"error_message": m.error_message,
}
@@ -118,6 +132,8 @@ async def get_host_metrics(
if not metrics:
return {"host_id": host_id, "collection_status": "no_data"}
collected_at = _to_app_timezone(metrics.collected_at)
return {
"host_id": metrics.host_id,
@@ -135,7 +151,8 @@ async def get_host_metrics(
"disk_info": metrics.disk_info,
"os_name": metrics.os_name,
"uptime_human": metrics.uptime_human,
"collected_at": metrics.collected_at,
"last_collected": collected_at,
"collected_at": collected_at,
"collection_status": "success" if not metrics.error_message else "failed",
"error_message": metrics.error_message,
}