import socket from datetime import datetime, timezone from fastapi import status from app.core.errors import raise_api_error from app.integrations.ssh_client import SSHClient, SSHClientError, SSHConnectionConfig from app.models.server import Server from app.schemas.server import ServerHealthRead, ServerMetricsRead class ServerRuntimeService: def __init__(self, ssh_client: SSHClient | None = None) -> None: self.ssh_client = ssh_client or SSHClient() def get_server_health(self, server: Server) -> ServerHealthRead: checked_at = datetime.now(timezone.utc) try: self._check_tcp_connectivity(server.host, server.port) return ServerHealthRead( server_id=server.id, reachable=True, status="ok", checked_at=checked_at, ) except OSError as exc: return ServerHealthRead( server_id=server.id, reachable=False, status="unreachable", checked_at=checked_at, detail=str(exc), ) def get_server_metrics(self, server: Server) -> ServerMetricsRead: if server.connection_type != "ssh": raise_api_error( status_code=status.HTTP_400_BAD_REQUEST, code="unsupported_connection_type", message="Metrics collection is currently implemented only for SSH servers.", ) if not server.ssh_username: raise_api_error( status_code=status.HTTP_400_BAD_REQUEST, code="ssh_username_required", message="Server ssh_username is required for metrics collection.", ) try: raw_metrics = self._collect_ssh_metrics(server) except SSHClientError as exc: raise_api_error( status_code=status.HTTP_502_BAD_GATEWAY, code="metrics_collection_failed", message=f"Unable to collect metrics over SSH: {exc}", ) return ServerMetricsRead( server_id=server.id, cpu_percent=float(raw_metrics["cpu_percent"]), memory_percent=float(raw_metrics["memory_percent"]), disk_percent=float(raw_metrics["disk_percent"]), uptime_seconds=int(raw_metrics["uptime_seconds"]), load_average=[ float(value) for value in str(raw_metrics["load_average"]).replace(",", " ").split() ][:3], collected_at=datetime.now(timezone.utc), ) def _check_tcp_connectivity(self, host: str, port: int) -> None: with socket.create_connection((host, port), timeout=5): return def _collect_ssh_metrics(self, server: Server) -> dict[str, str]: connection = SSHConnectionConfig( host=server.host, port=server.port, username=server.ssh_username, ) commands = { "cpu_percent": ( "sh -lc \"" "read _ user nice system idle iowait irq softirq steal _ < /proc/stat; " "total1=$((user+nice+system+idle+iowait+irq+softirq+steal)); " "idle1=$((idle+iowait)); " "sleep 1; " "read _ user nice system idle iowait irq softirq steal _ < /proc/stat; " "total2=$((user+nice+system+idle+iowait+irq+softirq+steal)); " "idle2=$((idle+iowait)); " "total=$((total2-total1)); " "idle_delta=$((idle2-idle1)); " "awk -v t=$total -v i=$idle_delta 'BEGIN { if (t <= 0) printf \\\"0.00\\\"; else printf \\\"%.2f\\\", (1 - i/t) * 100 }'\"" ), "memory_percent": "free | awk '/Mem:/ {printf \"%.2f\", ($3 / $2) * 100}'", "disk_percent": "df -P / | awk 'NR==2 {gsub(/%/, \"\", $5); print $5}'", "uptime_seconds": "cut -d. -f1 /proc/uptime", "load_average": "awk '{print $1\" \"$2\" \"$3}' /proc/loadavg", } return self.ssh_client.run_commands(connection=connection, commands=commands)