#!/bin/bash # ============================================================================================== # ============================= Conf Cache Sync ================================================ # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Maintains a RAM-resident conf cache at /tmp/.cache/vv/d/. # Credentials and partner keys live in RAM only — never on disk across hosts. # # On array start (default / --array-start): # 1. Copy own conf to local cache # 2. Pull each available partner's conf from their disk → local cache # 3. Push own conf to each available partner's /tmp/.cache/vv/d/ cache # # On conf save (--push-only): # Fast path — push updated own conf to all partners' /tmp/.cache/vv/d/ only. # No pulls, no local cache rebuild. # # Cache is /tmp/.cache/vv/d (tmpfs) — cleared every reboot, repopulated by # this script on next array start. Scripts source from cache for partner vars; # own vars always come from disk (load_config.sh skips cached copy of own conf). # # Pull path resolves the remote's SCRIPTS_DIR from their varaverk.cfg so it # works whether the remote is in internal or appdata storage mode. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # 1. Gates — PARTNERSHIP_ENABLED, CONF_SYNC_ENABLED # 2. Cache own conf into the local RAM cache (skipped in --push-only / --pull-only) # 3. Per partner: # a. Resolve the partner's own SCRIPTS_DIR by reading their varaverk.cfg over SSH, # so a partner in appdata storage mode is still found # b. Pull — scp their host*.conf from their disk into our RAM cache # c. Push — scp our host*.conf into their RAM cache # A partner that fails SSH is counted and skipped; the others still sync. # # Every file written locally or remotely is restricted to 600, in a 700 directory. # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # RAM-Only Credentials # Partner credentials and keys never touch disk on the receiving server. # /tmp/.cache/vv/d is tmpfs — cleared every reboot. This is intentional: # partner conf files are not discoverable on disk between boots. # # Pull Resolves Remote Path # The pull step reads the remote's varaverk.cfg to discover their SCRIPTS_DIR # before SSHing for the conf. Works whether the remote is in internal or # appdata storage mode — no hardcoded path assumptions about the remote. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Root Enforcement # Reads the on-disk conf and writes the RAM cache; SSH/scp run as root. # # Lock Acquisition # acquire_lock prevents concurrent sync runs writing the same cache files. # # Partnership Gate # require_partnership exits early if PARTNERSHIP_ENABLED=false. # # CONF_SYNC_ENABLED Gate # Exits cleanly when disabled, without removing it from the schedule. # # Host Detection # detect_hosts() builds the partner list used for push/pull routing. # # SSH Reachability # Partners that fail SSH are counted and skipped, never fatal — one unreachable # partner does not prevent the others from syncing. # # SSH Timeouts # Every ssh and scp call is wrapped in timeout with ConnectTimeout and BatchMode, # so an unresponsive or password-prompting partner cannot stall the run. # # Remote Path Discovery # The partner's SCRIPTS_DIR is read from their own varaverk.cfg rather than assumed, # so a partner in appdata storage mode is still found. Falls back to the default # plugin path if the file cannot be read. # # Credential File Permissions # Cache directories are created 700 and every conf written 600 — on both ends. These # files carry NPM/lldap passwords and API keys, and the cache lives under a # world-readable /tmp path. The pushed copy is chmod'd on the partner too, since our # own credentials land on their disk. # # Dry Run Support # --dry-run reports every pull and push without transferring anything. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # master.conf # # CONF_SYNC_ENABLED # Master toggle for conf syncing (default: true) # # CONF_RAM_CACHE_DIR # tmpfs cache both ends read partner vars from (/tmp/.cache/vv/d). Cleared every # reboot, which is why conf_cache_save.sh / conf_cache_restore.sh exist. # # SSH_KEY # Key used for all partner ssh/scp operations # # PARTNERSHIP_ENABLED # Checked via require_partnership() # # host*.conf # # HOST* — hostnames used to build the partner list via detect_hosts() # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # conf_sync.sh Full sync: pull from all partners + push to all partners # conf_sync.sh --push-only Push own conf to all partners (fast, for conf-save hook) # conf_sync.sh --pull-only Pull partner confs into local cache only (for intermediate orch) # conf_sync.sh --dry-run Show what would happen, no changes # conf_sync.sh --log Verbose output # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../load_config.sh" PUSH_ONLY=false PULL_ONLY=false FILTERED_ARGS=() for arg in "$@"; do case "$arg" in --push-only) PUSH_ONLY=true ;; --pull-only) PULL_ONLY=true ;; *) FILTERED_ARGS+=("$arg") ;; esac done parse_args "${FILTERED_ARGS[@]}" if [[ "$EUID" -ne 0 ]]; then error "Must be run as root" exit 1 fi acquire_lock detect_hosts require_partnership if [[ "${CONF_SYNC_ENABLED:-true}" == false ]]; then log "CONF_SYNC_ENABLED=false — skipping" exit 0 fi CACHE_DIR="$CONF_RAM_CACHE_DIR" MY_CONF="$SCRIPTS_DIR/Configurations/${MY_ID,,}.conf" SSH_TIMEOUT=10 # Reads the remote's varaverk.cfg to find their actual SCRIPTS_DIR. # Handles the case where the remote is in appdata storage mode. _remote_scripts_dir() { local ip="$1" local cfg line sd cfg=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \ -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \ "root@${ip}" "cat /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null" 2>/dev/null) || true while IFS= read -r line; do [[ "$line" == SCRIPTS_DIR=* ]] || continue sd="${line#SCRIPTS_DIR=}"; sd="${sd//\"/}"; sd="${sd//\'/}" echo "$sd"; return done <<< "$cfg" echo "/boot/config/plugins/varaverk" } [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made" # ── Ensure cache dir exists ─────────────────────────────────────────────────── if [[ "$DRY_RUN" == false ]]; then # These conf files carry credentials (NPM/lldap passwords, API keys). The cache lives in # a world-readable /tmp path, so the directory and every file written into it below are # restricted explicitly rather than left at the default umask. mkdir -p "$CACHE_DIR" && chmod 700 "$CACHE_DIR" fi # ── Copy own conf into local cache ─────────────────────────────────────────── if [[ "$PUSH_ONLY" == false ]] && [[ "$PULL_ONLY" == false ]]; then if [[ -f "$MY_CONF" ]]; then if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would copy $(basename "$MY_CONF") → $CACHE_DIR/" else if cp "$MY_CONF" "$CACHE_DIR/${MY_ID,,}.conf" && \ chmod 600 "$CACHE_DIR/${MY_ID,,}.conf"; then echo "Own conf cached ✅" else warn "Failed to cache own conf" fi fi else warn "Own conf not found: $MY_CONF" fi fi # ── Per-partner sync ────────────────────────────────────────────────────────── PUSHED=0 PULLED=0 FAILED=0 for host_var in $(compgen -v | grep -E '^HOST[0-9]+$' | sort); do partner_host="${!host_var}" [[ -z "$partner_host" ]] && continue [[ "${host_var,,}" == "${MY_ID,,}" ]] && continue partner_slot="${host_var,,}" # e.g. host2 partner_ip=$(resolve_tailscale_ip "$partner_host" 2>/dev/null || true) if [[ -z "$partner_ip" ]]; then warn "$partner_host — cannot resolve Tailscale IP, skipping" (( FAILED++ )) continue fi # ── Pull: grab partner's conf from their disk → our local cache ────────── if [[ "$PUSH_ONLY" == false ]]; then remote_sd=$(_remote_scripts_dir "$partner_ip") remote_conf="${remote_sd}/Configurations/${partner_slot}.conf" if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would pull $partner_host:$remote_conf → $CACHE_DIR/${partner_slot}.conf" elif timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \ -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \ "root@${partner_ip}:${remote_conf}" \ "$CACHE_DIR/${partner_slot}.conf" 2>/dev/null; then chmod 600 "$CACHE_DIR/${partner_slot}.conf" 2>/dev/null echo "Pulled ${partner_slot}.conf from $partner_host ✅" (( PULLED++ )) else warn "Could not pull ${partner_slot}.conf from $partner_host" (( FAILED++ )) fi fi # ── Push: send own conf to partner's /tmp/.cache/vv/d/ cache ─────────── if [[ "$PULL_ONLY" == true ]]; then continue fi if [[ "$DRY_RUN" == true ]]; then warn "DRY RUN — would push ${MY_ID,,}.conf → $partner_host:/tmp/.cache/vv/d/" continue fi # Ensure partner's cache dir exists, then SCP own conf into it timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \ -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \ "root@${partner_ip}" "mkdir -p '$CACHE_DIR' && chmod 700 '$CACHE_DIR'" 2>/dev/null if timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \ -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \ "$MY_CONF" \ "root@${partner_ip}:${CACHE_DIR}/${MY_ID,,}.conf" 2>/dev/null; then # Our own conf lands on the partner carrying our credentials — restrict it there too. timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \ -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \ "root@${partner_ip}" "chmod 600 '${CACHE_DIR}/${MY_ID,,}.conf'" 2>/dev/null echo "Pushed ${MY_ID,,}.conf to $partner_host ✅" (( PUSHED++ )) else warn "Could not push to $partner_host" (( FAILED++ )) fi done # ── Summary ─────────────────────────────────────────────────────────────────── if [[ "$PUSH_ONLY" == true ]]; then info "Conf push complete — pushed to $PUSHED host(s)${FAILED:+, $FAILED failed}" elif [[ "$PULL_ONLY" == true ]]; then info "Conf pull complete — pulled $PULLED partner conf(s)${FAILED:+, $FAILED failed}" else info "Conf sync complete — pulled $PULLED, pushed $PUSHED${FAILED:+, $FAILED failed}" fi if [[ "$FAILED" -gt 0 ]]; then notify "Conf sync on $LOCAL_SERVER_NAME ($MY_ID) — $FAILED partner(s) failed. Partner config cache may be stale." \ "Conf Sync" "warning" exit 1 fi exit 0