/dev/null') ?: ''); $bootDisk = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : ''; $transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : 'unknown'; $isUsb = ($transport === 'usb'); preg_match('/version="([^"]+)"/', @file_get_contents('/etc/unraid-version') ?: '', $vm); echo json_encode([ 'ok' => true, 'hostname' => vv_get_hostname(), 'unraid_ver' => $vm[1] ?? 'unknown', 'transport' => $transport, 'boot_device' => $bootDisk ? '/dev/' . $bootDisk : 'unknown', 'mode' => $isUsb ? 'flash' : 'internal', 'scripts_dir' => SCRIPTS_DIR, ]); exit; } // ── GET/POST: generate local SSH keypair ────────────────────────────────────────────────────── if ($action === 'ssh_generate') { $script = SCRIPTS_DIR . '/Partnership/ssh_setup.sh'; if (!file_exists($script)) { echo json_encode(['ok' => false, 'error' => 'ssh_setup.sh not found']); exit; } exec('bash ' . escapeshellarg($script) . ' --local-only 2>&1', $out, $rc); // Derive pubkey path from hostname $hostname = vv_get_hostname(); $shortName = strtolower(preg_replace('/^unraid-/i', '', $hostname)); $pubPath = '/root/.ssh/' . $shortName . '_rsync_automation.pub'; $pubKey = trim(@file_get_contents($pubPath) ?: ''); echo json_encode([ 'ok' => $rc === 0 && !empty($pubKey), 'pubkey' => $pubKey, 'error' => ($rc !== 0) ? implode(' ', array_slice(array_filter(array_map('trim', $out)), -3)) : null, ]); exit; } // ── POST: run conf_populate.sh ───────────────────────────────────────────────────────────────── if ($action === 'populate') { $script = DEPLOY_DIR . '/conf_populate.sh'; if (!file_exists($script)) { echo json_encode(['ok' => false, 'error' => 'conf_populate.sh not found']); exit; } exec('bash ' . escapeshellarg($script) . ' --no-push 2>&1', $out, $rc); $lines = array_values(array_filter(array_map('trim', $out))); echo json_encode(['ok' => $rc === 0, 'lines' => array_slice($lines, 0, 20)]); exit; } if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode(['ok' => false, 'error' => 'Method not allowed']); exit; } $sshScript = SCRIPTS_DIR . '/Partnership/ssh_setup.sh'; // ── Pull master.conf from HOST1 via SSH (wizard or checklist) ──────────────────────────────── if ($action === 'pull') { $mySlot = trim($_POST['my_slot'] ?? '') ?: strtolower(vv_detect_host()); $myHostname = trim($_POST['my_hostname'] ?? '') ?: vv_get_hostname(); $host1Hostname = trim($_POST['host1_hostname'] ?? ''); if (!$host1Hostname) { $masterRaw = vv_read_conf_raw('master.conf'); preg_match('/^\s*HOST1\s*=\s*"([^"]*)"/m', $masterRaw, $_mh); $host1Hostname = trim($_mh[1] ?? ''); } if (!$host1Hostname) { echo json_encode(['ok' => false, 'error' => 'HOST1 hostname not set — fill in master.conf first']); exit; } if (!preg_match('/^host\d+$/', $mySlot)) { echo json_encode(['ok' => false, 'error' => 'Invalid slot']); exit; } $hostId = strtoupper($mySlot); $hostIdLow = strtolower($mySlot); // Derive SSH key path from this server's hostname $sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname ?: vv_get_hostname())); $sshKey = '/root/.ssh/' . $sshOwner . '_rsync_automation'; if (!file_exists($sshKey)) { echo json_encode(['ok' => false, 'error' => "SSH key not found at $sshKey — run Partnership/ssh_setup.sh first"]); exit; } // Resolve HOST1 Tailscale IP $ip = trim(shell_exec('tailscale ip -4 ' . escapeshellarg($host1Hostname) . ' 2>/dev/null') ?: ''); if (!$ip) { echo json_encode(['ok' => false, 'error' => "Cannot resolve Tailscale IP for $host1Hostname — is Tailscale running on both servers?"]); exit; } // Get HOST1's SCRIPTS_DIR from their varaverk.cfg $sshBase = 'ssh -i ' . escapeshellarg($sshKey) . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no root@' . $ip; $remoteCfg = trim(shell_exec($sshBase . ' "grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null"') ?: ''); preg_match('/SCRIPTS_DIR\s*=\s*["\']?([^"\']+)["\']?/', $remoteCfg, $sm); $remoteConf = rtrim($sm[1] ?? '/boot/config/plugins/varaverk', '/') . '/Configurations'; // SCP master.conf from HOST1 $localMaster = CONF_DIR . '/master.conf'; $src = escapeshellarg('root@' . $ip . ':' . $remoteConf . '/master.conf'); $cmd = 'scp -i ' . escapeshellarg($sshKey) . ' -o ConnectTimeout=10 -o StrictHostKeyChecking=no' . ' ' . $src . ' ' . escapeshellarg($localMaster) . ' 2>&1'; exec($cmd, $out, $rc); if ($rc !== 0) { echo json_encode(['ok' => false, 'error' => 'SCP failed: ' . implode('; ', $out) . ' — ensure your SSH key is authorised on HOST1 (run Partnership/ssh_setup.sh)']); exit; } // Create host conf from template if it doesn't exist $confFile = $hostIdLow . '.conf'; if (!file_exists(CONF_DIR . '/' . $confFile)) { $template = @file_get_contents(DEPLOY_DIR . '/host.conf.template') ?: ''; if ($template) { $bootPart2 = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: ''); $bootDisk2 = $bootPart2 ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart2) . ' 2>/dev/null') ?: '') : ''; $transport2 = $bootDisk2 ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk2) . ' 2>/dev/null') ?: '')) : ''; $storageInternal2 = ($transport2 !== 'usb') ? 'true' : 'false'; $conf = str_replace('HOSTN', $hostId, $template); $conf = str_replace('hostn', $hostIdLow, $conf); $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m', '${1}"' . $sshKey . '"', $conf); $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m', '${1}' . $storageInternal2, $conf); vv_write_conf_raw($confFile, $conf); } } if (file_exists($sshScript)) { exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null'); } $apiKeyResult = vv_auto_create_api_key($hostId, $confFile); $state = vv_setup_state_read(); $state['master_conf_pulled'] = 'true'; vv_setup_state_write($state); echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile, 'api_key' => $apiKeyResult, 'redirect' => '?tab=scheduler&vv_setup=' . $confFile]); exit; } // ── Default action: save (HOST1 first-run wizard) ──────────────────────────────────────────── $host1 = trim($_POST['host1'] ?? ''); $host2 = trim($_POST['host2'] ?? ''); $mySlot = trim($_POST['my_slot'] ?? 'host1'); $myHostname = trim($_POST['my_hostname'] ?? ''); if (empty($host1)) { echo json_encode(['ok' => false, 'error' => 'HOST1 hostname is required']); exit; } if (!preg_match('/^host\d+$/', $mySlot)) { echo json_encode(['ok' => false, 'error' => 'Invalid slot']); exit; } // Write HOST1 / HOST2 into master.conf $master = vv_read_conf_raw('master.conf'); if ($master === '') { echo json_encode(['ok' => false, 'error' => 'master.conf not found — check SCRIPTS_DIR in varaverk.cfg']); exit; } $master = preg_replace('/^(\s*HOST1\s*=\s*).*$/m', '${1}"' . addslashes($host1) . '"', $master); $master = preg_replace('/^(\s*HOST2\s*=\s*).*$/m', '${1}"' . addslashes($host2) . '"', $master); $slotNum = (int) preg_replace('/\D/', '', $mySlot); if ($slotNum > 2 && !empty($myHostname)) { $hostKey = 'HOST' . $slotNum; if (!preg_match('/^\s*' . $hostKey . '\s*=/m', $master)) { $master = preg_replace('/^(\s*HOST2\s*=.*$)/m', '$1' . "\n {$hostKey}=\"" . addslashes($myHostname) . '"', $master); } else { $master = preg_replace('/^(\s*' . $hostKey . '\s*=\s*).*$/m', '${1}"' . addslashes($myHostname) . '"', $master); } } if (!vv_write_conf_raw('master.conf', $master)) { echo json_encode(['ok' => false, 'error' => 'Failed to write master.conf']); exit; } // Create host*.conf from template $hostId = strtoupper($mySlot); $hostIdLow = strtolower($mySlot); $confFile = $hostIdLow . '.conf'; // Storage mode: use wizard selection, fall back to auto-detect from boot transport $smParam = trim($_POST['storage_mode'] ?? ''); if ($smParam === 'flash') { $storageInternal = 'false'; } elseif ($smParam === 'internal') { $storageInternal = 'true'; } else { $bootPart = trim(shell_exec('findmnt -n -o SOURCE /boot 2>/dev/null') ?: ''); $bootDisk = $bootPart ? trim(shell_exec('lsblk -no pkname ' . escapeshellarg($bootPart) . ' 2>/dev/null') ?: '') : ''; $transport = $bootDisk ? strtolower(trim(shell_exec('lsblk -dno TRAN /dev/' . escapeshellarg($bootDisk) . ' 2>/dev/null') ?: '')) : ''; $storageInternal = ($transport !== 'usb') ? 'true' : 'false'; } if (!file_exists(CONF_DIR . '/' . $confFile)) { $template = @file_get_contents(DEPLOY_DIR . '/host.conf.template') ?: ''; if ($template) { $sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname)); $sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation'; $conf = str_replace('HOSTN', $hostId, $template); $conf = str_replace('hostn', $hostIdLow, $conf); $conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m', '${1}"' . $sshKeyPath . '"', $conf); $conf = preg_replace('/^(\s*' . $hostId . '_STORAGE_MODE_INTERNAL\s*=\s*)\S+/m', '${1}' . $storageInternal, $conf); if (!vv_write_conf_raw($confFile, $conf)) { echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]); exit; } } } // Write setup state file — lets partner servers know HOST1 is configured vv_setup_state_write(['host1_hostname' => $host1]); // Auto-generate SSH keypair (local only — remote copy happens during onboarding) if (file_exists($sshScript)) { exec('bash ' . escapeshellarg($sshScript) . ' --local-only 2>/dev/null'); } // Auto-create Unraid API key and write into the fresh conf $apiKeyResult = vv_auto_create_api_key($hostId, $confFile); $targetDir = ($storageInternal === 'true') ? '/boot/config/plugins/varaverk' : '/mnt/user/appdata/Varaverk'; $needsMigration = (defined('SCRIPTS_DIR') && SCRIPTS_DIR !== $targetDir); echo json_encode([ 'ok' => true, 'host_id' => $hostId, 'api_key' => $apiKeyResult, 'needs_migration'=> $needsMigration, 'migrate_to' => $needsMigration ? ($storageInternal === 'true' ? 'internal' : 'flash') : null, 'redirect' => '?tab=scheduler&vv_setup=master.conf', ]);