af60ca446d
The fabledscryer->steward rename had only ever reached host_agent. The other five bundled plugins (http, snmp, traefik, unifi, docker) still imported `from fabledscryer.*` (package no longer exists) and read FABLEDSCRYER_* env vars — so every one of them was broken at import since the original rebrand. CI stayed green only because none are enabled by default and migrations don't import plugin modules. Now that they version in-tree, complete the rename: - fabledscryer.* -> steward.* imports across all five plugins - FABLEDSCRYER_* -> STEWARD_* in plugin migration env.py files - author/repository/homepage + user-facing 'Fabled Scryer' strings -> Steward - snmp/scheduler.py: also drop dead `now`/datetime; record_metric from steward Adds tests/test_no_legacy_names.py — fails if 'scryer'/'roundtable' ever reappear in shipped code (the drift bit twice; this stops a third time). Also clears pre-existing ruff lint debt (unused imports, semicolon statements, mid-file import) surfaced by the new lint lane. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
105 lines
3.2 KiB
Python
105 lines
3.2 KiB
Python
# plugins/http/checker.py
|
|
"""Core HTTP check logic — runs a single monitor check and returns a result dict."""
|
|
from __future__ import annotations
|
|
import asyncio
|
|
import logging
|
|
import ssl
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from urllib.parse import urlparse
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def _get_tls_expiry(hostname: str, port: int) -> datetime | None:
|
|
"""Attempt a bare TLS handshake to extract the certificate expiry date."""
|
|
ctx = ssl.create_default_context()
|
|
try:
|
|
reader, writer = await asyncio.wait_for(
|
|
asyncio.open_connection(hostname, port, ssl=ctx),
|
|
timeout=5.0,
|
|
)
|
|
cert = writer.get_extra_info("ssl_object").getpeercert()
|
|
writer.close()
|
|
try:
|
|
await writer.wait_closed()
|
|
except Exception:
|
|
pass
|
|
exp_str = cert.get("notAfter", "")
|
|
if not exp_str:
|
|
return None
|
|
# Format: "Mar 22 12:00:00 2027 GMT"
|
|
return datetime.strptime(exp_str, "%b %d %H:%M:%S %Y %Z").replace(
|
|
tzinfo=timezone.utc
|
|
)
|
|
except Exception as exc:
|
|
logger.debug("TLS check failed for %s:%d — %s", hostname, port, exc)
|
|
return None
|
|
|
|
|
|
async def run_check(
|
|
url: str,
|
|
method: str = "GET",
|
|
expected_status: int = 200,
|
|
content_match: str = "",
|
|
headers: dict | None = None,
|
|
timeout_seconds: int = 10,
|
|
follow_redirects: bool = True,
|
|
verify_ssl: bool = True,
|
|
) -> dict:
|
|
"""
|
|
Perform one HTTP check. Returns a dict with:
|
|
is_up, status_code, response_ms, content_matched, error_msg, tls_expires_at
|
|
"""
|
|
result: dict = {
|
|
"is_up": False,
|
|
"status_code": None,
|
|
"response_ms": None,
|
|
"content_matched": None,
|
|
"error_msg": None,
|
|
"tls_expires_at": None,
|
|
}
|
|
|
|
parsed = urlparse(url)
|
|
is_https = parsed.scheme.lower() == "https"
|
|
|
|
t0 = time.monotonic()
|
|
try:
|
|
async with httpx.AsyncClient(
|
|
follow_redirects=follow_redirects,
|
|
verify=verify_ssl,
|
|
timeout=timeout_seconds,
|
|
) as client:
|
|
response = await client.request(
|
|
method,
|
|
url,
|
|
headers=headers or {},
|
|
)
|
|
result["response_ms"] = round((time.monotonic() - t0) * 1000, 1)
|
|
result["status_code"] = response.status_code
|
|
|
|
status_ok = response.status_code == expected_status
|
|
if content_match:
|
|
matched = content_match in response.text
|
|
result["content_matched"] = matched
|
|
result["is_up"] = status_ok and matched
|
|
else:
|
|
result["is_up"] = status_ok
|
|
|
|
except httpx.TimeoutException:
|
|
result["response_ms"] = round((time.monotonic() - t0) * 1000, 1)
|
|
result["error_msg"] = "Timeout"
|
|
except httpx.ConnectError as exc:
|
|
result["error_msg"] = f"Connection error: {exc}"
|
|
except Exception as exc:
|
|
result["error_msg"] = str(exc)[:512]
|
|
|
|
# TLS expiry — only for HTTPS, and only when the check succeeded or we got a response
|
|
if is_https and result["status_code"] is not None:
|
|
port = parsed.port or 443
|
|
result["tls_expires_at"] = await _get_tls_expiry(parsed.hostname, port)
|
|
|
|
return result
|