Codebase-wide audit pass: fixed real bugs (SSH hangs missing BatchMode, local-outside-function no-ops, variable name collisions, a truncated ratio calc, wrong state-dir path, DARK vs NO_INTERNET drift, and more), then pulled logic that was duplicated across multiple scripts — arr cleanup safety gates, docker restart ordering, container maintenance stop/restart, watchdog state-file helpers, partnership role resolution, cert expiry checks, remote node discovery, and TMDB discovery scoring — into common.sh so each now has a single implementation.
180 lines
8.2 KiB
Bash
Executable File
180 lines
8.2 KiB
Bash
Executable File
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ============================= share_setup.sh =================================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Creates missing Unraid shares on the mirror during partnership onboarding.
|
|
# Safe to re-run — existing shares are never modified, only missing ones created.
|
|
#
|
|
# For each path in the owner's sync share lists (daily, weekly, critical,
|
|
# intermediate):
|
|
# - Extracts the top-level Unraid share name
|
|
# - Skips if the remote already has a .cfg for it
|
|
# - Uses the local share .cfg as a template: substitutes the remote's detected
|
|
# appdata pool, clears disk include/exclude (disk layouts differ per server)
|
|
# - Writes the .cfg to remote /boot/config/shares/ and mkdir -p the directory
|
|
# - For sub-paths (e.g. appdata-Fallback/Critical-Data), ensures the subdir
|
|
# exists after the top-level share is in place
|
|
#
|
|
# Pool detection: reads the remote's appdata.cfg to find its cache pool name,
|
|
# so appdata-type shares land on the right pool without hardcoding.
|
|
#
|
|
# Media shares (shareUseCache=no in local .cfg) are created as array-only.
|
|
# Remote admin assigns disk sets from the Unraid UI after onboarding.
|
|
#
|
|
# ==============================================================================================
|
|
# USAGE
|
|
# ==============================================================================================
|
|
#
|
|
# share_setup.sh
|
|
# Create missing shares on the configured mirror. Skips existing.
|
|
#
|
|
# share_setup.sh --dry-run
|
|
# Show what would be created without writing anything.
|
|
#
|
|
# ==============================================================================================
|
|
|
|
set -uo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$SCRIPT_DIR/../load_config.sh"
|
|
detect_hosts
|
|
parse_args "$@"
|
|
|
|
# ── Resolve mirror ────────────────────────────────────────────────────────────────────────────
|
|
|
|
[[ -z "${REMOTE_SERVER_NAME:-}" ]] && { error "Cannot determine mirror hostname — check HOST* in master.conf"; exit 1; }
|
|
MIRROR="$REMOTE_SERVER_NAME"
|
|
|
|
MIRROR_IP=$(resolve_tailscale_ip "$MIRROR")
|
|
[[ -z "$MIRROR_IP" ]] && { error "Cannot resolve $MIRROR Tailscale IP — is Tailscale running?"; exit 1; }
|
|
|
|
_ssh() { ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=no root@"$MIRROR_IP" "$@"; }
|
|
_scp() { scp -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=no "$@"; }
|
|
|
|
# ── Detect remote appdata pool ────────────────────────────────────────────────────────────────
|
|
|
|
REMOTE_POOL=$(_ssh "grep -oP '(?<=shareCachePool=\")[^\"]+' /boot/config/shares/appdata.cfg 2>/dev/null | head -1" 2>/dev/null || true)
|
|
REMOTE_POOL="${REMOTE_POOL:-cache}"
|
|
log "Remote appdata pool: $REMOTE_POOL"
|
|
|
|
# ── Collect paths from all sync share arrays ──────────────────────────────────────────────────
|
|
|
|
declare -a ALL_PATHS=()
|
|
|
|
_add_array() {
|
|
local varname="$1"
|
|
local -n _ref="$varname" 2>/dev/null || return 0
|
|
for entry in "${_ref[@]+"${_ref[@]}"}"; do
|
|
[[ -n "$entry" ]] && ALL_PATHS+=("${entry%%|*}")
|
|
done
|
|
}
|
|
|
|
_add_array "${MY_ID}_DAILY_SYNC_SHARES"
|
|
_add_array "${MY_ID}_WEEKLY_SYNC_SHARES"
|
|
_add_array "${MY_ID}_CRITICAL_SYNC_SHARES"
|
|
_add_array "${MY_ID}_INTERMEDIATE_SYNC_SHARES"
|
|
|
|
# ── Separate top-level shares from sub-paths ──────────────────────────────────────────────────
|
|
|
|
declare -A SEEN_TOP=()
|
|
declare -A SEEN_SUB=()
|
|
declare -a TOP_SHARES=()
|
|
declare -a SUB_PATHS=()
|
|
|
|
for path in "${ALL_PATHS[@]+"${ALL_PATHS[@]}"}"; do
|
|
[[ "$path" != /mnt/user/* ]] && continue
|
|
rel="${path#/mnt/user/}"
|
|
top="${rel%%/*}"
|
|
[[ -z "${SEEN_TOP[$top]+_}" ]] && { TOP_SHARES+=("$top"); SEEN_TOP["$top"]=1; }
|
|
if [[ "$rel" == */* && -z "${SEEN_SUB[$path]+_}" ]]; then
|
|
SUB_PATHS+=("$path"); SEEN_SUB["$path"]=1
|
|
fi
|
|
done
|
|
|
|
[[ ${#TOP_SHARES[@]} -eq 0 ]] && { echo "No shares to process."; exit 0; }
|
|
|
|
# ── Build .cfg for a share ────────────────────────────────────────────────────────────────────
|
|
# Uses local .cfg as template: substitutes pool, clears disk sets, drops comment.
|
|
# Falls back to a minimal cfg inferred from share name when no local .cfg exists.
|
|
|
|
_make_cfg() {
|
|
local share_name="$1"
|
|
local local_cfg="/boot/config/shares/${share_name}.cfg"
|
|
|
|
if [[ -f "$local_cfg" ]]; then
|
|
sed \
|
|
-e "s|^shareCachePool=\"[^\"]*\"|shareCachePool=\"${REMOTE_POOL}\"|" \
|
|
-e 's|^shareInclude="[^"]*"|shareInclude=""|' \
|
|
-e 's|^shareExclude="[^"]*"|shareExclude=""|' \
|
|
-e '/^shareComment=/d' \
|
|
"$local_cfg"
|
|
else
|
|
local use_cache="no" pool="" allocator="mostfree"
|
|
case "$share_name" in appdata*|Media_Server*)
|
|
use_cache="yes"; pool="$REMOTE_POOL"; allocator="highwater" ;;
|
|
esac
|
|
printf 'shareUseCache="%s"\nshareCachePool="%s"\nshareCachePool2=""\n' "$use_cache" "$pool"
|
|
printf 'shareCOW="auto"\nshareAllocator="%s"\nshareInclude=""\nshareExclude=""\n' "$allocator"
|
|
printf 'shareFloor="0"\nshareExport="e"\nshareCaseSensitive="auto"\n'
|
|
printf 'shareSecurity="private"\nshareReadList=""\nshareWriteList=""\n'
|
|
printf 'shareVolsizelimit=""\nshareExportNFS="-"\nshareExportNFSFsid="0"\n'
|
|
printf 'shareSecurityNFS="public"\nshareHostListNFS=""\n'
|
|
fi
|
|
}
|
|
|
|
# ── Process each top-level share ──────────────────────────────────────────────────────────────
|
|
|
|
TMPFILE=$(mktemp)
|
|
trap 'rm -f "$TMPFILE"' EXIT
|
|
|
|
CREATED=0 SKIPPED=0
|
|
|
|
for share in "${TOP_SHARES[@]}"; do
|
|
existing=$(_ssh "[[ -f '/boot/config/shares/${share}.cfg' ]] && echo yes || echo no" 2>/dev/null)
|
|
|
|
if [[ "$existing" == "yes" ]]; then
|
|
log " exists: $share"
|
|
(( SKIPPED++ )) || true
|
|
continue
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
local_cfg="/boot/config/shares/${share}.cfg"
|
|
if [[ -f "$local_cfg" ]]; then
|
|
use_cache=$(grep -oP '(?<=shareUseCache=")[^"]+' "$local_cfg" || echo "?")
|
|
echo " [dry-run] CREATE $share (useCache=$use_cache → pool: $REMOTE_POOL)"
|
|
else
|
|
echo " [dry-run] CREATE $share (no local cfg — minimal fallback)"
|
|
fi
|
|
(( CREATED++ )) || true
|
|
continue
|
|
fi
|
|
|
|
_make_cfg "$share" > "$TMPFILE"
|
|
_scp "$TMPFILE" "root@${MIRROR_IP}:/boot/config/shares/${share}.cfg" >/dev/null
|
|
_ssh "mkdir -p /mnt/user/${share}" >/dev/null
|
|
echo " Created: $share ✅"
|
|
(( CREATED++ )) || true
|
|
done
|
|
|
|
# ── Ensure sub-path directories exist ────────────────────────────────────────────────────────
|
|
|
|
for path in "${SUB_PATHS[@]+"${SUB_PATHS[@]}"}"; do
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
echo " [dry-run] MKDIR $path"
|
|
continue
|
|
fi
|
|
_ssh "mkdir -p '$path'" 2>/dev/null && log " mkdir: $path" || true
|
|
done
|
|
|
|
# ── Summary ───────────────────────────────────────────────────────────────────────────────────
|
|
|
|
echo ""
|
|
echo " Shares: $CREATED created, $SKIPPED already existed on $MIRROR"
|
|
[[ "$DRY_RUN" == true ]] && echo " (dry-run — nothing written)"
|
|
|
|
exit 0
|