#!/bin/bash # ============================================================================================== # ========================= Upgrade Webhook Setup ============================================= # ============================================================================================== # # PURPOSE # ───────────────────────────────────────────────────────────────────────────── # Registers the Varaverk upgrade webhook in Sonarr, Radarr, and Lidarr via # their notification APIs. Idempotent — skips any arr that already has it. # # Run once after first install, or any time you add a new arr or host. # The listener (start_webhook_listener.sh) must be running before arrs will # actually deliver events, but this script can register the connection first. # # ============================================================================================== # OPERATIONAL MODEL # ============================================================================================== # # 1. Generate WEBHOOK_SECRET in master.conf if empty # 2. Register webhook in each local arr (Sonarr / Radarr / Lidarr) # 3. SSH to remote host and run itself there (unless --local-only) # # ============================================================================================== # DESIGN PRINCIPLES # ============================================================================================== # # Idempotent Registration # Skips any arr that already has the webhook registered. Safe to re-run after # adding a new arr or after a conf change without creating duplicate entries. # # Self-Propagating # SSHes to the remote and runs itself with --local-only — one execution # configures both servers without a separate remote step. # # ============================================================================================== # OPERATIONAL SAFEGUARDS # ============================================================================================== # # Idempotent — skips arrs that already have the webhook registered # Secret auto-gen — WEBHOOK_SECRET generated if empty; never left blank # --local-only — used internally for SSH; prevents infinite recursion # --dry-run mode — shows what would be registered without making API calls # acquire_lock — prevents concurrent registration runs # # ============================================================================================== # CONFIGURATION # ============================================================================================== # # master.conf # # WEBHOOK_PORT # Port the listener binds and the registered webhook URL points at. 0 disables the # listener entirely (start_webhook_listener.sh exits early), so registering against # a port of 0 would produce URLs nothing is serving. # # WEBHOOK_SECRET # Shared secret embedded in the registered URL as ?key=. Generated here on first run # and written back into master.conf — the write-back is verified, because the arrs # are registered with this value and a failed persist would leave them holding a # secret this host does not have. # # host*.conf (aliased by detect_hosts()) # # SONARR_URL / SONARR_API_KEY # RADARR_URL / RADARR_API_KEY # LIDARR_URL / LIDARR_API_KEY # Each arr the webhook is registered in. An arr with no URL or key configured on # this host is skipped rather than failing the run. # # SSH_KEY # Used when propagating the same secret to the partner via OVERRIDE_SECRET. # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # webhook_setup.sh # Configure local arrs, then SSH to remote and configure remote arrs. # # webhook_setup.sh --local-only # Local arrs only (used internally when called via SSH on the remote). # # webhook_setup.sh --dry-run # Show what would be registered without making any changes. # # webhook_setup.sh --log # Verbose output. # # ============================================================================================== set -uo pipefail LOCAL_ONLY=false for arg in "$@"; do case "$arg" in --local-only) LOCAL_ONLY=true ;; esac done SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ECOSYSTEM_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" MASTER_CONF="$ECOSYSTEM_ROOT/Configurations/master.conf" source "$ECOSYSTEM_ROOT/load_config.sh" parse_args "$@" # Writes the generated secret into master.conf via sed -i and SSHes to the partner. if [[ "$EUID" -ne 0 ]]; then error "Must be run as root" exit 1 fi acquire_lock detect_hosts WEBHOOK_NAME="Varaverk Upgrade" # ── Resolve or generate the secret ────────────────────────────────────────── # OVERRIDE_SECRET env var is set when called recursively via SSH from the # primary host, so both ends use the same secret. # # Every write-back below is verified by re-reading master.conf. The secret gets baked into # the webhook URL registered in each arr — if the sed silently fails to match, the arrs end # up holding a secret this host does not have, and the listener rejects every delivery. _persist_secret() { local secret="$1" sed -i "s/WEBHOOK_SECRET=\"\"/WEBHOOK_SECRET=\"$secret\"/" "$MASTER_CONF" if ! grep -q "WEBHOOK_SECRET=\"$secret\"" "$MASTER_CONF" 2>/dev/null; then error "Could not persist WEBHOOK_SECRET to $MASTER_CONF" error "Registering the arrs now would leave them with a secret this host does not have" notify "Webhook setup aborted on $(hostname) — could not persist WEBHOOK_SECRET" \ "Webhook Setup" "warning" exit 1 fi } if [[ -n "${OVERRIDE_SECRET:-}" ]]; then if [[ -z "${WEBHOOK_SECRET:-}" ]]; then _persist_secret "$OVERRIDE_SECRET" fi WEBHOOK_SECRET="$OVERRIDE_SECRET" fi if [[ -z "${WEBHOOK_SECRET:-}" ]]; then if [[ "$DRY_RUN" == true ]]; then WEBHOOK_SECRET="" else if ! command -v openssl >/dev/null 2>&1; then error "openssl not found — cannot generate WEBHOOK_SECRET" exit 1 fi GENERATED=$(openssl rand -hex 32) _persist_secret "$GENERATED" WEBHOOK_SECRET="$GENERATED" echo "Generated WEBHOOK_SECRET — saved to master.conf" fi fi LOCAL_IP=$(hostname -I | awk '{print $1}') WEBHOOK_URL="http://${LOCAL_IP}:${WEBHOOK_PORT}/webhook?key=${WEBHOOK_SECRET}" # ── Helper: register webhook in one arr ───────────────────────────────────── _register() { local label="$1" base_url="$2" api_key="$3" api_ver="$4" local import_field="$5" # onDownload (Sonarr/Radarr) or onReleaseImport (Lidarr) if [[ "$DRY_RUN" == true ]]; then echo " [$label] Would register → $WEBHOOK_URL" return 0 fi # Check if already registered by name local existing existing=$(curl -sf --max-time 5 \ -H "X-Api-Key: $api_key" \ "$base_url/api/$api_ver/notification" 2>/dev/null) || { echo " [$label] Cannot reach arr — skipping" return 1 } if echo "$existing" | grep -q "\"name\":[[:space:]]*\"${WEBHOOK_NAME}\""; then echo " [$label] Already registered — skipping" return 0 fi local payload payload=$(printf '{ "name": "%s", "implementation": "Webhook", "configContract": "WebhookSettings", "onGrab": false, "%s": true, "onUpgrade": true, "onRename": false, "onHealthIssue": false, "includeHealthWarnings": false, "onApplicationUpdate": false, "tags": [], "fields": [ {"name": "url", "value": "%s"}, {"name": "method", "value": 1}, {"name": "username", "value": ""}, {"name": "password", "value": ""} ] }' "$WEBHOOK_NAME" "$import_field" "$WEBHOOK_URL") local http_code http_code=$(curl -sf --max-time 5 -o /dev/null -w '%{http_code}' \ -X POST \ -H "X-Api-Key: $api_key" \ -H 'Content-Type: application/json' \ -d "$payload" \ "$base_url/api/$api_ver/notification" 2>/dev/null) if [[ "$http_code" == "201" || "$http_code" == "200" ]]; then echo " [$label] Registered ✅ → $WEBHOOK_URL" else echo " [$label] Failed (HTTP ${http_code:-timeout})" return 1 fi } # ── Local arrs ─────────────────────────────────────────────────────────────── echo "" echo "━━━ Upgrade Webhook Setup — $MY_ID ($LOCAL_SERVER_NAME) ━━━" echo " LAN IP : $LOCAL_IP" echo " Port : $WEBHOOK_PORT" echo " URL : $WEBHOOK_URL" echo "" [[ -n "${SONARR_URL:-}" && -n "${SONARR_API_KEY:-}" ]] && \ _register "Sonarr" "$SONARR_URL" "$SONARR_API_KEY" "v3" "onDownload" [[ -n "${RADARR_URL:-}" && -n "${RADARR_API_KEY:-}" ]] && \ _register "Radarr" "$RADARR_URL" "$RADARR_API_KEY" "v3" "onDownload" [[ -n "${LIDARR_URL:-}" && -n "${LIDARR_API_KEY:-}" ]] && \ _register "Lidarr" "$LIDARR_URL" "$LIDARR_API_KEY" "v1" "onReleaseImport" # ── Remote host ─────────────────────────────────────────────────────────────── if [[ "$LOCAL_ONLY" == false && -n "${REMOTE_ID:-}" ]]; then echo "" echo "━━━ Configuring remote: $REMOTE_SERVER_NAME ━━━" remote_ip=$(resolve_tailscale_ip "$REMOTE_SERVER_NAME") || { echo " Cannot resolve Tailscale IP for $REMOTE_SERVER_NAME — skipping" echo " Run manually on that host: Tools/webhook_setup.sh --local-only" echo "" exit 0 } remote_flags="--local-only" [[ "$DRY_RUN" == true ]] && remote_flags="$remote_flags --dry-run" # The remote's own SCRIPTS_DIR, read from its varaverk.cfg — never the flash plugin path. # That was hardcoded here, so a partner in appdata storage mode got # "bash: .../Tools/webhook_setup.sh: No such file or directory" and the generic # "check SSH and that Varaverk is installed" message, which pointed at two things that # were both fine. HOST2 has run in appdata mode since it was installed. SSH_TIMEOUT="${SSH_TIMEOUT:-10}" remote_sd=$(resolve_remote_scripts_dir "$remote_ip" "$SSH_KEY" "no") remote_script="${remote_sd}/Tools/webhook_setup.sh" ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes -o StrictHostKeyChecking=no \ root@"$remote_ip" \ "[ -f '$remote_script' ] || { echo missing; exit 127; } OVERRIDE_SECRET='$WEBHOOK_SECRET' bash '$remote_script' $remote_flags" \ || echo " Remote setup failed — no webhook_setup.sh at $remote_script on $REMOTE_SERVER_NAME, or SSH refused" fi echo "" echo "Done."