Explorar o código

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).
maziggy hai 2 meses
pai
achega
58d1b1eb74

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 0 - 0
CHANGELOG.md


+ 9 - 1
backend/app/api/routes/system.py

@@ -498,7 +498,15 @@ async def get_system_info(
 
 
     # System info
     # System info
     memory = psutil.virtual_memory()
     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()
     uptime_seconds = (datetime.now() - boot_time).total_seconds()
 
 
     # Python and system info
     # Python and system info

+ 65 - 0
backend/tests/integration/test_system_api.py

@@ -29,6 +29,7 @@ class TestSystemAPI:
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
             )
             )
             mock_psutil.boot_time.return_value = 1700000000.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_count.return_value = 4
             mock_psutil.cpu_percent.return_value = 25.0
             mock_psutil.cpu_percent.return_value = 25.0
 
 
@@ -58,6 +59,7 @@ class TestSystemAPI:
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
             )
             )
             mock_psutil.boot_time.return_value = 1700000000.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_count.return_value = 4
             mock_psutil.cpu_percent.return_value = 25.0
             mock_psutil.cpu_percent.return_value = 25.0
 
 
@@ -82,6 +84,7 @@ class TestSystemAPI:
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
             )
             )
             mock_psutil.boot_time.return_value = 1700000000.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_count.return_value = 4
             mock_psutil.cpu_percent.return_value = 25.0
             mock_psutil.cpu_percent.return_value = 25.0
 
 
@@ -114,6 +117,7 @@ class TestSystemAPI:
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
             )
             )
             mock_psutil.boot_time.return_value = 1700000000.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_count.return_value = 4
             mock_psutil.cpu_percent.return_value = 25.0
             mock_psutil.cpu_percent.return_value = 25.0
 
 
@@ -144,6 +148,7 @@ class TestSystemAPI:
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
             )
             )
             mock_psutil.boot_time.return_value = 1700000000.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_count.return_value = 4
             mock_psutil.cpu_percent.return_value = 25.0
             mock_psutil.cpu_percent.return_value = 25.0
 
 
@@ -170,6 +175,7 @@ class TestSystemAPI:
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
             )
             )
             mock_psutil.boot_time.return_value = 1700000000.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_count.return_value = 4
             mock_psutil.cpu_percent.return_value = 25.0
             mock_psutil.cpu_percent.return_value = 25.0
 
 
@@ -200,6 +206,7 @@ class TestSystemAPI:
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
             )
             )
             mock_psutil.boot_time.return_value = 1700000000.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_count.return_value = 4
             mock_psutil.cpu_percent.return_value = 25.0
             mock_psutil.cpu_percent.return_value = 25.0
 
 
@@ -257,6 +264,7 @@ class TestSystemAPI:
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
                 total=16000000000, available=8000000000, used=8000000000, percent=50.0
             )
             )
             mock_psutil.boot_time.return_value = 1700000000.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_count.return_value = 4
             mock_psutil.cpu_percent.return_value = 25.0
             mock_psutil.cpu_percent.return_value = 25.0
             mock_pm._clients = {}
             mock_pm._clients = {}
@@ -271,6 +279,63 @@ class TestSystemAPI:
         assert db_info["archives_failed"] >= 1
         assert db_info["archives_failed"] >= 1
         assert db_info["total_print_time_seconds"] >= 5400
         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:
 class TestSystemHelperFunctions:
     """Tests for system info helper functions."""
     """Tests for system info helper functions."""

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio