Files
FabledSteward/steward/monitors/ping.py
T
bvandeusen 88ab5b917e chore: rename project Roundtable → Steward
Renames the Python package directory, CLI command, env var prefix,
docker-compose service/container/image, Postgres role/db, and all
visible branding. Marketing form is "Fabled Steward".

Clean break from the previous rebrand: drops the fabledscryer→roundtable
import shim in __init__.py and the FABLEDSCRYER_* env var fallback in
config.py and migrations/env.py. Env vars are now STEWARD_* only.

Heads-up for existing deployments:
- Postgres user/db renamed fabledscryer → steward in docker-compose.yml.
  Existing volumes need the role/db renamed inside Postgres, or override
  POSTGRES_USER/POSTGRES_DB to keep the old names.
- Host-agent systemd unit is now steward-agent.service. Existing agents
  keep running under the old name; reinstall to switch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 16:20:14 -04:00

70 lines
2.3 KiB
Python

from __future__ import annotations
import asyncio
import logging
import time
from sqlalchemy.ext.asyncio import AsyncSession
from steward.core.alerts import record_metric
from steward.models.hosts import Host, ProbeType
from steward.models.monitors import PingResult, PingStatus
logger = logging.getLogger(__name__)
TCP_TIMEOUT = 5.0
DEFAULT_TCP_PORT = 80
async def ping_check(host: Host, session: AsyncSession) -> None:
"""Probe a single host and write ping_result + metrics."""
if host.probe_type == ProbeType.icmp:
result = await _icmp_ping(host.address)
else:
result = await _tcp_ping(host.address, host.probe_port or DEFAULT_TCP_PORT)
status = PingStatus.up if result["up"] else PingStatus.down
session.add(PingResult(
host_id=host.id,
status=status,
response_time_ms=result.get("response_time_ms"),
))
await session.flush()
await record_metric(session, "ping", host.name, "up", 1.0 if result["up"] else 0.0)
await record_metric(
session, "ping", host.name, "response_time_ms",
result.get("response_time_ms") or 0.0,
)
async def _tcp_ping(address: str, port: int) -> dict:
start = time.monotonic()
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(address, port),
timeout=TCP_TIMEOUT,
)
writer.close()
await writer.wait_closed()
return {"up": True, "response_time_ms": round((time.monotonic() - start) * 1000, 2)}
except Exception:
return {"up": False, "response_time_ms": None}
async def _icmp_ping(address: str) -> dict:
"""Use system ping command (setuid binary; no raw socket privilege needed in Python)."""
start = time.monotonic()
try:
proc = await asyncio.create_subprocess_exec(
"ping", "-c", "1", "-W", "2", address,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.DEVNULL,
)
await asyncio.wait_for(proc.wait(), timeout=5.0)
if proc.returncode == 0:
return {"up": True, "response_time_ms": round((time.monotonic() - start) * 1000, 2)}
return {"up": False, "response_time_ms": None}
except Exception as exc:
logger.warning(f"ICMP ping failed for {address}: {exc}, falling back to TCP")
return await _tcp_ping(address, DEFAULT_TCP_PORT)