From a35df7638431139b7133b68efb5ed5ee49fd74be Mon Sep 17 00:00:00 2001 From: Cristi Date: Fri, 14 Aug 2026 23:36:03 +0300 Subject: [PATCH] feat: Enhance information displayed on the system status dashboard --- gns3server/api/routes/compute/compute.py | 23 +++++++++++++-- .../api/routes/controller/controller.py | 10 +++++++ tests/compute/test_statistics.py | 28 +++++++++++++++++++ tests/controller/test_statistics.py | 14 ++++++++++ 4 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 tests/compute/test_statistics.py create mode 100644 tests/controller/test_statistics.py diff --git a/gns3server/api/routes/compute/compute.py b/gns3server/api/routes/compute/compute.py index 3a1aa61af..1a862787c 100644 --- a/gns3server/api/routes/compute/compute.py +++ b/gns3server/api/routes/compute/compute.py @@ -21,7 +21,9 @@ API routes for compute. import os import psutil +import cpuinfo +from functools import lru_cache from gns3server.config import Config from gns3server.utils.cpu_percent import CpuPercent from gns3server.version import __version__ @@ -42,6 +44,11 @@ from typing import Optional, List router = APIRouter() +@lru_cache(maxsize=1) +def get_cpu_model() -> str: + return cpuinfo.get_cpu_info().get("brand_raw", "") + + @router.post("/projects/{project_id}/ports/udp", status_code=status.HTTP_201_CREATED) def allocate_udp_port(project_id: UUID) -> dict: """ @@ -119,10 +126,15 @@ def compute_statistics() -> dict: swap_free = psutil.swap_memory().free swap_used = psutil.swap_memory().used cpu_percent = int(CpuPercent.get()) - load_average_percent = [int(x / psutil.cpu_count() * 100) for x in psutil.getloadavg()] + cpu_count = psutil.cpu_count(logical=True) or 1 + cpu_count_physical = psutil.cpu_count(logical=False) + raw_load_average = psutil.getloadavg() + load_average = [round(x, 2) for x in raw_load_average] + load_average_percent = [round(x / cpu_count * 100, 2) for x in raw_load_average] memory_percent = int(psutil.virtual_memory().percent) swap_percent = int(psutil.swap_memory().percent) - disk_usage_percent = int(psutil.disk_usage(get_default_project_directory()).percent) + disk_usage = psutil.disk_usage(get_default_project_directory()) + disk_usage_percent = int(disk_usage.percent) except psutil.Error as e: raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR) # raise HTTPConflict(text="Psutil error detected: {}".format(e)) @@ -135,9 +147,16 @@ def compute_statistics() -> dict: "swap_free": swap_free, "swap_used": swap_used, "cpu_usage_percent": cpu_percent, + "cpu_count": cpu_count, + "cpu_count_physical": cpu_count_physical, + "cpu_model": get_cpu_model(), "memory_usage_percent": memory_percent, "swap_usage_percent": swap_percent, "disk_usage_percent": disk_usage_percent, + "disk_total": disk_usage.total, + "disk_used": disk_usage.used, + "disk_free": disk_usage.free, + "load_average": load_average, "load_average_percent": load_average_percent, } diff --git a/gns3server/api/routes/controller/controller.py b/gns3server/api/routes/controller/controller.py index 4f1a3e862..da00957a4 100644 --- a/gns3server/api/routes/controller/controller.py +++ b/gns3server/api/routes/controller/controller.py @@ -17,6 +17,8 @@ import asyncio import signal import os +import time +import psutil from fastapi import APIRouter, Request, Depends, WebSocket, WebSocketDisconnect, status from fastapi.responses import StreamingResponse @@ -42,6 +44,13 @@ log = logging.getLogger(__name__) router = APIRouter() +def get_server_uptime_seconds() -> int: + try: + return max(0, int(time.time() - psutil.Process().create_time())) + except psutil.Error: + return 0 + + @router.get( "/version", response_model=schemas.Version, @@ -246,6 +255,7 @@ async def statistics() -> dict: webwireshark_stats = await collect_webwireshark_stats(projects) return { + "uptime_seconds": get_server_uptime_seconds(), "computes": compute_statistics, "projects": project_stats, "nodes": node_stats, diff --git a/tests/compute/test_statistics.py b/tests/compute/test_statistics.py new file mode 100644 index 000000000..8fb721adc --- /dev/null +++ b/tests/compute/test_statistics.py @@ -0,0 +1,28 @@ +from types import SimpleNamespace +from unittest.mock import patch + +from gns3server.api.routes.compute.compute import compute_statistics + + +def test_compute_statistics_preserves_load_average_precision() -> None: + memory = SimpleNamespace(total=8192, available=4096, percent=50) + swap = SimpleNamespace(total=1024, free=512, used=512, percent=50) + disk = SimpleNamespace(total=16384, used=4096, free=12288, percent=25) + + with patch("gns3server.api.routes.compute.compute.CpuPercent.get", return_value=10), \ + patch("gns3server.api.routes.compute.compute.get_cpu_model", return_value="Test CPU"), \ + patch("gns3server.api.routes.compute.compute.psutil.virtual_memory", return_value=memory), \ + patch("gns3server.api.routes.compute.compute.psutil.swap_memory", return_value=swap), \ + patch("gns3server.api.routes.compute.compute.psutil.disk_usage", return_value=disk), \ + patch("gns3server.api.routes.compute.compute.psutil.cpu_count", return_value=4), \ + patch("gns3server.api.routes.compute.compute.psutil.getloadavg", return_value=(1.25, 2.5, 3.75)): + statistics = compute_statistics() + + assert statistics["load_average"] == [1.25, 2.5, 3.75] + assert statistics["load_average_percent"] == [31.25, 62.5, 93.75] + assert statistics["cpu_count"] == 4 + assert statistics["cpu_count_physical"] == 4 + assert statistics["cpu_model"] == "Test CPU" + assert statistics["disk_total"] == 16384 + assert statistics["disk_used"] == 4096 + assert statistics["disk_free"] == 12288 diff --git a/tests/controller/test_statistics.py b/tests/controller/test_statistics.py new file mode 100644 index 000000000..f184a92b8 --- /dev/null +++ b/tests/controller/test_statistics.py @@ -0,0 +1,14 @@ +from unittest.mock import MagicMock, patch + +from gns3server.api.routes.controller.controller import get_server_uptime_seconds + + +def test_server_uptime_uses_process_creation_time() -> None: + process = MagicMock() + process.create_time.return_value = 50.25 + + with patch("gns3server.api.routes.controller.controller.time.time", return_value=200.75), \ + patch("gns3server.api.routes.controller.controller.psutil.Process", return_value=process): + uptime = get_server_uptime_seconds() + + assert uptime == 150