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 <noreply@anthropic.com>
This commit is contained in:
@@ -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) ✅
|
- Receive and parse heartbeat datagrams (text or zlib-compressed) ✅
|
||||||
- Maintain host state and detect up/down transitions ✅
|
- 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) ✅
|
- WebSocket API for live updates (hosts & messages) ✅
|
||||||
- Notification pipeline (email, Pushover, Mattermost, Signal) ✅
|
- Notification pipeline (email, Pushover, Mattermost, Signal) ✅
|
||||||
- **User management & access control** ✅
|
- **User management & access control** ✅
|
||||||
@@ -398,6 +398,7 @@ hosts:
|
|||||||
owner: alice
|
owner: alice
|
||||||
managers: [bob]
|
managers: [bob]
|
||||||
monitors: [carol]
|
monitors: [carol]
|
||||||
|
dyndns: true # update DNS record when IP changes
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```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
|
- `logfile`: path to log file
|
||||||
- `pushsrv`: push service (`pushover`|`mattermost`|`all`)
|
- `pushsrv`: push service (`pushover`|`mattermost`|`all`)
|
||||||
- `interval` / `grace`: heartbeat timing configuration
|
- `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
|
- `nsupdate_bin`: path to nsupdate binary
|
||||||
- `ws_port`: port for plain WebSocket connections (default: 50005)
|
- `ws_port`: port for plain WebSocket connections (default: 50005)
|
||||||
- `wss_port`: port for secure WebSocket (WSS) connections (default: none).
|
- `wss_port`: port for secure WebSocket (WSS) connections (default: none).
|
||||||
@@ -666,6 +667,9 @@ dyndomains:
|
|||||||
- example.com
|
- example.com
|
||||||
nsupdate_bin: /usr/bin/nsupdate
|
nsupdate_bin: /usr/bin/nsupdate
|
||||||
pushsrv: pushover
|
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.
|
> 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.
|
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? ✨
|
|
||||||
|
|||||||
+9
-28
@@ -39,8 +39,6 @@ SERVER_DEFAULTS = {
|
|||||||
|
|
||||||
# Host management
|
# Host management
|
||||||
"hosts": {}, # Unified host definitions
|
"hosts": {}, # Unified host definitions
|
||||||
"dyndnshosts": [], # Hosts with dynamic DNS (legacy)
|
|
||||||
"drophosts": [], # Hosts to ignore
|
|
||||||
"dyndomains": ["wrede.org"],
|
"dyndomains": ["wrede.org"],
|
||||||
|
|
||||||
# DNS updates
|
# DNS updates
|
||||||
@@ -249,7 +247,7 @@ def get_watchhosts(config):
|
|||||||
"""Extract watched hostnames from config (hosts with watch: true).
|
"""Extract watched hostnames from config (hosts with watch: true).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of hostnames to watch
|
# List of hostnames to watch
|
||||||
"""
|
"""
|
||||||
watchhosts = []
|
watchhosts = []
|
||||||
hosts_config = config.get("hosts", {})
|
hosts_config = config.get("hosts", {})
|
||||||
@@ -261,31 +259,14 @@ def get_watchhosts(config):
|
|||||||
|
|
||||||
|
|
||||||
def get_dyndnshosts(config):
|
def get_dyndnshosts(config):
|
||||||
"""Extract dyndnshosts from config, supporting both new and legacy formats.
|
"""Return hostnames that have a dyndns setting in the hosts section."""
|
||||||
|
hosts_config = config.get("hosts", {})
|
||||||
Args:
|
if not isinstance(hosts_config, dict):
|
||||||
config: Configuration dictionary
|
return []
|
||||||
|
return [
|
||||||
Returns:
|
name for name, attrs in hosts_config.items()
|
||||||
List of hostnames with dynamic DNS
|
if isinstance(attrs, dict) and attrs.get("dyndns")
|
||||||
"""
|
]
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
def get_host_config(config, hostname):
|
def get_host_config(config, hostname):
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ _SERVER_KEYS = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Top-level keys managed by the 'dns' logical section
|
# 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):
|
def read_roundtrip(path: str):
|
||||||
|
|||||||
+18
-15
@@ -4,6 +4,9 @@ from __future__ import annotations
|
|||||||
from subprocess import Popen, PIPE, STDOUT
|
from subprocess import Popen, PIPE, STDOUT
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def create_nsupdate_payload(
|
def create_nsupdate_payload(
|
||||||
@@ -123,7 +126,6 @@ async def dns_update_worker(
|
|||||||
pass
|
pass
|
||||||
continue
|
continue
|
||||||
|
|
||||||
m = f"changed address to {addr}"
|
|
||||||
for dyndomain in cfg.get("dyndomains", []):
|
for dyndomain in cfg.get("dyndomains", []):
|
||||||
err = await loop.run_in_executor(
|
err = await loop.run_in_executor(
|
||||||
None,
|
None,
|
||||||
@@ -135,28 +137,29 @@ async def dns_update_worker(
|
|||||||
cfg.get("rndc_key", "/etc/dhcpc/rndc-key"),
|
cfg.get("rndc_key", "/etc/dhcpc/rndc-key"),
|
||||||
)
|
)
|
||||||
if err:
|
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)
|
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:
|
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:
|
try:
|
||||||
dnsq.task_done()
|
dnsq.task_done()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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(
|
def start_dns_worker(
|
||||||
hbdclass,
|
hbdclass,
|
||||||
|
|||||||
+1
-9
@@ -78,9 +78,7 @@ async def reload_configuration(config_obj, config_path, components):
|
|||||||
True if reload succeeded, False otherwise
|
True if reload succeeded, False otherwise
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
logger.info("=" * 60)
|
|
||||||
logger.info("Starting configuration reload...")
|
logger.info("Starting configuration reload...")
|
||||||
logger.info("=" * 60)
|
|
||||||
|
|
||||||
# Reload config file
|
# Reload config file
|
||||||
new_config = await config_obj.reload(config_path)
|
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:
|
# These are reloadable and effective immediately:
|
||||||
# - notification_channels
|
# - notification_channels
|
||||||
# - threshold_configs
|
# - threshold_configs
|
||||||
# - hosts (watchhosts, dyndnshosts, notification_channels)
|
# - hosts (watchhosts, dyndns, notification_channels)
|
||||||
# - grace period (used on next heartbeat)
|
# - grace period (used on next heartbeat)
|
||||||
# - debug/verbose flags (used on next message)
|
# - debug/verbose flags (used on next message)
|
||||||
|
|
||||||
logger.info("=" * 60)
|
|
||||||
logger.info("Configuration reload completed successfully")
|
logger.info("Configuration reload completed successfully")
|
||||||
logger.info("=" * 60)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -422,7 +418,6 @@ def load_pickled_hosts(config, hbdclass):
|
|||||||
pickfile = config.get("pickfile", "hbd.pickle")
|
pickfile = config.get("pickfile", "hbd.pickle")
|
||||||
dyndnshosts = config_mod.get_dyndnshosts(config)
|
dyndnshosts = config_mod.get_dyndnshosts(config)
|
||||||
watchhosts = config_mod.get_watchhosts(config)
|
watchhosts = config_mod.get_watchhosts(config)
|
||||||
drophosts = config.get("drophosts", [])
|
|
||||||
if 1 and os.path.exists(pickfile):
|
if 1 and os.path.exists(pickfile):
|
||||||
if config.get("verbose", False):
|
if config.get("verbose", False):
|
||||||
logger.info("opening pickls %s", pickfile)
|
logger.info("opening pickls %s", pickfile)
|
||||||
@@ -448,9 +443,6 @@ def load_pickled_hosts(config, hbdclass):
|
|||||||
hbdclass.Host.hosts[h].apply_access(
|
hbdclass.Host.hosts[h].apply_access(
|
||||||
access["owner"], access["managers"], access["monitors"]
|
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):
|
if config.get("verbose", False):
|
||||||
logger.info("%s pickled hosts loaded", len(hbdclass.Host.hosts))
|
logger.info("%s pickled hosts loaded", len(hbdclass.Host.hosts))
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ def test_handle_cmd_sends_command():
|
|||||||
import hbdclass
|
import hbdclass
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"config": {"watchhosts": [], "dyndnshosts": []},
|
"config": {"watchhosts": []},
|
||||||
"hbdclass": hbdclass,
|
"hbdclass": hbdclass,
|
||||||
"log": dummy_noop,
|
"log": dummy_noop,
|
||||||
"email": dummy_noop,
|
"email": dummy_noop,
|
||||||
|
|||||||
Reference in New Issue
Block a user