From ed310f0d8daf43855057f195b762f7e67d825772 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Wed, 10 Jun 2026 10:48:03 +0200 Subject: [PATCH] fix(status): keep non-HTTP services grey instead of red MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-service checks now only probe HTTP(S)-reachable services. SSH (22) and other non-web ports (DB, mail, DNS, raw TCP) stay 'unknown' (grey category colour) rather than going red — an open TCP socket doesn't prove the service is healthy, and a firewalled port flapped red misleadingly. ha-relevant: yes --- backend/app/services/status_checker.py | 47 +++++++++++++------------- backend/tests/test_status_checker.py | 33 ++++++++++-------- 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/backend/app/services/status_checker.py b/backend/app/services/status_checker.py index dd2a1da..8dc963a 100644 --- a/backend/app/services/status_checker.py +++ b/backend/app/services/status_checker.py @@ -122,10 +122,12 @@ async def _tcp_connect(host: str, port: int) -> bool: # --- Per-service status checks --- -# Ports that are definitely not HTTP/web — mirror of frontend serviceUrl.ts. -# SSH (22) is handled as a plain TCP check, so it is intentionally absent here. +# Ports that are not HTTP/web. These get NO status check — a service here stays +# grey (unknown) rather than going red. An open TCP socket doesn't prove the +# service is healthy, and a closed one flaps red misleadingly (e.g. SSH on a +# box that simply firewalls 22). Only HTTP(S)-reachable services are checked. _NON_HTTP_PORTS = frozenset({ - 21, 23, 25, 465, 587, 53, 110, 143, 993, 995, 389, 636, 445, 514, + 22, 21, 23, 25, 465, 587, 53, 110, 143, 993, 995, 389, 636, 445, 514, 1433, 3306, 5432, 5672, 6379, 9092, 11211, 27017, 27018, }) _HTTPS_PORTS = frozenset({443, 8443}) @@ -139,9 +141,10 @@ def _service_host(svc: dict[str, Any], host: str) -> str: async def check_service(svc: dict[str, Any], host: str | None) -> str: """Check a single service. Returns 'online' | 'offline' | 'unknown'. - Web services get an HTTP(S) GET; everything else with a port gets a TCP - connect. UDP services and port-less non-web services are 'unknown' so they - keep their category colour rather than flashing red. + Only HTTP(S)-reachable services get a real check (an HTTP GET). Everything + else — SSH, databases, mail, DNS, raw TCP, UDP, port-less — stays 'unknown' + so it keeps its category colour instead of flashing red. An open TCP socket + doesn't prove a non-web service is healthy, so we don't pretend it does. """ if not host or host.startswith("-"): return "unknown" @@ -151,24 +154,22 @@ async def check_service(svc: dict[str, Any], host: str | None) -> str: port = svc.get("port") port = int(port) if isinstance(port, int) or (isinstance(port, str) and port.isdigit()) else None - try: - if port is not None and port in _NON_HTTP_PORTS: - return "online" if await _tcp_connect(host, port) else "offline" - - name = str(svc.get("service_name", "")).lower() - is_web = port is None or port not in _NON_HTTP_PORTS - if is_web and (port is not None or "http" in name): - scheme = "https" if ( - port in _HTTPS_PORTS or "https" in name or "ssl" in name or "tls" in name - ) else "http" - url_host = _service_host(svc, host) - url = f"{scheme}://{url_host}" + (f":{port}" if port is not None else "") - return "online" if await _http_get(url, verify=False) else "offline" - - if port is not None: - return "online" if await _tcp_connect(host, port) else "offline" - + # Non-HTTP ports (SSH 22, DB, mail, …) are never checked — keep them grey. + if port is not None and port in _NON_HTTP_PORTS: return "unknown" + + name = str(svc.get("service_name", "")).lower() + is_web = port is not None or "http" in name + if not is_web: + return "unknown" + + try: + scheme = "https" if ( + port in _HTTPS_PORTS or "https" in name or "ssl" in name or "tls" in name + ) else "http" + url_host = _service_host(svc, host) + url = f"{scheme}://{url_host}" + (f":{port}" if port is not None else "") + return "online" if await _http_get(url, verify=False) else "offline" except Exception as exc: logger.debug("Service check failed for %s:%s (%s)", host, port, exc) return "offline" diff --git a/backend/tests/test_status_checker.py b/backend/tests/test_status_checker.py index d384f02..6555585 100644 --- a/backend/tests/test_status_checker.py +++ b/backend/tests/test_status_checker.py @@ -410,19 +410,25 @@ async def test_check_service_web_offline_when_http_fails(): @pytest.mark.asyncio -async def test_check_service_non_http_port_uses_tcp(): - captured = {} - - async def fake_tcp(host, port): - captured["host"] = host - captured["port"] = port - return True - +async def test_check_service_non_http_port_is_unknown(): + """Non-HTTP ports (DB, mail, …) stay grey — no TCP check, no red flap.""" svc = {"port": 5432, "protocol": "tcp", "service_name": "postgres"} - with patch("app.services.status_checker._tcp_connect", side_effect=fake_tcp): + with patch("app.services.status_checker._tcp_connect", new_callable=AsyncMock) as mock_tcp, \ + patch("app.services.status_checker._http_get", new_callable=AsyncMock) as mock_http: result = await check_service(svc, "10.0.0.1") - assert result == "online" - assert captured == {"host": "10.0.0.1", "port": 5432} + assert result == "unknown" + mock_tcp.assert_not_called() + mock_http.assert_not_called() + + +@pytest.mark.asyncio +async def test_check_service_ssh_port_22_is_unknown(): + """SSH (port 22) is never checked — keep it grey, not red/green.""" + svc = {"port": 22, "protocol": "tcp", "service_name": "ssh"} + with patch("app.services.status_checker._tcp_connect", new_callable=AsyncMock) as mock_tcp: + result = await check_service(svc, "10.0.0.1") + assert result == "unknown" + mock_tcp.assert_not_called() @pytest.mark.asyncio @@ -445,12 +451,11 @@ async def test_check_services_returns_status_per_service(): {"port": 80, "protocol": "tcp", "service_name": "http"}, {"port": 5432, "protocol": "tcp", "service_name": "postgres"}, ] - with patch("app.services.status_checker._http_get", new_callable=AsyncMock, return_value=True), \ - patch("app.services.status_checker._tcp_connect", new_callable=AsyncMock, return_value=False): + with patch("app.services.status_checker._http_get", new_callable=AsyncMock, return_value=True): results = await check_services("10.0.0.1", services) assert results == [ {"port": 80, "protocol": "tcp", "status": "online"}, - {"port": 5432, "protocol": "tcp", "status": "offline"}, + {"port": 5432, "protocol": "tcp", "status": "unknown"}, ]