From 5b89e4336db891a6294914beb80b7f1054cbc54b Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Fri, 17 Jul 2026 23:20:05 +1000 Subject: [PATCH] dns: deploy dns-updater daemon in place of the shell nsupdate script Replaces the profiles::dns::updater shell mechanism (dns-update.sh + dns-update.path/.service + the in-run exec) with the packaged dns-updater daemon, which watches the records file (inotify) and network interfaces and pushes TSIG-signed RFC2136 updates natively. - install the dns-updater package; manage /etc/dns-updater/env - run the packaged dns-updater.service, restarting only on env/key change (records-file edits are picked up by the daemon's own watch, no churn) - keep the concat records file and TSIG key file unchanged - ensure the old script + .path/.service units are absent - drop the now-dead dns-update.{sh,service,path}.epp templates The daemon reports per-zone RCODEs and a health status API, so failures like the recent empty-label/NOTZONE ones surface directly instead of as opaque nsupdate stderr. --- site/profiles/manifests/dns/updater.pp | 69 +++++++++++++------ .../templates/dns/dns-update.path.epp | 10 --- .../templates/dns/dns-update.service.epp | 9 --- site/profiles/templates/dns/dns-update.sh.epp | 64 ----------------- 4 files changed, 48 insertions(+), 104 deletions(-) delete mode 100644 site/profiles/templates/dns/dns-update.path.epp delete mode 100644 site/profiles/templates/dns/dns-update.service.epp delete mode 100644 site/profiles/templates/dns/dns-update.sh.epp diff --git a/site/profiles/manifests/dns/updater.pp b/site/profiles/manifests/dns/updater.pp index f2523fb..a28ab4e 100644 --- a/site/profiles/manifests/dns/updater.pp +++ b/site/profiles/manifests/dns/updater.pp @@ -23,6 +23,12 @@ class profiles::dns::updater ( Stdlib::AbsolutePath $state_dir = '/var/lib/dns-updater', Stdlib::AbsolutePath $config_dir = '/etc/dns-updater', Stdlib::AbsolutePath $master_basedir = lookup('profiles::dns::master::basedir'), + # dns-updater daemon (replaces the dns-update shell script). + String $package_ensure = 'installed', + Stdlib::AbsolutePath $api_socket = '/run/dns-updater/api.sock', + String $resync = '10m', + Enum['debug', 'info', 'warn', 'error'] $log_level = 'info', + Boolean $watch_interfaces = true, ) { $state_file = "${state_dir}/applied" @@ -91,36 +97,57 @@ class profiles::dns::updater ( })), } - file { '/usr/local/bin/dns-update': + # dns-updater daemon: watches the records file (inotify) and network + # interfaces, pushes TSIG-signed RFC2136 updates to $server natively. + package { 'dns-updater': + ensure => $package_ensure, + } + + $env_content = @("ENV") + # Managed by puppet (profiles::dns::updater). + DNS_UPDATER_SERVER=${server} + DNS_UPDATER_KEY_FILE=${key_file} + DNS_UPDATER_RECORDS_FILE=${records_file} + DNS_UPDATER_STATE_FILE=${state_file} + DNS_UPDATER_API=${api_socket} + DNS_UPDATER_RESYNC=${resync} + DNS_UPDATER_WATCH_INTERFACES=${watch_interfaces} + DNS_UPDATER_LOG_LEVEL=${log_level} + | ENV + + file { "${config_dir}/env": ensure => file, owner => 'root', group => 'root', - mode => '0755', - content => epp('profiles/dns/dns-update.sh.epp', { - 'server' => $server, - 'key_file' => $key_file, - 'records_file' => $records_file, - 'state_file' => $state_file, - }), + mode => '0644', + content => $env_content, + require => [File[$config_dir], Package['dns-updater']], + } + + # Restart only on config/key change; records-file changes are picked up by + # the daemon's own inotify watch, so no service churn on record edits. + service { 'dns-updater': + ensure => running, + enable => true, + subscribe => [File["${config_dir}/env"], File[$key_file]], + require => [Package['dns-updater'], Concat[$records_file], File[$key_file]], + } + + # Retire the previous shell-based mechanism. + file { '/usr/local/bin/dns-update': + ensure => absent, } systemd::unit_file { 'dns-update.service': - content => epp('profiles/dns/dns-update.service.epp', { 'script' => '/usr/local/bin/dns-update' }), + ensure => absent, + active => false, + enable => false, } - # The .path unit watches the records file and triggers the service. systemd::unit_file { 'dns-update.path': - content => epp('profiles/dns/dns-update.path.epp', { 'records_file' => $records_file }), - active => true, - enable => true, - } - - # Also apply within the puppet run whenever the records change. - exec { 'dns-update-apply': - command => '/usr/local/bin/dns-update', - refreshonly => true, - subscribe => Concat[$records_file], - require => [File['/usr/local/bin/dns-update'], File[$key_file]], + ensure => absent, + active => false, + enable => false, } } } diff --git a/site/profiles/templates/dns/dns-update.path.epp b/site/profiles/templates/dns/dns-update.path.epp deleted file mode 100644 index a842bec..0000000 --- a/site/profiles/templates/dns/dns-update.path.epp +++ /dev/null @@ -1,10 +0,0 @@ -<%- | String $records_file | -%> -[Unit] -Description=Watch the DNS records file and apply changes - -[Path] -PathModified=<%= $records_file %> -Unit=dns-update.service - -[Install] -WantedBy=multi-user.target diff --git a/site/profiles/templates/dns/dns-update.service.epp b/site/profiles/templates/dns/dns-update.service.epp deleted file mode 100644 index 7705360..0000000 --- a/site/profiles/templates/dns/dns-update.service.epp +++ /dev/null @@ -1,9 +0,0 @@ -<%- | String $script | -%> -[Unit] -Description=Apply host DNS records via nsupdate -After=network-online.target -Wants=network-online.target - -[Service] -Type=oneshot -ExecStart=<%= $script %> diff --git a/site/profiles/templates/dns/dns-update.sh.epp b/site/profiles/templates/dns/dns-update.sh.epp deleted file mode 100644 index ce3d872..0000000 --- a/site/profiles/templates/dns/dns-update.sh.epp +++ /dev/null @@ -1,64 +0,0 @@ -<%- | String $server, String $key_file, String $records_file, String $state_file | -%> -#!/bin/bash -# Managed by puppet (profiles::dns::updater). Applies this host's records to the -# authoritative DNS server via TSIG nsupdate. Only the delta since the last -# successful run is sent; removed records are deleted. -set -euo pipefail - -SERVER="<%= $server %>" -KEYFILE="<%= $key_file %>" -RECORDS="<%= $records_file %>" -STATE="<%= $state_file %>" - -[ -f "$RECORDS" ] || exit 0 -touch "$STATE" - -# Format per line: zone|name|type|ttl|value (name is relative to zone, or @). -desired="$(grep -vE '^[[:space:]]*(#|$)' "$RECORDS" | sort -u || true)" -applied="$(grep -vE '^[[:space:]]*(#|$)' "$STATE" 2>/dev/null | sort -u || true)" - -[ "$desired" = "$applied" ] && exit 0 - -fqdn() { # name zone - # $1 may be relative to the zone, "@"/empty for the apex, or already a FQDN - # (trailing dot). Only append the zone in the relative case; appending it to - # an already-qualified name yields a "..", which nsupdate rejects as an - # "invalid owner name: empty label". - case "$1" in - ''|'@') printf '%s.' "$2" ;; - *.) printf '%s' "$1" ;; - *) printf '%s.%s.' "$1" "$2" ;; - esac -} - -msg="$(mktemp)" -trap 'rm -f "$msg"' EXIT -printf 'server %s\n' "$SERVER" >> "$msg" - -# Process per zone so each UPDATE message targets a single zone. -zones="$(printf '%s\n%s\n' "$desired" "$applied" | cut -d'|' -f1 | sort -u | grep -v '^$' || true)" -for zone in $zones; do - printf 'zone %s.\n' "$zone" >> "$msg" - # Additions/updates: replace the RRset for every desired record in this zone. - printf '%s\n' "$desired" | awk -F'|' -v z="$zone" 'NF>=5 && $1==z' | \ - while IFS='|' read -r z name type ttl value; do - f="$(fqdn "$name" "$z")" - printf 'update delete %s %s\n' "$f" "$type" >> "$msg" - printf 'update add %s %s %s %s\n' "$f" "$ttl" "$type" "$value" >> "$msg" - done - # Deletions: records present last run but gone now. - comm -23 <(printf '%s\n' "$applied") <(printf '%s\n' "$desired") | \ - awk -F'|' -v z="$zone" 'NF>=5 && $1==z' | \ - while IFS='|' read -r z name type ttl value; do - f="$(fqdn "$name" "$z")" - printf 'update delete %s %s %s\n' "$f" "$type" "$value" >> "$msg" - done - printf 'send\n' >> "$msg" -done - -if nsupdate -k "$KEYFILE" "$msg"; then - printf '%s\n' "$desired" > "$STATE" -else - echo "dns-update: nsupdate to ${SERVER} failed" >&2 - exit 1 -fi