265 lines
12 KiB
Bash
265 lines
12 KiB
Bash
#!/bin/bash
|
|
# ==============================================================================================
|
|
# ================================= Unraid API Key Renewal ====================================
|
|
# ==============================================================================================
|
|
#
|
|
# PURPOSE
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Creates/overwrites the Varaverk API key in the unraid-api service registry at
|
|
# 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.
|
|
#
|
|
# Also updates HOST*_UNRAID_API_KEY in the local host conf so the partnership
|
|
# page always reflects the live key value.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL MODEL
|
|
# ==============================================================================================
|
|
#
|
|
# 1. Check whether a Varaverk key already exists in the unraid-api registry
|
|
# 2. Create or overwrite it — the registry is ephemeral, so re-registering is the norm
|
|
# 3. Write the resulting key into this host's conf, replacing any previous value
|
|
# 4. Push the key into each partner's OWN conf, at the path their varaverk.cfg reports
|
|
# 5. Report whether the key was created, refreshed, or unchanged
|
|
#
|
|
# Runs at array start. The registry does not survive OS updates or an unraid-api restart,
|
|
# which is why this re-registers unconditionally rather than only when the key is missing —
|
|
# a key present in the conf but absent from the registry is the exact failure it repairs.
|
|
#
|
|
# ==============================================================================================
|
|
# DESIGN PRINCIPLES
|
|
# ==============================================================================================
|
|
#
|
|
# Self-Healing at Boot
|
|
# The unraid-api registry is ephemeral — OS updates and service restarts clear
|
|
# it without warning. Running at every array start means the key is always
|
|
# present after boot without any manual intervention.
|
|
#
|
|
# Conf Stays Current
|
|
# HOST*_UNRAID_API_KEY in the local host conf is updated after every renewal.
|
|
# The partnership page reads the conf — it always reflects the live key value
|
|
# without a separate sync step.
|
|
#
|
|
# ==============================================================================================
|
|
# OPERATIONAL SAFEGUARDS
|
|
# ==============================================================================================
|
|
#
|
|
# acquire_lock — prevents concurrent renewal attempts at boot
|
|
# detect_hosts() — sets MY_ID to derive the correct conf var name
|
|
# Conf file check — aborts before any writes if the host conf is missing
|
|
# dry-run mode — shows what would happen without touching anything
|
|
#
|
|
# Remote path discovery
|
|
# The partner's conf path comes from resolve_remote_scripts_dir(), which reads their
|
|
# varaverk.cfg, so a partner in appdata storage mode is found. The path was hardcoded
|
|
# to the flash plugin directory, which is wrong for any such partner.
|
|
#
|
|
# Remote target must exist
|
|
# The pushed script refuses to create the conf and reports the path it looked at.
|
|
# resolve_remote_scripts_dir() falls back to our own SCRIPTS_DIR when the probe fails,
|
|
# and appending an API key to a merely plausible path is how the hardcoded version
|
|
# failed without saying so.
|
|
#
|
|
# ==============================================================================================
|
|
# CONFIGURATION
|
|
# ==============================================================================================
|
|
#
|
|
# host*.conf
|
|
#
|
|
# HOST*_UNRAID_API_KEY
|
|
# Written by this script every array start. Read by the plugin's PHP for enhanced
|
|
# monitoring. Treated as output, not input — an existing value is always replaced,
|
|
# because a conf value that no longer matches the registry is precisely the broken
|
|
# state this exists to fix.
|
|
#
|
|
# Platform-owned:
|
|
#
|
|
# The unraid-api service registry — ephemeral, cleared by OS updates and service restarts.
|
|
#
|
|
# ==============================================================================================
|
|
# RUNTIME MODES
|
|
# ==============================================================================================
|
|
#
|
|
# unraid_api_key_renew.sh
|
|
# Renew the key. Silent on success.
|
|
#
|
|
# unraid_api_key_renew.sh --dry-run
|
|
# Show what would happen — no changes made.
|
|
#
|
|
# unraid_api_key_renew.sh --log
|
|
# Verbose output.
|
|
#
|
|
# ==============================================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
source "$SCRIPT_DIR/../../../load_config.sh"
|
|
|
|
parse_args "$@"
|
|
|
|
# Rewrites the API key into host*.conf and registers it with the unraid-api service.
|
|
[[ "$EUID" -ne 0 ]] && { error "Must be run as root"; exit 1; }
|
|
|
|
acquire_lock
|
|
detect_hosts
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
CONF_FILE="$SCRIPT_DIR/../../../Configurations/${MY_ID,,}.conf"
|
|
VAR_NAME="${MY_ID}_UNRAID_API_KEY"
|
|
|
|
# Key name: "Varaverk <hostname>" stripping any unraid- prefix
|
|
# Space separator — unRAID API only allows letters, numbers, and spaces
|
|
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
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" == true ]]; then
|
|
warn "DRY RUN — would check registry for $KEY_NAME, renew only if missing"
|
|
exit 0
|
|
fi
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
# 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.
|
|
#
|
|
# Timeout was 5s — measured live 2026-07-19 at 2.2-2.7s baseline latency for this
|
|
# exact command with the system idle, leaving almost no margin. Any load spike
|
|
# (confirmed correlated with resource_watchdog "pressure escalating" events in the
|
|
# same log) pushed it past 5s, killing the lookup — the script then couldn't tell
|
|
# "timed out" from "genuinely not in the registry" and fell through to the create
|
|
# path, logging a false "API key renewed (registry had lost it)" even though the
|
|
# on-disk key file's timestamp never actually changed. 15s gives real headroom.
|
|
log "Checking unraid-api registry for $KEY_NAME..."
|
|
EXISTING=$(timeout 15 /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}"
|
|
# Always sync registry key → conf, even if the key was already there.
|
|
# Conf gets wiped on git pull / conf regeneration without touching the registry.
|
|
# Sourced, not pattern-matched. This was a grep -oP with a variable-length lookbehind, which
|
|
# Unraid's grep is ugrep and rejects outright — "length of lookbehind assertion is not
|
|
# limited", rc 2, swallowed by the || true. CONF_HAS_KEY was therefore always empty, the
|
|
# comparison below never matched, and this script logged "conf is stale — syncing" and
|
|
# rewrote the same key into the conf every fifteen minutes since it was written.
|
|
# Sourcing also means the value is read the way bash reads it, escapes and all.
|
|
CONF_HAS_KEY=$(bash -c 'source "$1" >/dev/null 2>&1 || exit 0; printf "%s" "${!2-}"' \
|
|
_ "$CONF_FILE" "$VAR_NAME" 2>/dev/null || true)
|
|
if [[ "$CONF_HAS_KEY" == "$KEY" ]]; then
|
|
echo "API key valid ✅ — $VAR_NAME = $PREVIEW"
|
|
log "Key in registry and conf — no action needed"
|
|
exit 0
|
|
fi
|
|
log "Key in registry but conf is stale — syncing..."
|
|
if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then
|
|
sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE"
|
|
else
|
|
echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE"
|
|
fi
|
|
echo "API key synced to conf ✅ — $VAR_NAME = $PREVIEW"
|
|
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
|
|
|
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then
|
|
sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE"
|
|
else
|
|
echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE"
|
|
fi
|
|
|
|
PREVIEW="${KEY:0:8}...${KEY: -4}"
|
|
log "Writing new key to: $CONF_FILE"
|
|
warn "API key renewed ✅ — $VAR_NAME = $PREVIEW (registry had lost it)"
|
|
|
|
# ── Push renewed key into each partner's OWN conf ─────────────────────────────
|
|
# Each host's conf is its complete keychest — no cross-host conf files needed.
|
|
# 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
|
|
|
|
# resolve_remote_scripts_dir() reads this; every inline timeout below already uses 10.
|
|
SSH_TIMEOUT=10
|
|
|
|
for host_var in $(compgen -v | grep -E '^HOST[0-9]+$'); 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)
|
|
[[ -z "$partner_ip" ]] && { log "Cannot resolve IP for $partner_host — skipping"; continue; }
|
|
|
|
# Target is the partner's OWN conf on their machine. Their SCRIPTS_DIR is read from their
|
|
# varaverk.cfg rather than assumed — this was hardcoded to the flash plugin path, so a
|
|
# partner in appdata storage mode had its key appended under a directory that does not
|
|
# exist there. HOST2 has run in appdata mode since it was installed.
|
|
partner_sd=$(resolve_remote_scripts_dir "$partner_ip" "$SSH_KEY" "no")
|
|
partner_conf="${partner_sd}/Configurations/${partner_slot}.conf"
|
|
tmp=$(mktemp /tmp/vv_kp_XXXXXX.sh)
|
|
remote="/tmp/vv_kp_${RANDOM}.sh"
|
|
chmod 700 "$tmp"
|
|
|
|
# Key stays in the temp file — never appears in SSH command args.
|
|
# The conf must already exist: resolve_remote_scripts_dir falls back to our own SCRIPTS_DIR
|
|
# when the probe fails, and appending a key to a path that is merely plausible is how the
|
|
# hardcoded version failed silently. Report the path instead of guessing.
|
|
cat > "$tmp" <<PUSHSCRIPT
|
|
#!/bin/sh
|
|
target='${partner_conf}'
|
|
if [ ! -f "\$target" ]; then
|
|
echo "missing:\$target"
|
|
exit 1
|
|
fi
|
|
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
|
|
push_out=$(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)
|
|
case "$push_out" in
|
|
*ok*) echo "Key pushed to $partner_host ✅" ;;
|
|
missing:*) warn "Key push to $partner_host failed — no conf at ${push_out#missing:}" ;;
|
|
*) warn "Key push to $partner_host failed — they can create their own copy" ;;
|
|
esac
|
|
else
|
|
warn "SCP to $partner_host failed — skipping"
|
|
fi
|
|
rm -f "$tmp"
|
|
done
|