diff --git a/systemgate/main.py b/systemgate/main.py index fc1a44a..f8c2be7 100644 --- a/systemgate/main.py +++ b/systemgate/main.py @@ -149,7 +149,10 @@ def vitals(): # container's own filesystem and saying so is the difference between # telemetry and a misleading number. disk_path = os.environ.get("SYSTEMGATE_DISK_PATH", "/") - disk = psutil.disk_usage(disk_path) + try: + disk = psutil.disk_usage(disk_path) + except OSError: + disk = None # Read from psutil itself rather than from the environment, so this reports # where the numbers came from instead of where they were asked to come from. @@ -167,7 +170,8 @@ def vitals(): "cpu_percent": psutil.cpu_percent(interval=0.05), "cpu_count": psutil.cpu_count(), "memory": psutil.virtual_memory()._asdict(), - "disk": disk._asdict(), + # null when the disk path cannot be read; source.disk_path says which path. + "disk": disk._asdict() if disk is not None else None, "load": os.getloadavg() if hasattr(os, "getloadavg") else [0.0, 0.0, 0.0], "temps": temps, "source": { @@ -214,7 +218,13 @@ def services(limit: int = 80): try: connections = psutil.net_connections(kind="inet") except (psutil.AccessDenied, OSError): - connections = [] + return { + "status": "unavailable", + "results": [], + "source": "psutil.net_connections", + "metadata_only": True, + "errors": ["service_enumeration_unavailable"], + } for conn in connections: if str(getattr(conn, "status", "") or "") != "LISTEN": continue diff --git a/tests/test_endpoints.py b/tests/test_endpoints.py index a27aeab..9bdf58c 100644 --- a/tests/test_endpoints.py +++ b/tests/test_endpoints.py @@ -123,6 +123,22 @@ def test_vitals_with_mocked_psutil(tmp_path, monkeypatch): assert data["memory"]["total"] == 100 +def test_vitals_survive_an_unreadable_disk_path(tmp_path, monkeypatch): + import systemgate.main as main + + monkeypatch.setenv("SYSTEMGATE_DISK_PATH", str(tmp_path / "missing")) + monkeypatch.setattr(main.psutil, "sensors_temperatures", lambda fahrenheit=False: {}, raising=False) + + with make_client(tmp_path, monkeypatch) as client: + response = client.get("/vitals", headers=headers()) + + assert response.status_code == 200 + data = response.json() + assert data["disk"] is None + assert data["source"]["disk_path"] == str(tmp_path / "missing") + assert data["memory"]["total"] > 0 + + def test_containers_truthfully_reports_unavailable(tmp_path, monkeypatch): with make_client(tmp_path, monkeypatch) as client: data = client.get("/containers", headers=headers()).json() diff --git a/tests/test_inventory_limits.py b/tests/test_inventory_limits.py index b9781ee..59bd9e7 100644 --- a/tests/test_inventory_limits.py +++ b/tests/test_inventory_limits.py @@ -212,6 +212,8 @@ def test_services_survive_a_denied_connection_listing(tmp_path, monkeypatch): assert response.status_code == 200 assert data["results"] == [] assert data["metadata_only"] is True + assert data["status"] == "unavailable" + assert data["errors"] == ["service_enumeration_unavailable"] def test_processes_sort_skip_and_clamp(tmp_path, monkeypatch):