diff --git a/Partnership/partnership_onboard.sh b/Partnership/partnership_onboard.sh index 4381e96..7b11250 100755 --- a/Partnership/partnership_onboard.sh +++ b/Partnership/partnership_onboard.sh @@ -124,6 +124,15 @@ # Partnership/partnership_onboard.sh --skip-arr-sync # Skip arr library bootstrap (Step 8) # +# Partnership/partnership_onboard.sh --phase1-only +# OWNER only: SSH key exchange + conf push. Safe to run before HOST2 has Varaverk. +# Writes HOST2_PHASE1_DONE=true to varaverk_setup.db. +# +# Partnership/partnership_onboard.sh --phase2-only +# OWNER only: container deploy + arr + onboard (skips SSH). Triggered automatically +# by HOST2 after it completes its Mirror-path onboard. Can also be run manually. +# Writes HOST2_PHASE2_DONE=true to varaverk_setup.db. +# # ============================================================================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -138,6 +147,8 @@ SKIP_SSH=false SKIP_AUTH_STACK=false SKIP_ARR_STACK=false SKIP_ARR_SYNC=false +PHASE1_ONLY=false # OWNER: SSH + conf push only (HOST2 not yet installed) +PHASE2_ONLY=false # OWNER: containers/arr/onboard only (triggered by HOST2 after it onboards) FILTERED_ARGS=() for arg in "$@"; do @@ -146,6 +157,8 @@ for arg in "$@"; do --skip-auth-stack) SKIP_AUTH_STACK=true ;; --skip-arr-stack) SKIP_ARR_STACK=true ;; --skip-arr-sync) SKIP_ARR_SYNC=true ;; + --phase1-only) PHASE1_ONLY=true ;; + --phase2-only) PHASE2_ONLY=true; SKIP_SSH=true ;; *) FILTERED_ARGS+=("$arg") ;; esac done @@ -187,6 +200,21 @@ EXTRA_FLAGS=() START=$(date +%s) +# ── Helper: write phase completion flag to setup.db + push to remotes ───────────────────────── +write_onboard_phase() { + local target_id="$1" phase="$2" + local key="${target_id}_PHASE${phase}_DONE" + local state_file="/boot/config/varaverk_setup.db" + [[ "$DRY_RUN" == true ]] && { warn "DRY RUN — would write ${key}=true"; return 0; } + if grep -q "^${key}=" "$state_file" 2>/dev/null; then + sed -i "s|^${key}=.*|${key}=true|" "$state_file" + else + echo "${key}=true" >> "$state_file" + fi + command -v php &>/dev/null && \ + php -r "require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; vv_push_setup_state();" 2>/dev/null || true +} + echo "" echo "━━━ $ICON_FALLBACK Partnership Onboard — $MY_ID ($LOCAL_SERVER_NAME) — $(date '+%Y-%m-%d %H:%M:%S') ━━━" echo "" @@ -443,10 +471,9 @@ deploy_xml_stack() { # ── MIRROR PATH ─────────────────────────────────────────────────────────────────────────────── # ============================================================================================== if [[ "$AM_MIRROR" == true ]]; then - echo "━━━ Step 1/1 — SSH Key Setup (Mirror) ━━━" + echo "━━━ Step 1/2 — SSH Key Setup (Mirror) ━━━" echo "" - echo " Mirror only needs SSH keys ready." - echo " Owner ($OWNER) completes the rest remotely." + echo " Mirror sets up SSH keys, then notifies Owner to run Phase 2." echo "" if [[ "$SKIP_SSH" == true ]]; then @@ -458,10 +485,46 @@ if [[ "$AM_MIRROR" == true ]]; then exit 1 fi + echo "" + echo "━━━ Step 2/2 — Notify Owner to Run Phase 2 ━━━" + echo "" + + OWNER_IP=$(resolve_tailscale_ip "$OWNER" 2>/dev/null || true) + PHASE2_TRIGGERED=false + + if [[ -n "$OWNER_IP" ]]; then + # Read OWNER's SCRIPTS_DIR from their varaverk.cfg — don't assume same path as mirror + OWNER_SCRIPTS_DIR=$(timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \ + -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \ + 'grep SCRIPTS_DIR /boot/config/plugins/varaverk/varaverk.cfg 2>/dev/null | cut -d= -f2 | tr -d "\"'"'"'" 2>/dev/null' 2>/dev/null | tr -d '[:space:]') + OWNER_SCRIPTS_DIR="${OWNER_SCRIPTS_DIR:-/mnt/user/appdata/Varaverk}" + + if [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — would SSH to $OWNER ($OWNER_IP) and trigger Phase 2" + PHASE2_TRIGGERED=true + elif timeout "$SSH_TIMEOUT" ssh -i "$SSH_KEY" \ + -o ConnectTimeout="$SSH_TIMEOUT" -o BatchMode=yes root@"$OWNER_IP" \ + "nohup bash '${OWNER_SCRIPTS_DIR}/Partnership/partnership_onboard.sh' --phase2-only > /tmp/vv_phase2_onboard.log 2>&1 & echo triggered" \ + 2>/dev/null | grep -q triggered; then + log "Phase 2 triggered on $OWNER ✅" + log "Watch progress on $OWNER: tail -f /tmp/vv_phase2_onboard.log" + PHASE2_TRIGGERED=true + else + warn "Could not auto-trigger Phase 2 on $OWNER" + fi + else + warn "Cannot resolve $OWNER Tailscale IP" + fi + echo "" echo "━━━━━ $ICON_SUMMARY MIRROR SETUP COMPLETE ━━━━━" - echo " SSH key: ready" - echo " Next: Run Partnership/partnership_onboard.sh on $OWNER to complete setup" + echo " SSH key: ready" + echo " Phase 2 on $OWNER: $( [[ "$PHASE2_TRIGGERED" == true ]] && echo "triggered ✅" || echo "needs manual trigger ⚠" )" + if [[ "$PHASE2_TRIGGERED" == false ]]; then + echo "" + echo " Run manually on $OWNER:" + echo " bash Partnership/partnership_onboard.sh --phase2-only" + fi echo "━━━━━━━━━━━━━━━━━━━━━━━" exit 0 fi @@ -473,6 +536,8 @@ fi MIRROR_IP=$(resolve_tailscale_ip "$MIRROR") [[ -z "$MIRROR_IP" ]] && { error "Cannot resolve $MIRROR Tailscale IP — is Tailscale running?"; exit 1; } log "Mirror: $MIRROR ($MIRROR_IP)" +[[ "$PHASE1_ONLY" == true ]] && log "Mode: Phase 1 only (SSH + conf push)" +[[ "$PHASE2_ONLY" == true ]] && log "Mode: Phase 2 only (containers + arr + onboard)" echo "" STEP_SSH_OK=false @@ -490,10 +555,11 @@ ARR_SYNC_OK=false MASTER_PUSH_OK=false # ── Step 1: SSH ─────────────────────────────────────────────────────────────────────────────── -echo "━━━ Step 1/8 — SSH Key Setup ━━━" +# Skipped when --phase2-only (SSH was already done in Phase 1). +echo "━━━ Step 1 — SSH Key Setup ━━━" if [[ "$SKIP_SSH" == true ]]; then - warn "Skipping (--skip-ssh)" + warn "Skipping (--skip-ssh / --phase2-only)" STEP_SSH_OK=true elif bash "$SCRIPT_DIR/ssh_setup.sh" "${EXTRA_FLAGS[@]}"; then log "SSH keys ready ✅" @@ -504,9 +570,61 @@ else exit 1 fi +# ── Phase 1 exit point ──────────────────────────────────────────────────────────────────────── +# --phase1-only: SSH + conf push is all HOST1 needs to do before HOST2 installs Varaverk. +# HOST2's wizard will detect the pushed master.conf + state file and take the correct path. +if [[ "$PHASE1_ONLY" == true ]]; then + echo "" + echo "━━━ Phase 1 — Conf Push ━━━" + + CONF_PUSH_OK=false + if [[ "$DRY_RUN" == true ]]; then + warn "DRY RUN — would push master.conf + state file to $MIRROR" + CONF_PUSH_OK=true + elif ! command -v php &>/dev/null; then + warn "php not available — push master.conf manually via Scheduler → master.conf → Save Conf" + else + push_output=$(php -r " +require_once '/usr/local/emhttp/plugins/varaverk/include/config.php'; +\$results = vv_push_master_conf(); +vv_push_setup_state(); +if (empty(\$results)) { echo 'no remote hosts'; exit(0); } +\$failed = 0; +foreach (\$results as \$r) { + echo \$r['host'] . ': ' . (\$r['ok'] ? 'pushed' : 'FAILED — ' . \$r['error']) . PHP_EOL; + if (!\$r['ok']) \$failed++; +} +exit(\$failed > 0 ? 1 : 0); +" 2>/dev/null) + push_rc=$? + echo "$push_output" + if [[ $push_rc -eq 0 ]]; then + log "Conf push complete ✅" + CONF_PUSH_OK=true + else + warn "Conf push had failures — retry via Scheduler → master.conf → Save Conf" + fi + fi + + [[ "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 1 + + END=$(date +%s) + echo "" + echo "━━━━━ $ICON_SUMMARY PHASE 1 COMPLETE ━━━━━" + echo " SSH keys: $( [[ "$STEP_SSH_OK" == true ]] && echo "ready ✅" || echo "skipped" )" + echo " Conf push: $( [[ "$CONF_PUSH_OK" == true ]] && echo "done ✅" || echo "⚠ manual needed" )" + echo " Duration: $(format_duration $(( END - START )))" + echo "" + echo " HOST2 ($MIRROR) can now install the Varaverk plugin." + echo " The wizard will detect the pushed conf and take the correct path." + echo " When HOST2 completes its onboard, it will automatically trigger Phase 2 here." + echo "━━━━━━━━━━━━━━━━━━━━━━━" + exit 0 +fi + # ── Step 2: Plugins ─────────────────────────────────────────────────────────────────────────── echo "" -echo "━━━ Step 2/8 — Plugin Install on Mirror ━━━" +echo "━━━ Step 2 — Plugin Install on Mirror ━━━" if [[ "${PARTNERSHIP_FOLDERVIEW3:-false}" == true ]] && [[ -n "${PARTNERSHIP_FOLDERVIEW3_URL:-}" ]]; then FV3_PRESENT=$(timeout "$SSH_TIMEOUT" ssh -i "$MIRROR_SSH_KEY" \ @@ -533,7 +651,7 @@ fi # ── Step 3: Stop mirror's existing auth stack ───────────────────────────────────────────────── echo "" -echo "━━━ Step 3/8 — Stop Mirror Auth Stack ━━━" +echo "━━━ Step 3 — Stop Mirror Auth Stack ━━━" if [[ "$SKIP_AUTH_STACK" == true ]]; then warn "Skipping (--skip-auth-stack)" @@ -543,7 +661,7 @@ fi # ── Step 4: Deploy auth stack on mirror ─────────────────────────────────────────────────────── echo "" -echo "━━━ Step 4/8 — Deploy Auth Stack on Mirror ━━━" +echo "━━━ Step 4 — Deploy Auth Stack on Mirror ━━━" if [[ "$SKIP_AUTH_STACK" == true ]]; then warn "Skipping (--skip-auth-stack)" @@ -561,7 +679,7 @@ fi # ── Step 5: Stop mirror's existing arr stack ────────────────────────────────────────────────── echo "" -echo "━━━ Step 5/8 — Stop Mirror Arr Stack ━━━" +echo "━━━ Step 5 — Stop Mirror Arr Stack ━━━" if [[ "$SKIP_ARR_STACK" == true ]]; then warn "Skipping (--skip-arr-stack)" @@ -574,7 +692,7 @@ fi # ── Step 6: Deploy arr stack on mirror ─────────────────────────────────────────────────────── echo "" -echo "━━━ Step 6/8 — Deploy Arr Stack on Mirror ━━━" +echo "━━━ Step 6 — Deploy Arr Stack on Mirror ━━━" if [[ "$SKIP_ARR_STACK" == true ]]; then warn "Skipping (--skip-arr-stack)" @@ -588,7 +706,7 @@ fi # ── Step 7: Partnership onboard ─────────────────────────────────────────────────────────────── echo "" -echo "━━━ Step 7/8 — Partnership Onboard ━━━" +echo "━━━ Step 7 — Partnership Onboard ━━━" if bash "$SCRIPTS_ROOT/Partnership/partnership_manager.sh" --onboard "${EXTRA_FLAGS[@]}"; then echo "Partnership onboard complete ✅" @@ -600,7 +718,7 @@ fi # ── Step 8: Arr library bootstrap ───────────────────────────────────────────────────────────── echo "" -echo "━━━ Step 8/8 — Arr Library Bootstrap ━━━" +echo "━━━ Step 8 — Arr Library Bootstrap ━━━" if [[ "$ONBOARD_OK" == false ]]; then warn "Skipping — onboard did not complete" @@ -620,7 +738,7 @@ fi # SSH is now established and all partners have the plugin installed. # Push the authoritative master.conf so every listed host is in sync immediately. echo "" -echo "━━━ $ICON_GEAR Step 9/9 — master.conf Push ━━━" +echo "━━━ $ICON_GEAR Step 9 — master.conf Push ━━━" if [[ "$ONBOARD_OK" == false ]]; then warn "Skipping — onboard did not complete" @@ -652,12 +770,16 @@ exit(\$failed > 0 ? 1 : 0); fi fi +# ── Write Phase 2 completion state ──────────────────────────────────────────────────────────── +[[ "$ONBOARD_OK" == true && "$DRY_RUN" == false ]] && write_onboard_phase "$MIRROR_ID" 2 + # ── Summary ─────────────────────────────────────────────────────────────────────────────────── END=$(date +%s) echo "" echo "━━━━━ $ICON_SUMMARY ONBOARD SUMMARY ━━━━━" echo " Owner: $MY_ID ($LOCAL_SERVER_NAME)" echo " Mirror: $MIRROR ($MIRROR_IP)" +[[ "$PHASE2_ONLY" == true ]] && echo " Mode: Phase 2 (triggered by HOST2 notification)" echo " Duration: $(format_duration $(( END - START )))" echo "" _ok() { [[ "$1" == true ]] && echo "✅" || echo "❌"; } diff --git a/Plugin/unraid/include/partnership.php b/Plugin/unraid/include/partnership.php index 859ee26..55a14fb 100644 --- a/Plugin/unraid/include/partnership.php +++ b/Plugin/unraid/include/partnership.php @@ -108,6 +108,7 @@ function vv_pt_nodes(): array { $vars = vv_conf_vars(); $tsPeers = vv_pt_ts_peers(); $ownerSlot = strtolower($vars['PARTNERSHIP_OWNER_HOST'] ?? ''); + $setupDb = vv_setup_state_read(); // SSH key for this host $myId = strtoupper($currentHost); @@ -150,18 +151,24 @@ function vv_pt_nodes(): array { ? vv_pt_local_system() : ($ts['online'] && $ts['ip'] && $mySshKey ? vv_pt_remote_system($ts['ip'], $mySshKey) : []); + // Onboard phase from setup.db — 0=not started, 1=SSH+conf done, 2=fully onboarded + $nodeIdUpper = strtoupper($slot); + $onboardPhase = ($setupDb[$nodeIdUpper . '_PHASE2_DONE'] ?? '') === 'true' ? 2 + : (($setupDb[$nodeIdUpper . '_PHASE1_DONE'] ?? '') === 'true' ? 1 : 0); + $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, + 'slot' => $slot, + 'id' => $nodeIdUpper, + '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, + 'onboard_phase' => $isMe ? null : $onboardPhase, ]; } return $nodes; diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index cf4301c..9c2a5c2 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -82,6 +82,7 @@ Partner +
Loading...
@@ -639,8 +640,8 @@ function vvPollMonitor() { const ramAvail = rs.mem_used_pct > 0; const cpuHue = cpuAvail ? Math.round(120 * (1 - rs.cpu_load / 100)) : 0; const memHue = ramAvail ? Math.round(120 * (1 - rs.mem_used_pct / 100)) : 0; - const cpuStr = cpuAvail ? `${rs.cpu_load}%` : ``; - const ramStr = ramAvail ? `${rs.mem_used_pct}%` : ``; + const cpuStr = cpuAvail ? `${rs.cpu_load}%${rs.cpu_threads ? ` · ${rs.cpu_threads}t` : ''}` : ``; + const ramStr = ramAvail ? `${rs.mem_used_pct}%${rs.mem_total_gb ? ` · ${rs.mem_total_gb}G` : ''}` : ``; const arrColor = rs.array_state === 'Started' || rs.array_state === 'STARTED' ? '#4caf50' : '#f44336'; const uptimeStr = rs.uptime && rs.uptime !== '—' ? rs.uptime : '—'; @@ -654,6 +655,7 @@ function vvPollMonitor() { Script sync ops are gated until versions match ` : ''; + const verRow = remoteVer ? `unRAID${remoteVer}` : ''; statsHtml = `
CPU${cpuStr} RAM${ramStr} @@ -661,6 +663,7 @@ function vvPollMonitor() { ${rs.array_state} Uptime ${uptimeStr} + ${verRow}
${verWarn}`; } else if (rs && rs.no_api_key) { statsHtml = `
API key not configured — complete Onboard to enable
`; diff --git a/Plugin/unraid/pages/partnership.php b/Plugin/unraid/pages/partnership.php index f4c4472..753b585 100644 --- a/Plugin/unraid/pages/partnership.php +++ b/Plugin/unraid/pages/partnership.php @@ -155,8 +155,17 @@ function _nodeCard(node) { const [ptCol, ptLbl] = ptStates[ptState] || ['#444', ptState || '—']; const ptUpdated = pt.updated ? _relTime(parseInt(pt.updated)) : null; + // Onboard phase (null = self, 0 = not started, 1 = SSH done, 2 = fully onboarded) + const phase = node.onboard_phase; + const phaseHtml = phase === null ? '' : + phase === 2 ? '' : + phase === 1 ? `
⏳ SSH ready · awaiting HOST2 onboard
` : + `
○ Not yet provisioned
`; + let body = ''; + body += phaseHtml; + // Network body += `
@@ -199,27 +208,82 @@ function _nodeCard(node) { // ── Actions ─────────────────────────────────────────────────────────────────── function _renderActions(nodes, cfg) { - // Onboard: only show when partnership is disabled (not yet set up) - // Offboard: only when enabled - // Transfer: only when enabled + we are owner - const isOwner = nodes.some(n => n.is_me && n.is_owner); - const remotes = nodes.filter(n => !n.is_me); - const remoteOpts = remotes.map(n => - ``).join(''); - + const isOwner = nodes.some(n => n.is_me && n.is_owner); + const remotes = nodes.filter(n => !n.is_me); const hasPartner = remotes.length > 0 && remotes.some(n => n.hostname); - let html = '
'; + let html = ''; - // Onboard — disabled until a partner hostname is configured in master.conf - html += ``; + // ── Per-partner onboard phase actions (owner only) ───────────────────────── + if (isOwner && hasPartner) { + for (const remote of remotes) { + const phase = remote.onboard_phase ?? 0; + html += `
`; + html += `
${remote.id} — ${remote.hostname}
`; + html += '
'; + + if (phase === 0) { + // Not yet provisioned — offer Phase 1 (safe before HOST2 has Varaverk) + html += ``; + html += ``; + } else if (phase === 1) { + // Phase 1 done — waiting for HOST2 to onboard and trigger Phase 2 + html += `⏳ Waiting for ${remote.id} to onboard…`; + html += ``; + } else { + // Phase 2 done — fully onboarded + html += `✅ Partnership established`; + html += ``; + } + + html += '
'; + + if (phase === 0) { + html += `
+ Phase 1 pushes SSH keys and master.conf to ${remote.hostname} before it has Varaverk installed. + ${remote.id} can then install the plugin — the wizard will detect the conf and complete setup automatically. +
`; + } else if (phase === 1) { + html += `
+ When ${remote.id} completes its onboard (Mirror path), it will SSH here and trigger Phase 2 automatically. + Use "Run Phase 2 Manually" if that notification didn't arrive. +
`; + } + + html += '
'; + } + } else if (!hasPartner) { + html += `
+ No partner hostname configured. Edit master.conf (Scheduler tab) and set HOST2. +
`; + } + + // ── Bottom row: Offboard + mirror Onboard ────────────────────────────────── + html += '
'; + + if (!isOwner) { + // Mirror: just runs its own path (SSH key + notifies owner) + html += ``; + } - // Offboard html += `
'; - // Contextual note under actions - if (!hasPartner) { - html += `
- No partner hostname configured. Edit master.conf (Scheduler tab) and set HOST2. -
`; - } else if (!cfg.enabled) { - html += `
- Onboard will generate your SSH key, exchange it with the partner, and establish the partnership. - Run on the mirror (HOST2) first, then on the owner (HOST1). -
`; - } - // Transfer — show manual command, too destructive to one-click if (cfg.enabled && isOwner) { const confirmStr = 'i-understand-this-transfers-ownership'; @@ -290,12 +342,38 @@ function _render(data) { // ── Actions ─────────────────────────────────────────────────────────────────── +function vvPtPhase1(btn, hostId) { + if (!confirm(`Phase 1: SSH key exchange + conf push to ${hostId}?\n\nSafe to run before ${hostId} has Varaverk installed.`)) return; + btn.disabled = true; + btn.textContent = '⟳ Starting…'; + fetch('/plugins/varaverk/api/run.php', { + method: 'POST', + body: new URLSearchParams({id: 'Partnership/partnership_onboard.sh', manual: '1', extra_args: '--phase1-only'}) + }).then(r => r.json()).then(d => { + if (!d.ok) { alert('Failed: ' + (d.error ?? 'Unknown error')); } + setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Phase 1: SSH + Conf Push'; }, 3000); + }); +} + +function vvPtPhase2(btn, hostId) { + if (!confirm(`Phase 2: Deploy containers + arr stack + establish partnership on ${hostId}?\n\nThis requires ${hostId} to have Varaverk installed and SSH keys set up.`)) return; + btn.disabled = true; + btn.textContent = '⟳ Starting…'; + fetch('/plugins/varaverk/api/run.php', { + method: 'POST', + body: new URLSearchParams({id: 'Partnership/partnership_onboard.sh', manual: '1', extra_args: '--phase2-only'}) + }).then(r => r.json()).then(d => { + if (!d.ok) { alert('Failed: ' + (d.error ?? 'Unknown error')); } + setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Run Phase 2 Manually'; }, 3000); + }); +} + function vvPtOnboard(btn) { - if (!confirm('Run partnership_onboard.sh?\n\nRun on the MIRROR first, then on the OWNER.')) return; + if (!confirm('Run full partnership_onboard.sh?\n\nRun on the MIRROR first, then on the OWNER.\n\nUse Phase 1 + Phase 2 buttons for step-by-step control.')) return; btn.disabled = true; btn.textContent = '⟳ Starting…'; vvRunById('Partnership/partnership_onboard.sh'); - setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Onboard'; }, 4000); + setTimeout(() => { btn.disabled = false; btn.textContent = '▶ Onboard (Mirror)'; }, 4000); } function vvPtOffboard(btn) {