943 lines
36 KiB
Bash
943 lines
36 KiB
Bash
#!/bin/bash
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ----------------------------- Partnership Manager --------------------------------------------
|
|
# -----------------------------------------------------------------------------------------------
|
|
# Manages the relationship lifecycle between two unRAID servers.
|
|
# HOST1 is always the owner (source of truth). HOST2 is always the mirror.
|
|
# PARTNERSHIP_OWNER_HOST flips to "HOST2" after --transfer.
|
|
#
|
|
# Modes:
|
|
# --onboard ← owner only — set up mirror relationship
|
|
# Reconfigures HOST2 auth WebUIs → HOST1 Tailscale IP
|
|
# HOST2 clicks NPM → gets HOST1's NPM automatically
|
|
#
|
|
# --offboard ← either server — clean separation
|
|
# Either party can initiate
|
|
# HOST2 offboard: writes state, reconfigures own WebUIs → localhost
|
|
# HOST1 sees it on next --check → finalises, runs final sync
|
|
# HOST1 offboard: final sync, reconfigures HOST2 WebUIs, removes Tailscale
|
|
# Both leave with current state, clean exit ✅
|
|
#
|
|
# --transfer ← owner only — flip ownership
|
|
# Requires confirmation string + health strike system
|
|
# Reconfigures both servers, flips PARTNERSHIP_OWNER_HOST in Master.conf
|
|
#
|
|
# --check ← called by critical_sync_maintenance.sh every 15min
|
|
# Reads both state files via SSH
|
|
# Detects offboard requests → finalises from owner side
|
|
# Updates last_seen_remote timestamp
|
|
# Increments offline counter → auto-offboards after threshold
|
|
# Silent when healthy
|
|
#
|
|
# --status ← either server — show current state
|
|
#
|
|
# State files on /boot/config (survives reboots, available before array):
|
|
# /boot/config/partnership_HOST1.db ← HOST1 writes only
|
|
# /boot/config/partnership_HOST2.db ← HOST2 writes only
|
|
# Propagated via SSH — no rsync needed
|
|
#
|
|
# All configuration in Master.conf under PARTNERSHIP section.
|
|
# -----------------------------------------------------------------------------------------------
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
source "$SCRIPT_DIR/../Master.conf"
|
|
source "$SCRIPT_DIR/../common.sh"
|
|
|
|
# Parse flags before parse_args
|
|
MODE=""
|
|
TRANSFER_CONFIRM_INPUT=""
|
|
REMOTE_SEEN=false
|
|
REMOTE_UNSEEN=false
|
|
FILTERED_ARGS=()
|
|
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--onboard) MODE="onboard" ;;
|
|
--offboard) MODE="offboard" ;;
|
|
--transfer) MODE="transfer" ;;
|
|
--check) MODE="check" ;;
|
|
--status) MODE="status" ;;
|
|
--confirm=*) TRANSFER_CONFIRM_INPUT="${arg#--confirm=}" ;;
|
|
--remote-seen) REMOTE_SEEN=true ;;
|
|
--remote-unseen) REMOTE_UNSEEN=true ;;
|
|
*) FILTERED_ARGS+=("$arg") ;;
|
|
esac
|
|
done
|
|
|
|
parse_args "${FILTERED_ARGS[@]}"
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ $ICON_GEAR Setup ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
detect_hosts
|
|
|
|
# Derive owner and mirror from PARTNERSHIP_OWNER_HOST
|
|
if [[ "${PARTNERSHIP_OWNER_HOST:-HOST1}" == "HOST1" ]]; then
|
|
OWNER="$HOST1"
|
|
MIRROR="$HOST2"
|
|
OWNER_SSH_KEY="$HOST1_SSH_KEY"
|
|
MIRROR_SSH_KEY="$HOST2_SSH_KEY"
|
|
else
|
|
OWNER="$HOST2"
|
|
MIRROR="$HOST1"
|
|
OWNER_SSH_KEY="$HOST2_SSH_KEY"
|
|
MIRROR_SSH_KEY="$HOST1_SSH_KEY"
|
|
fi
|
|
|
|
AM_OWNER=false
|
|
AM_MIRROR=false
|
|
[[ "$LOCAL_SERVER_NAME" == "$OWNER" ]] && AM_OWNER=true
|
|
[[ "$LOCAL_SERVER_NAME" == "$MIRROR" ]] && AM_MIRROR=true
|
|
|
|
# State files
|
|
LOCAL_STATE_FILE="/boot/config/partnership_${LOCAL_SERVER_NAME}.db"
|
|
REMOTE_STATE_FILE="/boot/config/partnership_${REMOTE_SERVER_NAME}.db"
|
|
OWNER_STATE_FILE="/boot/config/partnership_${OWNER}.db"
|
|
MIRROR_STATE_FILE="/boot/config/partnership_${MIRROR}.db"
|
|
|
|
# Offline counter file
|
|
OFFLINE_COUNTER="/boot/config/partnership_offline_days.db"
|
|
|
|
if [[ -z "$MODE" ]]; then
|
|
error "No mode specified"
|
|
echo "Usage:"
|
|
echo " partnership_manage.sh --onboard [--dry-run]"
|
|
echo " partnership_manage.sh --offboard [--dry-run]"
|
|
echo " partnership_manage.sh --transfer --confirm=... [--dry-run]"
|
|
echo " partnership_manage.sh --check --remote-seen|--remote-unseen"
|
|
echo " partnership_manage.sh --status"
|
|
exit 1
|
|
fi
|
|
|
|
# Role-based access control
|
|
if [[ "$AM_MIRROR" == true ]]; then
|
|
case "$MODE" in
|
|
onboard|transfer)
|
|
error "Only the owner ($OWNER) can run --$MODE"
|
|
error "Run from $OWNER or use --offboard to separate cleanly"
|
|
exit 1
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
# Acquire lock for all modes except check (check is called frequently, lock would pile up)
|
|
[[ "$MODE" != "check" ]] && acquire_lock "strict"
|
|
|
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# HELPERS
|
|
# -----------------------------------------------------------------------------------------------
|
|
|
|
read_state_file() {
|
|
local file="$1"
|
|
local key="$2"
|
|
grep "^${key}=" "$file" 2>/dev/null | cut -d= -f2
|
|
}
|
|
|
|
write_state_file() {
|
|
local file="$1"
|
|
shift
|
|
# flock to prevent concurrent writes
|
|
(
|
|
flock -x 200
|
|
cat > "$file" << EOF
|
|
state=${1:-UNKNOWN}
|
|
owner=${OWNER}
|
|
mirror=${MIRROR}
|
|
onboarded=${2:-}
|
|
offboarded=${3:-}
|
|
triggered_by=${4:-}
|
|
reason=${5:-}
|
|
last_seen_remote=${6:-}
|
|
updated=$(date '+%Y-%m-%d %H:%M:%S')
|
|
EOF
|
|
) 200>"${file}.lock"
|
|
}
|
|
|
|
push_state_to_remote() {
|
|
local local_file="$1"
|
|
local remote_ip="$2"
|
|
local ssh_key="$3"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — would push state file to remote"
|
|
return 0
|
|
fi
|
|
|
|
scp -i "$ssh_key" -o ConnectTimeout=10 \
|
|
"$local_file" "root@${remote_ip}:${local_file}" 2>/dev/null && \
|
|
success "State file pushed to remote ✅" || \
|
|
warn "Could not push state file to remote — will propagate on next sync"
|
|
}
|
|
|
|
read_remote_state() {
|
|
local remote_ip="$1"
|
|
local ssh_key="$2"
|
|
local remote_file="$3"
|
|
|
|
ssh -i "$ssh_key" -o ConnectTimeout=10 root@"$remote_ip" \
|
|
"cat '$remote_file' 2>/dev/null" 2>/dev/null
|
|
}
|
|
|
|
reconfigure_webui() {
|
|
local container="$1"
|
|
local port="$2"
|
|
local target_ip="$3"
|
|
local ssh_key="$4"
|
|
local remote_ip="$5"
|
|
local label="${6:-remote}"
|
|
|
|
info "Reconfiguring $container WebUI → ${target_ip}:${port} on $label..."
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — would reconfigure $container WebUI to http://${target_ip}:${port}/"
|
|
return 0
|
|
fi
|
|
|
|
local template
|
|
template=$(ssh -i "$ssh_key" -o ConnectTimeout=10 root@"$remote_ip" \
|
|
"grep -rl '<WebUI>' /boot/config/plugins/dockerMan/templates-user/ 2>/dev/null | \
|
|
xargs grep -l '\"$container\"' 2>/dev/null | head -1" 2>/dev/null)
|
|
|
|
if [[ -z "$template" ]]; then
|
|
warn "$container template not found on $label — WebUI needs manual reconfiguration"
|
|
return 1
|
|
fi
|
|
|
|
ssh -i "$ssh_key" -o ConnectTimeout=10 root@"$remote_ip" \
|
|
"sed -i 's|<WebUI>.*</WebUI>|<WebUI>http://${target_ip}:${port}/</WebUI>|g' '$template'" \
|
|
2>/dev/null && \
|
|
success "$container → http://${target_ip}:${port}/ ✅" || {
|
|
error "Failed to reconfigure $container WebUI"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
reconfigure_local_webuis() {
|
|
local target_ip="$1"
|
|
info "Reconfiguring local auth WebUIs → ${target_ip}..."
|
|
|
|
local failures=0
|
|
for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]:-}"; do
|
|
[[ -z "$entry" ]] && continue
|
|
local container="${entry%%|*}"
|
|
local port="${entry##*|}"
|
|
|
|
local template
|
|
template=$(grep -rl '<WebUI>' /boot/config/plugins/dockerMan/templates-user/ 2>/dev/null | \
|
|
xargs grep -l "\"$container\"" 2>/dev/null | head -1)
|
|
|
|
if [[ -z "$template" ]]; then
|
|
warn "$container template not found locally"
|
|
((failures++))
|
|
continue
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — would reconfigure $container → http://${target_ip}:${port}/"
|
|
continue
|
|
fi
|
|
|
|
sed -i "s|<WebUI>.*</WebUI>|<WebUI>http://${target_ip}:${port}/</WebUI>|g" \
|
|
"$template" 2>/dev/null && \
|
|
success "$container → http://${target_ip}:${port}/ ✅" || \
|
|
{ error "Failed to reconfigure $container"; ((failures++)); }
|
|
done
|
|
return $failures
|
|
}
|
|
|
|
get_tailscale_device_id() {
|
|
local hostname="$1"
|
|
curl -sf --max-time 10 \
|
|
-H "Authorization: Bearer $TAILSCALE_API_KEY" \
|
|
"https://api.tailscale.com/api/v2/tailnet/${TAILSCALE_TAILNET}/devices" \
|
|
2>/dev/null | \
|
|
grep -o "\"id\":\"[^\"]*\"[^}]*\"hostname\":\"${hostname}\"" | \
|
|
grep -o '"id":"[^"]*"' | \
|
|
grep -o '[^"]*"$' | tr -d '"'
|
|
}
|
|
|
|
remove_tailscale_device() {
|
|
local hostname="$1"
|
|
|
|
if [[ -z "${TAILSCALE_API_KEY:-}" ]] || [[ -z "${TAILSCALE_TAILNET:-}" ]]; then
|
|
warn "TAILSCALE_API_KEY or TAILSCALE_TAILNET not configured — skipping Tailscale removal"
|
|
return 1
|
|
fi
|
|
|
|
info "Looking up Tailscale device ID for $hostname..."
|
|
local device_id
|
|
device_id=$(get_tailscale_device_id "$hostname")
|
|
|
|
if [[ -z "$device_id" ]]; then
|
|
warn "Device $hostname not found in Tailscale — may already be removed"
|
|
return 0
|
|
fi
|
|
|
|
info "Removing $hostname (device $device_id) from Tailscale..."
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — would remove $hostname from Tailscale tailnet"
|
|
return 0
|
|
fi
|
|
|
|
curl -sf --max-time 10 -X DELETE \
|
|
-H "Authorization: Bearer $TAILSCALE_API_KEY" \
|
|
"https://api.tailscale.com/api/v2/devices/${device_id}" 2>/dev/null && \
|
|
success "$hostname removed from Tailscale ✅" || \
|
|
error "Failed to remove $hostname from Tailscale — remove manually"
|
|
}
|
|
|
|
check_both_healthy() {
|
|
mountpoint -q /mnt/user 2>/dev/null || { error "Local array not healthy"; return 1; }
|
|
|
|
local mirror_ip
|
|
mirror_ip=$(tailscale ip -4 "$MIRROR" 2>/dev/null)
|
|
[[ -z "$mirror_ip" ]] && { error "Cannot resolve $MIRROR Tailscale IP"; return 1; }
|
|
|
|
ssh -i "$MIRROR_SSH_KEY" -o ConnectTimeout=10 root@"$mirror_ip" \
|
|
"mountpoint -q /mnt/user && timeout 10 docker ps" >/dev/null 2>&1 || {
|
|
error "Mirror not healthy"
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
do_final_sync() {
|
|
info "Running final Critical-Data sync..."
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
bash "$SCRIPT_DIR/../Rsync/rsync.sh" \
|
|
"/mnt/user/appdata-Failover/Critical-Data" --log
|
|
bash "$SCRIPT_DIR/../Rsync/rsync.sh" \
|
|
"/mnt/user/Media_Server/Emby" --profile=emby-failover --log
|
|
else
|
|
warn "DRY RUN — would run final Critical-Data and emby-failover sync"
|
|
fi
|
|
success "Final sync complete — mirror has current state ✅"
|
|
}
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ STATUS ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
if [[ "$MODE" == "status" ]]; then
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY PARTNERSHIP STATUS ━━━━━"
|
|
|
|
OWNER_IP=$(tailscale ip -4 "$OWNER" 2>/dev/null || echo "unreachable")
|
|
MIRROR_IP=$(tailscale ip -4 "$MIRROR" 2>/dev/null || echo "unreachable")
|
|
|
|
echo " Owner: $OWNER ($OWNER_IP)"
|
|
echo " Mirror: $MIRROR ($MIRROR_IP)"
|
|
echo " Enabled: ${PARTNERSHIP_ENABLED:-false}"
|
|
echo " Running as: $LOCAL_SERVER_NAME ($( [[ "$AM_OWNER" == true ]] && echo "owner" || echo "mirror"))"
|
|
echo ""
|
|
|
|
# Local state
|
|
if [[ -f "$LOCAL_STATE_FILE" ]]; then
|
|
LOCAL_STATE=$(read_state_file "$LOCAL_STATE_FILE" "state")
|
|
LOCAL_ONBOARDED=$(read_state_file "$LOCAL_STATE_FILE" "onboarded")
|
|
LOCAL_OFFBOARDED=$(read_state_file "$LOCAL_STATE_FILE" "offboarded")
|
|
LOCAL_LAST_SEEN=$(read_state_file "$LOCAL_STATE_FILE" "last_seen_remote")
|
|
echo " Local state: $LOCAL_STATE"
|
|
[[ -n "$LOCAL_ONBOARDED" ]] && echo " Onboarded: $LOCAL_ONBOARDED"
|
|
[[ -n "$LOCAL_OFFBOARDED" ]] && echo " Offboarded: $LOCAL_OFFBOARDED"
|
|
[[ -n "$LOCAL_LAST_SEEN" ]] && echo " Remote last seen: $LOCAL_LAST_SEEN"
|
|
else
|
|
echo " Local state: no state file found"
|
|
fi
|
|
|
|
echo ""
|
|
|
|
# Remote state via SSH
|
|
REMOTE_IP=$(tailscale ip -4 "$REMOTE_SERVER_NAME" 2>/dev/null)
|
|
if [[ -n "$REMOTE_IP" ]]; then
|
|
REMOTE_CONTENT=$(read_remote_state "$REMOTE_IP" "$SSH_KEY" "$REMOTE_STATE_FILE")
|
|
if [[ -n "$REMOTE_CONTENT" ]]; then
|
|
REMOTE_STATE=$(echo "$REMOTE_CONTENT" | grep "^state=" | cut -d= -f2)
|
|
REMOTE_ONBOARDED=$(echo "$REMOTE_CONTENT" | grep "^onboarded=" | cut -d= -f2)
|
|
REMOTE_OFFBOARDED=$(echo "$REMOTE_CONTENT" | grep "^offboarded=" | cut -d= -f2)
|
|
echo " Remote state: $REMOTE_STATE"
|
|
[[ -n "$REMOTE_ONBOARDED" ]] && echo " Onboarded: $REMOTE_ONBOARDED"
|
|
[[ -n "$REMOTE_OFFBOARDED" ]] && echo " Offboarded: $REMOTE_OFFBOARDED"
|
|
|
|
# Check agreement
|
|
if [[ "$LOCAL_STATE" == "$REMOTE_STATE" ]]; then
|
|
echo ""
|
|
echo " ✅ Both servers agree: $LOCAL_STATE"
|
|
else
|
|
echo ""
|
|
echo " ⚠️ State mismatch — local: $LOCAL_STATE remote: $REMOTE_STATE"
|
|
echo " Run --onboard or --offboard to resolve"
|
|
fi
|
|
else
|
|
echo " Remote state: state file not found on $REMOTE_SERVER_NAME"
|
|
fi
|
|
else
|
|
echo " Remote state: $REMOTE_SERVER_NAME unreachable"
|
|
fi
|
|
|
|
echo ""
|
|
echo " Auth WebUIs configured:"
|
|
for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]:-}"; do
|
|
[[ -z "$entry" ]] && continue
|
|
echo " ${entry%%|*} → port ${entry##*|}"
|
|
done
|
|
|
|
# Offline counter
|
|
if [[ -f "$OFFLINE_COUNTER" ]]; then
|
|
OFFLINE_DAYS=$(cat "$OFFLINE_COUNTER" 2>/dev/null || echo 0)
|
|
[[ "$OFFLINE_DAYS" -gt 0 ]] && \
|
|
echo "" && \
|
|
echo " ⚠️ Remote offline counter: ${OFFLINE_DAYS}/${PARTNERSHIP_OFFLINE_THRESHOLD} days"
|
|
fi
|
|
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ CHECK (called every 15min by critical_sync_maintenance.sh) ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
if [[ "$MODE" == "check" ]]; then
|
|
|
|
# Update last_seen_remote and offline counter based on rsync outcome
|
|
if [[ "$REMOTE_SEEN" == true ]]; then
|
|
OFFLINE_COUNT=0
|
|
echo "0" > "$OFFLINE_COUNTER"
|
|
# Update last_seen_remote in local state file
|
|
if [[ -f "$LOCAL_STATE_FILE" ]]; then
|
|
sed -i "s|^last_seen_remote=.*|last_seen_remote=$(date '+%Y-%m-%d %H:%M:%S')|" \
|
|
"$LOCAL_STATE_FILE" 2>/dev/null
|
|
fi
|
|
elif [[ "$REMOTE_UNSEEN" == true ]]; then
|
|
OFFLINE_COUNT=$(cat "$OFFLINE_COUNTER" 2>/dev/null || echo 0)
|
|
OFFLINE_COUNT=$(( OFFLINE_COUNT + 1 ))
|
|
echo "$OFFLINE_COUNT" > "$OFFLINE_COUNTER"
|
|
|
|
# Check auto-offboard threshold
|
|
# Convert 15min intervals to days: threshold_days * 96 intervals/day
|
|
THRESHOLD_INTERVALS=$(( ${PARTNERSHIP_OFFLINE_THRESHOLD:-30} * 96 ))
|
|
if [[ "$OFFLINE_COUNT" -ge "$THRESHOLD_INTERVALS" ]]; then
|
|
warn "Remote offline for ${PARTNERSHIP_OFFLINE_THRESHOLD} days — triggering auto-offboard"
|
|
notify "Partnership auto-offboard on $(hostname) — $REMOTE_SERVER_NAME offline for ${PARTNERSHIP_OFFLINE_THRESHOLD} days. Both servers will offboard independently." \
|
|
"Partnership" "warning"
|
|
bash "$0" --offboard --reason="auto-offboard-timeout"
|
|
exit 0
|
|
fi
|
|
|
|
log "Partnership check — remote unseen (count: $OFFLINE_COUNT/$THRESHOLD_INTERVALS)"
|
|
fi
|
|
|
|
# Read remote state file
|
|
REMOTE_IP=$(tailscale ip -4 "$REMOTE_SERVER_NAME" 2>/dev/null)
|
|
if [[ -z "$REMOTE_IP" ]]; then
|
|
log "Partnership check — remote unreachable, skipping state check"
|
|
exit 0
|
|
fi
|
|
|
|
REMOTE_CONTENT=$(read_remote_state "$REMOTE_IP" "$SSH_KEY" "$REMOTE_STATE_FILE")
|
|
[[ -z "$REMOTE_CONTENT" ]] && log "Partnership check — remote state file not found" && exit 0
|
|
|
|
REMOTE_STATE=$(echo "$REMOTE_CONTENT" | grep "^state=" | cut -d= -f2)
|
|
LOCAL_STATE=$(read_state_file "$LOCAL_STATE_FILE" "state" 2>/dev/null || echo "UNKNOWN")
|
|
|
|
# Both agree — healthy
|
|
if [[ "$LOCAL_STATE" == "$REMOTE_STATE" ]] && [[ "$LOCAL_STATE" == "ACTIVE" ]]; then
|
|
log "Partnership check — ACTIVE, both servers agree ✅"
|
|
exit 0
|
|
fi
|
|
|
|
# Remote requested offboard
|
|
if [[ "$REMOTE_STATE" == "INACTIVE" ]] && [[ "$LOCAL_STATE" == "ACTIVE" ]]; then
|
|
info "Partnership check — $REMOTE_SERVER_NAME requested offboard"
|
|
|
|
if [[ "$AM_OWNER" == true ]]; then
|
|
info "Owner finalising offboard request from mirror..."
|
|
# Final sync — mirror leaves with current state
|
|
do_final_sync
|
|
# Update local state
|
|
NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
|
write_state_file "$LOCAL_STATE_FILE" \
|
|
"INACTIVE" "" "$NOW" "$REMOTE_SERVER_NAME" "mirror-requested"
|
|
# Disable critical rsync
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
sed -i 's/CRITICAL_RSYNC_ENABLED=true/CRITICAL_RSYNC_ENABLED=false/' \
|
|
"$SCRIPT_DIR/../Master.conf" 2>/dev/null
|
|
fi
|
|
# Tailscale removal after grace period
|
|
if [[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]]; then
|
|
local grace_seconds=$(( ${PARTNERSHIP_GRACE_HOURS:-3} * 3600 ))
|
|
info "Waiting ${PARTNERSHIP_GRACE_HOURS:-3}hr grace period for mirror to see notification..."
|
|
[[ "$DRY_RUN" == false ]] && sleep "$grace_seconds"
|
|
remove_tailscale_device "$MIRROR"
|
|
fi
|
|
notify "Partnership offboard finalised on $(hostname) — $MIRROR requested separation" \
|
|
"Partnership" "normal"
|
|
else
|
|
# Mirror sees owner is INACTIVE — clean up own side
|
|
info "Owner has offboarded — cleaning up mirror side..."
|
|
reconfigure_local_webuis "localhost"
|
|
NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
|
write_state_file "$LOCAL_STATE_FILE" \
|
|
"INACTIVE" "" "$NOW" "$OWNER" "owner-offboarded"
|
|
notify "Partnership ended on $(hostname) — $OWNER has offboarded. Auth WebUIs reconfigured to localhost." \
|
|
"Partnership" "normal"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# Both inactive — nothing to do
|
|
if [[ "$LOCAL_STATE" == "INACTIVE" ]] && [[ "$REMOTE_STATE" == "INACTIVE" ]]; then
|
|
log "Partnership check — INACTIVE on both servers"
|
|
exit 0
|
|
fi
|
|
|
|
log "Partnership check — local: $LOCAL_STATE remote: $REMOTE_STATE"
|
|
exit 0
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ ONBOARD ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
if [[ "$MODE" == "onboard" ]]; then
|
|
echo ""
|
|
echo "━━━ $ICON_FAILOVER Onboard — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
|
|
|
# Check already onboarded
|
|
if [[ -f "$LOCAL_STATE_FILE" ]]; then
|
|
CURRENT_STATE=$(read_state_file "$LOCAL_STATE_FILE" "state")
|
|
if [[ "$CURRENT_STATE" == "ACTIVE" ]]; then
|
|
info "Partnership already ACTIVE — no action needed"
|
|
info "Use --status for detail or --offboard to separate"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
info "Owner: $OWNER"
|
|
info "Mirror: $MIRROR"
|
|
|
|
# Pre-flight
|
|
echo ""
|
|
echo "━━━ $ICON_SHIELD Pre-flight ━━━"
|
|
|
|
resolve_remote_ip
|
|
check_connectivity
|
|
check_remote_array || exit 1
|
|
check_remote_docker || exit 1
|
|
|
|
OWNER_IP=$(tailscale ip -4 "$OWNER" 2>/dev/null)
|
|
MIRROR_IP=$(tailscale ip -4 "$MIRROR" 2>/dev/null)
|
|
|
|
[[ -z "$OWNER_IP" ]] && error "Cannot resolve $OWNER Tailscale IP" && exit 1
|
|
[[ -z "$MIRROR_IP" ]] && error "Cannot resolve $MIRROR Tailscale IP" && exit 1
|
|
|
|
success "Owner IP: $OWNER_IP"
|
|
success "Mirror IP: $MIRROR_IP"
|
|
|
|
# Reconfigure mirror WebUIs → owner IP
|
|
echo ""
|
|
echo "━━━ $ICON_CONTAINERS Reconfigure Mirror WebUIs → $OWNER_IP ━━━"
|
|
|
|
WEBUI_FAILURES=0
|
|
for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]:-}"; do
|
|
[[ -z "$entry" ]] && continue
|
|
container="${entry%%|*}"
|
|
port="${entry##*|}"
|
|
reconfigure_webui "$container" "$port" "$OWNER_IP" \
|
|
"$MIRROR_SSH_KEY" "$MIRROR_IP" "$MIRROR" || ((WEBUI_FAILURES++))
|
|
done
|
|
|
|
# Verify WebUI connectivity
|
|
if [[ "${PARTNERSHIP_ONBOARD_VERIFY:-true}" == true ]]; then
|
|
echo ""
|
|
echo "━━━ $ICON_HEALTH Verify WebUI Connectivity ━━━"
|
|
for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]:-}"; do
|
|
[[ -z "$entry" ]] && continue
|
|
container="${entry%%|*}"
|
|
port="${entry##*|}"
|
|
if curl -sf --max-time 10 "http://${OWNER_IP}:${port}/" >/dev/null 2>&1; then
|
|
success "$container reachable at http://${OWNER_IP}:${port}/ ✅"
|
|
else
|
|
warn "$container not reachable at http://${OWNER_IP}:${port}/ — may not be running"
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Write state files
|
|
echo ""
|
|
echo "━━━ $ICON_GEAR Write State ━━━"
|
|
NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
|
|
|
write_state_file "$LOCAL_STATE_FILE" "ACTIVE" "$NOW" "" "$LOCAL_SERVER_NAME" "onboard"
|
|
success "Local state: ACTIVE ✅"
|
|
|
|
# Push to remote
|
|
push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
|
|
|
# Reset offline counter
|
|
echo "0" > "$OFFLINE_COUNTER"
|
|
|
|
# Summary
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY ONBOARD SUMMARY ━━━━━"
|
|
echo " Owner: $OWNER ($OWNER_IP)"
|
|
echo " Mirror: $MIRROR ($MIRROR_IP)"
|
|
echo " WebUI failures: $WEBUI_FAILURES"
|
|
echo " Sync interval: ${PARTNERSHIP_SYNC_INTERVAL}min"
|
|
echo ""
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo "$ICON_WARN Status: DRY RUN — no changes made"
|
|
elif [[ "$WEBUI_FAILURES" -eq 0 ]]; then
|
|
echo "$ICON_DONE Status: $ICON_SUCCESS DONE"
|
|
[[ "${PARTNERSHIP_ONBOARD_NOTIFY:-true}" == true ]] && \
|
|
notify "Partnership onboard complete — $MIRROR is now mirroring $OWNER via Tailscale" \
|
|
"Partnership" "normal"
|
|
else
|
|
echo "$ICON_WARN Status: DONE with $WEBUI_FAILURES WebUI warning(s)"
|
|
notify "Partnership onboard complete with $WEBUI_FAILURES WebUI warning(s) on $(hostname)" \
|
|
"Partnership" "warning"
|
|
fi
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ OFFBOARD ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
if [[ "$MODE" == "offboard" ]]; then
|
|
echo ""
|
|
echo "━━━ $ICON_FAILOVER Offboard — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
|
|
|
# Check already offboarded
|
|
if [[ -f "$LOCAL_STATE_FILE" ]]; then
|
|
CURRENT_STATE=$(read_state_file "$LOCAL_STATE_FILE" "state")
|
|
if [[ "$CURRENT_STATE" == "INACTIVE" ]]; then
|
|
info "Partnership already INACTIVE"
|
|
info "Use --status to verify both servers agree"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
REASON="${REASON:-manual}"
|
|
|
|
# ── MIRROR-initiated offboard ────────────────────────────────────────────────────────────
|
|
if [[ "$AM_MIRROR" == true ]]; then
|
|
warn "$MIRROR is initiating offboard"
|
|
warn "This will reconfigure your local auth WebUIs → localhost"
|
|
warn "HOST1 will finalise the offboard on its next --check cycle"
|
|
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
echo ""
|
|
echo "You have 10 seconds to cancel (Ctrl+C)..."
|
|
sleep 10
|
|
fi
|
|
|
|
# Reconfigure own WebUIs → localhost
|
|
echo ""
|
|
echo "━━━ $ICON_CONTAINERS Reconfigure Local WebUIs → localhost ━━━"
|
|
reconfigure_local_webuis "localhost"
|
|
|
|
# Write own state file
|
|
NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
|
write_state_file "$LOCAL_STATE_FILE" \
|
|
"INACTIVE" "" "$NOW" "$LOCAL_SERVER_NAME" "$REASON"
|
|
success "Local state: INACTIVE ✅"
|
|
|
|
# Try to push state to owner immediately
|
|
OWNER_IP=$(tailscale ip -4 "$OWNER" 2>/dev/null)
|
|
if [[ -n "$OWNER_IP" ]]; then
|
|
push_state_to_remote "$LOCAL_STATE_FILE" "$OWNER_IP" "$MIRROR_SSH_KEY"
|
|
notify "Partnership offboard requested by $MIRROR — owner will finalise on next check cycle" \
|
|
"Partnership" "normal"
|
|
else
|
|
warn "$OWNER unreachable — state written locally, owner will see it when reachable"
|
|
fi
|
|
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY OFFBOARD SUMMARY ━━━━━"
|
|
echo " Your WebUIs: reconfigured → localhost ✅"
|
|
echo " State: INACTIVE written ✅"
|
|
echo " Owner: will finalise + run final sync on next --check ✅"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|
|
fi
|
|
|
|
# ── OWNER-initiated offboard ─────────────────────────────────────────────────────────────
|
|
warn "Offboarding $MIRROR from partnership"
|
|
warn "Final sync will run — mirror leaves with current state"
|
|
warn "$MIRROR auth WebUIs will be reconfigured → localhost"
|
|
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
echo ""
|
|
echo "You have 10 seconds to cancel (Ctrl+C)..."
|
|
sleep 10
|
|
echo "Proceeding..."
|
|
fi
|
|
|
|
resolve_remote_ip
|
|
|
|
# Stop any running rsync first
|
|
echo ""
|
|
echo "━━━ $ICON_STOP Stop Running Rsync ━━━"
|
|
bash "$SCRIPT_DIR/../unRAID_Essentials/rsync_stop.sh" --rsync-only 2>/dev/null || true
|
|
|
|
# Final sync
|
|
echo ""
|
|
echo "━━━ $ICON_SYNC Final Sync ━━━"
|
|
do_final_sync
|
|
|
|
MIRROR_IP=$(tailscale ip -4 "$MIRROR" 2>/dev/null)
|
|
MIRROR_REACHABLE=false
|
|
[[ -n "$MIRROR_IP" ]] && MIRROR_REACHABLE=true
|
|
|
|
# Reconfigure mirror WebUIs → localhost
|
|
echo ""
|
|
echo "━━━ $ICON_CONTAINERS Reconfigure Mirror WebUIs → localhost ━━━"
|
|
|
|
WEBUI_FAILURES=0
|
|
if [[ "$MIRROR_REACHABLE" == true ]]; then
|
|
for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]:-}"; do
|
|
[[ -z "$entry" ]] && continue
|
|
container="${entry%%|*}"
|
|
port="${entry##*|}"
|
|
reconfigure_webui "$container" "$port" "localhost" \
|
|
"$MIRROR_SSH_KEY" "$MIRROR_IP" "$MIRROR" || ((WEBUI_FAILURES++))
|
|
done
|
|
else
|
|
warn "$MIRROR unreachable — WebUI reconfiguration skipped"
|
|
warn "$MIRROR will reconfigure its own WebUIs when it sees INACTIVE state on next --check"
|
|
((WEBUI_FAILURES++))
|
|
fi
|
|
|
|
# Disable critical rsync
|
|
echo ""
|
|
echo "━━━ $ICON_GEAR Disable Critical Sync ━━━"
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
sed -i 's/CRITICAL_RSYNC_ENABLED=true/CRITICAL_RSYNC_ENABLED=false/' \
|
|
"$SCRIPT_DIR/../Master.conf" 2>/dev/null
|
|
success "CRITICAL_RSYNC_ENABLED=false ✅"
|
|
else
|
|
warn "DRY RUN — would set CRITICAL_RSYNC_ENABLED=false"
|
|
fi
|
|
|
|
# Write state files
|
|
echo ""
|
|
echo "━━━ $ICON_GEAR Write State ━━━"
|
|
NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
|
|
|
write_state_file "$LOCAL_STATE_FILE" \
|
|
"INACTIVE" "" "$NOW" "$LOCAL_SERVER_NAME" "$REASON"
|
|
success "Local state: INACTIVE ✅"
|
|
|
|
# Push to mirror
|
|
if [[ "$MIRROR_REACHABLE" == true ]]; then
|
|
push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
|
fi
|
|
|
|
# Tailscale removal
|
|
if [[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]]; then
|
|
echo ""
|
|
echo "━━━ $ICON_NET Tailscale Separation ━━━"
|
|
if [[ "$MIRROR_REACHABLE" == true ]]; then
|
|
local grace_seconds=$(( ${PARTNERSHIP_GRACE_HOURS:-3} * 3600 ))
|
|
info "Waiting ${PARTNERSHIP_GRACE_HOURS:-3}hr grace period for mirror to see state..."
|
|
[[ "$DRY_RUN" == false ]] && sleep "$grace_seconds"
|
|
fi
|
|
remove_tailscale_device "$MIRROR"
|
|
fi
|
|
|
|
# Backup notification
|
|
if [[ ${#PARTNERSHIP_MIRROR_BACKUPS[@]} -gt 0 ]]; then
|
|
echo ""
|
|
echo "━━━ $ICON_DISK Backup Handover ━━━"
|
|
info "Backups available for $MIRROR:"
|
|
for path in "${PARTNERSHIP_MIRROR_BACKUPS[@]}"; do
|
|
[[ -z "$path" ]] && continue
|
|
echo " $path"
|
|
done
|
|
info "Available for ${PARTNERSHIP_GRACE_HOURS:-3} hours — Tailscale access removed after this window"
|
|
notify "$MIRROR offboard complete — backups available for ${PARTNERSHIP_GRACE_HOURS:-3}hr. Tailscale access expires then. Certs valid ~60-90 days. Run tailscale up to rejoin." \
|
|
"Partnership" "warning"
|
|
fi
|
|
|
|
# Summary
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY OFFBOARD SUMMARY ━━━━━"
|
|
echo " Owner: $OWNER"
|
|
echo " Mirror: $MIRROR"
|
|
echo " Final sync: complete ✅"
|
|
echo " WebUI failures: $WEBUI_FAILURES"
|
|
echo " Critical rsync: disabled ✅"
|
|
echo " State: INACTIVE ✅"
|
|
[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \
|
|
echo " Tailscale: $MIRROR removed ✅"
|
|
echo ""
|
|
echo " $MIRROR leaves with:"
|
|
echo " ✓ Current auth config (final sync)"
|
|
echo " ✓ His own DDNS and updater"
|
|
echo " ✓ His own Git mirror"
|
|
echo " ✓ Auth WebUIs → localhost"
|
|
echo " ✓ ${PARTNERSHIP_GRACE_HOURS:-3}hr to collect backups before Tailscale removed"
|
|
echo " ✓ Certs valid ~60-90 more days"
|
|
echo " ✓ Full ecosystem — just stop the sync"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo ""
|
|
echo "$ICON_WARN Status: DRY RUN — no changes made"
|
|
else
|
|
echo ""
|
|
echo "$ICON_DONE Status: $ICON_SUCCESS DONE — clean separation complete"
|
|
fi
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------------------------
|
|
# ━━━ TRANSFER ━━━
|
|
# -----------------------------------------------------------------------------------------------
|
|
if [[ "$MODE" == "transfer" ]]; then
|
|
echo ""
|
|
echo "━━━ $ICON_FAILOVER Transfer Ownership — $(date '+%Y-%m-%d %H:%M:%S') ━━━"
|
|
echo ""
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo "⚠️ WARNING — OWNERSHIP TRANSFER"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
echo " Current owner: $OWNER"
|
|
echo " Current mirror: $MIRROR"
|
|
echo ""
|
|
echo " After transfer:"
|
|
echo " New owner: $MIRROR"
|
|
echo " New mirror: $OWNER"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
# Confirmation string
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
if [[ -z "$TRANSFER_CONFIRM_INPUT" ]]; then
|
|
echo ""
|
|
echo "To proceed type 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
|
|
success "Confirmation accepted"
|
|
else
|
|
warn "DRY RUN — confirmation check skipped"
|
|
fi
|
|
|
|
# Health strike system
|
|
echo ""
|
|
echo "━━━ $ICON_SHIELD Health Verification ━━━"
|
|
info "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++))
|
|
success "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
|
|
success "Both servers healthy — proceeding"
|
|
|
|
NEW_OWNER="$MIRROR"
|
|
NEW_MIRROR="$OWNER"
|
|
NEW_OWNER_IP=$(tailscale ip -4 "$NEW_OWNER" 2>/dev/null)
|
|
NEW_MIRROR_IP=$(tailscale ip -4 "$NEW_MIRROR" 2>/dev/null)
|
|
|
|
[[ -z "$NEW_OWNER_IP" ]] && error "Cannot resolve new owner Tailscale IP" && exit 1
|
|
|
|
# Final sync in current direction
|
|
echo ""
|
|
echo "━━━ $ICON_SYNC Pre-transfer Sync ━━━"
|
|
do_final_sync
|
|
|
|
# Reconfigure new mirror WebUIs → new owner
|
|
echo ""
|
|
echo "━━━ $ICON_CONTAINERS Reconfigure WebUIs ━━━"
|
|
info "New mirror ($NEW_MIRROR) WebUIs → new owner ($NEW_OWNER_IP)"
|
|
|
|
NEW_MIRROR_SSH="${HOST1_SSH_KEY}"
|
|
[[ "$NEW_MIRROR" == "$HOST2" ]] && NEW_MIRROR_SSH="${HOST2_SSH_KEY}"
|
|
NEW_OWNER_SSH="${HOST1_SSH_KEY}"
|
|
[[ "$NEW_OWNER" == "$HOST2" ]] && NEW_OWNER_SSH="${HOST2_SSH_KEY}"
|
|
|
|
WEBUI_FAILURES=0
|
|
for entry in "${PARTNERSHIP_AUTH_WEBUIS[@]:-}"; do
|
|
[[ -z "$entry" ]] && continue
|
|
container="${entry%%|*}"
|
|
port="${entry##*|}"
|
|
reconfigure_webui "$container" "$port" "$NEW_OWNER_IP" \
|
|
"$NEW_MIRROR_SSH" "$NEW_MIRROR_IP" "$NEW_MIRROR" || ((WEBUI_FAILURES++))
|
|
done
|
|
|
|
# New owner WebUIs → localhost (now manages directly)
|
|
info "New owner ($NEW_OWNER) WebUIs → localhost"
|
|
reconfigure_local_webuis "localhost"
|
|
|
|
# Flip PARTNERSHIP_OWNER_HOST in Master.conf on both servers
|
|
echo ""
|
|
echo "━━━ $ICON_GEAR Update Ownership ━━━"
|
|
NEW_OWNER_HOST="HOST1"
|
|
[[ "$NEW_OWNER" == "$HOST2" ]] && NEW_OWNER_HOST="HOST2"
|
|
|
|
if [[ "$DRY_RUN" == false ]]; then
|
|
sed -i "s/PARTNERSHIP_OWNER_HOST=.*/PARTNERSHIP_OWNER_HOST=\"$NEW_OWNER_HOST\"/" \
|
|
"$SCRIPT_DIR/../Master.conf" 2>/dev/null
|
|
success "Local Master.conf updated: PARTNERSHIP_OWNER_HOST=$NEW_OWNER_HOST ✅"
|
|
|
|
# Update remote
|
|
ssh -i "$NEW_MIRROR_SSH" -o ConnectTimeout=10 root@"$NEW_MIRROR_IP" \
|
|
"sed -i 's/PARTNERSHIP_OWNER_HOST=.*/PARTNERSHIP_OWNER_HOST=\"$NEW_OWNER_HOST\"/' \
|
|
'$SCRIPT_DIR/../Master.conf'" 2>/dev/null && \
|
|
success "Remote Master.conf updated ✅" || \
|
|
error "Failed to update remote Master.conf — update manually"
|
|
else
|
|
warn "DRY RUN — would set PARTNERSHIP_OWNER_HOST=$NEW_OWNER_HOST on both servers"
|
|
fi
|
|
|
|
# Write state files
|
|
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" "$NEW_MIRROR_IP" "$NEW_MIRROR_SSH"
|
|
|
|
# Summary
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY TRANSFER SUMMARY ━━━━━"
|
|
echo " New owner: $NEW_OWNER ($NEW_OWNER_IP)"
|
|
echo " New mirror: $NEW_MIRROR ($NEW_MIRROR_IP)"
|
|
echo " WebUI failures: $WEBUI_FAILURES"
|
|
echo " Sync direction: $NEW_OWNER → $NEW_MIRROR"
|
|
echo " Master.conf: PARTNERSHIP_OWNER_HOST=$NEW_OWNER_HOST"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo "$ICON_WARN Status: DRY RUN — no changes made"
|
|
else
|
|
echo "$ICON_DONE Status: $ICON_SUCCESS DONE — ownership transferred"
|
|
notify "Partnership ownership transferred — new owner: $NEW_OWNER new mirror: $NEW_MIRROR" \
|
|
"Partnership" "normal"
|
|
fi
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
|
exit 0
|
|
fi |