mirror of
https://github.com/samjage/metro-warden.git
synced 2026-06-06 04:20:42 +00:00
Initial commit: Metro Warden TUI network operations center
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
DNS Plugin — monitors DNS resolution, configured resolvers, and query health.
|
||||
|
||||
Publishes to:
|
||||
dns.resolvers — list of configured nameservers
|
||||
dns.health — last query round-trip time and result
|
||||
dns.query.result — result of a DNS query
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from plugins.base import BasePlugin
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_POLL_INTERVAL = 30.0
|
||||
HEALTH_CHECK_HOST = "one.one.one.one"
|
||||
|
||||
|
||||
def _read_resolvers() -> List[str]:
|
||||
"""Parse /etc/resolv.conf for nameserver entries."""
|
||||
resolvers: List[str] = []
|
||||
try:
|
||||
with open("/etc/resolv.conf") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if line.startswith("nameserver"):
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
resolvers.append(parts[1])
|
||||
except OSError:
|
||||
pass
|
||||
return resolvers
|
||||
|
||||
|
||||
def _check_dns_health(host: str) -> Dict:
|
||||
"""Perform a synchronous DNS health check and return timing info."""
|
||||
start = time.monotonic()
|
||||
error: Optional[str] = None
|
||||
resolved: Optional[str] = None
|
||||
try:
|
||||
resolved = socket.gethostbyname(host)
|
||||
except socket.gaierror as exc:
|
||||
error = str(exc)
|
||||
elapsed_ms = (time.monotonic() - start) * 1000.0
|
||||
return {
|
||||
"host": host,
|
||||
"resolved": resolved,
|
||||
"elapsed_ms": round(elapsed_ms, 2),
|
||||
"error": error,
|
||||
"healthy": error is None,
|
||||
}
|
||||
|
||||
|
||||
class DNSPlugin(BasePlugin):
|
||||
"""
|
||||
Monitors DNS resolver configuration and performs periodic health checks.
|
||||
Publishes resolver info and query health to the event bus.
|
||||
"""
|
||||
|
||||
name = "dns"
|
||||
version = "1.0.0"
|
||||
description = "Monitors DNS resolver configuration and query health"
|
||||
tags = ["network", "dns"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bus=None,
|
||||
state=None,
|
||||
poll_interval: float = DEFAULT_POLL_INTERVAL,
|
||||
health_host: str = HEALTH_CHECK_HOST,
|
||||
) -> None:
|
||||
super().__init__(bus=bus, state=state)
|
||||
self._poll_interval = poll_interval
|
||||
self._health_host = health_host
|
||||
self._task: asyncio.Task | None = None
|
||||
self._running = False
|
||||
|
||||
def on_load(self) -> None:
|
||||
super().on_load()
|
||||
self.subscribe("dns.query") # allow other plugins to request queries
|
||||
self._running = True
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
self._task = loop.create_task(self._poll_loop())
|
||||
except RuntimeError:
|
||||
self._log.debug("no running event loop at load time; task deferred")
|
||||
|
||||
def on_unload(self) -> None:
|
||||
self._running = False
|
||||
if self._task and not self._task.done():
|
||||
self._task.cancel()
|
||||
super().on_unload()
|
||||
|
||||
async def _poll_loop(self) -> None:
|
||||
self._log.debug("dns poll loop started (interval=%.1fs)", self._poll_interval)
|
||||
while self._running:
|
||||
try:
|
||||
resolvers = await asyncio.to_thread(_read_resolvers)
|
||||
self.state_set("dns.resolvers", resolvers)
|
||||
if self._bus:
|
||||
await self._bus.publish("dns.resolvers", {"nameservers": resolvers})
|
||||
|
||||
health = await asyncio.to_thread(_check_dns_health, self._health_host)
|
||||
self.state_set("dns.health", health)
|
||||
if self._bus:
|
||||
await self._bus.publish("dns.health", health)
|
||||
except asyncio.CancelledError:
|
||||
break
|
||||
except Exception as exc:
|
||||
self._log.error("dns poll error: %s", exc)
|
||||
await asyncio.sleep(self._poll_interval)
|
||||
self._log.debug("dns poll loop stopped")
|
||||
|
||||
def on_event(self, topic: str, data: Any) -> None:
|
||||
if topic == "dns.query" and isinstance(data, dict):
|
||||
host = data.get("host", "")
|
||||
if host:
|
||||
asyncio.ensure_future(self._resolve_and_publish(host))
|
||||
|
||||
async def _resolve_and_publish(self, host: str) -> None:
|
||||
result = await asyncio.to_thread(_check_dns_health, host)
|
||||
if self._bus:
|
||||
await self._bus.publish("dns.query.result", result)
|
||||
Reference in New Issue
Block a user