From d699a29fa9195a42e1229b4120690ee714ed1af7 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Tue, 12 May 2026 14:34:11 -0400 Subject: [PATCH] refactor: remove dyndnshosts/drophosts legacy config keys, fix DNS event logging - Remove dyndnshosts legacy list; dyndns is now set per-host in the hosts section - Remove drophosts config key and load-time deletion loop - Simplify get_dyndnshosts() to only read per-host dyndns attributes - Fix dns_update_worker to call eventlog with correct (host, level, msg) signature - Log INFO/ERROR events per domain on each DNS update instead of one batched message - Add logger to dns.py (was missing, causing NameError on update failure) - Update README and tests to reflect removed config keys Co-Authored-By: Claude Sonnet 4.6 --- README.md | 15 ++++++-------- hbd/server/config.py | 39 +++++++++-------------------------- hbd/server/configio.py | 2 +- hbd/server/dns.py | 33 +++++++++++++++-------------- hbd/server/main.py | 10 +-------- tests/test_handle_datagram.py | 2 +- 6 files changed, 37 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index ce73eee..2e01cd3 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A lightweight daemon that listens for UDP heartbeat messages and acts on them: k - Receive and parse heartbeat datagrams (text or zlib-compressed) ✅ - Maintain host state and detect up/down transitions ✅ -- Queue DNS updates via `nsupdate` and run them in a background thread ✅ +- Queue DNS updates via `nsupdate` and run them in an asyncio background task ✅ - WebSocket API for live updates (hosts & messages) ✅ - Notification pipeline (email, Pushover, Mattermost, Signal) ✅ - **User management & access control** ✅ @@ -398,6 +398,7 @@ hosts: owner: alice managers: [bob] monitors: [carol] + dyndns: true # update DNS record when IP changes ``` ```bash @@ -645,7 +646,7 @@ Set breakpoints in modules such as `hbd/server/udp.py`, `hbd/server/dns.py`, or - `logfile`: path to log file - `pushsrv`: push service (`pushover`|`mattermost`|`all`) - `interval` / `grace`: heartbeat timing configuration -- `dyndomains`: list of dyndomains to update via `nsupdate` +- `dyndomains`: list of DNS domains to update via `nsupdate` for hosts with `dyndns` set - `nsupdate_bin`: path to nsupdate binary - `ws_port`: port for plain WebSocket connections (default: 50005) - `wss_port`: port for secure WebSocket (WSS) connections (default: none). @@ -666,6 +667,9 @@ dyndomains: - example.com nsupdate_bin: /usr/bin/nsupdate pushsrv: pushover +hosts: + myhost: + dyndns: true # update DNS when this host's IP changes ``` > Tip: `SERVER_DEFAULTS` in `hbd/server/config.py` contains the canonical defaults and accepted configuration keys. @@ -769,10 +773,3 @@ Contributions welcome! Please: This repository is licensed under the MIT license. See `LICENSE` for details. --- - -If you'd like, I can also: - -- add a **GitHub Actions** workflow that runs tests and lint on push/PR 🔁 -- add a `CONTRIBUTING.md` template for PRs and code style 💬 - -Which one should I do next? ✨ diff --git a/hbd/server/config.py b/hbd/server/config.py index d6edea6..08eb549 100644 --- a/hbd/server/config.py +++ b/hbd/server/config.py @@ -39,10 +39,8 @@ SERVER_DEFAULTS = { # Host management "hosts": {}, # Unified host definitions - "dyndnshosts": [], # Hosts with dynamic DNS (legacy) - "drophosts": [], # Hosts to ignore "dyndomains": ["wrede.org"], - + # DNS updates "nsupdate_bin": "/usr/bin/nsupdate", @@ -249,7 +247,7 @@ def get_watchhosts(config): """Extract watched hostnames from config (hosts with watch: true). Returns: - List of hostnames to watch +# List of hostnames to watch """ watchhosts = [] hosts_config = config.get("hosts", {}) @@ -261,31 +259,14 @@ def get_watchhosts(config): def get_dyndnshosts(config): - """Extract dyndnshosts from config, supporting both new and legacy formats. - - Args: - config: Configuration dictionary - - Returns: - List of hostnames with dynamic DNS - """ - dyndnshosts = [] - - # New format: hosts section with dyndns attribute - if "hosts" in config: - hosts_config = config["hosts"] - if isinstance(hosts_config, dict): - for host_name, host_attrs in hosts_config.items(): - if isinstance(host_attrs, dict) and host_attrs.get("dyndns", False): - dyndnshosts.append(host_name) - - # Legacy format: dyndnshosts list/set - if "dyndnshosts" in config: - legacy_dyndnshosts = config.get("dyndnshosts", []) - if isinstance(legacy_dyndnshosts, (list, set)): - dyndnshosts.extend(legacy_dyndnshosts) - - return list(set(dyndnshosts)) # Remove duplicates + """Return hostnames that have a dyndns setting in the hosts section.""" + hosts_config = config.get("hosts", {}) + if not isinstance(hosts_config, dict): + return [] + return [ + name for name, attrs in hosts_config.items() + if isinstance(attrs, dict) and attrs.get("dyndns") + ] def get_host_config(config, hostname): diff --git a/hbd/server/configio.py b/hbd/server/configio.py index b5902cc..1787259 100644 --- a/hbd/server/configio.py +++ b/hbd/server/configio.py @@ -25,7 +25,7 @@ _SERVER_KEYS = [ ] # Top-level keys managed by the 'dns' logical section -_DNS_KEYS = ["nsupdate_bin", "dyndomains", "dyndnshosts", "drophosts"] +_DNS_KEYS = ["nsupdate_bin", "rndc_key", "dyndomains"] def read_roundtrip(path: str): diff --git a/hbd/server/dns.py b/hbd/server/dns.py index d52396f..a7f9310 100644 --- a/hbd/server/dns.py +++ b/hbd/server/dns.py @@ -4,6 +4,9 @@ from __future__ import annotations from subprocess import Popen, PIPE, STDOUT from typing import Optional import asyncio +import logging + +logger = logging.getLogger(__name__) def create_nsupdate_payload( @@ -123,7 +126,6 @@ async def dns_update_worker( pass continue - m = f"changed address to {addr}" for dyndomain in cfg.get("dyndomains", []): err = await loop.run_in_executor( None, @@ -135,28 +137,29 @@ async def dns_update_worker( cfg.get("rndc_key", "/etc/dhcpc/rndc-key"), ) if err: - m += f", DNS update failed: {err}" + m = f"DNS update failed for {addr} ({dyndomain}): {err}" logger.error("DNS update failed for %s: %s", name, err) + if log: + try: + await loop.run_in_executor(None, log, name, "ERROR", m) + except Exception: + pass else: - m += ", DNS updated." + m = f"DNS updated {name}.dy.{dyndomain} → {addr}" + if log: + try: + await loop.run_in_executor(None, log, name, "INFO", m) + except Exception: + pass + + if not cfg.get("dyndomains"): + logger.warning("DNS update triggered for %s but no dyndomains configured", name) try: dnsq.task_done() except Exception: pass - if log: - try: - await loop.run_in_executor(None, log, name, m) - except Exception: - pass - - if log: - try: - await loop.run_in_executor(None, log, None, "dns_update_worker exiting") - except Exception: - pass - def start_dns_worker( hbdclass, diff --git a/hbd/server/main.py b/hbd/server/main.py index 179c8c5..01cd4d0 100644 --- a/hbd/server/main.py +++ b/hbd/server/main.py @@ -78,9 +78,7 @@ async def reload_configuration(config_obj, config_path, components): True if reload succeeded, False otherwise """ try: - logger.info("=" * 60) logger.info("Starting configuration reload...") - logger.info("=" * 60) # Reload config file new_config = await config_obj.reload(config_path) @@ -115,13 +113,11 @@ async def reload_configuration(config_obj, config_path, components): # These are reloadable and effective immediately: # - notification_channels # - threshold_configs - # - hosts (watchhosts, dyndnshosts, notification_channels) + # - hosts (watchhosts, dyndns, notification_channels) # - grace period (used on next heartbeat) # - debug/verbose flags (used on next message) - logger.info("=" * 60) logger.info("Configuration reload completed successfully") - logger.info("=" * 60) return True except Exception as e: @@ -422,7 +418,6 @@ def load_pickled_hosts(config, hbdclass): pickfile = config.get("pickfile", "hbd.pickle") dyndnshosts = config_mod.get_dyndnshosts(config) watchhosts = config_mod.get_watchhosts(config) - drophosts = config.get("drophosts", []) if 1 and os.path.exists(pickfile): if config.get("verbose", False): logger.info("opening pickls %s", pickfile) @@ -448,9 +443,6 @@ def load_pickled_hosts(config, hbdclass): hbdclass.Host.hosts[h].apply_access( access["owner"], access["managers"], access["monitors"] ) - for h in drophosts: - if h in hbdclass.Host.hosts: - del hbdclass.Host.hosts[h] if config.get("verbose", False): logger.info("%s pickled hosts loaded", len(hbdclass.Host.hosts)) else: diff --git a/tests/test_handle_datagram.py b/tests/test_handle_datagram.py index f904558..db9c8e9 100644 --- a/tests/test_handle_datagram.py +++ b/tests/test_handle_datagram.py @@ -20,7 +20,7 @@ def test_handle_cmd_sends_command(): import hbdclass ctx = { - "config": {"watchhosts": [], "dyndnshosts": []}, + "config": {"watchhosts": []}, "hbdclass": hbdclass, "log": dummy_noop, "email": dummy_noop,