_STORAGE_MODE_INTERNAL. // That value decides where the plugin's data lives, and on HOST1 it is deliberately // pinned. Running detect on a host whose boot device reports an unexpected transport // would rewrite it. The action exists for onboarding a new host, where nothing has been // decided yet — it is not a repair tool for an established one. // // The conf write goes through the structured writer, so it is atomic and syntax-checked — // which is also why include/confform.php must be loaded here. It was previously absent and // the detect action fataled on an undefined function. // // REQUEST // GET|POST ?action=status current mode, detected mode, boot device, conf value // GET|POST ?action=api_status per-host API key presence and local API health // POST action=migrate to=internal|flash // POST action=detect write the detected mode into this host's conf // POST action=setup_apikeys run unraid_api_key_renew.sh // // RESPONSE // status {"ok":true,"current_mode","current_dir","internal_dir","flash_dir", // "transport","detected","boot_disk","conf_key","conf_val","array_started"} // api_status {"ok":true,"my_id","key_name","hosts":[…],"fallbacks":…} // migrate {"ok":bool,"exit":int,"output":"…"} // detect {"ok":bool,"detected":"true|false","transport":"…"} // setup_apikeys {"ok":bool,"exit":int,"output":"…"} // {"ok":false,"error":"Invalid target: …"|"storage_migrate.sh not found" // |"unraid_api_key_renew.sh not found"|"… timed out …"|"Unknown action"} // // DEPENDS ON // include/config.php SCRIPTS_DIR, vv_detect_host(), vv_conf_vars() // include/confform.php vv_conf_write_changes() // include/unraid_api.php vv_api_get_status() (loaded only by api_status) // Tools/storage_migrate.sh // System_Essentials/unraid_api_key_renew.sh // findmnt, lsblk, zpool // ═══════════════════════════════════════════════════════════════════════════════════════════════ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/config.php'; // vv_conf_write_changes() lives here — the detect action fatals without it. require_once dirname(__DIR__) . '/include/confform.php'; $action = $_GET['action'] ?? $_POST['action'] ?? ''; // ── Boot device detection ───────────────────────────────────────────────────── function vv_storage_detect_transport(): string { $src = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: ''); if (!$src) return 'unknown'; if (str_starts_with($src, '/dev/')) { // Block device partition — walk up to whole disk $disk = trim(shell_exec("lsblk -no pkname " . escapeshellarg($src) . " 2>/dev/null") ?: ''); if (!$disk) return 'unknown'; } else { // ZFS dataset (pool/dataset) — find a backing device via zpool $pool = explode('/', $src)[0]; $dev = trim(shell_exec("zpool list -vHp " . escapeshellarg($pool) . " 2>/dev/null | awk 'NR>1 && \$1~/^[a-z]/ && \$1!~/mirror|raidz|spare/{print \$1; exit}'") ?: ''); if (!$dev) return 'unknown'; // Strip partition suffix to get the whole-disk name $disk = trim(shell_exec("lsblk -no pkname /dev/" . escapeshellarg($dev) . " 2>/dev/null") ?: $dev); } return strtolower(trim(shell_exec("lsblk -dno TRAN /dev/" . escapeshellarg($disk) . " 2>/dev/null") ?: 'unknown')); } // ── Current mode status ─────────────────────────────────────────────────────── if ($action === 'status') { $transport = vv_storage_detect_transport(); $detected = ($transport === 'usb') ? 'flash' : 'internal'; $currentDir = SCRIPTS_DIR; $internalDir = '/boot/config/plugins/varaverk'; $flashDir = '/mnt/user/appdata/Varaverk'; $currentMode = ($currentDir === $internalDir) ? 'internal' : ($currentDir === $flashDir ? 'flash' : 'custom'); $myHost = vv_detect_host(); $vars = vv_conf_vars(); $confKey = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL'; $confVal = $vars[$confKey] ?? null; // Boot device name for display $bootSrc = trim(shell_exec("findmnt -n -o SOURCE /boot 2>/dev/null") ?: ''); $bootDisk = ''; if (str_starts_with($bootSrc, '/dev/')) { $bootDisk = trim(shell_exec("lsblk -no pkname " . escapeshellarg($bootSrc) . " 2>/dev/null") ?: ''); $bootDisk = $bootDisk ? '/dev/' . $bootDisk : ''; } elseif ($bootSrc) { $bootDisk = $bootSrc; // ZFS: show "flash/boot" } echo json_encode([ 'ok' => true, 'current_mode' => $currentMode, 'current_dir' => $currentDir, 'internal_dir' => $internalDir, 'flash_dir' => $flashDir, 'transport' => $transport, 'detected' => $detected, 'boot_disk' => $bootDisk ?: 'unknown', 'conf_key' => $confKey, 'conf_val' => $confVal, 'array_started'=> is_dir('/mnt/user') && count(scandir('/mnt/user')) > 2, ]); exit; } // ── Run migration ───────────────────────────────────────────────────────────── if ($action === 'migrate' && $_SERVER['REQUEST_METHOD'] === 'POST') { $to = trim($_POST['to'] ?? ''); if (!in_array($to, ['internal', 'flash'], true)) { echo json_encode(['ok' => false, 'error' => 'Invalid target: must be internal or flash']); exit; } $script = dirname(__DIR__) . '/Tools/storage_migrate.sh'; if (!file_exists($script)) { echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh not found']); exit; } // set_time_limit() does not cover exec() time on Linux, so the bound is external. set_time_limit(630); $output = []; $exit = 0; exec('timeout 600 bash ' . escapeshellarg($script) . ' --to=' . escapeshellarg($to) . ' 2>&1', $output, $exit); if ($exit === 124) { echo json_encode(['ok' => false, 'error' => 'storage_migrate.sh timed out after 600s', 'output' => implode("\n", $output)]); exit; } echo json_encode([ 'ok' => $exit === 0, 'exit' => $exit, 'output' => implode("\n", $output), ]); exit; } // ── Auto-detect and write to conf ───────────────────────────────────────────── if ($action === 'detect' && $_SERVER['REQUEST_METHOD'] === 'POST') { $transport = vv_storage_detect_transport(); $detected = ($transport === 'usb') ? 'false' : 'true'; $myHost = vv_detect_host(); $confKey = strtoupper($myHost) . '_STORAGE_MODE_INTERNAL'; $confFile = $myHost . '.conf'; $results = vv_conf_write_changes([[ 'file' => $confFile, 'key' => $confKey, 'value' => $detected, 'type' => 'scalar', ]]); $ok = !in_array(false, $results, true); echo json_encode(['ok' => $ok, 'detected' => $detected, 'transport' => $transport]); exit; } // ── Unraid API key status ───────────────────────────────────────────────────── if ($action === 'api_status') { require_once dirname(__DIR__) . '/include/unraid_api.php'; $localStatus = vv_api_get_status(); $vars = vv_conf_vars(); $myHost = vv_detect_host(); $myId = strtoupper($myHost); $hn = trim((string)shell_exec("hostname -s 2>/dev/null | sed 's/^[Uu][Nn][Rr][Aa][Ii][Dd]-//'")) ?: 'Varaverk'; $localKeyName = 'Varaverk ' . $hn; $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, 'my_id' => $myId, 'key_name' => $localKeyName, 'hosts' => $hosts, 'fallbacks' => $localStatus['fallbacks'], ]); exit; } // ── Setup/renew API keys (local + all partners via SSH) ─────────────────────── if ($action === 'setup_apikeys' && $_SERVER['REQUEST_METHOD'] === 'POST') { $script = SCRIPTS_DIR . '/Plugin/unraid/System_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() does not cover exec() time on Linux — this script reaches every // partner over SSH, so the bound has to be external. set_time_limit(150); $output = []; $exit = 0; exec('timeout 120 bash ' . escapeshellarg($script) . ' 2>&1', $output, $exit); if ($exit === 124) { echo json_encode(['ok' => false, 'error' => 'unraid_api_key_renew.sh timed out after 120s']); exit; } echo json_encode(['ok' => $exit === 0, 'exit' => $exit, 'output' => implode("\n", $output)]); exit; } echo json_encode(['ok' => false, 'error' => 'Unknown action']);