This commit is contained in:
@@ -19,3 +19,10 @@ inventory #307 v2, and git PFVCluster + KNEL/pfv-bms)
|
|||||||
- [x] HACS check: NOT installed (answer to founder) [#344]
|
- [x] HACS check: NOT installed (answer to founder) [#344]
|
||||||
- [x] Midea minisplit dongle found on WiFi (c4:39:60:59:0c:01 / 192.168.1.149) → #614 updated
|
- [x] Midea minisplit dongle found on WiFi (c4:39:60:59:0c:01 / 192.168.1.149) → #614 updated
|
||||||
- [x] Ticket #614 created (dynamic temp management); IaC items filed on #454; audit log #298
|
- [x] Ticket #614 created (dynamic temp management); IaC items filed on #454; audit log #298
|
||||||
|
- [x] Kuma: dup ICMP monitor 199 deleted; pfv-bms-ha-http (221) added, UP/200 verified [#344]
|
||||||
|
- [x] DNS perm-fix: sync-zones.sh reload-on-change (TDD, green) deployed + live-proven 75s propagation [#469]
|
||||||
|
- [x] pfv-tsys1-lan / pfv-tsys9-lan DNS records (LAN path for NUT)
|
||||||
|
- [x] UPS in HA: NUT renamed tsys1_ups/tsys9_ups, APC config entry via REST flow, sensors live (100%/Online) [#439]
|
||||||
|
- [x] Tripp Lite dropped off USB on tsys9 (physical, #372) — config ready, binds on return [#439]
|
||||||
|
- [x] SSH locked to key-only (workstation key), password path verified dead [#344]
|
||||||
|
- [x] HACS 2.0.5 installed on box (custom_components gitignored) — founder GitHub auth pending [#614]
|
||||||
|
|||||||
@@ -3,13 +3,15 @@
|
|||||||
# sync-zones.sh — rsync-based zone replication from primary to secondary
|
# sync-zones.sh — rsync-based zone replication from primary to secondary
|
||||||
#
|
#
|
||||||
# Runs on the SECONDARY (netinfra-02). Syncs the zones/ directory from the
|
# Runs on the SECONDARY (netinfra-02). Syncs the zones/ directory from the
|
||||||
# primary (netinfra-01) every 60 seconds. When a zone file changes, Technitium
|
# primary (netinfra-01) every 60 seconds. When rsync changes any zone file,
|
||||||
# detects the modification and reloads automatically.
|
# Technitium is reloaded (container restart) so it serves the fresh zones —
|
||||||
|
# Technitium does NOT reliably auto-detect externally modified zone files,
|
||||||
|
# which left the secondary serving stale records (seen 2026-09-01 [#344]).
|
||||||
#
|
#
|
||||||
# This is used instead of AXFR-based zone transfer because Technitium's zone
|
# This is used instead of AXFR-based zone transfer because Technitium's zone
|
||||||
# transfer mechanism uses port 53 (standard DNS), but on the netinfra hosts
|
# transfer mechanism uses port 53 (standard DNS), but on the netinfra hosts
|
||||||
# port 53 is Pi-hole and Technitium is on port 5300. rsync-based replication
|
# port 53 is Pi-hole and Technitium is on port 5300. rsync-based replication
|
||||||
# avoids the port conflict entirely.
|
# avoids the port conflict entirely. Native clustering tracked in [#469].
|
||||||
#
|
#
|
||||||
# Install as a systemd service/timer or run via cron:
|
# Install as a systemd service/timer or run via cron:
|
||||||
# * * * * * /home/localuser/services/technitium/sync-zones.sh
|
# * * * * * /home/localuser/services/technitium/sync-zones.sh
|
||||||
@@ -21,23 +23,50 @@ CONFIG_DIR="${CONFIG_DIR:-/home/localuser/services/technitium/config}"
|
|||||||
ZONE_DIR="$CONFIG_DIR/zones"
|
ZONE_DIR="$CONFIG_DIR/zones"
|
||||||
LOCK_FILE="/tmp/technitium-zone-sync.lock"
|
LOCK_FILE="/tmp/technitium-zone-sync.lock"
|
||||||
LOG_FILE="${LOG_FILE:-/home/localuser/services/technitium/sync.log}"
|
LOG_FILE="${LOG_FILE:-/home/localuser/services/technitium/sync.log}"
|
||||||
|
# Reload command; array form so tests can substitute a fake.
|
||||||
|
read -r -a RELOAD_CMD <<< "${RELOAD_CMD:-docker restart tsys-dns}"
|
||||||
|
export RELOAD_CMD
|
||||||
|
|
||||||
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >> "$LOG_FILE"; }
|
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >> "$LOG_FILE"; }
|
||||||
|
|
||||||
|
# Sync zones from the primary and reload Technitium when anything changed.
|
||||||
|
# Returns non-zero when rsync fails (reload failures are logged, not fatal).
|
||||||
|
sync_and_reload() {
|
||||||
|
local changed rc zone_count changed_count
|
||||||
|
# --temp-dir avoids partial writes; --delete removes zones deleted on
|
||||||
|
# primary; --itemize-changes reports exactly what changed.
|
||||||
|
changed=$(rsync -az --delete --itemize-changes --out-format='%n' --temp-dir=/tmp \
|
||||||
|
"${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" 2>> "$LOG_FILE")
|
||||||
|
rc=$?
|
||||||
|
if [ "$rc" -ne 0 ]; then
|
||||||
|
return "$rc"
|
||||||
|
fi
|
||||||
|
zone_count=$(find "$ZONE_DIR" -maxdepth 1 -type f | wc -l)
|
||||||
|
if [ -n "$changed" ]; then
|
||||||
|
changed_count=$(printf '%s\n' "$changed" | wc -l)
|
||||||
|
log "Sync complete: $zone_count zones, $changed_count changed — reloading Technitium"
|
||||||
|
if "${RELOAD_CMD[@]}" >> "$LOG_FILE" 2>&1; then
|
||||||
|
log "Technitium reload triggered"
|
||||||
|
else
|
||||||
|
log "WARN: reload command failed (rc=$?) — records may be stale until next change"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Sync complete: $zone_count zones (no changes)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
# Prevent overlapping runs
|
# Prevent overlapping runs
|
||||||
exec 9>"$LOCK_FILE" || exit 0
|
exec 9>"$LOCK_FILE" || exit 0
|
||||||
flock -n 9 || { log "another sync is running; skipping"; exit 0; }
|
flock -n 9 || { log "another sync is running; skipping"; exit 0; }
|
||||||
|
|
||||||
mkdir -p "$ZONE_DIR"
|
mkdir -p "$ZONE_DIR"
|
||||||
|
|
||||||
# rsync zones from primary. Use --temp-dir to avoid partial writes being
|
|
||||||
# picked up by Technitium, and --delete to remove zones deleted on primary.
|
|
||||||
log "Syncing zones from $PRIMARY_HOST..."
|
log "Syncing zones from $PRIMARY_HOST..."
|
||||||
if rsync -az --delete --temp-dir=/tmp \
|
sync_and_reload || { log "ERROR: rsync failed (rc=$?)"; exit 1; }
|
||||||
"${PRIMARY_HOST}:$ZONE_DIR/" "$ZONE_DIR/" >> "$LOG_FILE" 2>&1; then
|
}
|
||||||
zone_count=$(find "$ZONE_DIR" -maxdepth 1 -type f | wc -l)
|
|
||||||
log "Sync complete: $zone_count zones"
|
# Allow sourcing (unit tests) without executing the sync
|
||||||
else
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
||||||
log "ERROR: rsync failed (rc=$?)"
|
main
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
Executable
+128
@@ -0,0 +1,128 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Unit tests for netinfra/dns-cluster-setup/sync-zones.sh [#469]
|
||||||
|
# Verifies reload-on-change behavior: Technitium restart fires only when
|
||||||
|
# rsync actually changed zone files, and never on clean syncs.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
SCRIPT="$PROJECT_ROOT/netinfra/dns-cluster-setup/sync-zones.sh"
|
||||||
|
|
||||||
|
failed=0
|
||||||
|
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
export LOG_FILE="$TMP/sync.log"
|
||||||
|
|
||||||
|
# Script must exist and source cleanly (main guarded for sourcing)
|
||||||
|
if [[ -f "$SCRIPT" ]] && (source "$SCRIPT"); then
|
||||||
|
echo "✅ script exists + sources cleanly"
|
||||||
|
else
|
||||||
|
echo "❌ script missing or fails to source: $SCRIPT"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$SCRIPT"
|
||||||
|
|
||||||
|
# Fake command log: record invocations of rsync / reload / find
|
||||||
|
FAKE_BIN="$TMP/bin"
|
||||||
|
mkdir -p "$FAKE_BIN"
|
||||||
|
export PATH="$FAKE_BIN:$PATH"
|
||||||
|
|
||||||
|
make_rsync() {
|
||||||
|
# $1 = exit code, $2 = "changed" or "clean" (stdout of rsync)
|
||||||
|
cat > "$FAKE_BIN/rsync" <<EOF
|
||||||
|
#!/bin/bash
|
||||||
|
echo "$2"
|
||||||
|
exit $1
|
||||||
|
EOF
|
||||||
|
chmod +x "$FAKE_BIN/rsync"
|
||||||
|
}
|
||||||
|
|
||||||
|
make_reload() {
|
||||||
|
cat > "$FAKE_BIN/reload" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
echo "reload-fired" >> "$RELOAD_LOG"
|
||||||
|
EOF
|
||||||
|
chmod +x "$FAKE_BIN/reload"
|
||||||
|
}
|
||||||
|
|
||||||
|
RELOAD_LOG="$TMP/reload.log"
|
||||||
|
touch "$RELOAD_LOG"
|
||||||
|
export RELOAD_LOG
|
||||||
|
|
||||||
|
# find: minimal fake so zone counting works
|
||||||
|
mkdir -p "$FAKE_BIN/find-dir"
|
||||||
|
cat > "$FAKE_BIN/find" <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# report 3 fake zone files regardless of args
|
||||||
|
for i in 1 2 3; do echo "/zones/zone$i"; done
|
||||||
|
EOF
|
||||||
|
chmod +x "$FAKE_BIN/find"
|
||||||
|
|
||||||
|
test_reload_fires_on_change() {
|
||||||
|
make_rsync 0 "knel.net.zone"
|
||||||
|
make_reload
|
||||||
|
RELOAD_CMD=("$FAKE_BIN/reload")
|
||||||
|
export RELOAD_CMD
|
||||||
|
if sync_and_reload >/dev/null 2>&1 && [[ "$(cat "$RELOAD_LOG")" == "reload-fired" ]]; then
|
||||||
|
echo "✅ reload fires when rsync reports changed zones"
|
||||||
|
else
|
||||||
|
echo "❌ reload did not fire on changed sync"
|
||||||
|
((++failed))
|
||||||
|
fi
|
||||||
|
: > "$RELOAD_LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_no_reload_on_clean_sync() {
|
||||||
|
make_rsync 0 ""
|
||||||
|
make_reload
|
||||||
|
RELOAD_CMD=("$FAKE_BIN/reload")
|
||||||
|
export RELOAD_CMD
|
||||||
|
if sync_and_reload >/dev/null 2>&1 && [[ ! -s "$RELOAD_LOG" ]]; then
|
||||||
|
echo "✅ no reload on clean sync"
|
||||||
|
else
|
||||||
|
echo "❌ reload fired (or errored) on clean sync"
|
||||||
|
((++failed))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_rsync_failure_propagates() {
|
||||||
|
make_rsync 23 ""
|
||||||
|
RELOAD_CMD=("$FAKE_BIN/reload")
|
||||||
|
export RELOAD_CMD
|
||||||
|
if ! sync_and_reload >/dev/null 2>&1; then
|
||||||
|
echo "✅ rsync failure propagates non-zero rc"
|
||||||
|
else
|
||||||
|
echo "❌ rsync failure was swallowed"
|
||||||
|
((++failed))
|
||||||
|
fi
|
||||||
|
if [[ ! -s "$RELOAD_LOG" ]]; then
|
||||||
|
echo "✅ no reload attempted after rsync failure"
|
||||||
|
else
|
||||||
|
echo "❌ reload fired despite rsync failure"
|
||||||
|
((++failed))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_reload_failure_does_not_break_sync() {
|
||||||
|
make_rsync 0 "knel.net.zone"
|
||||||
|
RELOAD_CMD=("/bin/false")
|
||||||
|
export RELOAD_CMD
|
||||||
|
if sync_and_reload >/dev/null 2>&1; then
|
||||||
|
echo "✅ reload failure does not fail the sync run"
|
||||||
|
else
|
||||||
|
echo "❌ reload failure failed the whole sync"
|
||||||
|
((++failed))
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
test_reload_fires_on_change
|
||||||
|
test_no_reload_on_clean_sync
|
||||||
|
test_rsync_failure_propagates
|
||||||
|
test_reload_failure_does_not_break_sync
|
||||||
|
|
||||||
|
if ((failed > 0)); then
|
||||||
|
echo "❌ $failed sync-zones test(s) failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✅ all sync-zones tests passed"
|
||||||
Reference in New Issue
Block a user