feat(monitors): a DNS monitor can name a resolver; the host page puts Ansible above Docker (#5214)
DNS monitors take an optional Resolver, a server's IP. Set, the check asks that server directly for A and AAAA over UDP and records how long it took to answer; empty keeps today's lookup through the container's resolver. "Does my local DNS still answer for public names" and "does 1.1.1.1" are now separate monitors. No new dependency. On a host's page the Ansible card moves above the Docker list, which can run to dozens of containers and hid it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ pipeline) call the same code and refuse the same input for the same reasons.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from datetime import datetime, timedelta, timezone
|
||||
@@ -21,7 +22,7 @@ HTTP_METHODS = ("GET", "HEAD", "POST")
|
||||
CONFIG_KEYS: dict[str, tuple[str, ...]] = {
|
||||
MonitorType.icmp.value: (),
|
||||
MonitorType.tcp.value: ("port",),
|
||||
MonitorType.dns.value: ("expected_ip",),
|
||||
MonitorType.dns.value: ("expected_ip", "resolver"),
|
||||
MonitorType.http.value: ("method", "expected_status", "content_match", "headers",
|
||||
"timeout_seconds", "follow_redirects", "verify_ssl"),
|
||||
}
|
||||
@@ -56,6 +57,17 @@ def _headers(value) -> dict:
|
||||
return value
|
||||
|
||||
|
||||
def _resolver(value) -> str | None:
|
||||
"""A DNS server's IP address, or None for the container's own resolver."""
|
||||
value = str(value or "").strip()
|
||||
if not value:
|
||||
return None
|
||||
try:
|
||||
return str(ipaddress.ip_address(value))
|
||||
except ValueError:
|
||||
raise ValueError(f"resolver must be a DNS server's IP address (e.g. 1.1.1.1), got {value!r}.") from None
|
||||
|
||||
|
||||
def normalise_target(mtype: str, target: str) -> str:
|
||||
"""For http, default a bare host to https://; others pass through."""
|
||||
target = (target or "").strip()
|
||||
@@ -77,7 +89,8 @@ def build_config(mtype: str, opts: Mapping) -> dict:
|
||||
if mtype == MonitorType.tcp.value:
|
||||
return {"port": _whole("port", opts.get("port"), 80, 1, 65535)}
|
||||
if mtype == MonitorType.dns.value:
|
||||
return {"expected_ip": (str(opts.get("expected_ip") or "").strip() or None)}
|
||||
return {"expected_ip": (str(opts.get("expected_ip") or "").strip() or None),
|
||||
"resolver": _resolver(opts.get("resolver"))}
|
||||
if mtype == MonitorType.http.value:
|
||||
method = str(opts.get("method") or "GET").strip().upper()
|
||||
if method not in HTTP_METHODS:
|
||||
|
||||
Reference in New Issue
Block a user