221 lines
9.1 KiB
Bash
221 lines
9.1 KiB
Bash
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ============================= Remote Arr Cache Writer ========================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# SSHes to each remote partner host, calls vv_arrs_local_node() on their PHP
|
|
# stack, and caches the result locally in /tmp/vv_cache/arrs_remote_hostN.json.
|
|
#
|
|
# The arrs page reads these files for instant initial load without hitting the
|
|
# remote arr APIs on every page view. This script runs every 2 hours so remote
|
|
# library counts stay reasonably current without hammering the network.
|
|
#
|
|
# Accepts --host=HOST2 to refresh a single host (used by the UI refresh button).
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL MODEL
|
|
# ==============================================================================================
|
|
#
|
|
# Per partner host:
|
|
# 1. Resolve its Tailscale IP — unresolvable skips that host, not the run
|
|
# 2. SSH across and call vv_arrs_local_node() on its own PHP stack
|
|
# 3. Write the JSON to /tmp/vv_cache/arrs_remote_<hostid>.json locally
|
|
#
|
|
# The remote builds its own payload rather than this host querying the remote's arr APIs
|
|
# directly — the partner already has working local URLs and keys for its own arrs, so no
|
|
# cross-host credentials or path mapping are involved.
|
|
#
|
|
# Runs every 2 hours. The arrs page reads these files for an instant first paint and falls
|
|
# back to live calls when a file is missing or stale.
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# Cache-First, Never Live on Page Load
|
|
# Remote arr APIs have non-trivial latency — calling them on every page view
|
|
# would make the arrs page slow and fragile. Writing to /tmp/vv_cache/ on a
|
|
# 2-hour schedule decouples page load time from network availability.
|
|
#
|
|
# Single-Host Refresh for UI
|
|
# The UI refresh button passes --host=HOSTN to update one host without waiting
|
|
# for the full 2-hour cycle. Keeps the cache fresh when a user requests it.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# acquire_lock — prevents concurrent cache write runs
|
|
# detect_hosts() — MY_ID and partner host list
|
|
# SSH reachability — skips a host cleanly if it cannot be reached
|
|
# /tmp/vv_cache/ — auto-created if missing; cleared on reboot (intentional)
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# host*.conf
|
|
#
|
|
# SSH_KEY
|
|
# Used to reach each partner. Written by ssh_setup.sh.
|
|
#
|
|
# HOST* — partner hostnames, resolved to Tailscale IPs at runtime
|
|
#
|
|
# Cache output: /tmp/vv_cache/arrs_remote_<hostid>.json
|
|
# tmpfs, cleared on reboot. Regenerable by definition — losing it costs one slow page
|
|
# load, never correctness, which is why nothing here retries hard on failure.
|
|
#
|
|
# No arr credentials are read locally. Each partner uses its own SONARR_*/RADARR_*/LIDARR_*
|
|
# values on its own side, so this host never holds keys for a remote's arrs.
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# remote_arr_cache_writer.sh — refresh all remote hosts
|
|
# remote_arr_cache_writer.sh --host=HOST2 — refresh one host only
|
|
# remote_arr_cache_writer.sh --dry-run — show what would happen
|
|
# remote_arr_cache_writer.sh --log — verbose output
|
|
#
|
|
# ==============================================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$SCRIPT_DIR/../../../load_config.sh"
|
|
|
|
parse_args "$@"
|
|
|
|
# Reads $SSH_KEY from /root/.ssh to reach partner hosts.
|
|
[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
|
|
|
|
acquire_lock
|
|
detect_hosts
|
|
|
|
# Parse --host= from raw args
|
|
TARGET_HOST=""
|
|
for arg in "$@"; do
|
|
case "$arg" in --host=*) TARGET_HOST="${arg#--host=}" ;; esac
|
|
done
|
|
|
|
mkdir -p "$VV_CACHE_DIR"
|
|
|
|
log "$ICON_GEAR Config: target=${TARGET_HOST:-all hosts} ssh-key=${SSH_KEY}"
|
|
|
|
FETCH_OK=0
|
|
FETCH_FAIL=0
|
|
FETCH_SKIP=0
|
|
|
|
echo ""
|
|
echo "━━━ $ICON_SYNC Remote Arr Cache Writer ━━━"
|
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
|
[[ -n "$TARGET_HOST" ]] && echo " Target: $TARGET_HOST"
|
|
echo ""
|
|
|
|
for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do
|
|
[[ "$host_var" == "$MY_ID" ]] && continue
|
|
|
|
hostname="${!host_var:-}"
|
|
[[ -z "$hostname" ]] && continue
|
|
|
|
# If targeting a specific host, skip others
|
|
if [[ -n "$TARGET_HOST" ]]; then
|
|
[[ "${host_var,,}" != "${TARGET_HOST,,}" && "$host_var" != "$TARGET_HOST" ]] && continue
|
|
fi
|
|
|
|
host_id="${host_var,,}" # host1, host2, …
|
|
cache_file="$VV_CACHE_DIR/arrs_remote_${host_id}.json"
|
|
|
|
echo " $host_var ($hostname)…"
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn " DRY RUN — would SSH to $hostname and cache arr data"
|
|
(( FETCH_SKIP++ ))
|
|
continue
|
|
fi
|
|
|
|
# Resolve Tailscale IP
|
|
REMOTE_IP=$(resolve_tailscale_ip "$hostname")
|
|
if [[ -z "$REMOTE_IP" ]]; then
|
|
warn " $host_var: cannot resolve Tailscale IP for $hostname — skipping"
|
|
(( FETCH_FAIL++ ))
|
|
continue
|
|
fi
|
|
log " $host_var: resolved $hostname → $REMOTE_IP"
|
|
|
|
# Single SSH connection — fetch arrs + monitor stats together
|
|
RESULT=$(ssh -i "$SSH_KEY" \
|
|
-o ConnectTimeout=10 \
|
|
-o StrictHostKeyChecking=no \
|
|
-o BatchMode=yes \
|
|
"root@${REMOTE_IP}" \
|
|
"php -r \"
|
|
require_once '/usr/local/emhttp/plugins/varaverk/include/arrs.php';
|
|
require_once '/usr/local/emhttp/plugins/varaverk/include/unraid_api.php';
|
|
require_once '/usr/local/emhttp/plugins/varaverk/include/media.php';
|
|
echo json_encode([
|
|
'arrs' => vv_arrs_local_node(),
|
|
'monitor' => vv_local_host_stats(),
|
|
'media' => function_exists('vv_media_server_stats') ? vv_media_server_stats() : [],
|
|
]);
|
|
\"" 2>/dev/null)
|
|
|
|
if [[ -z "$RESULT" ]]; then
|
|
warn " $host_var: empty SSH response — skipping"
|
|
(( FETCH_FAIL++ ))
|
|
continue
|
|
fi
|
|
|
|
# Validate combined JSON
|
|
if ! echo "$RESULT" | php -r "
|
|
\$d = json_decode(file_get_contents('php://stdin'), true);
|
|
exit(!is_array(\$d) || !isset(\$d['arrs'], \$d['monitor']) ? 1 : 0);
|
|
" 2>/dev/null; then
|
|
warn " $host_var: invalid JSON — skipping"
|
|
log " Response: ${RESULT:0:200}"
|
|
(( FETCH_FAIL++ ))
|
|
continue
|
|
fi
|
|
|
|
# Save arr cache
|
|
echo "$RESULT" | php -r "
|
|
\$d = json_decode(file_get_contents('php://stdin'), true);
|
|
file_put_contents('$cache_file', json_encode(\$d['arrs']));
|
|
" 2>/dev/null
|
|
|
|
# Save monitor cache
|
|
MONITOR_CACHE="$VV_CACHE_DIR/monitor_remote_${host_id}.json"
|
|
echo "$RESULT" | php -r "
|
|
\$d = json_decode(file_get_contents('php://stdin'), true);
|
|
file_put_contents('$MONITOR_CACHE', json_encode(\$d['monitor']));
|
|
" 2>/dev/null
|
|
|
|
# Save media server cache. Collected here rather than queried directly because a partner's
|
|
# Emby URL is http://localhost:8096 — true on that host and meaningless from this one. The
|
|
# same reason the arrs are collected this way: each host answers about itself, and its API
|
|
# keys never leave it.
|
|
#
|
|
# Absent on a partner running an older build, which is why it is written only when present:
|
|
# an empty file would read as "no media servers there" rather than "not collected yet".
|
|
MEDIA_CACHE="$VV_CACHE_DIR/media_remote_${host_id}.json"
|
|
echo "$RESULT" | php -r "
|
|
\$d = json_decode(file_get_contents('php://stdin'), true);
|
|
if (!empty(\$d['media'])) file_put_contents('$MEDIA_CACHE', json_encode(\$d['media']));
|
|
" 2>/dev/null
|
|
|
|
CACHED_TYPES=$(echo "$RESULT" | php -r "
|
|
\$d = json_decode(file_get_contents('php://stdin'), true);
|
|
echo implode(', ', array_column(\$d['arrs']['arrs'] ?? [], 'type'));
|
|
" 2>/dev/null)
|
|
echo " $host_var: arrs [${CACHED_TYPES:-none}] + monitor stats cached ✅"
|
|
(( FETCH_OK++ ))
|
|
done
|
|
|
|
echo ""
|
|
echo "━━━━━ $ICON_SUMMARY Cache Writer Summary ━━━━━"
|
|
echo " $FETCH_OK updated · $FETCH_FAIL failed · $FETCH_SKIP skipped"
|
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
|
|
[[ "$FETCH_FAIL" -gt 0 ]] && exit 1
|
|
exit 0
|