Files
Varaverk/Plugin/unraid/api/setup.php
T
Gmer4Lfe 9c3ace95a7 Auth stack certs tab, arrs db fallbacks, cert monitor cache, conf parser fix
- Auth stack: fold cert monitor into Auth Stack page as fourth tab (Certs);
  remove standalone cert page and top-level tab
- cert_monitor.sh: write JSON status cache to State_Files/cert_status.json
  after each run; expose per-domain days/expiry via _CERT_DAYS/_CERT_EXPIRY globals
- api/cert.php: new — serves cached cert status; falls back to configured
  domains as UNKN when no cache exists; POST action=run triggers live check
- arrs db fallbacks: vv_arr_cleanup_stats/discovery_stats/recovery_stats now
  read from data/*.db files when log JSON files don't yet exist
- config.php vv_conf_vars(): unescape bash \$ → $ so passwords with dollar
  signs read correctly from conf files
- host1.conf: fill in HOST1_NPM_USER/PASS and HOST1_LLDAP_USER/PASS
- Partnership adapter pattern: Unraid-specific container logic extracted to
  Plugin/unraid/Partnership/; platform-agnostic structure stays in Partnership/
- First-run wizard: uniform multi-step flow for all hosts; HOST2 pull moved
  to checklist; auto SSH keygen and API key creation on save
- api/checklist.php: live setup checklist with pull_master action
- Fullscreen toggle: hide Unraid header/menu; state persists via localStorage
2026-06-05 23:17:30 -04:00

260 lines
11 KiB
PHP

<?php
header('Content-Type: application/json');
require_once dirname(__DIR__) . '/include/config.php';
$action = ($_SERVER['REQUEST_METHOD'] === 'GET')
? trim($_GET['action'] ?? '')
: trim($_POST['action'] ?? 'save');
// ── GET: detect environment ────────────────────────────────────────────────────────────────────
if ($action === 'detect') {
$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') ?: ''))
: '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 = SCRIPTS_DIR . '/Plugin/unraid/Tools/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(CONF_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';
if (!file_exists(CONF_DIR . '/' . $confFile)) {
$template = @file_get_contents(CONF_DIR . '/host.conf.template') ?: '';
if ($template) {
$sshOwner = strtolower(preg_replace('/^unraid-/i', '', $myHostname));
$sshKeyPath = '/root/.ssh/' . $sshOwner . '_rsync_automation';
// Auto-detect storage mode from boot device transport
$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';
$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);
echo json_encode([
'ok' => true,
'host_id' => $hostId,
'api_key' => $apiKeyResult,
'redirect' => '?tab=scheduler&vv_setup=master.conf',
]);