API key: use space separator, fix error-capture bug; add conf_sync and missing files from dev
unRAID API rejects underscores and hyphens in key names — only letters, numbers, and spaces allowed. Varaverk_HOST1 / Varaverk_Gmer4Lfe both fail; now uses "Varaverk <hostname>" (space). Also adds monitor_remote.php, conf_populate.sh, and conf_sync.sh from dev branch that were missing from production.
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
header('Content-Type: application/json');
|
||||||
|
header('Cache-Control: no-cache, no-store');
|
||||||
|
require_once dirname(__DIR__) . '/include/monitor.php';
|
||||||
|
|
||||||
|
$cacheFile = '/tmp/vv_cache_monitor_remote.json';
|
||||||
|
$cacheTTL = 3600;
|
||||||
|
|
||||||
|
if (!isset($_GET['live']) && file_exists($cacheFile) && (time() - filemtime($cacheFile)) < $cacheTTL) {
|
||||||
|
echo file_get_contents($cacheFile);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$out = json_encode([
|
||||||
|
'remote_hosts' => vv_remote_hosts_stats(),
|
||||||
|
'ts' => time(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
file_put_contents($cacheFile, $out);
|
||||||
|
echo $out;
|
||||||
Executable
+226
@@ -0,0 +1,226 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==============================================================================================
|
||||||
|
# ============================= Conf Auto-Populate =============================================
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# PURPOSE
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Reads credentials and settings from locally running services and writes
|
||||||
|
# them into the local host conf. Safe to run multiple times — only populates
|
||||||
|
# EMPTY fields, never overwrites existing values unless --overwrite is passed.
|
||||||
|
#
|
||||||
|
# After populating, pushes the updated conf to all partners via conf_sync.sh
|
||||||
|
# so they have the fresh keys in their /tmp/.vv/ cache immediately.
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
# AUTO-DETECTED FIELDS
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# HOSTN_RADARR_API_KEY from Radarr config.xml (found via docker volume mount)
|
||||||
|
# HOSTN_SONARR_API_KEY from Sonarr config.xml
|
||||||
|
# HOSTN_LIDARR_API_KEY from Lidarr config.xml
|
||||||
|
# HOSTN_SLSKD_API_KEY from slskd config.yml
|
||||||
|
# HOSTN_SABNZBD_API_KEY from sabnzbd.ini
|
||||||
|
# HOSTN_EMBY_CONTAINER fuzzy match from docker ps
|
||||||
|
# HOSTN_JELLYFIN_CONTAINER fuzzy match from docker ps
|
||||||
|
# HOSTN_RADARR_MOVIE_ROOT from Radarr rootFolder API
|
||||||
|
# HOSTN_SONARR_TV_ROOT from Sonarr rootFolder API
|
||||||
|
# HOSTN_LIDARR_MUSIC_ROOT from Lidarr rootFolder API
|
||||||
|
# HOSTN_SYS_WATCHDOG_NIC from ip route default gateway interface
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
# RUNTIME MODES
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# conf_populate.sh Populate empty fields only
|
||||||
|
# conf_populate.sh --overwrite Overwrite all detected fields (re-sync after arr key rotation)
|
||||||
|
# conf_populate.sh --dry-run Show what would be written, no changes
|
||||||
|
# conf_populate.sh --log Verbose output
|
||||||
|
# conf_populate.sh --no-push Skip pushing to partners after update
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../load_config.sh"
|
||||||
|
|
||||||
|
OVERWRITE=false
|
||||||
|
NO_PUSH=false
|
||||||
|
FILTERED_ARGS=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--overwrite) OVERWRITE=true ;;
|
||||||
|
--no-push) NO_PUSH=true ;;
|
||||||
|
*) FILTERED_ARGS+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
parse_args "${FILTERED_ARGS[@]}"
|
||||||
|
detect_hosts
|
||||||
|
|
||||||
|
[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
|
||||||
|
|
||||||
|
CONF_FILE="$SCRIPTS_ROOT/Configurations/${MY_ID,,}.conf"
|
||||||
|
[[ ! -f "$CONF_FILE" ]] && { error "Conf file not found: $CONF_FILE"; exit 1; }
|
||||||
|
|
||||||
|
UPDATED=0
|
||||||
|
SKIPPED=0
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━ $ICON_GEAR Conf Auto-Populate — $MY_ID ($LOCAL_SERVER_NAME) ━━━"
|
||||||
|
echo ""
|
||||||
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||||
|
[[ "$OVERWRITE" == true ]] && warn "OVERWRITE mode — existing values will be replaced"
|
||||||
|
|
||||||
|
# ── Helper: write a var into conf if empty (or --overwrite) ──────────────────
|
||||||
|
_set_conf_var() {
|
||||||
|
local var_name="$1" value="$2" label="$3"
|
||||||
|
[[ -z "$value" ]] && return
|
||||||
|
|
||||||
|
# Check current value in conf
|
||||||
|
local current
|
||||||
|
current=$(grep -oP "(?<=^\s*${var_name}=\")[^\"]*" "$CONF_FILE" 2>/dev/null | head -1)
|
||||||
|
|
||||||
|
if [[ -n "$current" ]] && [[ "$OVERWRITE" == false ]]; then
|
||||||
|
log "$label: already set (${current:0:8}…) — skipping"
|
||||||
|
(( SKIPPED++ ))
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
warn "DRY RUN — would set $var_name = ${value:0:8}…"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Update or append the var line
|
||||||
|
if grep -q "^\s*${var_name}=" "$CONF_FILE"; then
|
||||||
|
sed -i "s|^\(\s*${var_name}\s*=\s*\)\"[^\"]*\"|\1\"${value}\"|" "$CONF_FILE"
|
||||||
|
else
|
||||||
|
printf '\n %s="%s"\n' "$var_name" "$value" >> "$CONF_FILE"
|
||||||
|
fi
|
||||||
|
info "$label: set ✅"
|
||||||
|
(( UPDATED++ ))
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Helper: find arr config dir via docker volume mount ───────────────────────
|
||||||
|
# Looks for a container matching the pattern, then reads its /config volume path.
|
||||||
|
# Falls back to DOCKER_APPDATA_BASE/<ContainerName> if volume not found.
|
||||||
|
_arr_config_dir() {
|
||||||
|
local pattern="$1"
|
||||||
|
local container_name
|
||||||
|
container_name=$(docker ps -a --format '{{.Names}}' 2>/dev/null | \
|
||||||
|
grep -im1 "^${pattern}")
|
||||||
|
[[ -z "$container_name" ]] && return 1
|
||||||
|
|
||||||
|
local config_path
|
||||||
|
config_path=$(docker inspect "$container_name" 2>/dev/null | \
|
||||||
|
jq -r '.[0].Mounts[]? | select(.Destination == "/config") | .Source' 2>/dev/null | head -1)
|
||||||
|
[[ -z "$config_path" ]] && config_path="${DOCKER_APPDATA_BASE:-/mnt/user/appdata}/${container_name}"
|
||||||
|
|
||||||
|
[[ -d "$config_path" ]] && echo "$config_path" || return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Helper: read XML tag value ────────────────────────────────────────────────
|
||||||
|
_xml_val() {
|
||||||
|
local file="$1" tag="$2"
|
||||||
|
grep -oP "(?<=<${tag}>)[^<]+" "$file" 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Arr API keys + root paths ─────────────────────────────────────────────────────────────────
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
for arr in radarr sonarr lidarr; do
|
||||||
|
arr_upper="${arr^^}"
|
||||||
|
config_dir=$(_arr_config_dir "$arr") || {
|
||||||
|
log "${arr_upper}: no running container found — skipping"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
config_xml="${config_dir}/config.xml"
|
||||||
|
|
||||||
|
if [[ ! -f "$config_xml" ]]; then
|
||||||
|
log "${arr_upper}: config.xml not found at $config_xml — skipping"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
key=$(_xml_val "$config_xml" "ApiKey")
|
||||||
|
port=$(_xml_val "$config_xml" "Port")
|
||||||
|
url_base="http://localhost:${port:-$(case $arr in radarr) echo 7878;; sonarr) echo 8989;; lidarr) echo 8686;; esac)}"
|
||||||
|
|
||||||
|
_set_conf_var "${MY_ID}_${arr_upper}_API_KEY" "$key" "${arr_upper} API key"
|
||||||
|
|
||||||
|
# Root paths from arr's own rootFolder API
|
||||||
|
if [[ -n "$key" ]]; then
|
||||||
|
local api_ver; case "$arr" in lidarr) api_ver="v1" ;; *) api_ver="v3" ;; esac
|
||||||
|
root_json=$(curl -sf --max-time 5 \
|
||||||
|
-H "X-Api-Key: $key" "${url_base}/api/${api_ver}/rootfolder" 2>/dev/null)
|
||||||
|
root_path=$(echo "$root_json" | jq -r '.[0].path // empty' 2>/dev/null)
|
||||||
|
|
||||||
|
case "$arr" in
|
||||||
|
radarr) _set_conf_var "${MY_ID}_RADARR_MOVIE_ROOT" "$root_path" "Radarr movie root" ;;
|
||||||
|
sonarr) _set_conf_var "${MY_ID}_SONARR_TV_ROOT" "$root_path" "Sonarr TV root" ;;
|
||||||
|
lidarr) _set_conf_var "${MY_ID}_LIDARR_MUSIC_ROOT" "$root_path" "Lidarr music root" ;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── SABnzbd API key ───────────────────────────────────────────────────────────────────────────
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
sab_dir=$(_arr_config_dir "sabnzbd") && {
|
||||||
|
sab_ini=$(find "$sab_dir" -maxdepth 2 -name "sabnzbd.ini" 2>/dev/null | head -1)
|
||||||
|
if [[ -f "$sab_ini" ]]; then
|
||||||
|
sab_key=$(grep -oP '(?<=^api_key\s*=\s*)\S+' "$sab_ini" 2>/dev/null | head -1)
|
||||||
|
_set_conf_var "${MY_ID}_SABNZBD_API_KEY" "$sab_key" "SABnzbd API key"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── slskd API key ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
slskd_dir=$(_arr_config_dir "slskd") && {
|
||||||
|
slskd_yml=$(find "$slskd_dir" -maxdepth 2 -name "*.yml" -o -name "*.yaml" 2>/dev/null | head -1)
|
||||||
|
if [[ -f "$slskd_yml" ]]; then
|
||||||
|
slskd_key=$(grep -oP '(?<=api_key:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
|
||||||
|
[[ -z "$slskd_key" ]] && \
|
||||||
|
slskd_key=$(grep -oP '(?<=apikey:\s)[\w-]+' "$slskd_yml" 2>/dev/null | head -1)
|
||||||
|
_set_conf_var "${MY_ID}_SLSKD_API_KEY" "$slskd_key" "slskd API key"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Container names ───────────────────────────────────────────────────────────────────────────
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
for pattern in "emby" "jellyfin"; do
|
||||||
|
container=$(docker ps -a --format '{{.Names}}' 2>/dev/null | grep -im1 "^${pattern}")
|
||||||
|
[[ -z "$container" ]] && continue
|
||||||
|
case "$pattern" in
|
||||||
|
emby) _set_conf_var "${MY_ID}_EMBY_CONTAINER" "$container" "Emby container name" ;;
|
||||||
|
jellyfin) _set_conf_var "${MY_ID}_JELLYFIN_CONTAINER" "$container" "Jellyfin container name" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Network interface ─────────────────────────────────────────────────────────────────────────
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
nic=$(ip route show default 2>/dev/null | grep -oP '(?<=dev )\S+' | head -1)
|
||||||
|
_set_conf_var "${MY_ID}_SYS_WATCHDOG_NIC" "$nic" "Default NIC"
|
||||||
|
|
||||||
|
# ==============================================================================================
|
||||||
|
# ── Summary + push ────────────────────────────────────────────────────────────────────────────
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━ $ICON_SUMMARY Conf Populate Summary ━━━━━"
|
||||||
|
echo " Updated: $UPDATED field(s)"
|
||||||
|
echo " Skipped: $SKIPPED already set"
|
||||||
|
echo " Conf: $CONF_FILE"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
|
||||||
|
if [[ "$UPDATED" -gt 0 ]] && [[ "$DRY_RUN" == false ]] && [[ "$NO_PUSH" == false ]]; then
|
||||||
|
echo ""
|
||||||
|
info "Pushing updated conf to partners..."
|
||||||
|
bash "$SCRIPTS_ROOT/unRAID_Essentials/conf_sync.sh" --push-only "${EXTRA_FLAGS[@]}" || true
|
||||||
|
fi
|
||||||
Executable
+138
@@ -0,0 +1,138 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==============================================================================================
|
||||||
|
# ============================= Conf Cache Sync ================================================
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# PURPOSE
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Maintains a RAM-resident conf cache at /tmp/.vv/config/cached/.confs/.
|
||||||
|
# Credentials and partner keys live in RAM only — never on disk across hosts.
|
||||||
|
#
|
||||||
|
# On array start (default / --array-start):
|
||||||
|
# 1. Copy own conf to local cache
|
||||||
|
# 2. Pull each available partner's conf from their disk → local cache
|
||||||
|
# 3. Push own conf to each available partner's /tmp/.vv/ cache
|
||||||
|
#
|
||||||
|
# On conf save (--push-only):
|
||||||
|
# Fast path — push updated own conf to all partners' /tmp/.vv/ cache only.
|
||||||
|
# No pulls, no local cache rebuild.
|
||||||
|
#
|
||||||
|
# Cache is /tmp (tmpfs) — cleared every reboot, repopulated by this script
|
||||||
|
# on next array start. Scripts source from cache for partner vars; own vars
|
||||||
|
# always come from disk (load_config.sh skips cached copy of own conf).
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
# RUNTIME MODES
|
||||||
|
# ==============================================================================================
|
||||||
|
#
|
||||||
|
# conf_sync.sh Full sync: pull from all partners + push to all partners
|
||||||
|
# conf_sync.sh --push-only Push own conf to all partners (fast, for conf-save hook)
|
||||||
|
# conf_sync.sh --dry-run Show what would happen, no changes
|
||||||
|
# conf_sync.sh --log Verbose output
|
||||||
|
#
|
||||||
|
# ==============================================================================================
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
source "$SCRIPT_DIR/../load_config.sh"
|
||||||
|
|
||||||
|
PUSH_ONLY=false
|
||||||
|
FILTERED_ARGS=()
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--push-only) PUSH_ONLY=true ;;
|
||||||
|
*) FILTERED_ARGS+=("$arg") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
parse_args "${FILTERED_ARGS[@]}"
|
||||||
|
detect_hosts
|
||||||
|
|
||||||
|
CACHE_DIR="/tmp/.vv/config/cached/.confs"
|
||||||
|
MY_CONF="$SCRIPTS_ROOT/Configurations/${MY_ID,,}.conf"
|
||||||
|
SSH_TIMEOUT=10
|
||||||
|
|
||||||
|
[[ "$DRY_RUN" == true ]] && warn "DRY RUN — no changes will be made"
|
||||||
|
|
||||||
|
# ── Ensure cache dir exists ───────────────────────────────────────────────────
|
||||||
|
if [[ "$DRY_RUN" == false ]]; then
|
||||||
|
mkdir -p "$CACHE_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Copy own conf into local cache ───────────────────────────────────────────
|
||||||
|
if [[ "$PUSH_ONLY" == false ]]; then
|
||||||
|
if [[ -f "$MY_CONF" ]]; then
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
warn "DRY RUN — would copy $(basename "$MY_CONF") → $CACHE_DIR/"
|
||||||
|
else
|
||||||
|
cp "$MY_CONF" "$CACHE_DIR/${MY_ID,,}.conf" && \
|
||||||
|
log "Own conf cached ✅" || warn "Failed to cache own conf"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "Own conf not found: $MY_CONF"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Per-partner sync ──────────────────────────────────────────────────────────
|
||||||
|
PUSHED=0
|
||||||
|
PULLED=0
|
||||||
|
FAILED=0
|
||||||
|
|
||||||
|
for host_var in $(compgen -v | grep -E '^HOST[0-9]+$' | sort); do
|
||||||
|
partner_host="${!host_var}"
|
||||||
|
[[ -z "$partner_host" ]] && continue
|
||||||
|
[[ "${host_var,,}" == "${MY_ID,,}" ]] && continue
|
||||||
|
|
||||||
|
partner_slot="${host_var,,}" # e.g. host2
|
||||||
|
partner_ip=$(resolve_tailscale_ip "$partner_host" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ -z "$partner_ip" ]]; then
|
||||||
|
warn "$partner_host — cannot resolve Tailscale IP, skipping"
|
||||||
|
(( FAILED++ ))
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Pull: grab partner's conf from their disk → our local cache ──────────
|
||||||
|
if [[ "$PUSH_ONLY" == false ]]; then
|
||||||
|
remote_conf="/boot/config/plugins/varaverk/Configurations/${partner_slot}.conf"
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
warn "DRY RUN — would pull $partner_host:$remote_conf → $CACHE_DIR/${partner_slot}.conf"
|
||||||
|
elif timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||||
|
"root@${partner_ip}:${remote_conf}" \
|
||||||
|
"$CACHE_DIR/${partner_slot}.conf" 2>/dev/null; then
|
||||||
|
log "Pulled ${partner_slot}.conf from $partner_host ✅"
|
||||||
|
(( PULLED++ ))
|
||||||
|
else
|
||||||
|
warn "Could not pull ${partner_slot}.conf from $partner_host"
|
||||||
|
(( FAILED++ ))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Push: send own conf to partner's /tmp/.vv/ cache ────────────────────
|
||||||
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
|
warn "DRY RUN — would push ${MY_ID,,}.conf → $partner_host:/tmp/.vv/config/cached/.confs/"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure partner's cache dir exists, then SCP own conf into it
|
||||||
|
timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||||
|
"root@${partner_ip}" "mkdir -p '$CACHE_DIR'" 2>/dev/null
|
||||||
|
|
||||||
|
if timeout "$SSH_TIMEOUT" scp -i "$SSH_KEY" \
|
||||||
|
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
|
||||||
|
"$MY_CONF" \
|
||||||
|
"root@${partner_ip}:${CACHE_DIR}/${MY_ID,,}.conf" 2>/dev/null; then
|
||||||
|
log "Pushed ${MY_ID,,}.conf to $partner_host ✅"
|
||||||
|
(( PUSHED++ ))
|
||||||
|
else
|
||||||
|
warn "Could not push to $partner_host"
|
||||||
|
(( FAILED++ ))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||||
|
if [[ "$PUSH_ONLY" == true ]]; then
|
||||||
|
info "Conf push complete — pushed to $PUSHED host(s)${FAILED:+, $FAILED failed}"
|
||||||
|
else
|
||||||
|
info "Conf sync complete — pulled $PULLED, pushed $PUSHED${FAILED:+, $FAILED failed}"
|
||||||
|
fi
|
||||||
@@ -5,223 +5,149 @@
|
|||||||
#
|
#
|
||||||
# PURPOSE
|
# PURPOSE
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
# Manages per-host named API keys in each machine's Unraid registry so every
|
# Creates/overwrites the Varaverk API key in the unraid-api service registry at
|
||||||
# host can call every other host's GraphQL API directly for real-time monitoring.
|
# array start. The registry is ephemeral — OS updates and service restarts clear
|
||||||
|
# it. This script re-registers the key every boot so Varaverk's enhanced
|
||||||
|
# monitoring self-heals without manual intervention.
|
||||||
#
|
#
|
||||||
# KEY MODEL
|
# Also updates HOST*_UNRAID_API_KEY in the local host conf so the partnership
|
||||||
# ─────────────────────────────────────────────────────────────────────────────
|
# page always reflects the live key value.
|
||||||
# HOST1's Unraid registry holds:
|
|
||||||
# Varaverk_HOST1 — HOST1's own key (for HOST1 to call its own API)
|
|
||||||
# Varaverk_HOST2 — HOST2's access key to HOST1's API (removed on offboard)
|
|
||||||
#
|
|
||||||
# HOST2's Unraid registry holds:
|
|
||||||
# Varaverk_HOST2 — HOST2's own key
|
|
||||||
# Varaverk_HOST1 — HOST1's access key to HOST2's API (removed on offboard)
|
|
||||||
#
|
|
||||||
# host1.conf holds:
|
|
||||||
# HOST1_UNRAID_API_KEY — value of Varaverk_HOST1 from HOST1's registry
|
|
||||||
# HOST2_UNRAID_API_KEY — value of Varaverk_HOST1 from HOST2's registry
|
|
||||||
# (HOST1 uses this to call HOST2's GraphQL directly)
|
|
||||||
#
|
|
||||||
# host2.conf holds the mirror:
|
|
||||||
# HOST2_UNRAID_API_KEY — value of Varaverk_HOST2 from HOST2's registry
|
|
||||||
# HOST1_UNRAID_API_KEY — value of Varaverk_HOST2 from HOST1's registry
|
|
||||||
# (HOST2 uses this to call HOST1's GraphQL directly)
|
|
||||||
#
|
|
||||||
# ── vv_remote_hosts_stats() picks up HOST2_UNRAID_API_KEY from host1.conf
|
|
||||||
# automatically — direct GraphQL, no SSH needed for remote monitoring.
|
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
# RUNTIME MODES
|
# RUNTIME MODES
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
#
|
#
|
||||||
# unraid_api_key_renew.sh
|
# unraid_api_key_renew.sh
|
||||||
# Local only — runs at array start. Re-registers Varaverk_HOST1 if the
|
# Renew the key. Silent on success.
|
||||||
# ephemeral registry lost it. Fast, no SSH.
|
|
||||||
#
|
#
|
||||||
# unraid_api_key_renew.sh --all-hosts
|
# unraid_api_key_renew.sh --dry-run
|
||||||
# Full cross-host key setup. Run once from Settings → API Keys → Setup.
|
# Show what would happen — no changes made.
|
||||||
# For each partner:
|
|
||||||
# • Creates Varaverk_HOST1 on PARTNER's registry → stores value in local host*.conf
|
|
||||||
# • Creates Varaverk_HOST2 on LOCAL registry → SSHes value to partner's host*.conf
|
|
||||||
#
|
#
|
||||||
# unraid_api_key_renew.sh --dry-run — show what would happen
|
# unraid_api_key_renew.sh --log
|
||||||
# unraid_api_key_renew.sh --log — verbose output
|
# Verbose output.
|
||||||
#
|
#
|
||||||
# ==============================================================================================
|
# ==============================================================================================
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
source "$SCRIPT_DIR/../load_config.sh"
|
source "$SCRIPT_DIR/../load_config.sh"
|
||||||
|
|
||||||
parse_args "$@"
|
parse_args "$@"
|
||||||
acquire_lock
|
acquire_lock
|
||||||
detect_hosts
|
detect_hosts
|
||||||
|
|
||||||
# Parse --all-hosts
|
|
||||||
ALL_HOSTS=false
|
|
||||||
for _arg in "$@"; do [[ "$_arg" == "--all-hosts" ]] && ALL_HOSTS=true; done
|
|
||||||
unset _arg
|
|
||||||
|
|
||||||
LOCAL_CONF="$SCRIPT_DIR/../Configurations/${MY_ID,,}.conf"
|
|
||||||
LOCAL_KEY_NAME="Varaverk_${MY_ID}"
|
|
||||||
LOCAL_VAR="${MY_ID}_UNRAID_API_KEY"
|
|
||||||
|
|
||||||
log "$ICON_GEAR Local conf: $LOCAL_CONF"
|
|
||||||
log "$ICON_GEAR Key name: $LOCAL_KEY_NAME"
|
|
||||||
log "$ICON_GEAR All hosts: $ALL_HOSTS"
|
|
||||||
|
|
||||||
if [[ ! -f "$LOCAL_CONF" ]]; then
|
|
||||||
error "Conf file not found: $LOCAL_CONF"; exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
[[ "$DRY_RUN" == true ]] && {
|
|
||||||
warn "DRY RUN — would check/create $LOCAL_KEY_NAME in registry and sync to host conf"
|
|
||||||
[[ "$ALL_HOSTS" == true ]] && warn "DRY RUN — would also SSH all partners for cross-host key setup"
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Helper: ensure a named key exists in the LOCAL registry, return its value ──
|
|
||||||
_ensure_local_key() {
|
|
||||||
local name="$1"
|
|
||||||
local existing key
|
|
||||||
existing=$(timeout 5 /usr/local/sbin/unraid-api apikey --name "$name" --json </dev/null 2>/dev/null)
|
|
||||||
key=$(echo "$existing" | jq -r '.key // empty' 2>/dev/null)
|
|
||||||
if [[ -n "$key" ]]; then
|
|
||||||
echo "$key"; return 0
|
|
||||||
fi
|
|
||||||
local raw
|
|
||||||
raw=$(timeout 10 /usr/local/sbin/unraid-api apikey \
|
|
||||||
--name "$name" --create --overwrite \
|
|
||||||
--description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1)
|
|
||||||
key=$(echo "$raw" | jq -r '.key // empty' 2>/dev/null)
|
|
||||||
[[ -z "$key" ]] && { error "Failed to create $name: ${raw:0:200}"; return 1; }
|
|
||||||
echo "$key"
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Helper: write HOST*_UNRAID_API_KEY into a conf file ────────────────────────
|
|
||||||
_write_key() {
|
|
||||||
local conf="$1" var="$2" key="$3"
|
|
||||||
[[ ! -f "$conf" ]] && return 1
|
|
||||||
if grep -q "^\s*${var}\s*=" "$conf"; then
|
|
||||||
sed -i "s|^\(\s*${var}\s*=\s*\)\"[^\"]*\"|\1\"${key}\"|" "$conf"
|
|
||||||
else
|
|
||||||
printf ' %s="%s"\n' "$var" "$key" >> "$conf"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# ── Helper: ensure a named key exists on a REMOTE registry via SSH ─────────────
|
|
||||||
_ensure_remote_key() {
|
|
||||||
local ssh_key="$1" remote_ip="$2" key_name="$3"
|
|
||||||
ssh -i "$ssh_key" \
|
|
||||||
-o ConnectTimeout=10 -o StrictHostKeyChecking=no -o BatchMode=yes \
|
|
||||||
"root@${remote_ip}" "
|
|
||||||
EXISTING=\$(timeout 5 /usr/local/sbin/unraid-api apikey --name '${key_name}' --json </dev/null 2>/dev/null)
|
|
||||||
KEY=\$(echo \"\$EXISTING\" | jq -r '.key // empty' 2>/dev/null)
|
|
||||||
if [[ -n \"\$KEY\" ]]; then
|
|
||||||
echo \"\$KEY\"
|
|
||||||
else
|
|
||||||
timeout 10 /usr/local/sbin/unraid-api apikey \\
|
|
||||||
--name '${key_name}' --create --overwrite \\
|
|
||||||
--description 'Varaverk plugin' --roles ADMIN --json </dev/null 2>/dev/null \\
|
|
||||||
| jq -r '.key // empty' 2>/dev/null
|
|
||||||
fi
|
|
||||||
" 2>/dev/null | tr -d '[:space:]'
|
|
||||||
}
|
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# Step 1 — Local key (always)
|
CONF_FILE="$SCRIPT_DIR/../Configurations/${MY_ID,,}.conf"
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
VAR_NAME="${MY_ID}_UNRAID_API_KEY"
|
||||||
echo ""
|
|
||||||
echo "━━━ $ICON_GEAR Local — $LOCAL_KEY_NAME ━━━"
|
|
||||||
|
|
||||||
LOCAL_KEY=$(_ensure_local_key "$LOCAL_KEY_NAME")
|
# Key name: "Varaverk <hostname>" stripping any unraid- prefix
|
||||||
if [[ -z "$LOCAL_KEY" ]]; then
|
# Space separator — unRAID API only allows letters, numbers, and spaces
|
||||||
error "Could not obtain $LOCAL_KEY_NAME from registry"
|
HOSTNAME_SUFFIX=$(hostname -s 2>/dev/null | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//' || hostname -s)
|
||||||
|
KEY_NAME="Varaverk ${HOSTNAME_SUFFIX}"
|
||||||
|
|
||||||
|
log "$ICON_GEAR Conf file: $CONF_FILE"
|
||||||
|
log "$ICON_GEAR Key var: $VAR_NAME"
|
||||||
|
log "$ICON_GEAR Key name: $KEY_NAME"
|
||||||
|
|
||||||
|
if [[ ! -f "$CONF_FILE" ]]; then
|
||||||
|
error "Conf file not found: $CONF_FILE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Sync to conf only if changed
|
if [[ "$DRY_RUN" == true ]]; then
|
||||||
CONF_KEY=$(grep "^\s*${LOCAL_VAR}\s*=" "$LOCAL_CONF" 2>/dev/null | sed 's/.*="\(.*\)".*/\1/' | tr -d '[:space:]')
|
warn "DRY RUN — would check registry for $KEY_NAME, renew only if missing"
|
||||||
if [[ "$CONF_KEY" != "$LOCAL_KEY" ]]; then
|
exit 0
|
||||||
_write_key "$LOCAL_CONF" "$LOCAL_VAR" "$LOCAL_KEY"
|
|
||||||
warn "Synced $LOCAL_VAR → ${LOCAL_KEY:0:8}...${LOCAL_KEY: -4}"
|
|
||||||
else
|
|
||||||
echo " $LOCAL_VAR valid ✅ — ${LOCAL_KEY:0:8}...${LOCAL_KEY: -4}"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ "$ALL_HOSTS" != true ]] && exit 0
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Check if key already exists in the unraid-api registry before creating.
|
||||||
|
# --overwrite generates a new key value every time, invalidating the old one.
|
||||||
|
# Only renew if the registry has lost it.
|
||||||
|
log "Checking unraid-api registry for $KEY_NAME..."
|
||||||
|
EXISTING=$(timeout 5 /usr/local/sbin/unraid-api apikey --name "$KEY_NAME" --json </dev/null 2>/dev/null)
|
||||||
|
KEY=$(echo "$EXISTING" | jq -r '.key // empty' 2>/dev/null)
|
||||||
|
|
||||||
|
if [[ -n "$KEY" ]]; then
|
||||||
|
PREVIEW="${KEY:0:8}...${KEY: -4}"
|
||||||
|
echo "API key valid ✅ — $VAR_NAME = $PREVIEW"
|
||||||
|
log "Key found in registry — no renewal needed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Key not found in registry — creating new key..."
|
||||||
|
|
||||||
|
RAW=$(timeout 10 /usr/local/sbin/unraid-api apikey \
|
||||||
|
--name "$KEY_NAME" --create --overwrite \
|
||||||
|
--description "Varaverk plugin" --roles ADMIN --json </dev/null 2>&1)
|
||||||
|
|
||||||
|
if [[ -z "$RAW" ]]; then
|
||||||
|
error "unraid-api returned no output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
KEY=$(echo "$RAW" | jq -r '.key // empty' 2>/dev/null)
|
||||||
|
if [[ -z "$KEY" ]]; then
|
||||||
|
error "No key in unraid-api response: ${RAW:0:200}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
# Step 2 — Cross-host key setup (--all-hosts)
|
if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE"
|
||||||
echo ""
|
else
|
||||||
echo "━━━ $ICON_SYNC Cross-host key setup ━━━"
|
echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
PARTNER_OK=0; PARTNER_FAIL=0
|
PREVIEW="${KEY:0:8}...${KEY: -4}"
|
||||||
|
log "Writing new key to: $CONF_FILE"
|
||||||
|
warn "API key renewed ✅ — $VAR_NAME = $PREVIEW (registry had lost it)"
|
||||||
|
|
||||||
for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do
|
# ── Push renewed key into each partner's OWN conf ─────────────────────────────
|
||||||
[[ "$host_var" == "$MY_ID" ]] && continue
|
# Each host's conf is its complete keychest — no cross-host conf files needed.
|
||||||
hostname="${!host_var:-}"; [[ -z "$hostname" ]] && continue
|
# SSH_KEY is set by detect_hosts() — this server's outbound private key.
|
||||||
|
if [[ -z "$SSH_KEY" ]]; then
|
||||||
|
log "No SSH key configured — skipping partner push"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
r_var="${host_var}_UNRAID_API_KEY" # e.g. HOST2_UNRAID_API_KEY
|
for host_var in $(compgen -v | grep -E '^HOST[0-9]+$'); do
|
||||||
r_my_key_name="Varaverk_${MY_ID}" # key HOST1 uses on HOST2's registry
|
partner_host="${!host_var}"
|
||||||
r_their_key_name="Varaverk_${host_var}" # key HOST2 uses on HOST1's registry
|
[[ -z "$partner_host" ]] && continue
|
||||||
r_conf="/boot/config/plugins/varaverk/Configurations/${host_var,,}.conf"
|
[[ "${host_var,,}" == "${MY_ID,,}" ]] && continue
|
||||||
|
|
||||||
echo ""
|
partner_slot="${host_var,,}" # e.g. host2
|
||||||
echo " $host_var ($hostname)"
|
partner_ip=$(resolve_tailscale_ip "$partner_host" 2>/dev/null || true)
|
||||||
|
[[ -z "$partner_ip" ]] && { log "Cannot resolve IP for $partner_host — skipping"; continue; }
|
||||||
|
|
||||||
REMOTE_IP=$(resolve_tailscale_ip "$hostname")
|
# Target is the partner's OWN conf on their machine
|
||||||
if [[ -z "$REMOTE_IP" ]]; then
|
partner_conf="/boot/config/plugins/varaverk/Configurations/${partner_slot}.conf"
|
||||||
warn " $host_var: cannot resolve Tailscale IP — skipping"
|
tmp=$(mktemp /tmp/vv_kp_XXXXXX.sh)
|
||||||
(( PARTNER_FAIL++ )); continue
|
remote="/tmp/vv_kp_${RANDOM}.sh"
|
||||||
|
chmod 700 "$tmp"
|
||||||
|
|
||||||
|
# Key stays in the temp file — never appears in SSH command args
|
||||||
|
cat > "$tmp" <<PUSHSCRIPT
|
||||||
|
#!/bin/sh
|
||||||
|
target='${partner_conf}'
|
||||||
|
if grep -q "\b${VAR_NAME}\b" "\$target" 2>/dev/null; then
|
||||||
|
sed -i 's|^\(\\s*${VAR_NAME}\\s*=\\s*\)"[^"]*"|\1"${KEY}"|' "\$target"
|
||||||
|
else
|
||||||
|
printf ' ${VAR_NAME}="%s"\n' '${KEY}' >> "\$target"
|
||||||
|
fi
|
||||||
|
echo ok
|
||||||
|
PUSHSCRIPT
|
||||||
|
|
||||||
|
if timeout 10 scp -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
|
||||||
|
-o StrictHostKeyChecking=no "$tmp" "root@${partner_ip}:${remote}" 2>/dev/null; then
|
||||||
|
if timeout 10 ssh -i "$SSH_KEY" -o ConnectTimeout=10 -o BatchMode=yes \
|
||||||
|
-o StrictHostKeyChecking=no "root@${partner_ip}" \
|
||||||
|
"bash '${remote}'; rc=\$?; rm -f '${remote}'; exit \$rc" 2>/dev/null | grep -q ok; then
|
||||||
|
log "Key pushed to $partner_host ✅"
|
||||||
|
else
|
||||||
|
warn "Key push to $partner_host failed — they can create their own copy"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
warn "SCP to $partner_host failed — skipping"
|
||||||
fi
|
fi
|
||||||
|
rm -f "$tmp"
|
||||||
# ── A: Get HOST1's access key from HOST2's registry ────────────────────────
|
|
||||||
# Creates Varaverk_HOST1 on HOST2 → store in host1.conf as HOST2_UNRAID_API_KEY
|
|
||||||
log " Creating $r_my_key_name on $host_var registry…"
|
|
||||||
MY_KEY_ON_REMOTE=$(_ensure_remote_key "$SSH_KEY" "$REMOTE_IP" "$r_my_key_name")
|
|
||||||
if [[ -z "$MY_KEY_ON_REMOTE" ]]; then
|
|
||||||
warn " $host_var: could not create $r_my_key_name on their registry"
|
|
||||||
(( PARTNER_FAIL++ )); continue
|
|
||||||
fi
|
|
||||||
_write_key "$LOCAL_CONF" "$r_var" "$MY_KEY_ON_REMOTE"
|
|
||||||
echo " A ✅ $r_my_key_name on $host_var → stored as $r_var in ${MY_ID,,}.conf"
|
|
||||||
|
|
||||||
# ── B: Create HOST2's access key on HOST1's registry ──────────────────────
|
|
||||||
# Creates Varaverk_HOST2 on HOST1 → store in host2.conf as HOST1_UNRAID_API_KEY
|
|
||||||
log " Creating $r_their_key_name on local registry…"
|
|
||||||
THEIR_KEY_ON_LOCAL=$(_ensure_local_key "$r_their_key_name")
|
|
||||||
if [[ -z "$THEIR_KEY_ON_LOCAL" ]]; then
|
|
||||||
warn " Could not create $r_their_key_name on local registry"
|
|
||||||
(( PARTNER_FAIL++ )); continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Write to partner's conf via SSH
|
|
||||||
ssh -i "$SSH_KEY" \
|
|
||||||
-o ConnectTimeout=10 -o StrictHostKeyChecking=no -o BatchMode=yes \
|
|
||||||
"root@${REMOTE_IP}" "
|
|
||||||
CONF='$r_conf'
|
|
||||||
VAR='${MY_ID}_UNRAID_API_KEY'
|
|
||||||
KEY='$THEIR_KEY_ON_LOCAL'
|
|
||||||
[[ ! -f \"\$CONF\" ]] && exit 1
|
|
||||||
if grep -q \"^\s*\${VAR}\s*=\" \"\$CONF\"; then
|
|
||||||
sed -i \"s|\(\s*\${VAR}\s*=\s*\)\\\"[^\\\"]*\\\"|\1\\\"\${KEY}\\\"|\" \"\$CONF\"
|
|
||||||
else
|
|
||||||
printf ' %s=\"%s\"\n' \"\$VAR\" \"\$KEY\" >> \"\$CONF\"
|
|
||||||
fi
|
|
||||||
" 2>/dev/null \
|
|
||||||
&& echo " B ✅ $r_their_key_name on local → stored as ${MY_ID}_UNRAID_API_KEY in ${host_var,,}.conf" \
|
|
||||||
|| warn " B: could not write ${MY_ID}_UNRAID_API_KEY to ${host_var,,}.conf"
|
|
||||||
|
|
||||||
(( PARTNER_OK++ ))
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "━━━━━ $ICON_SUMMARY Key Setup Summary ━━━━━"
|
|
||||||
echo " Local key: $LOCAL_VAR ✅"
|
|
||||||
echo " Partner setup: $PARTNER_OK ok · $PARTNER_FAIL failed"
|
|
||||||
echo ""
|
|
||||||
echo " HOST1 can now call HOST2's GraphQL directly using HOST2_UNRAID_API_KEY"
|
|
||||||
echo " HOST2 can now call HOST1's GraphQL directly using HOST1_UNRAID_API_KEY"
|
|
||||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
||||||
|
|||||||
Reference in New Issue
Block a user