102 lines
4.1 KiB
Python
102 lines
4.1 KiB
Python
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)
|