Setup wizard: full first-run flow for HOST1 and HOST2

Varaverk.page: also shows wizard when local host.conf is missing
  (handles master.conf pushed by HOST1 before HOST2 installs plugin).

pages/setup.php: three wizard flows
  - Standard: blank master.conf, fill hostnames, redirect to scheduler
  - Host2/state file: state file detected, pull master.conf from HOST1 via SSH
  - Conf-only: master.conf already filled (was pushed), just create local host.conf

api/setup.php:
  - save action: writes master.conf + host.conf, creates varaverk_setup.db state file,
    redirects to ?tab=scheduler&vv_setup=master.conf
  - pull action: resolves HOST1 Tailscale IP, queries HOST1 SCRIPTS_DIR, SCPs
    master.conf, creates local host.conf, redirects to ?tab=scheduler&vv_setup=hostN.conf

include/config.php: vv_setup_state_read/write/push helpers.
  vv_push_setup_state() pushes varaverk_setup.db to /boot/config/ on all known
  remotes — no plugin-readiness probe needed (flash is always accessible).

api/rawconf.php: calls vv_push_setup_state() alongside master.conf push.

pages/scheduler.php: setup mode via ?vv_setup=<conf> URL param.
  Auto-opens the specified conf file on page load (DOMContentLoaded).
  vvSaveRawConf: in setup mode, skips confirm dialog and forces sequence:
    master.conf save → auto-open hostN.conf
    hostN.conf save → redirect to Monitor (setup complete)
This commit is contained in:
Gmer4Lfe
2026-05-30 15:43:04 -04:00
parent 1e9681a71e
commit af89258bbd
6 changed files with 396 additions and 242 deletions
+5 -1
View File
@@ -23,7 +23,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
exit;
}
$written = vv_write_conf_raw($file, $content);
$push = ($written && $file === 'master.conf') ? vv_push_master_conf() : [];
$push = [];
if ($written && $file === 'master.conf') {
$push = vv_push_master_conf();
vv_push_setup_state();
}
echo json_encode(['ok' => $written, 'push' => $push]);
exit;
}
+103 -48
View File
@@ -7,58 +7,119 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
exit;
}
$action = trim($_POST['action'] ?? 'save');
// ── HOST2 pull: pull master.conf from HOST1 via SSH ──────────────────────────────────────────
if ($action === 'pull') {
$host1Hostname = trim($_POST['host1_hostname'] ?? '');
$mySlot = trim($_POST['my_slot'] ?? 'host2');
$myHostname = trim($_POST['my_hostname'] ?? '');
if (!$host1Hostname) {
echo json_encode(['ok' => false, 'error' => 'HOST1 hostname required']);
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] ?? '/mnt/user/appdata/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) {
$hostname = $myHostname ?: vv_get_hostname();
$sshKeyPath = $sshKey;
$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);
vv_write_conf_raw($confFile, $conf);
}
}
echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile,
'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'); // 'host1', 'host2', 'host3', ...
$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;
}
// Validate slot — must be host\d+
if (!preg_match('/^host\d+$/', $mySlot)) {
echo json_encode(['ok' => false, 'error' => 'Invalid slot']);
exit;
}
// ── Write HOST1 / HOST2 into master.conf ─────────────────────────────────────────────────────
// 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;
}
// Replace HOST1 and HOST2 lines (preserving indentation)
$master = preg_replace(
'/^(\s*HOST1\s*=\s*).*$/m',
'${1}"' . addslashes($host1) . '"',
$master
);
$master = preg_replace(
'/^(\s*HOST2\s*=\s*).*$/m',
'${1}"' . addslashes($host2) . '"',
$master
);
$master = preg_replace('/^(\s*HOST1\s*=\s*).*$/m', '${1}"' . addslashes($host1) . '"', $master);
$master = preg_replace('/^(\s*HOST2\s*=\s*).*$/m', '${1}"' . addslashes($host2) . '"', $master);
// For partner slots beyond HOST2 — ensure the slot line exists in master.conf
$slotNum = (int) preg_replace('/\D/', '', $mySlot);
if ($slotNum > 2 && !empty($myHostname)) {
$hostKey = 'HOST' . $slotNum;
if (!preg_match('/^\s*' . $hostKey . '\s*=/m', $master)) {
// Append after HOST2 line
$master = preg_replace(
'/^(\s*HOST2\s*=.*$)/m',
'$1' . "\n {$hostKey}=\"" . addslashes($myHostname) . '"',
$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
);
$master = preg_replace('/^(\s*' . $hostKey . '\s*=\s*).*$/m',
'${1}"' . addslashes($myHostname) . '"', $master);
}
}
@@ -67,38 +128,32 @@ if (!vv_write_conf_raw('master.conf', $master)) {
exit;
}
// ── Create host*.conf from template ──────────────────────────────────────────────────────────
$hostId = strtoupper($mySlot); // HOST1, HOST2, HOST3 ...
$hostIdLow = strtolower($mySlot); // host1, host2, host3 ...
// Create host*.conf from template
$hostId = strtoupper($mySlot);
$hostIdLow = strtolower($mySlot);
$confFile = $hostIdLow . '.conf';
// Only create if it doesn't already exist — never overwrite an existing conf
if (!file_exists(CONF_DIR . '/' . $confFile)) {
$template = @file_get_contents(CONF_DIR . '/host.conf.template');
if ($template === false) {
$template = @file_get_contents(SCRIPTS_DIR . '/Configurations/host.conf.template');
}
if ($template !== false) {
// Derive SSH key path from hostname convention
// unRAID-MyServer → /root/.ssh/myserver_rsync_automation
$template = @file_get_contents(CONF_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);
// Pre-fill the SSH key path
$conf = preg_replace(
'/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
'${1}"' . $sshKeyPath . '"',
$conf
);
$conf = preg_replace('/^(\s*' . $hostId . '_SSH_KEY\s*=\s*)""/m',
'${1}"' . $sshKeyPath . '"', $conf);
if (!vv_write_conf_raw($confFile, $conf)) {
echo json_encode(['ok' => false, 'error' => "Failed to write $confFile"]);
exit;
}
}
// Template missing is non-fatal — user can create the conf manually
}
echo json_encode(['ok' => true, 'host_id' => $hostId, 'conf_file' => $confFile]);
// Write setup state file — lets partner servers know HOST1 is configured
vv_setup_state_write(['host1_hostname' => $host1]);
echo json_encode([
'ok' => true,
'host_id' => $hostId,
'redirect' => '?tab=scheduler&vv_setup=master.conf',
]);