feat: Tailscale resolution hardening, partnership offboard completion, Emby provisioning
common.sh:
- Add resolve_tailscale_ip() helper — tries `tailscale ip -4` first, falls back to
parsing `tailscale status` output; handles hosts where MagicDNS short-name resolution
is not active
- Add PARTNERSHIP_OWN_CONTAINERS alias in detect_hosts()
- Add aliasing for 4 Emby provisioning vars (PARTNERSHIP_PROVISION_EMBY_ADMIN,
PARTNERSHIP_EMBY_ADMIN_USER, PARTNERSHIP_EMBY_ADMIN_PASS, PARTNERSHIP_EMBY_PORT)
Partnership/partnership_manager.sh:
- Replace 9 bare `tailscale ip -4` calls with resolve_tailscale_ip()
- Add read_remote_conf_var() and read_remote_conf_array() — SSH to mirror, source its
own load_config.sh + detect_hosts(), return aliased variable; solves sparse-checkout
problem where HOST1 cannot read master_host2.conf directly
- Add derive_short_name() — strips unraid- prefix, capitalises first char
- Add cleanup_partner_containers() — removes partner containers via FolderView3 folder
if enabled, else falls back to FALLBACK_*_COVERS_*_TIER* arrays
- Add cleanup_owner_containers_on_mirror() — SSH to mirror, stops and removes containers
matching *-${OWNER_SHORT} naming convention
- Add start_own_stack() and start_mirror_own_stack() — restart own containers locally
or on mirror via SSH using PARTNERSHIP_OWN_CONTAINERS
- Add provision_emby_admin() — reads mirror credentials via read_remote_conf_var, checks
for username collision, creates user + sets password + grants admin policy via Emby API
- Add revoke_emby_admin() — looks up mirror username on local Emby, deletes via REST API
- Wire offboard paths (both mirror-initiated and owner-initiated) to call container
cleanup and stack restart; update --check finalisation paths accordingly
- Fix write_state_file in --onboard not gated on DRY_RUN (was writing ACTIVE state on
dry runs)
master_host1.conf:
- Add HOST1_PARTNERSHIP_OWN_CONTAINERS array
- Add partnership Emby provisioning config (toggle + port + per-host credentials)
master_host2.conf:
- Add HOST2_PARTNERSHIP_OWN_CONTAINERS array
- Add HOST2_PARTNERSHIP_EMBY_ADMIN_USER and HOST2_PARTNERSHIP_EMBY_ADMIN_PASS
Tailscale fix applied to:
- Initial_run/ssh_setup.sh (2 callsites)
- unRAID_Essentials/rsync_stop.sh (1 callsite)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
6948755c86
commit
0ae31b5fa6
@@ -627,11 +627,317 @@ gather_partner_fallback_containers() {
|
||||
done
|
||||
}
|
||||
|
||||
# Read a scalar var from the mirror's own config via SSH.
|
||||
# Sources load_config.sh + detect_hosts() on the remote so HOST* aliasing works.
|
||||
read_remote_conf_var() {
|
||||
local mirror_ip="$1" var_name="$2"
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$mirror_ip" \
|
||||
"source '$SCRIPT_DIR/../load_config.sh' 2>/dev/null
|
||||
detect_hosts 2>/dev/null
|
||||
printf '%s' \"\${${var_name}:-}\"" 2>/dev/null
|
||||
}
|
||||
|
||||
# Read an array var from the mirror's own config via SSH — one element per line.
|
||||
read_remote_conf_array() {
|
||||
local mirror_ip="$1" var_name="$2"
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$mirror_ip" \
|
||||
"source '$SCRIPT_DIR/../load_config.sh' 2>/dev/null
|
||||
detect_hosts 2>/dev/null
|
||||
printf '%s\n' \"\${${var_name}[@]:-}\"" 2>/dev/null
|
||||
}
|
||||
|
||||
# Returns just the short name portion: "unRAID-Gmer4Lfe" → "Gmer4Lfe"
|
||||
derive_short_name() {
|
||||
local hostname="$1"
|
||||
local short="${hostname,,}"
|
||||
[[ "$short" == unraid-* ]] && short="${short:7}"
|
||||
echo "${short^}"
|
||||
}
|
||||
|
||||
# Start this server's own parked containers after partnership ends.
|
||||
start_own_stack() {
|
||||
echo ""
|
||||
echo "━━━ $ICON_START Restart Own Stack ━━━"
|
||||
if [[ ${#PARTNERSHIP_OWN_CONTAINERS[@]} -eq 0 ]]; then
|
||||
log "No PARTNERSHIP_OWN_CONTAINERS configured — skipping"
|
||||
return 0
|
||||
fi
|
||||
for container in "${PARTNERSHIP_OWN_CONTAINERS[@]}"; do
|
||||
[[ -z "$container" ]] && continue
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would start: $container"
|
||||
continue
|
||||
fi
|
||||
if timeout "${DOCKER_TIMEOUT:-30}" docker start "$container" >/dev/null 2>&1; then
|
||||
log "$container started ✅"
|
||||
else
|
||||
warn "$container failed to start — check manually"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# Remove partnership containers on this server.
|
||||
# Uses FolderView3 folder if enabled (precise list), else falls back to FALLBACK_*_COVERS_* config.
|
||||
cleanup_partner_containers() {
|
||||
local folder_name="$1"
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
||||
folderview3_remove_partner_folder "$folder_name"
|
||||
else
|
||||
declare -a containers=()
|
||||
gather_partner_fallback_containers containers
|
||||
if [[ ${#containers[@]} -eq 0 ]]; then
|
||||
log "No partner containers found to remove"
|
||||
return 0
|
||||
fi
|
||||
for container in "${containers[@]}"; do
|
||||
[[ -z "$container" ]] && continue
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would stop + rm: $container"
|
||||
continue
|
||||
fi
|
||||
if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
|
||||
timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1 || true
|
||||
timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1 && \
|
||||
log "$container removed ✅" || warn "$container rm failed"
|
||||
else
|
||||
log "$container not found — skipping"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# SSH to mirror — remove all containers named *-${OWNER_SHORT} (owner's deployed containers).
|
||||
cleanup_owner_containers_on_mirror() {
|
||||
local mirror_ip="$1"
|
||||
local owner_short
|
||||
owner_short=$(derive_short_name "$OWNER")
|
||||
|
||||
log "Removing owner-deployed containers from $MIRROR..."
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would remove *-${owner_short} containers from $MIRROR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local container_list
|
||||
container_list=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
|
||||
"docker ps -a --format '{{.Names}}' 2>/dev/null | grep -i -- '-${owner_short}$'" 2>/dev/null)
|
||||
|
||||
if [[ -z "$container_list" ]]; then
|
||||
log "No *-${owner_short} containers found on $MIRROR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r container; do
|
||||
[[ -z "$container" ]] && continue
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
|
||||
"docker stop '$container' >/dev/null 2>&1
|
||||
docker rm '$container' >/dev/null 2>&1 && echo removed" 2>/dev/null | \
|
||||
grep -q removed && \
|
||||
log "$container removed from $MIRROR ✅" || \
|
||||
warn "Failed to remove $container from $MIRROR"
|
||||
done <<< "$container_list"
|
||||
}
|
||||
|
||||
# SSH to mirror — start mirror's own parked containers.
|
||||
# Reads PARTNERSHIP_OWN_CONTAINERS from the mirror's own conf via SSH.
|
||||
start_mirror_own_stack() {
|
||||
local mirror_ip="$1"
|
||||
|
||||
log "Reading own stack list from $MIRROR conf..."
|
||||
local -a mirror_own=()
|
||||
mapfile -t mirror_own < <(read_remote_conf_array "$mirror_ip" "PARTNERSHIP_OWN_CONTAINERS")
|
||||
# Remove empty entries
|
||||
local -a filtered=()
|
||||
for c in "${mirror_own[@]}"; do [[ -n "$c" ]] && filtered+=("$c"); done
|
||||
mirror_own=("${filtered[@]}")
|
||||
|
||||
if [[ ${#mirror_own[@]} -eq 0 ]]; then
|
||||
log "No PARTNERSHIP_OWN_CONTAINERS configured on $MIRROR — skipping remote stack restart"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Restarting own stack on $MIRROR: ${mirror_own[*]}"
|
||||
for container in "${mirror_own[@]}"; do
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would start $container on $MIRROR"
|
||||
continue
|
||||
fi
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$mirror_ip" \
|
||||
"docker start '$container' >/dev/null 2>&1 && echo started" 2>/dev/null | \
|
||||
grep -q started && \
|
||||
log "$container started on $MIRROR ✅" || \
|
||||
warn "$container failed to start on $MIRROR — check manually"
|
||||
done
|
||||
}
|
||||
|
||||
# Create the mirror's Emby admin account on the owner's deployed Emby.
|
||||
# Credentials come from the MIRROR's conf (HOST*_PARTNERSHIP_EMBY_ADMIN_USER/PASS).
|
||||
# Checks for username collision before creating — exits with guidance if taken.
|
||||
provision_emby_admin() {
|
||||
local mirror_ip="$1"
|
||||
local emby_port="${PARTNERSHIP_EMBY_PORT:-8096}"
|
||||
local emby_url="http://${mirror_ip}:${emby_port}"
|
||||
|
||||
echo ""
|
||||
echo "━━━ $ICON_EMBY Emby Admin Provisioning ━━━"
|
||||
|
||||
if [[ "${PARTNERSHIP_PROVISION_EMBY_ADMIN:-false}" != true ]]; then
|
||||
log "PARTNERSHIP_PROVISION_EMBY_ADMIN=false — skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$EMBY_API_KEY" ]]; then
|
||||
warn "EMBY_API_KEY not set — skipping Emby admin provisioning"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Read mirror's desired credentials from their own conf via SSH
|
||||
log "Reading Emby credentials from $MIRROR conf..."
|
||||
local username password
|
||||
username=$(read_remote_conf_var "$mirror_ip" "PARTNERSHIP_EMBY_ADMIN_USER")
|
||||
password=$(read_remote_conf_var "$mirror_ip" "PARTNERSHIP_EMBY_ADMIN_PASS")
|
||||
[[ -z "$username" ]] && username="$(derive_short_name "$MIRROR")"
|
||||
|
||||
if [[ -z "$password" ]]; then
|
||||
warn "PARTNERSHIP_EMBY_ADMIN_PASS is empty in $MIRROR conf"
|
||||
warn "$MIRROR must set HOST${MIRROR_ID: -1}_PARTNERSHIP_EMBY_ADMIN_PASS before onboard"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would create Emby admin '$username' at $emby_url"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Collision check — username already exists?
|
||||
local existing_users
|
||||
existing_users=$(curl -sf --max-time 15 \
|
||||
-H "X-Emby-Authorization: MediaBrowser Token=\"$EMBY_API_KEY\"" \
|
||||
"${emby_url}/Users" 2>/dev/null)
|
||||
|
||||
if echo "$existing_users" | grep -q "\"Name\":\"${username}\""; then
|
||||
error "Emby username '${username}' is already taken on the shared instance"
|
||||
error "Options:"
|
||||
error " 1. Sign in with that account — it may already be yours"
|
||||
error " 2. Set a different name in ${user_var} and re-run --onboard"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Creating Emby admin '$username' at $emby_url..."
|
||||
|
||||
local create_response http_code body
|
||||
create_response=$(curl -sf --max-time 15 -w "\n%{http_code}" \
|
||||
-X POST \
|
||||
-H "X-Emby-Authorization: MediaBrowser Token=\"$EMBY_API_KEY\"" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"Name\": \"$username\"}" \
|
||||
"${emby_url}/Users/New" 2>/dev/null)
|
||||
http_code=$(echo "$create_response" | tail -1)
|
||||
body=$(echo "$create_response" | head -n -1)
|
||||
|
||||
if [[ "$http_code" != "200" ]] && [[ "$http_code" != "204" ]]; then
|
||||
warn "Failed to create Emby user '$username' (HTTP $http_code)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local user_id
|
||||
user_id=$(echo "$body" | grep -o '"Id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
if [[ -z "$user_id" ]]; then
|
||||
warn "Emby user created but could not parse user ID — set password manually"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local pw_code
|
||||
pw_code=$(curl -sf --max-time 15 -w "%{http_code}" -o /dev/null \
|
||||
-X POST \
|
||||
-H "X-Emby-Authorization: MediaBrowser Token=\"$EMBY_API_KEY\"" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"NewPw\": \"$password\"}" \
|
||||
"${emby_url}/Users/${user_id}/Password" 2>/dev/null)
|
||||
|
||||
if [[ "$pw_code" == "200" ]] || [[ "$pw_code" == "204" ]]; then
|
||||
log "Emby admin '$username' created (id: $user_id) ✅"
|
||||
else
|
||||
warn "User created but password set failed (HTTP $pw_code) — set password manually"
|
||||
fi
|
||||
|
||||
curl -sf --max-time 15 -o /dev/null \
|
||||
-X POST \
|
||||
-H "X-Emby-Authorization: MediaBrowser Token=\"$EMBY_API_KEY\"" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"IsAdministrator": true, "IsDisabled": false}' \
|
||||
"${emby_url}/Users/${user_id}/Policy" 2>/dev/null && \
|
||||
log "$username granted admin policy ✅" || \
|
||||
warn "Could not set admin policy — grant manually in Emby dashboard"
|
||||
}
|
||||
|
||||
# Delete the mirror's Emby admin account from the owner's deployed Emby.
|
||||
revoke_emby_admin() {
|
||||
local mirror_ip="$1"
|
||||
local emby_port="${PARTNERSHIP_EMBY_PORT:-8096}"
|
||||
local emby_url="http://${mirror_ip}:${emby_port}"
|
||||
|
||||
echo ""
|
||||
echo "━━━ $ICON_EMBY Emby Admin Revocation ━━━"
|
||||
|
||||
local username
|
||||
username=$(read_remote_conf_var "$mirror_ip" "PARTNERSHIP_EMBY_ADMIN_USER")
|
||||
[[ -z "$username" ]] && username="$(derive_short_name "$MIRROR")"
|
||||
|
||||
if [[ "${PARTNERSHIP_PROVISION_EMBY_ADMIN:-false}" != true ]]; then
|
||||
log "PARTNERSHIP_PROVISION_EMBY_ADMIN=false — skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -z "$EMBY_API_KEY" ]]; then
|
||||
warn "EMBY_API_KEY not set — skipping Emby admin revocation"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would delete Emby admin '$username' at $emby_url"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Looking up Emby user '$username' at $emby_url..."
|
||||
|
||||
local users_json user_id
|
||||
users_json=$(curl -sf --max-time 15 \
|
||||
-H "X-Emby-Authorization: MediaBrowser Token=\"$EMBY_API_KEY\"" \
|
||||
"${emby_url}/Users" 2>/dev/null)
|
||||
|
||||
user_id=$(echo "$users_json" | \
|
||||
grep -o "\"Id\":\"[^\"]*\"[^}]*\"Name\":\"${username}\"" | \
|
||||
grep -o '"Id":"[^"]*"' | cut -d'"' -f4 | head -1)
|
||||
|
||||
if [[ -z "$user_id" ]]; then
|
||||
warn "Emby user '$username' not found at $emby_url — may already be removed"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local del_code
|
||||
del_code=$(curl -sf --max-time 15 -w "%{http_code}" -o /dev/null \
|
||||
-X DELETE \
|
||||
-H "X-Emby-Authorization: MediaBrowser Token=\"$EMBY_API_KEY\"" \
|
||||
"${emby_url}/Users/${user_id}" 2>/dev/null)
|
||||
|
||||
if [[ "$del_code" == "200" ]] || [[ "$del_code" == "204" ]] || [[ "$del_code" == "404" ]]; then
|
||||
log "Emby admin '$username' removed ✅"
|
||||
else
|
||||
warn "Failed to delete Emby user '$username' (HTTP $del_code) — remove manually"
|
||||
fi
|
||||
}
|
||||
|
||||
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)
|
||||
mirror_ip=$(resolve_tailscale_ip "$MIRROR")
|
||||
[[ -z "$mirror_ip" ]] && { error "Cannot resolve $MIRROR Tailscale IP"; return 1; }
|
||||
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
|
||||
@@ -717,8 +1023,8 @@ fi
|
||||
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")
|
||||
OWNER_IP=$(resolve_tailscale_ip "$OWNER" || echo "unreachable")
|
||||
MIRROR_IP=$(resolve_tailscale_ip "$MIRROR" || echo "unreachable")
|
||||
echo " $ICON_HOST My ID: $MY_ID ($LOCAL_SERVER_NAME)"
|
||||
echo " Owner: $OWNER_ID ($OWNER — $OWNER_IP)"
|
||||
echo " Mirror: $MIRROR_ID ($MIRROR — $MIRROR_IP)"
|
||||
@@ -743,7 +1049,7 @@ if [[ "$MODE" == "status" ]]; then
|
||||
echo ""
|
||||
|
||||
# Remote state
|
||||
REMOTE_IP=$(tailscale ip -4 "${REMOTE_SERVER_NAME,,}" 2>/dev/null)
|
||||
REMOTE_IP=$(resolve_tailscale_ip "$REMOTE_SERVER_NAME")
|
||||
if [[ -n "$REMOTE_IP" ]]; then
|
||||
REMOTE_CONTENT=$(read_remote_state "$REMOTE_IP" "$SSH_KEY" "$REMOTE_STATE_FILE")
|
||||
if [[ -n "$REMOTE_CONTENT" ]]; then
|
||||
@@ -851,7 +1157,7 @@ if [[ "$MODE" == "check" ]]; then
|
||||
fi
|
||||
|
||||
# Read remote state file
|
||||
REMOTE_IP=$(tailscale ip -4 "${REMOTE_SERVER_NAME,,}" 2>/dev/null)
|
||||
REMOTE_IP=$(resolve_tailscale_ip "$REMOTE_SERVER_NAME")
|
||||
if [[ -z "$REMOTE_IP" ]]; then
|
||||
log "Partnership check — remote unreachable, skipping state check"
|
||||
exit 0
|
||||
@@ -884,6 +1190,10 @@ if [[ "$MODE" == "check" ]]; then
|
||||
warn "Owner finalising offboard request from mirror..."
|
||||
do_final_sync
|
||||
|
||||
# Remove owner's fallback containers, restart own stack
|
||||
cleanup_partner_containers "$(derive_partner_folder_name "$MIRROR")"
|
||||
start_own_stack
|
||||
|
||||
NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
write_state_file "$LOCAL_STATE_FILE" \
|
||||
"INACTIVE" "" "$NOW" "$REMOTE_SERVER_NAME" "mirror-requested"
|
||||
@@ -915,6 +1225,8 @@ if [[ "$MODE" == "check" ]]; then
|
||||
# Mirror sees owner is INACTIVE — clean up own side
|
||||
warn "Owner has offboarded — cleaning up mirror side..."
|
||||
reconfigure_local_webuis "localhost"
|
||||
cleanup_partner_containers "$(derive_partner_folder_name "$OWNER")"
|
||||
start_own_stack
|
||||
NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
write_state_file "$LOCAL_STATE_FILE" \
|
||||
"INACTIVE" "" "$NOW" "$OWNER" "owner-offboarded"
|
||||
@@ -973,8 +1285,8 @@ if [[ "$MODE" == "onboard" ]]; then
|
||||
check_remote_array || exit 1
|
||||
check_remote_docker_daemon || exit 1
|
||||
|
||||
OWNER_IP=$(tailscale ip -4 "${OWNER,,}" 2>/dev/null)
|
||||
MIRROR_IP=$(tailscale ip -4 "${MIRROR,,}" 2>/dev/null)
|
||||
OWNER_IP=$(resolve_tailscale_ip "$OWNER")
|
||||
MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
|
||||
|
||||
[[ -z "$OWNER_IP" ]] && { error "Cannot resolve $OWNER Tailscale IP"; exit 1; }
|
||||
[[ -z "$MIRROR_IP" ]] && { error "Cannot resolve $MIRROR Tailscale IP"; exit 1; }
|
||||
@@ -1016,13 +1328,15 @@ if [[ "$MODE" == "onboard" ]]; then
|
||||
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"
|
||||
log "Local state: ACTIVE ✅"
|
||||
|
||||
[[ "$DRY_RUN" == false ]] && remove_from_blocklist "$MIRROR"
|
||||
|
||||
push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
||||
echo "0" > "$OFFLINE_COUNTER"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
write_state_file "$LOCAL_STATE_FILE" "ACTIVE" "$NOW" "" "$LOCAL_SERVER_NAME" "onboard"
|
||||
log "Local state: ACTIVE ✅"
|
||||
remove_from_blocklist "$MIRROR"
|
||||
push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
||||
echo "0" > "$OFFLINE_COUNTER"
|
||||
else
|
||||
warn "DRY RUN — would write ACTIVE state and push to remote"
|
||||
fi
|
||||
|
||||
# FolderView3 — create partner folder with this server's failover containers for remote
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
||||
@@ -1038,6 +1352,9 @@ if [[ "$MODE" == "onboard" ]]; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# Emby admin provisioning — runs after container deployment (deploy step not yet built)
|
||||
provision_emby_admin "$MIRROR_IP"
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY ONBOARD SUMMARY ━━━━━"
|
||||
@@ -1094,13 +1411,12 @@ if [[ "$MODE" == "offboard" ]]; then
|
||||
echo "━━━ $ICON_CONTAINERS Reconfigure Local WebUIs → localhost ━━━"
|
||||
reconfigure_local_webuis "localhost"
|
||||
|
||||
# FolderView3 — remove partner folder and clean containers
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS FolderView3 Cleanup ━━━"
|
||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$OWNER")
|
||||
folderview3_remove_partner_folder "$PARTNER_FOLDER_NAME"
|
||||
fi
|
||||
# Remove owner's deployed containers + restart own stack
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS Cleanup Partner Containers ━━━"
|
||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$OWNER")
|
||||
cleanup_partner_containers "$PARTNER_FOLDER_NAME"
|
||||
start_own_stack
|
||||
|
||||
NOW=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
write_state_file "$LOCAL_STATE_FILE" \
|
||||
@@ -1109,7 +1425,7 @@ if [[ "$MODE" == "offboard" ]]; then
|
||||
|
||||
[[ "$DRY_RUN" == false ]] && add_to_blocklist "$OWNER" "$REASON"
|
||||
|
||||
OWNER_IP=$(tailscale ip -4 "${OWNER,,}" 2>/dev/null)
|
||||
OWNER_IP=$(resolve_tailscale_ip "$OWNER")
|
||||
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" \
|
||||
@@ -1151,7 +1467,7 @@ if [[ "$MODE" == "offboard" ]]; then
|
||||
echo "━━━ $ICON_SYNC Final Sync ━━━"
|
||||
do_final_sync
|
||||
|
||||
MIRROR_IP=$(tailscale ip -4 "${MIRROR,,}" 2>/dev/null)
|
||||
MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
|
||||
MIRROR_REACHABLE=false
|
||||
[[ -n "$MIRROR_IP" ]] && MIRROR_REACHABLE=true
|
||||
|
||||
@@ -1199,14 +1515,27 @@ if [[ "$MODE" == "offboard" ]]; then
|
||||
push_state_to_remote "$LOCAL_STATE_FILE" "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
||||
fi
|
||||
|
||||
# FolderView3 — remove partner folder and clean containers on this (owner) side
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS FolderView3 Cleanup ━━━"
|
||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
|
||||
folderview3_remove_partner_folder "$PARTNER_FOLDER_NAME"
|
||||
# Local: remove fallback-coverage containers for mirror, restart own stack
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS Local Container Cleanup ━━━"
|
||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
|
||||
cleanup_partner_containers "$PARTNER_FOLDER_NAME"
|
||||
start_own_stack
|
||||
|
||||
# Remote: remove owner's deployed containers from mirror, restart mirror's own stack
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS Remote Container Cleanup ━━━"
|
||||
if [[ "$MIRROR_REACHABLE" == true ]]; then
|
||||
cleanup_owner_containers_on_mirror "$MIRROR_IP"
|
||||
start_mirror_own_stack "$MIRROR_IP"
|
||||
else
|
||||
warn "$MIRROR unreachable — remote container cleanup skipped"
|
||||
warn "Run 'partnership_manager.sh --offboard' on $MIRROR to clean up manually"
|
||||
fi
|
||||
|
||||
# Emby admin revocation — before SSH key revocation while Emby still reachable
|
||||
[[ "$MIRROR_REACHABLE" == true ]] && revoke_emby_admin "$MIRROR_IP"
|
||||
|
||||
# SSH key revocation — mutual, both directions
|
||||
# Must run before Tailscale removal (SSH needs network) and after state is pushed
|
||||
SSH_REVOKE_REMOTE_OK=false
|
||||
@@ -1354,8 +1683,8 @@ if [[ "$MODE" == "transfer" ]]; then
|
||||
NEW_OWNER_SSH_KEY="${!NEW_OWNER_SSH_KEY_VAR}"
|
||||
NEW_MIRROR_SSH_KEY="${!NEW_MIRROR_SSH_KEY_VAR}"
|
||||
|
||||
NEW_OWNER_IP=$(tailscale ip -4 "${NEW_OWNER,,}" 2>/dev/null)
|
||||
NEW_MIRROR_IP=$(tailscale ip -4 "${NEW_MIRROR,,}" 2>/dev/null)
|
||||
NEW_OWNER_IP=$(resolve_tailscale_ip "$NEW_OWNER")
|
||||
NEW_MIRROR_IP=$(resolve_tailscale_ip "$NEW_MIRROR")
|
||||
|
||||
[[ -z "$NEW_OWNER_IP" ]] && { error "Cannot resolve new owner Tailscale IP"; exit 1; }
|
||||
[[ -z "$NEW_MIRROR_IP" ]] && { error "Cannot resolve new mirror Tailscale IP"; exit 1; }
|
||||
|
||||
Reference in New Issue
Block a user