mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
fix(system): use PID 1 create_time for uptime/boot time on containers (#1690)
System -> Uptime / Boot Time read psutil.boot_time(), which on shared-kernel containers (Docker, LXC, Proxmox containers) is /proc/stat:btime - the host kernel's boot time, not the container's. Reporter on Proxmox LXC saw the Proxmox node's uptime instead of the Bambuddy container's. PID 1 is the container's entrypoint (or the host init on bare metal), and its create_time is the POSIX wall-clock timestamp of when it started. Switching to psutil.Process(1).create_time() reports the right value on containers and matches host boot within a sub-second on bare metal. Defensive fallback to psutil.boot_time() on psutil.Error / OSError so the endpoint still returns 200 with the best-available answer if /proc/1/stat is unreadable (locked-down container, custom seccomp policy).
This commit is contained in:
@@ -498,7 +498,15 @@ async def get_system_info(
|
||||
|
||||
# System info
|
||||
memory = psutil.virtual_memory()
|
||||
boot_time = datetime.fromtimestamp(psutil.boot_time())
|
||||
# PID 1's create_time is the right uptime anchor in containerised installs
|
||||
# (Docker, LXC) — psutil.boot_time() reads /proc/stat:btime which on a
|
||||
# shared-kernel container is the host's boot time, not the container's
|
||||
# (#1690). On bare metal / VMs PID 1 is the host init, which starts at
|
||||
# boot, so the value matches psutil.boot_time() within a sub-second.
|
||||
try:
|
||||
boot_time = datetime.fromtimestamp(psutil.Process(1).create_time())
|
||||
except (psutil.Error, OSError):
|
||||
boot_time = datetime.fromtimestamp(psutil.boot_time())
|
||||
uptime_seconds = (datetime.now() - boot_time).total_seconds()
|
||||
|
||||
# Python and system info
|
||||
|
||||
@@ -29,6 +29,7 @@ class TestSystemAPI:
|
||||
total=16000000000, available=8000000000, used=8000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.boot_time.return_value = 1700000000.0
|
||||
mock_psutil.Process.return_value.create_time.return_value = 1700000000.0
|
||||
mock_psutil.cpu_count.return_value = 4
|
||||
mock_psutil.cpu_percent.return_value = 25.0
|
||||
|
||||
@@ -58,6 +59,7 @@ class TestSystemAPI:
|
||||
total=16000000000, available=8000000000, used=8000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.boot_time.return_value = 1700000000.0
|
||||
mock_psutil.Process.return_value.create_time.return_value = 1700000000.0
|
||||
mock_psutil.cpu_count.return_value = 4
|
||||
mock_psutil.cpu_percent.return_value = 25.0
|
||||
|
||||
@@ -82,6 +84,7 @@ class TestSystemAPI:
|
||||
total=16000000000, available=8000000000, used=8000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.boot_time.return_value = 1700000000.0
|
||||
mock_psutil.Process.return_value.create_time.return_value = 1700000000.0
|
||||
mock_psutil.cpu_count.return_value = 4
|
||||
mock_psutil.cpu_percent.return_value = 25.0
|
||||
|
||||
@@ -114,6 +117,7 @@ class TestSystemAPI:
|
||||
total=16000000000, available=8000000000, used=8000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.boot_time.return_value = 1700000000.0
|
||||
mock_psutil.Process.return_value.create_time.return_value = 1700000000.0
|
||||
mock_psutil.cpu_count.return_value = 4
|
||||
mock_psutil.cpu_percent.return_value = 25.0
|
||||
|
||||
@@ -144,6 +148,7 @@ class TestSystemAPI:
|
||||
total=16000000000, available=8000000000, used=8000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.boot_time.return_value = 1700000000.0
|
||||
mock_psutil.Process.return_value.create_time.return_value = 1700000000.0
|
||||
mock_psutil.cpu_count.return_value = 4
|
||||
mock_psutil.cpu_percent.return_value = 25.0
|
||||
|
||||
@@ -170,6 +175,7 @@ class TestSystemAPI:
|
||||
total=16000000000, available=8000000000, used=8000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.boot_time.return_value = 1700000000.0
|
||||
mock_psutil.Process.return_value.create_time.return_value = 1700000000.0
|
||||
mock_psutil.cpu_count.return_value = 4
|
||||
mock_psutil.cpu_percent.return_value = 25.0
|
||||
|
||||
@@ -200,6 +206,7 @@ class TestSystemAPI:
|
||||
total=16000000000, available=8000000000, used=8000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.boot_time.return_value = 1700000000.0
|
||||
mock_psutil.Process.return_value.create_time.return_value = 1700000000.0
|
||||
mock_psutil.cpu_count.return_value = 4
|
||||
mock_psutil.cpu_percent.return_value = 25.0
|
||||
|
||||
@@ -257,6 +264,7 @@ class TestSystemAPI:
|
||||
total=16000000000, available=8000000000, used=8000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.boot_time.return_value = 1700000000.0
|
||||
mock_psutil.Process.return_value.create_time.return_value = 1700000000.0
|
||||
mock_psutil.cpu_count.return_value = 4
|
||||
mock_psutil.cpu_percent.return_value = 25.0
|
||||
mock_pm._clients = {}
|
||||
@@ -271,6 +279,63 @@ class TestSystemAPI:
|
||||
assert db_info["archives_failed"] >= 1
|
||||
assert db_info["total_print_time_seconds"] >= 5400
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_boot_time_uses_pid1_create_time(self, async_client: AsyncClient):
|
||||
"""#1690: container installs (Docker/LXC) share the host kernel, so
|
||||
psutil.boot_time() returns the host's boot time instead of the
|
||||
container's. Reading PID 1's create_time gives the container start
|
||||
time on containers and matches host boot on bare metal."""
|
||||
with patch("backend.app.api.routes.system.psutil") as mock_psutil:
|
||||
mock_psutil.disk_usage.return_value = MagicMock(
|
||||
total=500000000000, used=250000000000, free=250000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(
|
||||
total=16000000000, available=8000000000, used=8000000000, percent=50.0
|
||||
)
|
||||
# Host boot is FOUR DAYS earlier than the container's PID 1 start.
|
||||
# The route must report the PID 1 value, not the host value.
|
||||
mock_psutil.boot_time.return_value = 1700000000.0
|
||||
mock_psutil.Process.return_value.create_time.return_value = 1700345600.0
|
||||
mock_psutil.cpu_count.return_value = 4
|
||||
mock_psutil.cpu_percent.return_value = 25.0
|
||||
|
||||
response = await async_client.get("/api/v1/system/info")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["system"]["boot_time"].startswith("2023-11-18T") # 1700345600 UTC
|
||||
# PID 1 was queried with pid=1 (not the worker pid).
|
||||
mock_psutil.Process.assert_called_with(1)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_boot_time_falls_back_to_psutil_boot_time_on_pid1_failure(self, async_client: AsyncClient):
|
||||
"""If PID 1 is unreadable (rare — locked-down container, /proc not
|
||||
mounted), fall back to psutil.boot_time() so the endpoint still
|
||||
returns 200 with the best available answer."""
|
||||
import psutil as real_psutil
|
||||
|
||||
with patch("backend.app.api.routes.system.psutil") as mock_psutil:
|
||||
mock_psutil.disk_usage.return_value = MagicMock(
|
||||
total=500000000000, used=250000000000, free=250000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.virtual_memory.return_value = MagicMock(
|
||||
total=16000000000, available=8000000000, used=8000000000, percent=50.0
|
||||
)
|
||||
mock_psutil.boot_time.return_value = 1700000000.0
|
||||
# Use the real exception classes so the route's except clause matches.
|
||||
mock_psutil.Error = real_psutil.Error
|
||||
mock_psutil.Process.side_effect = real_psutil.NoSuchProcess(1)
|
||||
mock_psutil.cpu_count.return_value = 4
|
||||
mock_psutil.cpu_percent.return_value = 25.0
|
||||
|
||||
response = await async_client.get("/api/v1/system/info")
|
||||
|
||||
assert response.status_code == 200
|
||||
result = response.json()
|
||||
assert result["system"]["boot_time"].startswith("2023-11-14T") # 1700000000 UTC
|
||||
|
||||
|
||||
class TestSystemHelperFunctions:
|
||||
"""Tests for system info helper functions."""
|
||||
|
||||
Reference in New Issue
Block a user