from __future__ import annotations import asyncio import logging import socket from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from roundtable.core.alerts import record_metric from roundtable.models.hosts import Host from roundtable.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": []}