Files
RelayOps/backend/app/services/server_runtime.py
2026-06-07 02:32:28 +03:00

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)