Auth stack certs tab, arrs db fallbacks, cert monitor cache, conf parser fix
- Auth stack: fold cert monitor into Auth Stack page as fourth tab (Certs); remove standalone cert page and top-level tab - cert_monitor.sh: write JSON status cache to State_Files/cert_status.json after each run; expose per-domain days/expiry via _CERT_DAYS/_CERT_EXPIRY globals - api/cert.php: new — serves cached cert status; falls back to configured domains as UNKN when no cache exists; POST action=run triggers live check - arrs db fallbacks: vv_arr_cleanup_stats/discovery_stats/recovery_stats now read from data/*.db files when log JSON files don't yet exist - config.php vv_conf_vars(): unescape bash \$ → $ so passwords with dollar signs read correctly from conf files - host1.conf: fill in HOST1_NPM_USER/PASS and HOST1_LLDAP_USER/PASS - Partnership adapter pattern: Unraid-specific container logic extracted to Plugin/unraid/Partnership/; platform-agnostic structure stays in Partnership/ - First-run wizard: uniform multi-step flow for all hosts; HOST2 pull moved to checklist; auto SSH keygen and API key creation on save - api/checklist.php: live setup checklist with pull_master action - Fullscreen toggle: hide Unraid header/menu; state persists via localStorage
This commit is contained in:
@@ -157,7 +157,6 @@ HOST2 (mirror) runs:
|
||||
HOST1 (owner) runs:
|
||||
partnership_onboard.sh
|
||||
├─ ssh_setup.sh generates keypair, copies to mirror
|
||||
├─ [plugin install on mirror] FolderView3 if configured
|
||||
├─ [stop mirror auth stack] PARTNERSHIP_REPLACE_CONTAINERS via SSH
|
||||
├─ deploy_container_from_xml() pushes auth XMLs to mirror + starts containers
|
||||
│ └─ wait_for_container_healthy() Mariadb/Redis health-checked before Authelia
|
||||
|
||||
@@ -274,8 +274,8 @@ MIRROR_STATE_FILE="${STATE_DIR:-/boot/config}/partnership_${MIRROR}.db"
|
||||
OFFLINE_COUNTER="${STATE_DIR:-/boot/config}/partnership_offline_days.db"
|
||||
|
||||
# ── Exit Trap — restart locally stopped containers if script crashes mid-cleanup ──────────────
|
||||
# Used by folderview3_remove_partner_folder() and cleanup_partner_containers() — also shared
|
||||
# with partnership_offboard.sh which sources this file and registers the same trap.
|
||||
# Used by cleanup_partner_containers() — also shared with partnership_offboard.sh which
|
||||
# sources this file and registers the same trap.
|
||||
declare -a _PM_TRAP_STOPPED=()
|
||||
_pm_trap_restart_stopped() {
|
||||
[[ ${#_PM_TRAP_STOPPED[@]} -eq 0 ]] && return
|
||||
@@ -573,166 +573,6 @@ do_ssh_key_revocation() {
|
||||
fi
|
||||
}
|
||||
|
||||
# ── FolderView3 integration ───────────────────────────────────────────────────────────────────
|
||||
FOLDERVIEW3_DIR="/usr/local/emhttp/plugins/folder.view3"
|
||||
FOLDERVIEW3_JSON="/boot/config/plugins/folder.view3/docker.json"
|
||||
|
||||
# Derive short partner name: strip unraid- prefix (case-insensitive) if present
|
||||
derive_partner_folder_name() {
|
||||
local hostname="$1"
|
||||
local short="${hostname,,}"
|
||||
[[ "$short" == unraid-* ]] && short="${short:7}"
|
||||
# Capitalise first char for readability: jayred365 → Jayred365-fallback
|
||||
echo "${short^}-Fallback"
|
||||
}
|
||||
|
||||
folderview3_ensure_plugin() {
|
||||
if [[ -d "$FOLDERVIEW3_DIR" ]]; then
|
||||
log "FolderView3 plugin present ✅"
|
||||
return 0
|
||||
fi
|
||||
if [[ -z "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
|
||||
warn "FolderView3 plugin not installed and PARTNERSHIP_FOLDERVIEW3_URL is empty"
|
||||
warn "Install manually from Community Applications or set PARTNERSHIP_FOLDERVIEW3_URL in master.conf"
|
||||
return 1
|
||||
fi
|
||||
warn "FolderView3 not found — installing from CA..."
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would run: plugin install $PARTNERSHIP_FOLDERVIEW3_URL"
|
||||
return 0
|
||||
fi
|
||||
plugin install "$PARTNERSHIP_FOLDERVIEW3_URL" 2>/dev/null && \
|
||||
log "FolderView3 installed ✅" || {
|
||||
warn "FolderView3 install failed — folder will not be created"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
folderview3_create_partner_folder() {
|
||||
local partner_name="$1"
|
||||
shift
|
||||
local containers=("$@")
|
||||
|
||||
log "FolderView3: creating folder '$partner_name' with ${#containers[@]} container(s)..."
|
||||
|
||||
folderview3_ensure_plugin || return 1
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would create FolderView3 folder: $partner_name"
|
||||
for c in "${containers[@]}"; do
|
||||
[[ -n "$c" ]] && warn " DRY RUN — container: $c"
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Initialise JSON if missing
|
||||
if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
|
||||
mkdir -p "$(dirname "$FOLDERVIEW3_JSON")"
|
||||
echo '{}' > "$FOLDERVIEW3_JSON"
|
||||
fi
|
||||
|
||||
# Check folder doesn't already exist
|
||||
local existing
|
||||
existing=$(jq -r --arg name "$partner_name" \
|
||||
'to_entries[] | select(.value.name == $name) | .key' \
|
||||
"$FOLDERVIEW3_JSON" 2>/dev/null)
|
||||
if [[ -n "$existing" ]]; then
|
||||
log "FolderView3: folder '$partner_name' already exists (id: $existing) — skipping"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Build containers JSON array
|
||||
local containers_json
|
||||
containers_json=$(printf '%s\n' "${containers[@]}" | \
|
||||
grep -v '^$' | jq -R . | jq -s .)
|
||||
|
||||
# Generate a stable random ID from timestamp+name
|
||||
local folder_id
|
||||
folder_id=$(echo "${partner_name}$(date +%s%N)" | md5sum | head -c 12)
|
||||
|
||||
jq --arg id "$folder_id" --arg name "$partner_name" \
|
||||
--argjson containers "$containers_json" \
|
||||
'.[$id] = {"name": $name, "containers": $containers, "containerImages": {}}' \
|
||||
"$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
|
||||
mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
|
||||
log "FolderView3: folder '$partner_name' created with ${#containers[@]} container(s) ✅" || {
|
||||
warn "FolderView3: failed to write JSON — check $FOLDERVIEW3_JSON"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
folderview3_remove_partner_folder() {
|
||||
local partner_name="$1"
|
||||
|
||||
log "FolderView3: removing folder '$partner_name' and stopping its containers..."
|
||||
|
||||
if [[ ! -f "$FOLDERVIEW3_JSON" ]]; then
|
||||
log "FolderView3: JSON not found — nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
warn "jq not found — cannot manage FolderView3 JSON"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Get containers from this folder
|
||||
local containers_json
|
||||
containers_json=$(jq -r --arg name "$partner_name" \
|
||||
'to_entries[] | select(.value.name == $name) | .value.containers[]' \
|
||||
"$FOLDERVIEW3_JSON" 2>/dev/null)
|
||||
|
||||
if [[ -z "$containers_json" ]]; then
|
||||
log "FolderView3: folder '$partner_name' not found — nothing to remove"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Stop and remove each container in the folder
|
||||
local stopped=0 removed=0 failed=0
|
||||
while IFS= read -r container; do
|
||||
[[ -z "$container" ]] && continue
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would stop + rm: $container"
|
||||
continue
|
||||
fi
|
||||
# Stop if running
|
||||
if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$container" >/dev/null 2>&1; then
|
||||
if timeout "${DOCKER_TIMEOUT:-30}" docker stop "$container" >/dev/null 2>&1; then
|
||||
log "$container stopped ✅"
|
||||
_PM_TRAP_STOPPED+=("$container")
|
||||
(( stopped++ ))
|
||||
else
|
||||
warn "$container stop failed"
|
||||
(( failed++ ))
|
||||
fi
|
||||
if timeout "${DOCKER_TIMEOUT:-30}" docker rm "$container" >/dev/null 2>&1; then
|
||||
log "$container removed ✅"
|
||||
(( removed++ ))
|
||||
else
|
||||
warn "$container rm failed (may not exist)"
|
||||
fi
|
||||
else
|
||||
log "$container not found locally — skipping"
|
||||
fi
|
||||
done <<< "$containers_json"
|
||||
|
||||
# Remove folder entry from JSON
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
jq --arg name "$partner_name" \
|
||||
'with_entries(select(.value.name != $name))' \
|
||||
"$FOLDERVIEW3_JSON" > "${FOLDERVIEW3_JSON}.tmp" && \
|
||||
mv "${FOLDERVIEW3_JSON}.tmp" "$FOLDERVIEW3_JSON" && \
|
||||
log "FolderView3: folder '$partner_name' removed ✅" || \
|
||||
warn "FolderView3: failed to remove folder from JSON"
|
||||
else
|
||||
warn "DRY RUN — would remove folder '$partner_name' from $FOLDERVIEW3_JSON"
|
||||
fi
|
||||
|
||||
[[ "$DRY_RUN" == false ]] && \
|
||||
log "FolderView3 cleanup: $stopped stopped, $removed removed, $failed failed"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Gather all partner fallback containers for this server (all tiers)
|
||||
gather_partner_fallback_containers() {
|
||||
local out_var="$1"
|
||||
@@ -803,19 +643,14 @@ start_own_stack() {
|
||||
}
|
||||
|
||||
# Remove partnership containers on this server + their appdata bind-mount paths.
|
||||
# Uses FolderView3 folder if enabled (precise list), else falls back to FALLBACK_*_COVERS_* config.
|
||||
# Appdata paths collected via docker inspect BEFORE removal — inspect fails on removed containers.
|
||||
# Safety gate: only paths matching /mnt/*/appdata* are deleted.
|
||||
cleanup_partner_containers() {
|
||||
local folder_name="$1"
|
||||
declare -a containers=()
|
||||
gather_partner_fallback_containers containers
|
||||
|
||||
if [[ ${#containers[@]} -eq 0 ]]; then
|
||||
log "No partner containers found to remove"
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
||||
folderview3_remove_partner_folder "$folder_name"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -832,25 +667,21 @@ cleanup_partner_containers() {
|
||||
done
|
||||
|
||||
# Remove containers
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
||||
folderview3_remove_partner_folder "$folder_name"
|
||||
else
|
||||
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
|
||||
_PM_TRAP_STOPPED+=("$container")
|
||||
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
|
||||
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
|
||||
_PM_TRAP_STOPPED+=("$container")
|
||||
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
|
||||
|
||||
# Delete appdata after containers are gone
|
||||
while IFS= read -r path; do
|
||||
@@ -1271,27 +1102,6 @@ if [[ "$MODE" == "status" ]]; then
|
||||
echo " ${entry%%|*} → port ${entry##*|}"
|
||||
done
|
||||
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
||||
echo ""
|
||||
FOLDER_STATUS_NAME=$(derive_partner_folder_name "$REMOTE_SERVER_NAME")
|
||||
if [[ -f "$FOLDERVIEW3_JSON" ]]; then
|
||||
FOLDER_EXISTS=$(jq -r --arg name "$FOLDER_STATUS_NAME" \
|
||||
'to_entries[] | select(.value.name == $name) | .key' \
|
||||
"$FOLDERVIEW3_JSON" 2>/dev/null)
|
||||
if [[ -n "$FOLDER_EXISTS" ]]; then
|
||||
FOLDER_CONTAINERS=$(jq -r --arg name "$FOLDER_STATUS_NAME" \
|
||||
'[to_entries[] | select(.value.name == $name) | .value.containers[]] | join(", ")' \
|
||||
"$FOLDERVIEW3_JSON" 2>/dev/null)
|
||||
echo " FolderView3: $FOLDER_STATUS_NAME ✅"
|
||||
echo " Containers: $FOLDER_CONTAINERS"
|
||||
else
|
||||
echo " FolderView3: $FOLDER_STATUS_NAME — not found in JSON"
|
||||
fi
|
||||
else
|
||||
echo " FolderView3: config not found ($FOLDERVIEW3_JSON)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -f "$OFFLINE_COUNTER" ]]; then
|
||||
OFFLINE_DAYS=$(cat "$OFFLINE_COUNTER" 2>/dev/null || echo 0)
|
||||
[[ "$OFFLINE_DAYS" -gt 0 ]] && \
|
||||
@@ -1414,26 +1224,12 @@ if [[ "$MODE" == "onboard" ]]; then
|
||||
# ── LOCAL-ONLY PATH ──────────────────────────────────────────────────────────
|
||||
# HOST1-local setup steps that don't need HOST2 present. Called from
|
||||
# partnership_onboard.sh --phase1-only so HOST1 can complete its own side
|
||||
# (FolderView3, setup.db flag) while waiting for HOST2 to install and onboard.
|
||||
# (PARTNERSHIP_ENABLED flag, setup.db) while waiting for HOST2 to install and onboard.
|
||||
if [[ "$LOCAL_ONLY" == true ]]; then
|
||||
log "Mode: local-only — skipping remote pre-flight and WebUI steps"
|
||||
log "Owner: $OWNER_ID ($OWNER) · Mirror: $MIRROR_ID ($MIRROR)"
|
||||
echo ""
|
||||
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
||||
echo "FolderView3 Integration"
|
||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
|
||||
declare -a PARTNER_CONTAINERS=()
|
||||
gather_partner_fallback_containers PARTNER_CONTAINERS
|
||||
if [[ ${#PARTNER_CONTAINERS[@]} -gt 0 ]]; then
|
||||
folderview3_create_partner_folder "$PARTNER_FOLDER_NAME" "${PARTNER_CONTAINERS[@]}"
|
||||
else
|
||||
log "No FALLBACK_${MY_ID}_COVERS_${MIRROR_ID}_TIER* containers — skipping folder"
|
||||
fi
|
||||
else
|
||||
log "FolderView3 not configured — skipping"
|
||||
fi
|
||||
|
||||
# Enable partnership in master.conf + push to all hosts
|
||||
echo ""
|
||||
echo "Enabling partnership in master.conf..."
|
||||
@@ -1570,20 +1366,6 @@ if (vv_write_conf_raw('master.conf', \$master)) {
|
||||
warn "DRY RUN — would write ACTIVE state and push to remote"
|
||||
fi
|
||||
|
||||
# FolderView3 — create partner folder with this server's fallback containers for remote
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS FolderView3 Integration ━━━"
|
||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
|
||||
declare -a PARTNER_CONTAINERS=()
|
||||
gather_partner_fallback_containers PARTNER_CONTAINERS
|
||||
if [[ ${#PARTNER_CONTAINERS[@]} -gt 0 ]]; then
|
||||
folderview3_create_partner_folder "$PARTNER_FOLDER_NAME" "${PARTNER_CONTAINERS[@]}"
|
||||
else
|
||||
log "No FALLBACK_${MY_ID}_COVERS_${REMOTE_ID}_TIER* containers configured — skipping folder creation"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Emby admin provisioning — runs after container deployment (deploy step not yet built)
|
||||
provision_emby_admin "$MIRROR_IP"
|
||||
|
||||
|
||||
@@ -71,10 +71,10 @@
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCRIPTS_ROOT="$SCRIPT_DIR/.."
|
||||
TEMPLATES_DIR="/boot/config/plugins/dockerMan/templates-user"
|
||||
SSH_TIMEOUT=15
|
||||
|
||||
source "$SCRIPTS_ROOT/load_config.sh"
|
||||
source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
|
||||
|
||||
# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
|
||||
REASON="manual"
|
||||
@@ -148,140 +148,6 @@ echo " Reason: $REASON"
|
||||
echo ""
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── HELPER: remove owner-deployed containers from a remote host ───────────────────────────────
|
||||
#
|
||||
# Uses PARTNERSHIP_AUTH_STACK + PARTNERSHIP_ARR_STACK arrays (owner's conf) to derive
|
||||
# container names from local XML templates. SSHes to remote to stop, remove, and delete
|
||||
# appdata. Appdata paths are collected via docker inspect before removal so they aren't
|
||||
# lost once the container is gone. Safety gate: only /mnt/*/appdata* paths are deleted.
|
||||
# ==============================================================================================
|
||||
cleanup_deployed_stack_on_remote() {
|
||||
local remote_ip="$1" ssh_key="$2"
|
||||
local -a xml_names=()
|
||||
[[ ${#PARTNERSHIP_AUTH_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_AUTH_STACK[@]}")
|
||||
[[ ${#PARTNERSHIP_ARR_STACK[@]} -gt 0 ]] && xml_names+=("${PARTNERSHIP_ARR_STACK[@]}")
|
||||
|
||||
if [[ ${#xml_names[@]} -eq 0 ]]; then
|
||||
log "No auth/arr stack arrays configured — skipping deployed stack cleanup"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Removing owner-deployed containers (auth/arr stacks) from $MIRROR..."
|
||||
for xml_name in "${xml_names[@]}"; do
|
||||
[[ -z "$xml_name" ]] && continue
|
||||
local xml_file="${TEMPLATES_DIR}/${xml_name}"
|
||||
if [[ ! -f "$xml_file" ]]; then
|
||||
warn " $xml_name not found in local $TEMPLATES_DIR — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
local cname
|
||||
cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
|
||||
[[ -z "$cname" ]] && continue
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn " DRY RUN — would stop + rm $cname on $MIRROR"
|
||||
warn " DRY RUN — would delete appdata for $cname on $MIRROR"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Collect appdata paths via docker inspect before removal
|
||||
local appdata_paths
|
||||
appdata_paths=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
|
||||
"docker inspect --format '{{range .HostConfig.Binds}}{{println .}}{{end}}' '$cname' 2>/dev/null \
|
||||
| awk -F: '{print \$1}' | grep '^/mnt/.*/appdata'" 2>/dev/null)
|
||||
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
||||
"docker stop '$cname' >/dev/null 2>&1
|
||||
docker rm '$cname' >/dev/null 2>&1 && echo removed" 2>/dev/null | \
|
||||
grep -q removed && \
|
||||
log " $cname removed from $MIRROR ✅" || \
|
||||
log " $cname not found on $MIRROR — skipping"
|
||||
|
||||
while IFS= read -r path; do
|
||||
[[ -z "$path" ]] && continue
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
||||
"rm -rf '$path' && echo removed" 2>/dev/null | grep -q removed && \
|
||||
log " Appdata removed on $MIRROR: $path ✅" || \
|
||||
warn " Failed to remove appdata on $MIRROR: $path"
|
||||
done <<< "$appdata_paths"
|
||||
done
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ── HELPER: remove owner-deployed containers locally (mirror-initiated offboard) ─────────────
|
||||
#
|
||||
# SSHes to owner to read PARTNERSHIP_AUTH_STACK + PARTNERSHIP_ARR_STACK, then uses the
|
||||
# local templates-user/ copies (SCPed there during onboard) to get container names and
|
||||
# appdata paths. Appdata collected before removal. Skips gracefully if owner unreachable.
|
||||
# ==============================================================================================
|
||||
cleanup_deployed_stack_locally() {
|
||||
local owner_ip="$1" ssh_key="$2"
|
||||
local -a xml_names=()
|
||||
|
||||
if [[ -n "$owner_ip" ]]; then
|
||||
local -a auth_arr arr_arr
|
||||
mapfile -t auth_arr < <(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$owner_ip" \
|
||||
"source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
|
||||
detect_hosts 2>/dev/null
|
||||
printf '%s\n' \"\${PARTNERSHIP_AUTH_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
|
||||
mapfile -t arr_arr < <(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$owner_ip" \
|
||||
"source '$SCRIPTS_ROOT/load_config.sh' 2>/dev/null
|
||||
detect_hosts 2>/dev/null
|
||||
printf '%s\n' \"\${PARTNERSHIP_ARR_STACK[@]:-}\"" 2>/dev/null | grep -v '^$')
|
||||
xml_names=("${auth_arr[@]}" "${arr_arr[@]}")
|
||||
fi
|
||||
|
||||
if [[ ${#xml_names[@]} -eq 0 ]]; then
|
||||
log "Could not read deployed stack from owner — skipping auth/arr cleanup"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Removing owner-deployed containers (auth/arr stacks) locally..."
|
||||
for xml_name in "${xml_names[@]}"; do
|
||||
[[ -z "$xml_name" ]] && continue
|
||||
local xml_file="${TEMPLATES_DIR}/${xml_name}"
|
||||
if [[ ! -f "$xml_file" ]]; then
|
||||
warn " $xml_name not found locally — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
local cname
|
||||
cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
|
||||
[[ -z "$cname" ]] && continue
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn " DRY RUN — would stop + rm $cname"
|
||||
warn " DRY RUN — would delete appdata for $cname"
|
||||
continue
|
||||
fi
|
||||
|
||||
local appdata_paths=""
|
||||
if timeout "${DOCKER_TIMEOUT:-30}" docker inspect "$cname" >/dev/null 2>&1; then
|
||||
appdata_paths=$(docker inspect \
|
||||
--format '{{range .HostConfig.Binds}}{{println .}}{{end}}' \
|
||||
"$cname" 2>/dev/null | awk -F: '{print $1}' | grep '^/mnt/.*/appdata')
|
||||
timeout "${DOCKER_TIMEOUT:-30}" docker stop "$cname" >/dev/null 2>&1 || true
|
||||
_PM_TRAP_STOPPED+=("$cname")
|
||||
timeout "${DOCKER_TIMEOUT:-30}" docker rm "$cname" >/dev/null 2>&1 && \
|
||||
log " $cname removed ✅" || warn " $cname rm failed"
|
||||
else
|
||||
log " $cname not found locally — skipping"
|
||||
fi
|
||||
|
||||
while IFS= read -r path; do
|
||||
[[ -z "$path" ]] && continue
|
||||
rm -rf "$path" && log " Appdata removed: $path ✅" || warn " Failed to remove: $path"
|
||||
done <<< "$appdata_paths"
|
||||
done
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ── HELPER: revoke own admin account from local Emby instance ────────────────────────────────
|
||||
#
|
||||
@@ -401,8 +267,7 @@ if [[ "$AM_MIRROR" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS Step 4/8 — Fallback Container Cleanup ━━━"
|
||||
|
||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$OWNER")
|
||||
cleanup_partner_containers "$PARTNER_FOLDER_NAME" || STEP_FALLBACK_CLEANUP_OK=false
|
||||
cleanup_partner_containers || STEP_FALLBACK_CLEANUP_OK=false
|
||||
|
||||
# ── Step 5: Disable critical sync ─────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
@@ -559,8 +424,7 @@ fi
|
||||
echo ""
|
||||
echo "━━━ $ICON_CONTAINERS Step 5/10 — Local Container Cleanup ━━━"
|
||||
|
||||
PARTNER_FOLDER_NAME=$(derive_partner_folder_name "$MIRROR")
|
||||
cleanup_partner_containers "$PARTNER_FOLDER_NAME"
|
||||
cleanup_partner_containers
|
||||
|
||||
# ── Step 6: Restart own stack ─────────────────────────────────────────────────────────────────
|
||||
start_own_stack
|
||||
@@ -574,26 +438,6 @@ if [[ "$MIRROR_REACHABLE" == true ]]; then
|
||||
cleanup_deployed_stack_on_remote "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
||||
# Remove fallback coverage containers (by *-owner_short naming pattern)
|
||||
cleanup_owner_containers_on_mirror "$MIRROR_IP"
|
||||
# Remove mirror's FolderView3 fallback folder (owner's containers were hosted there)
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]]; then
|
||||
OWNER_FOLDER_ON_MIRROR=$(derive_partner_folder_name "$OWNER")
|
||||
log "Removing FolderView3 folder '$OWNER_FOLDER_ON_MIRROR' from $MIRROR..."
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" root@"$MIRROR_IP" \
|
||||
"fv3='/boot/config/plugins/folder.view3/docker.json'
|
||||
[[ -f \"\$fv3\" ]] && command -v jq >/dev/null 2>&1 && \
|
||||
jq --arg n '$OWNER_FOLDER_ON_MIRROR' \
|
||||
'with_entries(select(.value.name != \$n))' \
|
||||
\"\$fv3\" > \"\${fv3}.tmp\" && \
|
||||
mv \"\${fv3}.tmp\" \"\$fv3\" && echo removed" 2>/dev/null | \
|
||||
grep -q removed && \
|
||||
log "FolderView3 '$OWNER_FOLDER_ON_MIRROR' removed from $MIRROR ✅" || \
|
||||
warn "FolderView3 folder not found on $MIRROR or jq unavailable — skipping"
|
||||
else
|
||||
warn "DRY RUN — would remove FolderView3 folder '$OWNER_FOLDER_ON_MIRROR' from $MIRROR"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "$MIRROR unreachable — remote container cleanup skipped"
|
||||
warn "Run 'partnership_offboard.sh' on $MIRROR to clean up manually"
|
||||
@@ -696,8 +540,6 @@ echo " Step 9 — Keys revoked: $(_revoke_status)"
|
||||
echo " Step 10 — State: INACTIVE ✅"
|
||||
echo ""
|
||||
echo " Blocklist: $MIRROR blocked — re-onboard to permit access again ✅"
|
||||
[[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && \
|
||||
echo " FolderView3: ${PARTNER_FOLDER_NAME:-} (local) + mirror remote cleaned ✅"
|
||||
[[ "${PARTNERSHIP_REMOVE_TAILSCALE:-true}" == true ]] && \
|
||||
echo " Tailscale: $MIRROR removed ✅"
|
||||
echo ""
|
||||
|
||||
@@ -20,15 +20,14 @@
|
||||
#
|
||||
# OWNER PATH (8 steps)
|
||||
# Step 1: SSH key setup — generate keypair, install on mirror, update conf
|
||||
# Step 2: Plugin install — FolderView3 and required plugins on mirror
|
||||
# Step 3: Stop mirror auth — stop mirror's existing auth containers before replacing
|
||||
# Step 4: Deploy auth stack — push XMLs, pull images, create + start on mirror
|
||||
# Step 2: Stop mirror auth — stop mirror's existing auth containers before replacing
|
||||
# Step 3: Deploy auth stack — push XMLs, pull images, create + start on mirror
|
||||
# Mariadb/Redis health-checked before Authelia deploys
|
||||
# Step 5: Stop mirror arr — stop mirror's existing arr containers before replacing
|
||||
# Step 6: Deploy arr stack — push arr XMLs, pull images, create + start on mirror
|
||||
# Step 7: Partnership onboard — configure WebUIs → owner IP, write state, FolderView3, Emby
|
||||
# Step 8: Arr bootstrap — bidirectional library sync (arr_sync.sh)
|
||||
# Step 9: Conf push — push master.conf + setup state to all listed hosts
|
||||
# Step 4: Stop mirror arr — stop mirror's existing arr containers before replacing
|
||||
# Step 5: Deploy arr stack — push arr XMLs, pull images, create + start on mirror
|
||||
# Step 6: Partnership onboard — configure WebUIs → owner IP, write state, Emby
|
||||
# Step 7: Arr bootstrap — bidirectional library sync (arr_sync.sh)
|
||||
# Step 8: Conf push — push master.conf + setup state to all listed hosts
|
||||
#
|
||||
# ==============================================================================================
|
||||
# DESIGN PRINCIPLES
|
||||
@@ -137,10 +136,10 @@
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SCRIPTS_ROOT="$SCRIPT_DIR/.."
|
||||
TEMPLATES_DIR="/boot/config/plugins/dockerMan/templates-user"
|
||||
SSH_TIMEOUT=15
|
||||
|
||||
source "$SCRIPTS_ROOT/load_config.sh"
|
||||
source "$SCRIPTS_ROOT/Plugin/$PLATFORM/Partnership/containers.sh"
|
||||
|
||||
# ── Parse flags ───────────────────────────────────────────────────────────────────────────────
|
||||
SKIP_SSH=false
|
||||
@@ -224,165 +223,6 @@ echo " Partner: $( [[ "$AM_OWNER" == true ]] && echo "$MIRROR_ID ($MIRROR)" ||
|
||||
echo ""
|
||||
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no permanent changes will be made"
|
||||
|
||||
# ==============================================================================================
|
||||
# ── HELPER: deploy a container from a local Unraid template XML to a remote host ─────────────
|
||||
#
|
||||
# Parses Port / Path / Variable Config entries from the XML, SCPs the template and a
|
||||
# self-contained deploy script to the remote, executes it, then cleans up both sides.
|
||||
# Credentials are never passed as SSH command-line args — they stay in the SCPed script.
|
||||
# ==============================================================================================
|
||||
deploy_container_from_xml() {
|
||||
local xml_file="$1" remote_ip="$2" ssh_key="$3"
|
||||
local xml_name
|
||||
xml_name=$(basename "$xml_file")
|
||||
|
||||
# Extract top-level fields
|
||||
local name repo network extra privileged
|
||||
name=$( awk 'match($0,/<Name>([^<]+)<\/Name>/, a){print a[1];exit}' "$xml_file")
|
||||
repo=$( awk 'match($0,/<Repository>([^<]+)<\/Repository>/,a){print a[1];exit}' "$xml_file")
|
||||
network=$( awk 'match($0,/<Network>([^<]+)<\/Network>/, a){print a[1];exit}' "$xml_file")
|
||||
extra=$( awk 'match($0,/<ExtraParams>([^<]*)<\/ExtraParams>/,a){print a[1];exit}' "$xml_file")
|
||||
privileged=$( awk 'match($0,/<Privileged>([^<]+)<\/Privileged>/,a){print a[1];exit}' "$xml_file")
|
||||
|
||||
if [[ -z "$name" || -z "$repo" ]]; then
|
||||
warn " Cannot parse Name/Repository from $xml_name — skipping"
|
||||
return 1
|
||||
fi
|
||||
|
||||
log "Deploying $name..."
|
||||
|
||||
# SCP the XML so Unraid Docker Manager recognises and can manage the container
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
|
||||
"$xml_file" "root@${remote_ip}:${TEMPLATES_DIR}/${xml_name}" 2>/dev/null || {
|
||||
warn " SCP failed for $xml_name — skipping $name"
|
||||
return 1
|
||||
}
|
||||
else
|
||||
warn " DRY RUN — would SCP $xml_name → $MIRROR:${TEMPLATES_DIR}/"
|
||||
fi
|
||||
|
||||
# Build a self-contained deploy script locally.
|
||||
# Writing to a temp file keeps credentials out of SSH command strings.
|
||||
local tmp_script
|
||||
tmp_script=$(mktemp /tmp/deploy_XXXXXX.sh)
|
||||
chmod 600 "$tmp_script"
|
||||
|
||||
{
|
||||
echo "#!/bin/bash"
|
||||
echo "set -e"
|
||||
echo ""
|
||||
printf "docker pull %q 2>/dev/null || true\n" "$repo"
|
||||
printf "docker stop %q 2>/dev/null || true\n" "$name"
|
||||
printf "docker rm %q 2>/dev/null || true\n" "$name"
|
||||
echo ""
|
||||
printf "docker create --name %q --restart=unless-stopped" "$name"
|
||||
[[ -n "$network" ]] && printf " --network=%q" "$network"
|
||||
[[ "$privileged" == "true" ]] && printf " --privileged"
|
||||
[[ -n "$extra" ]] && printf " %s" "$extra"
|
||||
|
||||
# Port mappings → -p host:container/proto
|
||||
awk '/Type="Port"/ {
|
||||
match($0, /Target="([^"]+)"/, t)
|
||||
match($0, /Mode="([^"]+)"/, m)
|
||||
match($0, />([^<]+)<\/Config>/, v)
|
||||
if (t[1] != "" && v[1] != "") {
|
||||
proto = (m[1] == "udp") ? "udp" : "tcp"
|
||||
printf " -p %s:%s/%s", v[1], t[1], proto
|
||||
}
|
||||
}' "$xml_file"
|
||||
|
||||
# Volume mappings → -v 'host:container:mode'
|
||||
awk 'BEGIN{q=sprintf("%c",39)} /Type="Path"/ {
|
||||
match($0, /Target="([^"]+)"/, t)
|
||||
match($0, /Mode="([^"]+)"/, m)
|
||||
match($0, />([^<]+)<\/Config>/, v)
|
||||
if (t[1] != "" && v[1] != "") {
|
||||
mode = (m[1] == "ro") ? "ro" : "rw"
|
||||
printf " -v %s%s:%s:%s%s", q, v[1], t[1], mode, q
|
||||
}
|
||||
}' "$xml_file"
|
||||
|
||||
# Environment variables → -e 'KEY=VALUE' (single-quoted to protect $ and special chars)
|
||||
awk 'BEGIN{q=sprintf("%c",39)} /Type="Variable"/ {
|
||||
match($0, /Target="([^"]+)"/, t)
|
||||
match($0, />([^<]+)<\/Config>/, v)
|
||||
if (t[1] != "" && v[1] != "") {
|
||||
printf " -e %s%s=%s%s", q, t[1], v[1], q
|
||||
}
|
||||
}' "$xml_file"
|
||||
|
||||
printf " %q\n" "$repo"
|
||||
echo ""
|
||||
printf "docker start %q && echo 'deployed:%s'\n" "$name" "$name"
|
||||
} > "$tmp_script"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
warn " DRY RUN — would deploy $name on $MIRROR"
|
||||
rm -f "$tmp_script"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# SCP deploy script → remote, execute, clean up both sides
|
||||
local remote_script="/tmp/deploy_${name//[^a-zA-Z0-9_]/_}.sh"
|
||||
|
||||
if timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
|
||||
"$tmp_script" "root@${remote_ip}:${remote_script}" 2>/dev/null && \
|
||||
timeout 120 ssh -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" root@"$remote_ip" \
|
||||
"bash '$remote_script' 2>&1; rc=\$?; rm -f '$remote_script'; exit \$rc" 2>/dev/null | \
|
||||
grep -q "deployed:${name}"; then
|
||||
log " $name deployed ✅"
|
||||
rm -f "$tmp_script"
|
||||
return 0
|
||||
else
|
||||
warn " $name deployment failed — check $MIRROR manually"
|
||||
rm -f "$tmp_script"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ── HELPER: wait for a container on the remote to be healthy/running ─────────────────────────
|
||||
#
|
||||
# Polls docker inspect on the remote. Prefers the health status if a healthcheck is defined;
|
||||
# falls back to the running state for containers with no healthcheck. Non-fatal after timeout
|
||||
# — Authelia may take time to fully initialize but the deploy itself succeeded.
|
||||
# ==============================================================================================
|
||||
wait_for_container_healthy() {
|
||||
local name="$1" remote_ip="$2" ssh_key="$3"
|
||||
local max_wait=60 interval=5 elapsed=0
|
||||
|
||||
[[ "$DRY_RUN" == true ]] && return 0
|
||||
|
||||
log " Waiting for $name to be ready..."
|
||||
while (( elapsed < max_wait )); do
|
||||
local status
|
||||
status=$(timeout "$SSH_TIMEOUT" ssh -i "$ssh_key" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$remote_ip" \
|
||||
"h=\$(docker inspect --format '{{.State.Health.Status}}' '$name' 2>/dev/null)
|
||||
r=\$(docker inspect --format '{{.State.Running}}' '$name' 2>/dev/null)
|
||||
echo \${h:-\$r}" 2>/dev/null)
|
||||
|
||||
case "$status" in
|
||||
healthy|true)
|
||||
log " $name ready ✅"
|
||||
return 0
|
||||
;;
|
||||
starting|unhealthy|false|"")
|
||||
sleep "$interval"
|
||||
(( elapsed += interval ))
|
||||
;;
|
||||
*)
|
||||
sleep "$interval"
|
||||
(( elapsed += interval ))
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
warn " $name not confirmed healthy after ${max_wait}s — continuing (may affect dependents)"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ── HELPER: stop containers on the mirror by reading its own conf via SSH ────────────────────
|
||||
#
|
||||
@@ -427,46 +267,6 @@ stop_mirror_stack() {
|
||||
done
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ── HELPER: deploy a stack of XMLs to the mirror, health-checking db deps between batches ────
|
||||
#
|
||||
# Sets globals _STACK_DEPLOYED and _STACK_FAILED rather than printing to stdout.
|
||||
# This avoids the process-substitution capture problem: warn() writes to stdout, so any
|
||||
# read -r X Y < <(func) would capture warn output as the count values.
|
||||
# ==============================================================================================
|
||||
_STACK_DEPLOYED=0
|
||||
_STACK_FAILED=0
|
||||
|
||||
deploy_xml_stack() {
|
||||
local -n xml_array_ref="$1"
|
||||
_STACK_DEPLOYED=0
|
||||
_STACK_FAILED=0
|
||||
|
||||
for xml_name in "${xml_array_ref[@]}"; do
|
||||
local xml_file="${TEMPLATES_DIR}/${xml_name}"
|
||||
if [[ ! -f "$xml_file" ]]; then
|
||||
warn "$xml_name not found in $TEMPLATES_DIR — skipping"
|
||||
(( _STACK_FAILED++ ))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract container name to use for health-wait matching
|
||||
local cname
|
||||
cname=$(awk 'match($0,/<Name>([^<]+)<\/Name>/,a){print a[1];exit}' "$xml_file")
|
||||
|
||||
if deploy_container_from_xml "$xml_file" "$MIRROR_IP" "$MIRROR_SSH_KEY"; then
|
||||
(( _STACK_DEPLOYED++ ))
|
||||
# Health-check database deps before continuing — they must be ready before
|
||||
# Authelia/app containers that depend on them can start cleanly.
|
||||
if [[ -n "$cname" ]] && echo "$cname" | grep -qiE 'mariadb|redis|postgres|mysql'; then
|
||||
wait_for_container_healthy "$cname" "$MIRROR_IP" "$MIRROR_SSH_KEY"
|
||||
fi
|
||||
else
|
||||
(( _STACK_FAILED++ ))
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# ==============================================================================================
|
||||
# ── MIRROR PATH ───────────────────────────────────────────────────────────────────────────────
|
||||
# ==============================================================================================
|
||||
@@ -541,7 +341,6 @@ log "Mirror: $MIRROR ($MIRROR_IP)"
|
||||
echo ""
|
||||
|
||||
STEP_SSH_OK=false
|
||||
STEP_PLUGINS_OK=true
|
||||
STEP_STOP_AUTH_OK=true
|
||||
STEP_AUTH_OK=true
|
||||
AUTH_DEPLOYED=0
|
||||
@@ -620,7 +419,7 @@ if [[ "$PHASE1_ONLY" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━ Phase 1 — HOST1 Local Setup (SSH pending) ━━━"
|
||||
bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
|
||||
warn "Local setup had issues — FolderView3 may need manual setup"
|
||||
warn "Local setup had issues — check partnership_manager.sh output above"
|
||||
|
||||
END=$(date +%s)
|
||||
echo ""
|
||||
@@ -674,7 +473,7 @@ exit(\$failed > 0 ? 1 : 0);
|
||||
echo ""
|
||||
echo "━━━ Phase 1 — HOST1 Local Setup ━━━"
|
||||
bash "$SCRIPT_DIR/partnership_manager.sh" --onboard --local-only "${EXTRA_FLAGS[@]}" || \
|
||||
warn "Local setup had issues — FolderView3 may need manual setup"
|
||||
warn "Local setup had issues — check partnership_manager.sh output above"
|
||||
|
||||
[[ "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 1
|
||||
|
||||
@@ -693,36 +492,9 @@ exit(\$failed > 0 ? 1 : 0);
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Step 2: Plugins ───────────────────────────────────────────────────────────────────────────
|
||||
# ── Step 2: Stop mirror's existing auth stack ─────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━ Step 2 — Plugin Install on Mirror ━━━"
|
||||
|
||||
if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && [[ -n "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then
|
||||
FV3_PRESENT=$(timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \
|
||||
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$MIRROR_IP" \
|
||||
"test -d /usr/local/emhttp/plugins/folder.view3 && echo yes" 2>/dev/null)
|
||||
|
||||
if [[ "$FV3_PRESENT" == "yes" ]]; then
|
||||
log "FolderView3 already installed on $MIRROR ✅"
|
||||
elif [[ "$DRY_RUN" == true ]]; then
|
||||
warn "DRY RUN — would install FolderView3 on $MIRROR"
|
||||
else
|
||||
log "Installing FolderView3 on $MIRROR..."
|
||||
timeout 60 ssh -i "$MIRROR_SSH_KEY" -o ConnectTimeout="$SSH_TIMEOUT" root@"$MIRROR_IP" \
|
||||
"plugin install '$PARTNERSHIP_FOLDERVIEW3_URL' 2>/dev/null && echo installed" \
|
||||
2>/dev/null | grep -q installed && \
|
||||
log "FolderView3 installed ✅" || {
|
||||
warn "FolderView3 install failed — install manually from Community Applications"
|
||||
STEP_PLUGINS_OK=false
|
||||
}
|
||||
fi
|
||||
else
|
||||
log "FolderView3 not configured — skipping"
|
||||
fi
|
||||
|
||||
# ── Step 3: Stop mirror's existing auth stack ─────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━ Step 3 — Stop Mirror Auth Stack ━━━"
|
||||
echo "━━━ Step 2 — Stop Mirror Auth Stack ━━━"
|
||||
|
||||
if [[ "$SKIP_AUTH_STACK" == true ]]; then
|
||||
warn "Skipping (--skip-auth-stack)"
|
||||
@@ -732,7 +504,7 @@ fi
|
||||
|
||||
# ── Step 4: Deploy auth stack on mirror ───────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━ Step 4 — Deploy Auth Stack on Mirror ━━━"
|
||||
echo "━━━ Step 3 — Deploy Auth Stack on Mirror ━━━"
|
||||
|
||||
if [[ "$SKIP_AUTH_STACK" == true ]]; then
|
||||
warn "Skipping (--skip-auth-stack)"
|
||||
@@ -750,7 +522,7 @@ fi
|
||||
|
||||
# ── Step 5: Stop mirror's existing arr stack ──────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━ Step 5 — Stop Mirror Arr Stack ━━━"
|
||||
echo "━━━ Step 4 — Stop Mirror Arr Stack ━━━"
|
||||
|
||||
if [[ "$SKIP_ARR_STACK" == true ]]; then
|
||||
warn "Skipping (--skip-arr-stack)"
|
||||
@@ -763,7 +535,7 @@ fi
|
||||
|
||||
# ── Step 6: Deploy arr stack on mirror ───────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━ Step 6 — Deploy Arr Stack on Mirror ━━━"
|
||||
echo "━━━ Step 5 — Deploy Arr Stack on Mirror ━━━"
|
||||
|
||||
if [[ "$SKIP_ARR_STACK" == true ]]; then
|
||||
warn "Skipping (--skip-arr-stack)"
|
||||
@@ -777,7 +549,7 @@ fi
|
||||
|
||||
# ── Step 7: Partnership onboard ───────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━ Step 7 — Partnership Onboard ━━━"
|
||||
echo "━━━ Step 6 — Partnership Onboard ━━━"
|
||||
|
||||
if bash "$SCRIPTS_ROOT/Partnership/partnership_manager.sh" --onboard "${EXTRA_FLAGS[@]}"; then
|
||||
echo "Partnership onboard complete ✅"
|
||||
@@ -789,7 +561,7 @@ fi
|
||||
|
||||
# ── Step 8: Arr library bootstrap ─────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "━━━ Step 8 — Arr Library Bootstrap ━━━"
|
||||
echo "━━━ Step 7 — Arr Library Bootstrap ━━━"
|
||||
|
||||
if [[ "$ONBOARD_OK" == false ]]; then
|
||||
warn "Skipping — onboard did not complete"
|
||||
@@ -809,7 +581,7 @@ fi
|
||||
# SSH is now established and all partners have the plugin installed.
|
||||
# Push the authoritative master.conf so every listed host is in sync immediately.
|
||||
echo ""
|
||||
echo "━━━ $ICON_GEAR Step 9 — master.conf Push ━━━"
|
||||
echo "━━━ $ICON_GEAR Step 8 — master.conf Push ━━━"
|
||||
|
||||
if [[ "$ONBOARD_OK" == false ]]; then
|
||||
warn "Skipping — onboard did not complete"
|
||||
@@ -857,14 +629,13 @@ _ok() { [[ "$1" == true ]] && echo "✅" || echo "❌"; }
|
||||
_skip() { [[ "$1" == true ]] && echo "skipped" || echo "$(_ok "$2")"; }
|
||||
|
||||
echo " Step 1 — SSH keys: $(_skip "$SKIP_SSH" "$STEP_SSH_OK")"
|
||||
echo " Step 2 — Plugins: $(_ok "$STEP_PLUGINS_OK")"
|
||||
echo " Step 3 — Stop auth: $(_skip "$SKIP_AUTH_STACK" "$STEP_STOP_AUTH_OK")"
|
||||
echo " Step 4 — Auth stack: $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
|
||||
echo " Step 5 — Stop arr: $(_skip "$SKIP_ARR_STACK" "$STEP_STOP_ARR_OK")"
|
||||
echo " Step 6 — Arr stack: $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
|
||||
echo " Step 7 — Onboard: $(_ok "$ONBOARD_OK")"
|
||||
echo " Step 8 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
|
||||
echo " Step 9 — Conf push: $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"
|
||||
echo " Step 2 — Stop auth: $(_skip "$SKIP_AUTH_STACK" "$STEP_STOP_AUTH_OK")"
|
||||
echo " Step 3 — Auth stack: $( [[ "$SKIP_AUTH_STACK" == true ]] && echo "skipped" || echo "${AUTH_DEPLOYED} deployed, ${AUTH_FAILED} failed" )"
|
||||
echo " Step 4 — Stop arr: $(_skip "$SKIP_ARR_STACK" "$STEP_STOP_ARR_OK")"
|
||||
echo " Step 5 — Arr stack: $( [[ "$SKIP_ARR_STACK" == true ]] && echo "skipped" || echo "${ARR_DEPLOYED} deployed, ${ARR_FAILED} failed" )"
|
||||
echo " Step 6 — Onboard: $(_ok "$ONBOARD_OK")"
|
||||
echo " Step 7 — Arr bootstrap: $( [[ "$SKIP_ARR_SYNC" == true || "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$ARR_SYNC_OK")" )"
|
||||
echo " Step 8 — Conf push: $( [[ "$ONBOARD_OK" == false ]] && echo "skipped" || echo "$(_ok "$MASTER_PUSH_OK")" )"
|
||||
echo ""
|
||||
|
||||
if [[ "$ONBOARD_OK" == true ]]; then
|
||||
|
||||
@@ -45,14 +45,16 @@ source "$SCRIPTS_ROOT/load_config.sh"
|
||||
# ── Parse --force before parse_args ───────────────────────────────────────────────────────────
|
||||
MODE="setup"
|
||||
FORCE=false
|
||||
LOCAL_ONLY=false
|
||||
FILTERED_ARGS=()
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--force) FORCE=true ;;
|
||||
--validate) MODE="validate" ;;
|
||||
--status) MODE="status" ;;
|
||||
*) FILTERED_ARGS+=("$arg") ;;
|
||||
--force) FORCE=true ;;
|
||||
--validate) MODE="validate" ;;
|
||||
--status) MODE="status" ;;
|
||||
--local-only) LOCAL_ONLY=true ;;
|
||||
*) FILTERED_ARGS+=("$arg") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
@@ -325,6 +327,19 @@ else
|
||||
fi
|
||||
|
||||
# ── Copy to remote ────────────────────────────────────────────────────────────────────────────
|
||||
if [[ "$LOCAL_ONLY" == true ]]; then
|
||||
echo ""
|
||||
echo "━━━━━ $ICON_SUMMARY SSH SETUP SUMMARY (local) ━━━━━"
|
||||
echo " Key: $SSH_KEY_PATH"
|
||||
echo " Pubkey: $SSH_PUB_PATH"
|
||||
echo " Conf: ${KEY_CONF_VAR} in host${HOST_NUM}.conf"
|
||||
echo ""
|
||||
warn "Local setup complete — copy public key to remote manually:"
|
||||
warn " ssh-copy-id -i $SSH_PUB_PATH root@<remote>"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "━━━ $ICON_NET Copy Public Key to Remote ($REMOTE_SERVER_NAME) ━━━"
|
||||
|
||||
@@ -356,7 +371,6 @@ echo "━━━ $ICON_VERIFY Verify SSH Auth ━━━"
|
||||
if [[ "$DRY_RUN" == false ]]; then
|
||||
if test_ssh_auth "$REMOTE_SERVER"; then
|
||||
echo "SSH auth to $REMOTE_SERVER_NAME working ✅"
|
||||
# Reset any existing strikes
|
||||
if [[ -f "$SSH_STRIKE_FILE" ]]; then
|
||||
write_strike_file 0 "" "$(date '+%Y-%m-%d %H:%M:%S')"
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user