($v['PARTNERSHIP_ENABLED'] ?? 'false') === 'true', 'owner_host' => $v['PARTNERSHIP_OWNER_HOST'] ?? '', 'sync_min' => (int)($v['PARTNERSHIP_SYNC_INTERVAL'] ?? 15), 'grace_hours' => (int)($v['PARTNERSHIP_GRACE_HOURS'] ?? 6), 'offline_threshold' => (int)($v['PARTNERSHIP_OFFLINE_THRESHOLD'] ?? 30), 'remove_tailscale' => ($v['PARTNERSHIP_REMOVE_TAILSCALE'] ?? 'true') === 'true', 'folderview3' => ($v['PARTNERSHIP_FOLDERVIEW3'] ?? 'false') === 'true', 'tailscale_configured' => !empty($v['TAILSCALE_API_KEY']) && !empty($v['TAILSCALE_TAILNET']), ]; } // ── State file parser ───────────────────────────────────────────────────────── function vv_pt_read_db(string $path): array { if (!file_exists($path)) return []; $out = []; foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { [$k, $v] = array_pad(explode('=', trim($line), 2), 2, ''); if ($k) $out[trim($k)] = trim($v, '"\''); } return $out; } // ── Tailscale peers ─────────────────────────────────────────────────────────── function vv_pt_ts_peers(): array { $raw = shell_exec('tailscale status --json 2>/dev/null') ?: '{}'; $data = json_decode($raw, true) ?: []; $peers = []; // Self $self = $data['Self'] ?? []; $selfLabel = strtolower(explode('.', $self['DNSName'] ?? '')[0]); if ($selfLabel) { $peers[$selfLabel] = [ 'online' => true, 'active' => true, 'ip' => $self['TailscaleIPs'][0] ?? null, ]; } // Peers foreach ($data['Peer'] ?? [] as $peer) { $label = strtolower(explode('.', $peer['DNSName'] ?? '')[0]); if (!$label) continue; $peers[$label] = [ 'online' => (bool)($peer['Online'] ?? false), 'active' => (bool)($peer['Active'] ?? false), 'ip' => $peer['TailscaleIPs'][0] ?? null, ]; } return $peers; } // ── SSH helper — run a single command on a remote host ──────────────────────── function vv_pt_ssh(string $ip, string $sshKey, string $cmd, int $timeout = 4): string { if (!$ip || !$sshKey || !file_exists($sshKey)) return ''; $full = sprintf( 'ssh -i %s -o ConnectTimeout=%d -o StrictHostKeyChecking=no -o BatchMode=yes root@%s %s 2>/dev/null', escapeshellarg($sshKey), $timeout, escapeshellarg($ip), escapeshellarg($cmd) ); return shell_exec($full) ?: ''; } // ── System info ─────────────────────────────────────────────────────────────── function vv_pt_local_system(): array { $ver = ''; if (file_exists('/etc/unraid-version')) { preg_match('/VERSION="([^"]+)"/', file_get_contents('/etc/unraid-version'), $m); $ver = $m[1] ?? ''; } $uptime = 0; if (file_exists('/proc/uptime')) { $uptime = (int)explode(' ', file_get_contents('/proc/uptime'))[0]; } return ['unraid_version' => $ver, 'uptime_sec' => $uptime]; } function vv_pt_remote_system(string $ip, string $sshKey): array { $out = vv_pt_ssh($ip, $sshKey, 'printf "%s\nUPTIME:%s\n" "$(cat /etc/unraid-version 2>/dev/null)" "$(cat /proc/uptime 2>/dev/null)"'); $ver = ''; preg_match('/VERSION="([^"]+)"/', $out, $m); if ($m) $ver = $m[1]; $uptime = 0; if (preg_match('/UPTIME:([\d.]+)/', $out, $m)) $uptime = (int)$m[1]; return ['unraid_version' => $ver, 'uptime_sec' => $uptime]; } // ── Per-node data ───────────────────────────────────────────────────────────── function vv_pt_nodes(): array { $currentHost = vv_detect_host(); $hosts = vv_arr_known_hosts(); // ['host1' => 'hostname', ...] $vars = vv_conf_vars(); $tsPeers = vv_pt_ts_peers(); $ownerSlot = strtolower($vars['PARTNERSHIP_OWNER_HOST'] ?? ''); // SSH key for this host $myId = strtoupper($currentHost); $myRaw = vv_read_conf_raw($currentHost . '.conf'); $mySshKey = vv_arr_scalar($myRaw, $myId . '_SSH_KEY'); $nodes = []; foreach ($hosts as $slot => $hostname) { $isMe = ($slot === $currentHost || $currentHost === 'unknown'); $isOwner = (strtolower($ownerSlot) === $slot); // Tailscale $tsLabel = strtolower($hostname); $ts = $tsPeers[$tsLabel] ?? ['online' => null, 'active' => false, 'ip' => null]; // Fallback state $fbState = 'UNKNOWN'; $fbPath = '/boot/config/fallback_state.db'; if ($isMe) { $fb = vv_pt_read_db($fbPath); $fbState = $fb['state'] ?? 'UNKNOWN'; } elseif ($ts['online'] && $ts['ip'] && $mySshKey) { $out = vv_pt_ssh($ts['ip'], $mySshKey, 'cat /boot/config/fallback_state.db 2>/dev/null'); if ($out) { $fb = []; foreach (explode("\n", $out) as $line) { [$k, $v] = array_pad(explode('=', trim($line), 2), 2, ''); if ($k) $fb[trim($k)] = trim($v, '"\''); } $fbState = $fb['state'] ?? 'UNKNOWN'; } } // Partnership DB — local only (each server writes its own) $dbPath = "/boot/config/partnership_{$hostname}.db"; $ptDb = vv_pt_read_db($dbPath); // System info $system = $isMe ? vv_pt_local_system() : ($ts['online'] && $ts['ip'] && $mySshKey ? vv_pt_remote_system($ts['ip'], $mySshKey) : []); $nodes[] = [ 'slot' => $slot, 'id' => strtoupper($slot), 'hostname' => $hostname, 'is_me' => $isMe, 'is_owner' => $isOwner, 'ts_online' => $ts['online'], 'ts_active' => $ts['active'], 'ts_ip' => $ts['ip'], 'fallback' => $fbState, 'partnership' => $ptDb, 'system' => $system, ]; } return $nodes; } // ── Entry point ─────────────────────────────────────────────────────────────── function vv_partnership_all(): array { return [ 'config' => vv_pt_config(), 'nodes' => vv_pt_nodes(), 'ts' => time(), ]; }