#!/bin/bash # ============================================================================================== # ============================= Critical Sync Maintenance ====================================== # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Orchestrator for time-sensitive syncs running every 30 minutes. Keeps the # mirror current between the less frequent daily and weekly windows. # Schedule: */30 * * * * (every 30 minutes via User Scripts plugin) # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # 1. Critical-Data rsync — auth stack, NPM config, certs (containers stopped both sides) # 2. CRITICAL_MAINTENANCE_SCRIPTS — play_state_sync + any other per-window scripts # 3. partnership --check — read both state files, detect changes, act accordingly # # RSYNC GATE # RSYNC_ENABLED=false → skips all syncs (global gate) # CRITICAL_RSYNC_ENABLED=false → skips critical syncs only (per-orchestrator gate) # partnership --check always runs regardless — state check doesn't need rsync. # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # Silent When Healthy # Runs 48 times per day — clean runs must produce zero output. Only failures # and notable events produce visible output. # # Auth-First Window # Auth stack changes (new users, proxy rules, certs) propagate within 30min. # Emby watch states stay in sync — mirror users see correct playback position. # Partnership state changes detected and acted on quickly. # # Strict Lock, Never Queue # acquire_lock "strict" — if the previous 30-min run is still going, skip # this cycle entirely. Critical-Data taking > 30min is a problem worth # knowing about. Strict mode prevents pile-up without waiting. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Root Enforcement # rsync over SSH and container stop/start both require root. # # Lock Acquisition # acquire_lock in strict mode — a cycle is skipped rather than queued if the previous # one is still running. At a 30-minute cadence, queuing would let a slow sync stack # windows behind it. # # Host Detection # detect_hosts() sets MY_ID and REMOTE_ID for routing and logs. # # Empty Job List Guard # Exits with an error and a notification if CRITICAL_MAINTENANCE_SCRIPTS is empty — # a silently empty critical tier would stop downloader resets and play-state sync # while still reporting success every 30 minutes. # # Remote IP Resolution # resolve_remote_ip confirms the partner is reachable before any transfer is attempted. # # RSYNC_ENABLED Gate # The global kill switch is respected before any rsync call, so disabling rsync # ecosystem-wide genuinely stops it here too. # # Non-Fatal Steps # A failing job is recorded and the rest of the tier still runs. # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # master.conf # # CRITICAL_RSYNC_ENABLED — enable/disable rsync section # CRITICAL_SYNC_SHARES — shares synced every 30min (HOST*_CRITICAL_SYNC_SHARES) # CRITICAL_MAINTENANCE_SCRIPTS — scripts run in critical window (optional) # PARTNERSHIP_ENABLED — enable/disable partnership check # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # critical_sync_maintenance.sh # Normal run. # # critical_sync_maintenance.sh --dry-run # Preview syncs without transferring. # # critical_sync_maintenance.sh --log # Verbose per-share output. # # critical_sync_maintenance.sh --status # Show configuration and exit. # # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ECOSYSTEM_ROOT="$(cd "$SCRIPT_DIR/.." && 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 "strict" detect_hosts # An unconfigured job list would run nothing and still report "0/0 passed" — indistinguishable # from a healthy run. Fail loudly instead of silently doing no work. if [[ ${#CRITICAL_MAINTENANCE_SCRIPTS[@]} -eq 0 ]]; then error "CRITICAL_MAINTENANCE_SCRIPTS is empty — no critical maintenance scripts will run" error "Check CRITICAL_MAINTENANCE_SCRIPTS in master.conf" notify "critical maintenance scripts skipped on $(hostname) ($MY_ID) — CRITICAL_MAINTENANCE_SCRIPTS is empty" \ "$(basename "$0" .sh)" "warning" exit 1 fi resolve_remote_ip [[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made" # ============================================================================================== # ━━━ Status ━━━ # ============================================================================================== if [[ "$SHOW_STATUS" == true ]]; then echo "" echo "━━━━━ $ICON_SUMMARY CRITICAL SYNC STATUS ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_NET Remote: $REMOTE_ID ($REMOTE_SERVER_NAME)" echo "$ICON_SYNC Rsync enabled: ${RSYNC_ENABLED:-true}" echo "$ICON_SYNC Critical enabled: ${CRITICAL_RSYNC_ENABLED:-false}" echo "$ICON_SHIELD Partnership: ${PARTNERSHIP_ENABLED:-false}" echo "" echo "━━━ Critical Sync Shares ━━━" if [[ ${#CRITICAL_SYNC_SHARES[@]} -eq 0 ]]; then warn " No CRITICAL_SYNC_SHARES configured" else for share in "${CRITICAL_SYNC_SHARES[@]}"; do [[ -z "$share" ]] && continue SHARE_PATH="${share%%|*}" SHARE_PROFILE="${share##*|}" SHARE_NAME=$(basename "$SHARE_PATH") [[ "$SHARE_PATH" == "$SHARE_PROFILE" ]] && \ echo " $ICON_SYNC $SHARE_NAME — no profile" || \ echo " $ICON_SYNC $SHARE_NAME — profile: $SHARE_PROFILE" done fi echo "" echo "━━━ Critical Maintenance Scripts ━━━" if [[ ${#CRITICAL_MAINTENANCE_SCRIPTS[@]} -eq 0 ]]; then echo " None configured" else for entry in "${CRITICAL_MAINTENANCE_SCRIPTS[@]}"; do [[ -z "$entry" || "$entry" == \#* ]] && continue echo " $ICON_GEAR $(basename "${entry%% *}")" done fi echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi # ============================================================================================== # ━━━ Critical Shares Sync ━━━ # ============================================================================================== START=$(date +%s) RSYNC_OK=false PASS=() FAIL=() log "$ICON_SYNC Critical shares (${#CRITICAL_SYNC_SHARES[@]}): $(for s in "${CRITICAL_SYNC_SHARES[@]}"; do printf '%s ' "$(basename "${s%%|*}")"; done)" [[ ${#CRITICAL_MAINTENANCE_SCRIPTS[@]} -gt 0 ]] && \ log "$ICON_GEAR Maintenance scripts: $(for s in "${CRITICAL_MAINTENANCE_SCRIPTS[@]}"; do printf '%s ' "$(basename "${s%% *}")"; done)" if ! check_rsync_enabled "CRITICAL"; then echo "Critical rsync disabled — skipping sync, running partnership check only" elif [[ ${#CRITICAL_SYNC_SHARES[@]} -eq 0 ]]; then warn "CRITICAL_RSYNC_ENABLED=true but CRITICAL_SYNC_SHARES is empty for $MY_ID" warn "Check HOST*_CRITICAL_SYNC_SHARES in host*.conf" else echo "Critical sync — $MY_ID → $REMOTE_ID — $(date '+%H:%M:%S')" # Build dry-run flag to pass through RSYNC_DRY="" [[ "$DRY_RUN" == true ]] && RSYNC_DRY="--dry-run" for share in "${CRITICAL_SYNC_SHARES[@]}"; do [[ -z "$share" ]] && continue # Parse optional profile flag: "/path/to/share|profile-name" SHARE_PATH="${share%%|*}" SHARE_PROFILE="${share##*|}" SHARE_NAME=$(basename "$SHARE_PATH") SHARE_START=$(date +%s) if [[ "$SHARE_PATH" == "$SHARE_PROFILE" ]]; then bash "$SCRIPT_DIR/../Rsync/rsync.sh" "$SHARE_PATH" $RSYNC_DRY else bash "$SCRIPT_DIR/../Rsync/rsync.sh" "$SHARE_PATH" \ --profile="$SHARE_PROFILE" $RSYNC_DRY fi RSYNC_EXIT=$? SHARE_DUR=$(format_duration $(( $(date +%s) - SHARE_START ))) if [[ "$RSYNC_EXIT" -eq 0 ]]; then PASS+=("$SHARE_NAME") echo "$SHARE_NAME — done in $SHARE_DUR ✅" RSYNC_OK=true else FAIL+=("$SHARE_NAME") error "$SHARE_NAME — failed after $SHARE_DUR (exit $RSYNC_EXIT)" fi done fi # ============================================================================================== # ━━━ Critical Maintenance Scripts ━━━ # ============================================================================================== JOB_PASS=() JOB_FAIL=() for script_entry in "${CRITICAL_MAINTENANCE_SCRIPTS[@]}"; do [[ -z "$script_entry" || "$script_entry" == \#* ]] && continue run_orch_child "$script_entry" done # ============================================================================================== # ━━━ Partnership Check ━━━ # ============================================================================================== if [[ "${PARTNERSHIP_ENABLED:-false}" == true ]]; then PARTNER_DRY="" [[ "$DRY_RUN" == true ]] && PARTNER_DRY="--dry-run" if [[ "$RSYNC_OK" == true ]]; then bash "$SCRIPT_DIR/../Partnership/partnership_manager.sh" \ --check --remote-seen $PARTNER_DRY else bash "$SCRIPT_DIR/../Partnership/partnership_manager.sh" \ --check --remote-unseen $PARTNER_DRY fi else echo "Partnership disabled — skipping check" fi # ============================================================================================== # ━━━ Summary ━━━ # ============================================================================================== END=$(date +%s) DURATION=$(format_duration $(( END - START ))) TOTAL_FAIL=$(( ${#FAIL[@]} + ${#JOB_FAIL[@]} )) # Minimal one-liner when healthy — 30-min cadence, keep it quiet. Full detail on failure. if [[ "$TOTAL_FAIL" -gt 0 ]]; then echo "" echo "━━━━━ $ICON_SUMMARY CRITICAL SYNC SUMMARY ━━━━━" echo "$ICON_HOST Identity: $MY_ID ($LOCAL_SERVER_NAME)" echo "$ICON_TIME Duration: $DURATION" [[ ${#PASS[@]} -gt 0 ]] && echo "Synced: ${PASS[*]}" [[ ${#FAIL[@]} -gt 0 ]] && echo "$ICON_ERROR Failed shares: ${FAIL[*]}" [[ ${#JOB_FAIL[@]} -gt 0 ]] && echo "$ICON_ERROR Failed jobs: ${JOB_FAIL[*]}" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" notify "Critical sync failed on $(hostname) ($MY_ID) — ${FAIL[*]} ${JOB_FAIL[*]}" \ "Critical Sync" "warning" exit 1 else echo "Critical sync complete — $MY_ID — ${DURATION} — ${#PASS[@]} share(s), ${#JOB_PASS[@]} job(s)" fi exit 0