Files
FabledSteward/fabledscryer/monitors/dns.py
T
bvandeusen 230b542015 feat: rename to FabledScryer, multi-dashboard system, plugin management, branding
- Rename package fablednetmon → fabledscryer throughout
- Multi-dashboard: ownership, per-user defaults, HTMX edit (add/remove/reorder)
- Read-only share tokens scoped to individual dashboards
- Dashboard edit is HTMX-driven (no page reloads)
- Plugin management system: remote catalog, download/install, hot-reload, in-app restart
- plugin_index.py: fetch/cache remote index.yaml; default URL → bvandeusen/fabledscryer-plugins
- plugin_manager.py: download_and_install_plugin, hot_reload_plugin, restart_app
  - ZIP extraction handles GitHub archive formats (name-v1.0.0/, name-main/)
- Settings split into tabbed sections: General, Notifications, Ansible, Plugins
- Plugins tab: catalog browser (HTMX), install/activate/update/restart actions
- UI/branding: dark palette (#07071a), crystal ball SVG logo, animated star field,
  Libertinus Serif applied to headings, nav, labels, and section titles
- Widget registry (core/widgets.py) for dashboard plugin integration
- UPS widget.html (dashboard card) and settings/_tabs.html include
- Migrations 0005–0008: dashboards, is_default, ownership, share tokens
- docs/plugins/: writing-a-plugin.md updated with publishing guide,
  index.yaml.example template for fabledscryer-plugins repo

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:27:56 -04:00

72 lines
2.5 KiB
Python

from __future__ import annotations
import asyncio
import logging
import socket
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from fabledscryer.core.alerts import record_metric
from fabledscryer.models.hosts import Host
from fabledscryer.models.monitors import DnsResult, DnsStatus
logger = logging.getLogger(__name__)
async def dns_check(host: Host, session: AsyncSession) -> None:
"""Resolve host and write dns_result + metrics."""
result = await _resolve(host.address)
if result["resolved"]:
resolved_ip = result["ip"] # first A/AAAA record (stored in DB)
all_ips = result["all_ips"]
# Check against expected IP: pass only if at least one returned record matches
if host.dns_expected_ip and host.dns_expected_ip not in all_ips:
status = DnsStatus.failed
dns_resolved_metric = 0.0
else:
status = DnsStatus.resolved
dns_resolved_metric = 1.0
# Detect IP change vs most recent successful resolution
prev_result = await session.execute(
select(DnsResult)
.where(DnsResult.host_id == host.id, DnsResult.status == DnsStatus.resolved)
.order_by(DnsResult.resolved_at.desc())
.limit(1)
)
prev = prev_result.scalar_one_or_none()
ip_changed = 1.0 if (prev and prev.resolved_ip != resolved_ip) else 0.0
else:
resolved_ip = None
status = DnsStatus.failed
dns_resolved_metric = 0.0
ip_changed = 0.0
session.add(DnsResult(
host_id=host.id,
status=status,
resolved_ip=resolved_ip,
))
await session.flush()
await record_metric(session, "dns", host.name, "resolved", dns_resolved_metric)
await record_metric(session, "dns", host.name, "ip_changed", ip_changed)
async def _resolve(address: str) -> dict:
"""Resolve hostname. Returns all A/AAAA records; stores first as resolved_ip."""
try:
loop = asyncio.get_running_loop()
infos = await loop.run_in_executor(
None, socket.getaddrinfo, address, None, socket.AF_UNSPEC, socket.SOCK_STREAM
)
if infos:
all_ips = [info[4][0] for info in infos]
return {"resolved": True, "ip": all_ips[0], "all_ips": all_ips}
return {"resolved": False, "ip": None, "all_ips": []}
except Exception as exc:
logger.debug(f"DNS resolution failed for {address}: {exc}")
return {"resolved": False, "ip": None, "all_ips": []}