vv_push_master_conf() in config.php: finds all remote HOST* entries, resolves Tailscale IPs, SCPs master.conf to each. rawconf.php calls it after a successful write when file=master.conf. Scheduler save button shows sync status: "✓ Saved · synced to HOST2" or "✓ Saved · push failed: HOST2" on error. Also fix path bug in partnership_transfer.sh — was SCPing to appdata root instead of Configurations/.
339 lines
15 KiB
Bash
Executable File
339 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ============================= Partnership Transfer ============================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Transfers ownership from the current owner to the current mirror. After
|
|
# transfer the roles are swapped: what was the mirror becomes the new owner,
|
|
# and what was the owner becomes the new mirror.
|
|
#
|
|
# No containers are moved — only config and WebUI targets are updated. Both
|
|
# servers remain in the partnership; the sync direction reverses on the next
|
|
# fallback.sh / critical_sync_maintenance.sh cycle.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL MODEL
|
|
# ==============================================================================================
|
|
#
|
|
# OWNER ONLY — mirror cannot run this script.
|
|
#
|
|
# Step 1: Health Verification — both servers pass N consecutive health checks
|
|
# Step 2: Pre-transfer Sync — final sync in current direction (owner → mirror)
|
|
# Step 3: Reconfigure WebUIs — new owner WebUIs → localhost
|
|
# new mirror WebUIs → new owner IP
|
|
# Step 4: Flip Ownership — update PARTNERSHIP_OWNER_HOST in master.conf
|
|
# on both servers
|
|
# Step 5: Write State — ACTIVE written locally and pushed to new mirror
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# Owner-Only Enforcement
|
|
# The script reads MY_ID from detect_hosts() and exits immediately if it is
|
|
# not the current PARTNERSHIP_OWNER_HOST. The mirror cannot run a transfer.
|
|
#
|
|
# Explicit Confirmation String
|
|
# Requires the exact passphrase from PARTNERSHIP_TRANSFER_CONFIRM via
|
|
# --confirm=<value>. Without a matching string the transfer is cancelled
|
|
# before any steps execute. Prevents accidental ownership changes.
|
|
#
|
|
# Active Partnership Guard
|
|
# Reads the local state file and exits if the current state is INACTIVE.
|
|
# A transfer without an active partnership has no defined outcome.
|
|
#
|
|
# Dual Health Verification
|
|
# Both servers must pass PARTNERSHIP_TRANSFER_STRIKES consecutive health
|
|
# checks before proceeding. A single failure resets the strike counter.
|
|
# After PARTNERSHIP_TRANSFER_MAX_ATTEMPTS total attempts the transfer aborts.
|
|
#
|
|
# Pre-transfer Final Sync
|
|
# A full sync in the current direction (owner → mirror) runs immediately
|
|
# before roles flip. Ensures the mirror is current before it becomes the owner.
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# master.conf
|
|
#
|
|
# PARTNERSHIP_OWNER_HOST
|
|
# Current owner host ID (e.g. "HOST1"). Updated on both servers after transfer.
|
|
#
|
|
# PARTNERSHIP_TRANSFER_CONFIRM
|
|
# Exact string required to confirm transfer (default: "i-understand-this-transfers-ownership").
|
|
# Pass via --confirm=<value>.
|
|
#
|
|
# PARTNERSHIP_TRANSFER_STRIKES
|
|
# Consecutive health checks both servers must pass before transfer proceeds (default: 3).
|
|
#
|
|
# PARTNERSHIP_TRANSFER_MAX_ATTEMPTS
|
|
# Max health check attempts before giving up (default: 20).
|
|
#
|
|
# CRITICAL_SYNC_SHARES
|
|
# Array of "path|profile" or "path" entries for do_final_sync().
|
|
#
|
|
# PARTNERSHIP_AUTH_WEBUIS
|
|
# Array of "ContainerName|WebUIPort" entries reconfigured during transfer.
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# Partnership/partnership_transfer.sh --confirm=i-understand-this-transfers-ownership
|
|
# Full transfer — owner detected automatically.
|
|
#
|
|
# Partnership/partnership_transfer.sh --confirm=i-understand-this-transfers-ownership --dry-run
|
|
# Preview all steps without executing. Confirmation check is skipped in dry-run mode.
|
|
#
|
|
# Partnership/partnership_transfer.sh --confirm=i-understand-this-transfers-ownership --log
|
|
# Verbose per-step output.
|
|
#
|
|
# ==============================================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
SCRIPTS_ROOT="$SCRIPT_DIR/.."
|
|
SSH_TIMEOUT=15
|
|
|
|
source "$SCRIPTS_ROOT/load_config.sh"
|
|
|
|
# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
|
|
TRANSFER_CONFIRM_INPUT=""
|
|
FILTERED_ARGS=()
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--confirm=*) TRANSFER_CONFIRM_INPUT="${arg#--confirm=}" ;;
|
|
*) FILTERED_ARGS+=("$arg") ;;
|
|
esac
|
|
done
|
|
|
|
parse_args "${FILTERED_ARGS[@]}"
|
|
|
|
# ── Source partnership_manager.sh for shared helpers ──────────────────────────────────────────
|
|
# PARTNERSHIP_LIB_MODE=1 skips mode dispatch — functions are defined, nothing is executed.
|
|
PARTNERSHIP_LIB_MODE=1 source "$SCRIPT_DIR/partnership_manager.sh"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Setup ━━━
|
|
# ==============================================================================================
|
|
[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
|
|
|
|
if ! command -v docker &>/dev/null; then
|
|
error "Docker command not found"
|
|
exit 1
|
|
fi
|
|
|
|
validate_unraid_cmd \
|
|
"/usr/local/emhttp/plugins/dynamix/scripts/notify" \
|
|
"" "" \
|
|
"unRAID notify script" || warn "unRAID notify script not found — native notifications disabled"
|
|
|
|
detect_hosts
|
|
|
|
OWNER_ID="${PARTNERSHIP_OWNER_HOST:-HOST1}"
|
|
MIRROR_ID=$( [[ "$OWNER_ID" == "HOST1" ]] && echo "HOST2" || echo "HOST1" )
|
|
OWNER="${!OWNER_ID}"
|
|
MIRROR="${!MIRROR_ID}"
|
|
MIRROR_SSH_KEY="$SSH_KEY"
|
|
OWNER_SSH_KEY="$SSH_KEY"
|
|
|
|
AM_OWNER=false
|
|
AM_MIRROR=false
|
|
[[ "$MY_ID" == "$OWNER_ID" ]] && AM_OWNER=true
|
|
[[ "$MY_ID" == "$MIRROR_ID" ]] && AM_MIRROR=true
|
|
|
|
LOCAL_STATE_FILE="/boot/config/partnership_${LOCAL_SERVER_NAME}.db"
|
|
REMOTE_STATE_FILE="/boot/config/partnership_${REMOTE_SERVER_NAME}.db"
|
|
|
|
acquire_lock "strict"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Preflight ━━━
|
|
# ==============================================================================================
|
|
echo ""
|
|
echo "━━━ $ICON_FALLBACK Partnership Transfer — $MY_ID ($LOCAL_SERVER_NAME) — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
|
echo ""
|
|
|
|
if [[ "$AM_MIRROR" == true ]]; then
|
|
error "Only the owner ($OWNER / $OWNER_ID) can run --transfer"
|
|
error "Run from $OWNER, or use --offboard and re-onboard with roles swapped"
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -f "$LOCAL_STATE_FILE" ]]; then
|
|
CURRENT_STATE=$(read_state_file "$LOCAL_STATE_FILE" "state")
|
|
if [[ "$CURRENT_STATE" == "INACTIVE" ]]; then
|
|
error "No active partnership — transfer requires an active partnership"
|
|
error "If roles are already correct, check PARTNERSHIP_OWNER_HOST in master.conf"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"
|
|
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "⚠️ WARNING — OWNERSHIP TRANSFER"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo " Current owner: $OWNER_ID ($OWNER)"
|
|
echo " Current mirror: $MIRROR_ID ($MIRROR)"
|
|
echo ""
|
|
echo " After transfer:"
|
|
echo " New owner: $MIRROR_ID ($MIRROR)"
|
|
echo " New mirror: $OWNER_ID ($OWNER)"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Confirmation ━━━
|
|
# ==============================================================================================
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
if [[ -z "$TRANSFER_CONFIRM_INPUT" ]]; then
|
|
echo ""
|
|
echo "To proceed, pass exactly:"
|
|
echo " --confirm=${PARTNERSHIP_TRANSFER_CONFIRM}"
|
|
echo ""
|
|
error "Transfer cancelled — confirmation required"
|
|
exit 1
|
|
fi
|
|
if [[ "$TRANSFER_CONFIRM_INPUT" != "$PARTNERSHIP_TRANSFER_CONFIRM" ]]; then
|
|
error "Confirmation string does not match — transfer cancelled"
|
|
exit 1
|
|
fi
|
|
log "Confirmation accepted"
|
|
else
|
|
warn "DRY RUN — confirmation check skipped"
|
|
fi
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Step 1: Health Verification ━━━
|
|
# ==============================================================================================
|
|
echo ""
|
|
echo "━━━ $ICON_SHIELD Step 1: Health Verification ━━━"
|
|
log "Both servers must pass ${PARTNERSHIP_TRANSFER_STRIKES} consecutive health checks"
|
|
|
|
STRIKES=0
|
|
ATTEMPTS=0
|
|
MAX_ATTEMPTS="${PARTNERSHIP_TRANSFER_MAX_ATTEMPTS:-20}"
|
|
|
|
while [[ "$STRIKES" -lt "$PARTNERSHIP_TRANSFER_STRIKES" ]]; do
|
|
(( ATTEMPTS++ ))
|
|
if [[ "$ATTEMPTS" -gt "$MAX_ATTEMPTS" ]]; then
|
|
error "Health checks failed after $MAX_ATTEMPTS attempts — servers not stable"
|
|
error "Transfer cancelled — try again when both servers are healthy"
|
|
exit 1
|
|
fi
|
|
|
|
if check_both_healthy; then
|
|
(( STRIKES++ ))
|
|
log "Health check passed ($STRIKES/${PARTNERSHIP_TRANSFER_STRIKES})"
|
|
[[ "$STRIKES" -lt "$PARTNERSHIP_TRANSFER_STRIKES" ]] && sleep 10
|
|
else
|
|
warn "Health check failed — resetting (attempt $ATTEMPTS/$MAX_ATTEMPTS)"
|
|
STRIKES=0
|
|
sleep 30
|
|
fi
|
|
done
|
|
warn "Both servers healthy — proceeding ✅"
|
|
|
|
# Resolve IPs after health checks confirm reachability
|
|
MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
|
|
OWNER_IP=$(resolve_tailscale_ip "$OWNER")
|
|
[[ -z "$MIRROR_IP" ]] && { error "Cannot resolve mirror Tailscale IP"; exit 1; }
|
|
|
|
# Compute post-transfer roles
|
|
NEW_OWNER_ID="$MIRROR_ID"
|
|
NEW_MIRROR_ID="$OWNER_ID"
|
|
NEW_OWNER="$MIRROR"
|
|
NEW_MIRROR="$OWNER"
|
|
NEW_OWNER_IP="$MIRROR_IP"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Step 2: Pre-transfer Sync ━━━
|
|
# ==============================================================================================
|
|
echo ""
|
|
echo "━━━ $ICON_SYNC Step 2: Pre-transfer Sync ━━━"
|
|
do_final_sync
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Step 3: Reconfigure WebUIs ━━━
|
|
# ==============================================================================================
|
|
echo ""
|
|
echo "━━━ $ICON_CONTAINERS Step 3: Reconfigure WebUIs ━━━"
|
|
|
|
WEBUI_FAILURES=0
|
|
|
|
# New owner (current mirror, HOST2) WebUIs → localhost — it now manages itself directly
|
|
log "New owner ($NEW_OWNER) WebUIs → localhost"
|
|
for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]}"; do
|
|
[[ -z "$entry" ]] && continue
|
|
container="${entry%%|*}"
|
|
port="${entry##*|}"
|
|
reconfigure_webui "$container" "$port" "localhost" \
|
|
"$SSH_KEY" "$MIRROR_IP" "$MIRROR" || (( WEBUI_FAILURES++ ))
|
|
done
|
|
|
|
# New mirror (us, HOST1) WebUIs → new owner IP — defers to new owner going forward
|
|
log "New mirror ($NEW_MIRROR) WebUIs → $NEW_OWNER_IP"
|
|
reconfigure_local_webuis "$NEW_OWNER_IP"
|
|
WEBUI_RC=$?
|
|
[[ "$WEBUI_RC" -gt 0 ]] && (( WEBUI_FAILURES += WEBUI_RC ))
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Step 4: Flip Ownership in master.conf ━━━
|
|
# ==============================================================================================
|
|
echo ""
|
|
echo "━━━ $ICON_GEAR Step 4: Flip Ownership ━━━"
|
|
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
update_master_conf "PARTNERSHIP_OWNER_HOST" "\"$NEW_OWNER_ID\""
|
|
|
|
# Push updated master.conf to new owner so both servers agree immediately.
|
|
# master.conf is shared — host-specific credentials live in host*.conf.
|
|
scp -i "$SSH_KEY" \
|
|
-o ConnectTimeout="$SSH_TIMEOUT" \
|
|
-o StrictHostKeyChecking=no \
|
|
"$SCRIPTS_ROOT/Configurations/master.conf" \
|
|
"root@${MIRROR_IP}:/mnt/user/appdata/Varaverk/Configurations/master.conf" 2>/dev/null && \
|
|
log "master.conf pushed to $NEW_OWNER ✅" || \
|
|
error "Failed to push master.conf to $NEW_OWNER — set PARTNERSHIP_OWNER_HOST=\"$NEW_OWNER_ID\" manually"
|
|
else
|
|
warn "DRY RUN — would set PARTNERSHIP_OWNER_HOST=$NEW_OWNER_ID on both servers"
|
|
fi
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Step 5: Write State ━━━
|
|
# ==============================================================================================
|
|
echo ""
|
|
echo "━━━ $ICON_SYNC Step 5: Write State ━━━"
|
|
|
|
NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
|
write_state_file "$LOCAL_STATE_FILE" "ACTIVE" "$NOW" "" "$LOCAL_SERVER_NAME" "transfer"
|
|
push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$SSH_KEY"
|
|
|
|
# ==============================================================================================
|
|
# ━━━ Summary ━━━
|
|
# ==============================================================================================
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY TRANSFER SUMMARY ━━━━━"
|
|
echo " New owner: $NEW_OWNER_ID ($NEW_OWNER — $NEW_OWNER_IP)"
|
|
echo " New mirror: $NEW_MIRROR_ID ($NEW_MIRROR)"
|
|
echo " WebUI failures: $WEBUI_FAILURES"
|
|
echo " Sync direction: $NEW_OWNER → $NEW_MIRROR (next cycle)"
|
|
echo " Ownership: PARTNERSHIP_OWNER_HOST=$NEW_OWNER_ID"
|
|
echo ""
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — no changes made"
|
|
else
|
|
warn "$ICON_DONE DONE — ownership transferred to $NEW_OWNER_ID ($NEW_OWNER) ✅"
|
|
echo "fallback.sh and critical_sync_maintenance.sh will adapt on next cycle"
|
|
echo "No containers were moved — only config and WebUI targets updated"
|
|
[[ "$WEBUI_FAILURES" -gt 0 ]] && \
|
|
warn "$WEBUI_FAILURES WebUI(s) failed — check templates manually"
|
|
notify "Partnership ownership transferred — new owner: $NEW_OWNER ($NEW_OWNER_ID)" \
|
|
"Partnership" "normal"
|
|
fi
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|