diff --git a/scripts/lib/health.py b/scripts/lib/health.py index dde81a8..d713866 100644 --- a/scripts/lib/health.py +++ b/scripts/lib/health.py @@ -102,17 +102,29 @@ def send_html(self, html: str, status: int = 200) -> None: def _check_openvpn() -> bool: - """Check if at least one OpenVPN process is running.""" - result = subprocess.run(["pgrep", "-x", "openvpn"], capture_output=True) + """Check if at least one OpenVPN process is running. + + A host without pgrep reads as not running, so a /metrics scrape still answers. + """ + try: + result = subprocess.run(["pgrep", "-x", "openvpn"], capture_output=True) + except FileNotFoundError: + return False return result.returncode == 0 def _check_wireguard() -> bool: - """Check if wg0 interface exists and has a listening port.""" - result = subprocess.run( - ["ip", "link", "show", "wg0"], - capture_output=True, - ) + """Check if wg0 interface exists and has a listening port. + + A host without iproute2 reads as no interface, so a /metrics scrape still answers. + """ + try: + result = subprocess.run( + ["ip", "link", "show", "wg0"], + capture_output=True, + ) + except FileNotFoundError: + return False return result.returncode == 0 diff --git a/tests/unit/test_health.py b/tests/unit/test_health.py index a7bb738..f7d6811 100644 --- a/tests/unit/test_health.py +++ b/tests/unit/test_health.py @@ -118,6 +118,18 @@ def test_readiness_transitions(self, obs_server): assert status3 == 503 +class TestVpnChecksWithoutTheirTools: + """A host missing pgrep or iproute2 reads as VPN down, never an exception.""" + + @pytest.mark.parametrize("check", ["_check_openvpn", "_check_wireguard"]) + def test_missing_binary_reads_as_down(self, monkeypatch, check): + def missing(*_args: Any, **_kwargs: Any) -> None: + raise FileNotFoundError(2, "No such file or directory") + + monkeypatch.setattr(lib_health.subprocess, "run", missing) + assert getattr(lib_health, check)() is False + + class TestMetricsOnObservabilityPort: """A wired metrics object is served from the same port."""