Skip the key step when SSH already works, and read the install path live on both sides

This commit is contained in:
Gmer4Lfe
2026-08-17 07:04:46 -04:00
parent 7e7d845f61
commit d8e3c09b57
4 changed files with 60 additions and 33 deletions
+19
View File
@@ -315,14 +315,33 @@ if [[ "$AM_MIRROR" == true ]]; then
echo " Mirror sets up SSH keys, then notifies Owner to run Phase 2."
echo ""
# Tested before attempted, the same guard the owner's phase 1 has had all along.
#
# The key step is a TERMINAL step by design — ssh_setup.sh runs ssh-copy-id, which prompts for
# the owner's root password on a first install. Once the operator has done that in a terminal,
# pressing ▶ Onboard ran the whole thing again: another ssh-copy-id, this time from the WebGUI
# with no TTY to answer the prompt, which fails and aborts the run at Step 1 — so the button
# whose entire job is Step 2 could never reach it.
#
# Working SSH is the actual precondition, not "have we run the setup script". If it already
# works there is nothing to install, whichever route installed it.
OWNER_IP_PRE=$(resolve_tailscale_ip "$OWNER" 2>/dev/null || true)
if [[ "$SKIP_SSH" == true ]]; then
warn "Skipping SSH setup (--skip-ssh)"
elif [[ -n "$OWNER_IP_PRE" ]] && timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \
-o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes -o StrictHostKeyChecking=no \
root@"$OWNER_IP_PRE" exit 0 2>/dev/null; then
echo "SSH to $OWNER already works ✅ — key already installed, skipping setup"
elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then
echo "SSH key ready ✅"
else
error "SSH key setup failed"
error "Install the key from a terminal on this host — ssh-copy-id needs $OWNER's password,"
error "and a WebGUI button has no way to answer that prompt:"
error " bash $SCRIPTS_ROOT/Partnership/partnership_onboard.sh --phase1-only"
exit 1
fi
unset OWNER_IP_PRE
# Stop after the key when asked. ssh_setup.sh runs ssh-copy-id, which prompts for the
# owner's root password on a first install — answerable in a terminal, never from the
+18
View File
@@ -203,6 +203,24 @@ function vvPrompt(text, def, opts) {
//
// Not in js/varaverk.js: that loads below the tab include, and the wizard returns before it.
// Where Varaverk is installed on THIS host, read live rather than baked into the page.
//
// Two reasons it cannot be a render-time constant. The wizard can move it: choosing appdata in
// step 1 triggers a migration and step 2 renders in the same page load, so PHP's value names the
// pre-migration location. And each host chooses independently — the owner may be on flash while
// the mirror is on appdata — so a panel rendered on either side must ask, not assume.
//
// It matters because the value ends up in a command the operator pastes into a root terminal.
// api/setup.php?action=detect re-reads varaverk.cfg. Cached; the panels re-render often.
let _vvScriptsDir = null;
function vvScriptsDir(fallback) {
if (_vvScriptsDir) return Promise.resolve(_vvScriptsDir);
return fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now())
.then(r => r.json())
.then(d => (_vvScriptsDir = (d && d.scripts_dir) || fallback))
.catch(() => (_vvScriptsDir = fallback));
}
// api/run.php answers when a job is LAUNCHED, not finished. An empty body is never success —
// Unraid's CSRF guard exits with one, and so does a PHP fatal.
function _vvPtRun(id, extraArgs) {
+10 -1
View File
@@ -626,6 +626,11 @@ async function vvPtSaveSettings(btn) {
// ── Private page logic ─────────────────────────────────────────────────────────
(function() {
// Resolve the install path once, up front, and redraw when it lands. termCmd is built
// synchronously inside _renderActions, so without this the first paint would use the
// render-time fallback — right on a flash host, wrong on one that moved to appdata.
vvScriptsDir(<?= json_encode(SCRIPTS_DIR) ?>).then(() => { if (_vvPtReload) _vvPtReload(); });
// ── Helpers ───────────────────────────────────────────────────────────────────
function _row(lbl, val) {
@@ -950,7 +955,11 @@ function _renderActions(nodes, cfg) {
const remotes = nodes.filter(n => !n.is_me);
const hasPartner = remotes.some(n => n.hostname);
const termBase = `https://${window.location.hostname}/webterminal/ttyd/`;
const termCmd = `bash <?= SCRIPTS_DIR ?>/Partnership/partnership_onboard.sh --phase1-only`;
// _vvScriptsDir is filled by the live probe below and falls back to the render-time value.
// Both matter: a host in appdata must be handed its appdata path, and this panel is rendered
// on whichever side is the mirror — so it cannot assume either layout. The owner and the
// mirror are independently either flash or appdata.
const termCmd = `bash ${_vvScriptsDir || <?= json_encode(SCRIPTS_DIR) ?>}/Partnership/partnership_onboard.sh --phase1-only`;
let html = '';
+13 -32
View File
@@ -72,6 +72,18 @@ if ($vvMasterRaw !== '' && $detectedHostname !== '') {
if ($vvDetected['role'] === 'partner') $vvDetected['primary'] = $vvSlots['host1'] ?? '';
}
// ── How far this host's own onboarding has got ───────────────────────────────────────────────
// Lets the inline join panel show the SSH step or the join button rather than both. Same flags
// api/checklist.php reads, and the same two spellings, because the state file has been written
// by both bash and PHP over its life.
if ($vvDetected['slot'] !== '') {
$vvSt = vv_setup_state_read();
$vvSlUp = strtoupper($vvDetected['slot']);
$vvDetected['phase'] =
(!empty($vvSt[$vvSlUp . '_PHASE2_DONE']) || !empty($vvSt[$vvDetected['slot'] . '_phase2_done'])) ? 2
: ((!empty($vvSt[$vvSlUp . '_PHASE1_DONE']) || !empty($vvSt[$vvDetected['slot'] . '_phase1_done'])) ? 1 : 0);
}
// ── Custom docker networks, adopted from the owner ───────────────────────────────────────────
// master.conf names the hosts; it does not name the networks, which live in each host's own
// host*.conf. The owner's copy is available here anyway: onboard Phase 1 caches it into
@@ -84,17 +96,6 @@ if ($vvMasterRaw !== '' && $detectedHostname !== '') {
//
// Adopting the owner's list also means docker_network_connect.sh will recreate the network here
// after an Unraid update wipes it, which is the whole reason that script exists.
// How far this host's own onboarding has got, so the inline join panel can show the SSH step or
// the join button rather than both. Same flags api/checklist.php reads, and the same two
// spellings, because the state file has been written by both bash and PHP over its life.
if ($vvDetected['slot'] !== '') {
$vvSt = vv_setup_state_read();
$vvSlUp = strtoupper($vvDetected['slot']);
$vvDetected['phase'] =
(!empty($vvSt[$vvSlUp . '_PHASE2_DONE']) || !empty($vvSt[$vvDetected['slot'] . '_phase2_done'])) ? 2
: ((!empty($vvSt[$vvSlUp . '_PHASE1_DONE']) || !empty($vvSt[$vvDetected['slot'] . '_phase1_done'])) ? 1 : 0);
}
if ($vvDetected['role'] === 'partner' && $vvDetected['slot'] !== '') {
$vvOwnerConf = VV_CONF_RAM_CACHE_DIR . '/host1.conf';
if (is_readable($vvOwnerConf)) {
@@ -506,26 +507,6 @@ function vvDeferItem(btn, id, defer) {
//
// Only while it is still outstanding: once the mirror has joined, the partnership row in the
// checklist above says so and a second copy of the panel is noise.
// SCRIPTS_DIR is read live rather than baked into the page, because the wizard can MOVE it.
//
// Choosing appdata storage in step 1 triggers a migration, and step 2 renders in the same page
// load — so anything PHP substituted at render time still names the pre-migration location. A
// host that migrated was handed "bash /boot/config/plugins/varaverk/…" to paste, which does not
// exist there. The command is the one thing on this panel that must be right: it is pasted into
// a root terminal.
//
// api/setup.php?action=detect re-reads varaverk.cfg, so it reports wherever the install actually
// ended up. Cached after the first call — the checklist re-renders on every action.
let _vvScriptsDir = null;
function vvScriptsDir() {
if (_vvScriptsDir) return Promise.resolve(_vvScriptsDir);
return fetch('/plugins/varaverk/api/setup.php?action=detect&_=' + Date.now())
.then(r => r.json())
.then(d => (_vvScriptsDir = (d && d.scripts_dir) || <?= json_encode(SCRIPTS_DIR) ?>))
// Falling back to the render-time value is still better than no command at all; it is only
// wrong in the case this function exists to handle, and the panel shows the path either way.
.catch(() => (_vvScriptsDir = <?= json_encode(SCRIPTS_DIR) ?>));
}
function vvRenderOnboardPanel(d) {
const el = document.getElementById('vv-onboard-panel');
@@ -534,7 +515,7 @@ function vvRenderOnboardPanel(d) {
const pt = (d.items || []).find(i => i.id === 'partnership');
if (!partner || !pt || pt.ok) { el.innerHTML = ''; return; }
vvScriptsDir().then(dir => {
vvScriptsDir(<?= json_encode(SCRIPTS_DIR) ?>).then(dir => {
el.innerHTML = '<hr class="vv-hr">'
+ '<div class="vv-cl-title">Join the partnership</div>'
+ vvRenderMirrorOnboard({