diff --git a/Plugin/unraid/api/storage.php b/Plugin/unraid/api/storage.php index 45248e1..360706b 100644 --- a/Plugin/unraid/api/storage.php +++ b/Plugin/unraid/api/storage.php @@ -95,35 +95,56 @@ if ($action === 'detect' && $_SERVER['REQUEST_METHOD'] === 'POST') { exit; } -// ── Unraid API key status ───────────────────────────────────────────────────── +// ── Unraid API key status — all hosts ──────────────────────────────────────── if ($action === 'api_status') { require_once dirname(__DIR__) . '/include/unraid_api.php'; - $data = vv_api_data(); - $status = vv_api_get_status(); - $vars = vv_conf_vars(); - $myHost = vv_detect_host(); - $keyVar = strtoupper($myHost) . '_UNRAID_API_KEY'; - $key = $vars[$keyVar] ?? ''; + $localStatus = vv_api_get_status(); + $vars = vv_conf_vars(); + $myHost = vv_detect_host(); + $myId = strtoupper($myHost); + + // Collect status for every configured host + $hosts = []; + foreach ($vars as $k => $v) { + if (!preg_match('/^HOST(\d+)$/', $k, $m) || !$v) continue; + $id = 'HOST' . $m[1]; + $keyVar = $id . '_UNRAID_API_KEY'; + $key = $vars[$keyVar] ?? ''; + $isLocal = ($id === $myId); + $hosts[] = [ + 'host_id' => $id, + 'hostname' => $v, + 'is_local' => $isLocal, + 'key_var' => $keyVar, + 'key_present' => !empty($key), + 'key_preview' => $key ? substr($key, 0, 8) . '...' . substr($key, -4) : null, + 'api_ok' => $isLocal + ? (!$localStatus['key_missing'] && $localStatus['available']) + : !empty($key), + ]; + } + usort($hosts, fn($a,$b) => strcmp($a['host_id'], $b['host_id'])); + echo json_encode([ - 'ok' => true, - 'key_present' => !empty($key), - 'key_preview' => $key ? substr($key, 0, 8) . '...' . substr($key, -4) : null, - 'api_ok' => !empty($key) && !$status['key_missing'] && $status['available'], - 'key_missing' => $status['key_missing'], - 'fallbacks' => $status['fallbacks'], + 'ok' => true, + 'my_id' => $myId, + 'hosts' => $hosts, + 'fallbacks' => $localStatus['fallbacks'], ]); exit; } -// ── Renew Unraid API key ────────────────────────────────────────────────────── -if ($action === 'renew_apikey' && $_SERVER['REQUEST_METHOD'] === 'POST') { +// ── Setup/renew API keys (local + all partners via SSH) ─────────────────────── +if ($action === 'setup_apikeys' && $_SERVER['REQUEST_METHOD'] === 'POST') { $script = SCRIPTS_DIR . '/unRAID_Essentials/unraid_api_key_renew.sh'; if (!file_exists($script)) { echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh not found']); exit; } - set_time_limit(30); + $allHosts = ($_POST['all_hosts'] ?? '0') === '1'; + $flags = $allHosts ? ' --all-hosts' : ''; + set_time_limit(60); $output = []; $exit = 0; - exec('bash ' . escapeshellarg($script) . ' 2>&1', $output, $exit); + exec('bash ' . escapeshellarg($script) . $flags . ' 2>&1', $output, $exit); echo json_encode(['ok' => $exit === 0, 'exit' => $exit, 'output' => implode("\n", $output)]); exit; } diff --git a/Plugin/unraid/pages/settings.php b/Plugin/unraid/pages/settings.php index 1ccdbc5..ea396cd 100644 --- a/Plugin/unraid/pages/settings.php +++ b/Plugin/unraid/pages/settings.php @@ -145,28 +145,23 @@ $_apiPreview = $_apiKey ? substr($_apiKey, 0, 8) . '...' . substr($_apiKey, -4)
- Unraid API Key - + Unraid API Keys +
-
- - Key: -  ·  Var: - - No key found in .conf — click Renew to register one. - -
+
- The Unraid API key is ephemeral — it clears on OS updates and service restarts. - unraid_api_key_renew.sh - runs at array start to auto-recover, but if the monitor page shows API errors, renew here. + Each host registers a key named Varaverk_HOST1, + Varaverk_HOST2, etc. + Keys are stored in master.conf + so every host can query every other host's GraphQL API directly — no SSH needed for remote monitoring.
- - + + +

@@ -337,46 +332,74 @@ function vvNtfSaveWebhook() {
     .catch(() => { btn.disabled = false; btn.textContent = 'Save'; fb.style.color='#ef5350'; fb.textContent='Request failed'; });
 }
 
-// ── Unraid API Key ────────────────────────────────────────────────────────────
+// ── Unraid API Keys ───────────────────────────────────────────────────────────
 function vvApiCheck() {
-  const btn  = document.getElementById('vv-api-check-btn');
-  const fb   = document.getElementById('vv-api-fb');
-  const badge= document.getElementById('vv-api-badge');
-  btn.disabled = true; btn.textContent = 'Checking…'; fb.textContent = '';
+  const btn   = document.getElementById('vv-api-check-btn');
+  const badge = document.getElementById('vv-api-badge');
+  const hosts = document.getElementById('vv-api-hosts');
+  btn.disabled = true; btn.textContent = 'Checking…';
   fetch('/plugins/varaverk/api/storage.php?action=api_status&_=' + Date.now())
     .then(r => r.json())
     .then(d => {
-      btn.disabled = false; btn.textContent = 'Check status';
-      if (!d.ok) { fb.style.color='#ef5350'; fb.textContent='Request failed'; return; }
-      badge.textContent  = d.api_ok ? 'ok' : d.key_missing ? 'missing' : 'error';
-      badge.className    = 'vv-set-badge ' + (d.api_ok ? 'internal' : 'flash');
-      fb.style.color     = d.api_ok ? '#4caf50' : '#ef5350';
-      fb.textContent     = d.api_ok ? 'API responding ✓'
-                         : d.key_missing ? 'Key missing from conf — click Renew'
-                         : 'API unreachable — ' + (d.fallbacks?.length ? d.fallbacks.join(', ') + ' using fallback' : 'check Unraid API service');
+      btn.disabled = false; btn.textContent = '↻ Check status';
+      if (!d.ok) return;
+      const allOk = d.hosts.every(h => h.api_ok);
+      const anyMissing = d.hosts.some(h => !h.key_present);
+      badge.textContent = allOk ? 'all ok' : anyMissing ? 'keys missing' : 'partial';
+      badge.className   = 'vv-set-badge ' + (allOk ? 'internal' : 'flash');
+
+      if (hosts) hosts.innerHTML = d.hosts.map(h => {
+        const local  = h.is_local ? ' local' : '';
+        const keyBit = h.key_present
+          ? `${h.key_preview}`
+          : `not set`;
+        const dot = h.api_ok ? '#4caf50' : '#555';
+        const name = `Varaverk_${h.host_id}`;
+        return `
+ + ${h.host_id}${local} + ${name} + ${keyBit} +
`; + }).join(''); }) - .catch(() => { btn.disabled = false; btn.textContent = 'Check status'; fb.style.color='#ef5350'; fb.textContent='Request failed'; }); + .catch(() => { btn.disabled = false; btn.textContent = '↻ Check status'; }); } -function vvApiRenew() { - const btn = document.getElementById('vv-api-renew-btn'); - const out = document.getElementById('vv-api-out'); - const fb = document.getElementById('vv-api-fb'); - const badge= document.getElementById('vv-api-badge'); - btn.disabled = true; btn.textContent = 'Renewing…'; fb.textContent = ''; - out.textContent = ''; out.style.display = ''; - const fd = new FormData(); fd.append('action', 'renew_apikey'); +function vvApiRenew(allHosts) { + const localBtn = document.getElementById('vv-api-local-btn'); + const allBtn = document.getElementById('vv-api-all-btn'); + const out = document.getElementById('vv-api-out'); + const fb = document.getElementById('vv-api-fb'); + const badge = document.getElementById('vv-api-badge'); + const activeBtn = allHosts ? allBtn : localBtn; + + [localBtn, allBtn].forEach(b => { if(b) b.disabled = true; }); + activeBtn.textContent = allHosts ? 'Setting up…' : 'Renewing…'; + fb.textContent = ''; out.textContent = ''; out.style.display = ''; + + const fd = new FormData(); + fd.append('action', 'setup_apikeys'); + fd.append('all_hosts', allHosts ? '1' : '0'); + fetch('/plugins/varaverk/api/storage.php', { method: 'POST', body: fd }) .then(r => r.json()) .then(d => { - btn.disabled = false; btn.textContent = 'Renew key'; - out.textContent = d.output || '(no output)'; - out.scrollTop = out.scrollHeight; - badge.textContent = d.ok ? 'ok' : 'error'; - badge.className = 'vv-set-badge ' + (d.ok ? 'internal' : 'flash'); + localBtn.disabled = false; localBtn.textContent = 'Renew local'; + allBtn.disabled = false; allBtn.textContent = 'Setup all host keys'; + out.textContent = d.output || '(no output)'; + out.scrollTop = out.scrollHeight; fb.style.color = d.ok ? '#4caf50' : '#ef5350'; - fb.textContent = d.ok ? 'Key renewed ✓' : 'Renewal failed — see output'; + fb.textContent = d.ok ? '✓ Done' : 'Failed — see output'; + if (d.ok) { badge.textContent = ''; vvApiCheck(); } }) - .catch(() => { btn.disabled = false; btn.textContent = 'Renew key'; fb.style.color='#ef5350'; fb.textContent='Request failed'; }); + .catch(() => { + localBtn.disabled = false; localBtn.textContent = 'Renew local'; + allBtn.disabled = false; allBtn.textContent = 'Setup all host keys'; + fb.style.color = '#ef5350'; fb.textContent = 'Request failed'; + }); } + +// Auto-check on load +vvApiCheck(); diff --git a/unRAID_Essentials/unraid_api_key_renew.sh b/unRAID_Essentials/unraid_api_key_renew.sh index 251b8df..a8b93fb 100755 --- a/unRAID_Essentials/unraid_api_key_renew.sh +++ b/unRAID_Essentials/unraid_api_key_renew.sh @@ -5,26 +5,31 @@ # # 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. +# Creates/syncs the Varaverk API key in the Unraid registry. The registry is +# ephemeral — OS updates and service restarts clear it. This script re-registers +# every boot so 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. +# Each host's key is named "Varaverk_HOST1", "Varaverk_HOST2", etc., and stored +# in master.conf (shared) so all hosts can call each other's GraphQL API directly +# for real-time monitoring without SSH. +# +# --all-hosts also SSHes to each partner, creates their key there, and writes +# all keys into master.conf. Run once from Settings → API Key → Setup +# to fully wire cross-host API access. # # ============================================================================================== # RUNTIME MODES # ============================================================================================== # # unraid_api_key_renew.sh -# Renew the key. Silent on success. +# Renew local key only — runs at array start, fast. +# +# unraid_api_key_renew.sh --all-hosts +# Renew local key AND SSH to each partner to create/sync their key. +# Writes all keys into master.conf and pushes to partners. # # unraid_api_key_renew.sh --dry-run -# Show what would happen — no changes made. -# # unraid_api_key_renew.sh --log -# Verbose output. # # ============================================================================================== @@ -37,11 +42,20 @@ acquire_lock detect_hosts # ────────────────────────────────────────────────────────────────────────────── -CONF_FILE="$SCRIPT_DIR/../Configurations/${MY_ID,,}.conf" -VAR_NAME="${MY_ID}_UNRAID_API_KEY" +# Parse --all-hosts from raw args (parse_args doesn't handle this flag) +ALL_HOSTS=false +for _arg in "$@"; do [[ "$_arg" == "--all-hosts" ]] && ALL_HOSTS=true; done +unset _arg -log "$ICON_GEAR Conf file: $CONF_FILE" -log "$ICON_GEAR Key var: $VAR_NAME" +CONF_FILE="$SCRIPT_DIR/../Configurations/${MY_ID,,}.conf" +MASTER_CONF="$SCRIPT_DIR/../Configurations/master.conf" +VAR_NAME="${MY_ID}_UNRAID_API_KEY" +KEY_NAME="Varaverk_${MY_ID}" + +log "$ICON_GEAR Conf file: $CONF_FILE" +log "$ICON_GEAR Key name: $KEY_NAME" +log "$ICON_GEAR Key var: $VAR_NAME" +log "$ICON_GEAR All hosts: $ALL_HOSTS" if [[ ! -f "$CONF_FILE" ]]; then error "Conf file not found: $CONF_FILE" @@ -49,63 +63,161 @@ if [[ ! -f "$CONF_FILE" ]]; then fi if [[ "$DRY_RUN" == true ]]; then - warn "DRY RUN — would check registry, renew only if key missing" + warn "DRY RUN — would check registry, renew if missing, write to host conf + master.conf" + [[ "$ALL_HOSTS" == true ]] && warn "DRY RUN — would also SSH to all partners and sync their keys" exit 0 fi +# ── Helper: write a key variable into a conf file ───────────────────────────── +_write_key_to_conf() { + 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 + echo " ${var}=\"${key}\"" >> "$conf" + fi +} + +# ── Helper: push master.conf to all partners ────────────────────────────────── +_push_master() { + php -r " + require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; + require_once '/usr/local/emhttp/plugins/varaverk/include/confform.php'; + vv_push_master_conf(); + " 2>/dev/null && log "master.conf pushed to partner(s)" || warn "master.conf push failed (partner offline?)" +} + # ────────────────────────────────────────────────────────────────────────────── -# 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 existing Varaverk key..." -EXISTING=$(timeout 5 /usr/local/sbin/unraid-api apikey --name "Varaverk" --json /dev/null) +# Step 1: Local key — check registry, create if missing, sync to conf +# ────────────────────────────────────────────────────────────────────────────── +echo "" +echo "━━━ $ICON_GEAR Local key ($KEY_NAME) ━━━" + +EXISTING=$(timeout 5 /usr/local/sbin/unraid-api apikey --name "$KEY_NAME" --json /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 — prevents stale key mismatch after reboot/update - CONF_KEY=$(grep "^\s*${VAR_NAME}\s*=" "$CONF_FILE" 2>/dev/null | \ - sed 's/.*="\(.*\)".*/\1/' | tr -d '[:space:]') - if [[ "$CONF_KEY" == "$KEY" ]]; then + CONF_KEY=$(grep "^\s*${VAR_NAME}\s*=" "$CONF_FILE" 2>/dev/null | sed 's/.*="\(.*\)".*/\1/' | tr -d '[:space:]') + MASTER_KEY=$(grep "^\s*${VAR_NAME}\s*=" "$MASTER_CONF" 2>/dev/null | sed 's/.*="\(.*\)".*/\1/' | tr -d '[:space:]') + + if [[ "$CONF_KEY" == "$KEY" && "$MASTER_KEY" == "$KEY" ]]; then echo "API key valid ✅ — $VAR_NAME = $PREVIEW" - log "Key in sync — no update needed" - exit 0 - fi - log "Registry key differs from conf — syncing..." - if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then - sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE" + log "Key in sync across host conf + master.conf" else - echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE" + log "Syncing key to conf files..." + _write_key_to_conf "$CONF_FILE" "$VAR_NAME" "$KEY" + _write_key_to_conf "$MASTER_CONF" "$VAR_NAME" "$KEY" + _push_master + warn "API key synced ✅ — $VAR_NAME = $PREVIEW" fi - warn "API key synced to conf ✅ — $VAR_NAME = $PREVIEW" - exit 0 -fi +else + log "Key not found in registry — creating $KEY_NAME..." + RAW=$(timeout 10 /usr/local/sbin/unraid-api apikey \ + --name "$KEY_NAME" --create --overwrite \ + --description "Varaverk plugin" --roles ADMIN --json &1) -log "Key not found in registry — creating new key..." + KEY=$(echo "$RAW" | jq -r '.key // empty' 2>/dev/null) + if [[ -z "$KEY" ]]; then + error "unraid-api returned no key: ${RAW:0:200}" + exit 1 + fi -RAW=$(timeout 10 /usr/local/sbin/unraid-api apikey \ - --name "Varaverk" --create --overwrite \ - --description "Varaverk plugin" --roles ADMIN --json &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 + _write_key_to_conf "$CONF_FILE" "$VAR_NAME" "$KEY" + _write_key_to_conf "$MASTER_CONF" "$VAR_NAME" "$KEY" + _push_master + PREVIEW="${KEY:0:8}...${KEY: -4}" + warn "API key created ✅ — $VAR_NAME = $PREVIEW" fi # ────────────────────────────────────────────────────────────────────────────── -if grep -q "^\s*${VAR_NAME}\s*=" "$CONF_FILE"; then - sed -i "s|^\(\s*${VAR_NAME}\s*=\s*\)\"[^\"]*\"|\1\"${KEY}\"|" "$CONF_FILE" -else - # Field missing from conf — append it - echo " ${VAR_NAME}=\"${KEY}\"" >> "$CONF_FILE" +# Step 2 (--all-hosts): SSH to each partner, create their key, write to master.conf +# ────────────────────────────────────────────────────────────────────────────── +[[ "$ALL_HOSTS" != true ]] && exit 0 + +echo "" +echo "━━━ $ICON_SYNC Partner keys ━━━" + +PARTNER_OK=0 +PARTNER_FAIL=0 + +for host_var in HOST1 HOST2 HOST3 HOST4 HOST5 HOST6 HOST7 HOST8; do + [[ "$host_var" == "$MY_ID" ]] && continue + hostname="${!host_var:-}" + [[ -z "$hostname" ]] && continue + + r_var_name="${host_var}_UNRAID_API_KEY" + r_key_name="Varaverk_${host_var}" + r_conf_path="/boot/config/plugins/varaverk/Configurations/${host_var,,}.conf" + + echo " $host_var ($hostname)…" + + REMOTE_IP=$(resolve_tailscale_ip "$hostname") + if [[ -z "$REMOTE_IP" ]]; then + warn " $host_var: cannot resolve Tailscale IP — skipping" + (( PARTNER_FAIL++ )) + continue + fi + + # SSH: check for existing key, create if missing, return the key value + REMOTE_KEY=$(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 '${r_key_name}' --json /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 '${r_key_name}' --create --overwrite \\ + --description 'Varaverk plugin' --roles ADMIN --json /dev/null \\ + | jq -r '.key // empty' 2>/dev/null + fi + " 2>/dev/null | tr -d '[:space:]') + + if [[ -z "$REMOTE_KEY" ]]; then + warn " $host_var: could not get key from $hostname — skipping" + (( PARTNER_FAIL++ )) + continue + fi + + R_PREVIEW="${REMOTE_KEY:0:8}...${REMOTE_KEY: -4}" + + # Write remote key to master.conf locally + _write_key_to_conf "$MASTER_CONF" "$r_var_name" "$REMOTE_KEY" + + # Also write to remote's host*.conf so they have it locally + ssh -i "$SSH_KEY" \ + -o ConnectTimeout=10 \ + -o StrictHostKeyChecking=no \ + -o BatchMode=yes \ + "root@${REMOTE_IP}" " + CONF='${r_conf_path}' + if [[ -f \"\$CONF\" ]]; then + if grep -q '^\s*${r_var_name}\s*=' \"\$CONF\"; then + sed -i \"s|^\(\s*${r_var_name}\s*=\s*\)\\\"[^\\\"]*\\\"|\1\\\"${REMOTE_KEY}\\\"|\" \"\$CONF\" + else + echo ' ${r_var_name}=\\\"${REMOTE_KEY}\\\"' >> \"\$CONF\" + fi + fi + " 2>/dev/null + + echo " $host_var: $r_key_name = $R_PREVIEW ✅" + (( PARTNER_OK++ )) +done + +# Push master.conf with all updated keys to all partners +if (( PARTNER_OK > 0 )); then + echo "" + echo " Pushing master.conf with all keys…" + _push_master 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)" +echo "" +echo "━━━━━ $ICON_SUMMARY Key Setup Summary ━━━━━" +echo " Local: ✅ $VAR_NAME" +echo " Partners: $PARTNER_OK updated · $PARTNER_FAIL failed" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"