feat(partnership): extract transfer into partnership_transfer.sh
Follows the same extraction pattern as partnership_offboard.sh.
partnership_manager.sh --transfer now exec-forwards to the new script.
Legacy inline block preserved under `if false` for reference.
Fixes in partnership_transfer.sh vs the original inline code:
- WebUI reconfiguration corrected: SSH to new owner (mirror) to set localhost;
reconfigure_local_webuis() on current server to point to new owner IP
- master.conf remote update uses scp push instead of self-SSH with wrong path
- SSH key uses $SSH_KEY directly (was using undefined {HOST*}_SSH_KEY expansion)
This commit is contained in:
@@ -1545,6 +1545,13 @@ fi
|
||||
# ━━━ Transfer ━━━
|
||||
# ==============================================================================================
|
||||
if [[ "$MODE" == "transfer" ]]; then
|
||||
exec bash "$SCRIPT_DIR/partnership_transfer.sh" \
|
||||
${TRANSFER_CONFIRM_INPUT:+--confirm="$TRANSFER_CONFIRM_INPUT"} \
|
||||
"${FILTERED_ARGS[@]}"
|
||||
fi
|
||||
|
||||
# (legacy inline transfer block — kept below for reference, unreachable)
|
||||
if false; then
|
||||
echo ""
|
||||
echo "━━━ $ICON_FALLBACK Transfer Ownership — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
||||
echo ""
|
||||
|
||||
Executable
+312
@@ -0,0 +1,312 @@
|
||||
#!/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
|
||||
#
|
||||
# ==============================================================================================
|
||||
# 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/master.conf" \
|
||||
"root@${MIRROR_IP}:/mnt/user/appdata/unraid_scripts/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
|
||||
Reference in New Issue
Block a user