From 9361b9c4c1e0ae1ce3400bd34b2cf201bc297ffb Mon Sep 17 00:00:00 2001 From: root Date: Fri, 21 Aug 2026 13:55:43 +0200 Subject: [PATCH] Make restore-from-standby actually work, not just look wired MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps found testing this live from the standby: 1. restore.html's "Restore on This Server" was checked by default regardless of RUNNING_ON_MAIN_SERVER — on the standby that option is nonsensical (no local cluster/kubectl at all) and restore_start() would have just tried and failed confusingly. Now: that radio is disabled with an explanatory note when not on the main server, "External Machine" is checked instead and pre-filled with the tunnel details (localhost:2224, contabo-key) so restoring from the standby just targets the real main server without the user having to know any of that. Added a matching server-side guard in restore_start() for target=='local' + not RUNNING_ON_MAIN_SERVER (defense in depth — the UI already prevents it, this catches a direct API call too). Also fixed refreshSystemMetrics() in platform.js, which would have overwritten the disabled option's label with the (now-correct, see previous commit) main-server hostname — looking like a working local target when it isn't. 2. sync-standby-platform.sh only ever mirrored platform/ — but restore_start() references /root/CloudOps/backup/restore-k8s-apps.sh as a fixed absolute path to scp to the remote target, and that directory never existed on the VM at all. Every restore attempt from the standby failed immediately with "restore-k8s-apps.sh not found", regardless of target. Now mirrors /root/CloudOps/backup/ too. Verified end-to-end for real: triggered a restore of frappe/erpnext from the standby's actual web UI (target=remote, localhost:2224) — connected over the tunnel, copied the backup archive + script to the main server, ran restore-k8s-apps.sh there, scaled the deployment down/up, restored the DB. Confirmed after: 740 tables in the DB, /api/method/ping responding on the live pod. Not a dry run — a real restore, actually initiated from the standby machine. --- platform/app.py | 14 ++++++++++++++ platform/static/js/platform.js | 5 ++++- platform/templates/pages/restore.html | 25 ++++++++++++++++++------- scripts/sync-standby-platform.sh | 19 +++++++++++++++++++ 4 files changed, 55 insertions(+), 8 deletions(-) diff --git a/platform/app.py b/platform/app.py index c1cc5c5..2590420 100644 --- a/platform/app.py +++ b/platform/app.py @@ -193,6 +193,14 @@ def restore_page(): vm_backups=get_vm_backups(), restore_prefill=prefill, main_server=MAIN_SERVER_IP, + running_on_main_server=RUNNING_ON_MAIN_SERVER, + # Only meaningful when running as the standby: "restore on this + # server" is nonsensical there (no local cluster/kubectl at all), + # so pre-fill the remote-target fields with the tunnel back to the + # real main server instead of leaving the user to find these values. + tunnel_remote_ip=MAIN_SERVER_SSH_HOST, + tunnel_remote_port=MAIN_SERVER_SSH_PORT, + tunnel_ssh_key=MAIN_SERVER_KEY, active_page='restore', page_title='Restore', page_subtitle='backup → target' @@ -836,6 +844,12 @@ def restore_start(): return jsonify({'error': f'restore-k8s-apps.sh not found at {restore_script_local}'}), 500 if target == 'local': + if not RUNNING_ON_MAIN_SERVER: + return jsonify({ + 'error': 'This is the standby — there is no local cluster here to restore ' + 'into. Use "External Machine" to restore onto the real main server ' + 'over the tunnel instead.' + }), 400 hostname = os.uname().nodename os.makedirs('/root/tmp', exist_ok=True) session_dir = f"/root/tmp/restore-session-{uuid.uuid4().hex[:8]}" diff --git a/platform/static/js/platform.js b/platform/static/js/platform.js index 4b5b391..63a6db0 100644 --- a/platform/static/js/platform.js +++ b/platform/static/js/platform.js @@ -170,7 +170,10 @@ async function refreshSystemMetrics() { if (el('g-disk')) el('g-disk').style.width = Math.min(parseFloat(d.disk_pct) || 0, 100) + '%'; setText('uptime-chip', d.uptime); if (el('settings-uptime')) el('settings-uptime').value = d.uptime; - if (d.hostname && el('this-server-desc')) el('this-server-desc').textContent = d.hostname; + const localTargetRadio = document.querySelector('input[name="restore_target"][value="local"]'); + if (d.hostname && el('this-server-desc') && !(localTargetRadio && localTargetRadio.disabled)) { + el('this-server-desc').textContent = d.hostname; + } } catch (_) {} } diff --git a/platform/templates/pages/restore.html b/platform/templates/pages/restore.html index 2504e74..d7ce07a 100644 --- a/platform/templates/pages/restore.html +++ b/platform/templates/pages/restore.html @@ -63,26 +63,37 @@
STEP 2 — SELECT RESTORE TARGET
+ {% if not running_on_main_server %} +
+ + Running as the standby — there's no local cluster here to restore into. + "External Machine" is pre-filled to reach the real main server over the standby's + SSH tunnel; only change these if you actually mean a different target. +
+ {% endif %}
-
-