#!/bin/bash # ============================================================================================== # ============================= Conf Cache Watchdog ============================================ # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Maintains the persistent partner conf backup at $PERSISTENT_CONF_CACHE. # Runs every 15 minutes via SYSTEM_WATCHDOG_SCRIPTS. # # When remote is OFFLINE: # Writes/refreshes partner confs from RAM cache → persistent location. # Means if this host reboots while remote is still down, conf_cache_restore.sh # can load the partner vars into RAM so fallback.sh has what it needs. # # When remote is ONLINE: # Removes the persistent backup if present — not needed, conf_sync.sh will # pull fresh on next boot. # # Silent when remote is online and no backup exists (normal state). # No-op when FALLBACK_ENABLED=false or CONF_SYNC_ENABLED=false. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # One decision, driven entirely by remote reachability: # # 1. Gates # → PARTNERSHIP_ENABLED, FALLBACK_ENABLED, CONF_SYNC_ENABLED, REMOTE_ID # → any gate closed means exit 0, no work, no output # # 2. ping_remote # REACHABLE → remove $PERSISTENT_CONF_CACHE if it exists, exit # UNREACHABLE → refresh the backup from the RAM cache # # 3. Refresh (remote offline only) # → copy every host*.conf from the RAM cache except this host's own # → own conf is excluded: it is already on disk, the backup exists # solely to survive a reboot without the partner's vars # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # Online = No Backup Needed # When the remote is reachable, conf_sync.sh will pull fresh on the next boot. # The persistent backup is removed — a stale backup is worse than no backup # because it can mask a connectivity problem that conf_sync.sh would catch. # # Offline = Stay Ready # While the remote is down, the RAM cache is the best available copy of partner # vars. Refreshing the persistent backup every 15 minutes ensures it reflects # the last-known-good state, not an old copy from days earlier. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Root Enforcement # Writes to and removes $PERSISTENT_CONF_CACHE, which lives under the plugin # directory and is not user-writable. # # Lock Acquisition # acquire_lock() prevents concurrent execution. Without it a slow run can still # be copying confs into the backup while the next run, seeing the remote back # online, rm -rf's the directory out from under it. # # Cache Path Sanity Guard # PERSISTENT_CONF_CACHE is validated as an absolute path at least three levels # deep before any rm -rf. It is built from ${SCRIPTS_DIR} — if that is ever # unset the path collapses toward the filesystem root, and this script would # otherwise recursively delete whatever it collapsed to. # # Partnership Gate # require_partnership() exits early if PARTNERSHIP_ENABLED=false. # # FALLBACK_ENABLED / CONF_SYNC_ENABLED Gates # Exits cleanly if either is disabled — the backup only has meaning when # fallback can actually consume it. # # REMOTE_ID Presence Check # Exits if partner identity is unset. Without a partner there is nothing to # back up and the own-conf exclusion below could not be applied correctly. # # Own-Conf Exclusion # This host's own conf is never written into the partner backup. Restoring it # later would overwrite live local config with a stale copy. # # Dry Run Support # --dry-run reports every removal and copy without touching the backup. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # master.conf # # PERSISTENT_CONF_CACHE # Destination for the partner conf backup. Must survive a reboot, so it # lives under ${SCRIPTS_DIR}, not in /tmp. # # FALLBACK_ENABLED # Master fallback toggle. Backup is pointless when fallback cannot run. # # CONF_SYNC_ENABLED # Conf sync toggle. When off, no RAM cache is being maintained to back up. # # PARTNERSHIP_ENABLED # Checked via require_partnership(). # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # conf_cache_watchdog.sh # Refresh or remove the persistent partner conf backup based on remote state # # conf_cache_watchdog.sh --dry-run # Report what would be written or removed without changing the backup # # conf_cache_watchdog.sh --log # Verbose per-file output # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../../load_config.sh" parse_args "$@" # ============================================================================================== # ━━━ Setup ━━━ # ============================================================================================== if [[ "$EUID" -ne 0 ]]; then error "Must be run as root" exit 1 fi acquire_lock detect_hosts require_partnership [[ "${FALLBACK_ENABLED:-false}" != "true" ]] && exit 0 [[ "${CONF_SYNC_ENABLED:-true}" != "true" ]] && exit 0 [[ -z "${REMOTE_ID:-}" ]] && exit 0 # The exported variable, not a literal. This was hardcoded while load_config.sh already exported # CONF_RAM_CACHE_DIR, so the watchdog would have gone on watching an empty directory the moment # that path moved — and reported the cache healthy because nothing was ever missing from a # location nothing writes to. RAM_CACHE="$CONF_RAM_CACHE_DIR" SAVE_DIR="${PERSISTENT_CONF_CACHE:-}" # SAVE_DIR is rm -rf'd below and is built from ${SCRIPTS_DIR}. If that is ever unset the # path collapses toward / — require an absolute path at least three levels deep so a # collapsed or empty value can never name a system directory. _slashes="${SAVE_DIR//[^\/]/}" if [[ -z "$SAVE_DIR" || "$SAVE_DIR" != /* || "${#_slashes}" -lt 3 ]]; then error "PERSISTENT_CONF_CACHE is unset or unsafe ('${SAVE_DIR:-unset}') — refusing to manage conf backup" notify "conf_cache_watchdog aborted on $(hostname) — PERSISTENT_CONF_CACHE is '${SAVE_DIR:-unset}'" \ "Conf Cache Watchdog" "warning" exit 1 fi unset _slashes if ping_remote; then if [[ -d "$SAVE_DIR" ]]; then [[ "$DRY_RUN" == true ]] && warn "DRY RUN — would remove $SAVE_DIR (remote back online)" && exit 0 rm -rf "$SAVE_DIR" log "Remote online — persistent conf backup removed" fi exit 0 fi # Remote offline — write/refresh backup from RAM cache if [[ ! -d "$RAM_CACHE" ]]; then log "Remote offline but RAM cache not populated — nothing to back up" exit 0 fi saved=0 for conf in "$RAM_CACHE"/host*.conf; do [[ -f "$conf" ]] || continue base="$(basename "$conf")" [[ "${base,,}" == "${MY_ID,,}.conf" ]] && continue if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would write $base → $SAVE_DIR/" (( saved++ )) continue fi mkdir -p "$SAVE_DIR" cp "$conf" "$SAVE_DIR/$base" && (( saved++ )) done [[ "$saved" -gt 0 ]] && log "Remote offline — partner conf backup refreshed ($saved file(s))" exit 0