Wire management-platform's backup/restore routes to the k8s scripts
api_backup_run and restore_start were still hardcoded to the Docker-era backup-myapps.sh/restore-myapps.sh, which target Docker volumes that no longer exist for the 5 apps now running in k3s (n8n, odoo, mautic, nextcloud, frappe/erpnext) — every backup/restore triggered from the UI has been silently hollow for these apps since the migration. - api_backup_run: /root/backup-myapps.sh -> /root/CloudOps/backup/backup-k8s-apps.sh - restore_start: switched from the old image-relative path (which resolved to /app/restore-myapps.sh, baked into the Docker image from platform/) to an absolute /root/CloudOps/backup/restore-k8s-apps.sh path — the new backup/ folder is a sibling of platform/, not part of the Docker build context, so it's only reachable via the pod's existing /root hostPath mount, the same way api_backup_run already reaches its script. --apps flag shape and the job-log streaming contract are unchanged, so no frontend/template changes needed for the core flow. modules/backups.py's three hardcoded myapps-backup-* references (get_local_backups, get_vm_backups x2, delete_backup's filename regex) now also recognize the new myapps-k8s-backup-* prefix, so both backup lineages stay visible/manageable in the UI. Depends on the previous commit (scoped ServiceAccount + k3s kubectl access for the pod) to actually function when triggered from the UI. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -424,7 +424,7 @@ def api_backup_run():
|
|||||||
'message': 'Manual backup can only be triggered from the main server platform.'
|
'message': 'Manual backup can only be triggered from the main server platform.'
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
script = '/root/backup-myapps.sh'
|
script = '/root/CloudOps/backup/backup-k8s-apps.sh'
|
||||||
if not os.path.exists(script):
|
if not os.path.exists(script):
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
@@ -818,11 +818,14 @@ def restore_start():
|
|||||||
if not os.path.exists(backup_path):
|
if not os.path.exists(backup_path):
|
||||||
return jsonify({'error': f'Not found: {backup_path}'}), 400
|
return jsonify({'error': f'Not found: {backup_path}'}), 400
|
||||||
|
|
||||||
restore_script_local = os.path.join(
|
# Absolute /root path (not image-relative like the old restore-myapps.sh
|
||||||
os.path.dirname(os.path.abspath(__file__)), 'restore-myapps.sh'
|
# was) — backup/restore-k8s-apps.sh lives in the CloudOps repo's backup/
|
||||||
)
|
# folder, a sibling of platform/, so it's never baked into the Docker
|
||||||
|
# image; it's reached the same way api_backup_run's script is, via the
|
||||||
|
# pod's existing rw hostPath mount of /root.
|
||||||
|
restore_script_local = '/root/CloudOps/backup/restore-k8s-apps.sh'
|
||||||
if not os.path.exists(restore_script_local):
|
if not os.path.exists(restore_script_local):
|
||||||
return jsonify({'error': f'restore-myapps.sh not found at {restore_script_local}'}), 500
|
return jsonify({'error': f'restore-k8s-apps.sh not found at {restore_script_local}'}), 500
|
||||||
|
|
||||||
if target == 'local':
|
if target == 'local':
|
||||||
hostname = os.uname().nodename
|
hostname = os.uname().nodename
|
||||||
@@ -835,9 +838,9 @@ def restore_start():
|
|||||||
f"mkdir -p {session_dir} && "
|
f"mkdir -p {session_dir} && "
|
||||||
f"echo 'Extracting backup...' && "
|
f"echo 'Extracting backup...' && "
|
||||||
f"tar -xzf {backup_path} -C {session_dir} --strip-components=1 && "
|
f"tar -xzf {backup_path} -C {session_dir} --strip-components=1 && "
|
||||||
f"cp {restore_script_local} {session_dir}/restore-myapps.sh && "
|
f"cp {restore_script_local} {session_dir}/restore-k8s-apps.sh && "
|
||||||
f"chmod +x {session_dir}/restore-myapps.sh && "
|
f"chmod +x {session_dir}/restore-k8s-apps.sh && "
|
||||||
f"cd {session_dir} && bash restore-myapps.sh{apps_flag} ; "
|
f"cd {session_dir} && bash restore-k8s-apps.sh{apps_flag} ; "
|
||||||
f"EXIT=$? ; rm -rf {session_dir} ; exit $EXIT"
|
f"EXIT=$? ; rm -rf {session_dir} ; exit $EXIT"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@@ -865,12 +868,12 @@ def restore_start():
|
|||||||
f"echo 'Copying backup archive...' && "
|
f"echo 'Copying backup archive...' && "
|
||||||
f"{scp_prefix} {backup_path} {remote_user}@{remote_ip}:{remote_dest}/{backup_file} && "
|
f"{scp_prefix} {backup_path} {remote_user}@{remote_ip}:{remote_dest}/{backup_file} && "
|
||||||
f"echo 'Copying restore script...' && "
|
f"echo 'Copying restore script...' && "
|
||||||
f"{scp_prefix} {restore_script_local} {remote_user}@{remote_ip}:{remote_dest}/restore-myapps.sh && "
|
f"{scp_prefix} {restore_script_local} {remote_user}@{remote_ip}:{remote_dest}/restore-k8s-apps.sh && "
|
||||||
f"echo 'Running restore on {remote_ip}:{remote_port}...' && "
|
f"echo 'Running restore on {remote_ip}:{remote_port}...' && "
|
||||||
f"{ssh_prefix} {remote_user}@{remote_ip} "
|
f"{ssh_prefix} {remote_user}@{remote_ip} "
|
||||||
f"'set -e && cd {remote_dest} && "
|
f"'set -e && cd {remote_dest} && "
|
||||||
f"tar -xzf {backup_file} --strip-components=1 && "
|
f"tar -xzf {backup_file} --strip-components=1 && "
|
||||||
f"chmod +x restore-myapps.sh && bash restore-myapps.sh{apps_flag}' ; "
|
f"chmod +x restore-k8s-apps.sh && bash restore-k8s-apps.sh{apps_flag}' ; "
|
||||||
f"EXIT=$? ; "
|
f"EXIT=$? ; "
|
||||||
f"{ssh_prefix} {remote_user}@{remote_ip} 'rm -rf {remote_dest}' 2>/dev/null ; "
|
f"{ssh_prefix} {remote_user}@{remote_ip} 'rm -rf {remote_dest}' 2>/dev/null ; "
|
||||||
f"exit $EXIT"
|
f"exit $EXIT"
|
||||||
|
|||||||
@@ -56,8 +56,12 @@ def _ssh_main(remote_cmd, timeout=30):
|
|||||||
# ────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def get_local_backups():
|
def get_local_backups():
|
||||||
|
# myapps-backup-* is the legacy Docker-era lineage; myapps-k8s-backup-*
|
||||||
|
# is the k8s-native one (backup-k8s-apps.sh). Listed together, sorted
|
||||||
|
# by mtime, so history from both stays visible in the UI.
|
||||||
stdout, _ = _ssh_main(
|
stdout, _ = _ssh_main(
|
||||||
"ls -t /root/backups/myapps-backup-*.tar.gz 2>/dev/null | head -20"
|
"ls -t /root/backups/myapps-backup-*.tar.gz "
|
||||||
|
"/root/backups/myapps-k8s-backup-*.tar.gz 2>/dev/null | head -20"
|
||||||
)
|
)
|
||||||
files = []
|
files = []
|
||||||
if stdout:
|
if stdout:
|
||||||
@@ -77,7 +81,8 @@ def get_vm_backups():
|
|||||||
f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 "
|
f"-o StrictHostKeyChecking=no -o ConnectTimeout=10 "
|
||||||
f"-o BatchMode=yes "
|
f"-o BatchMode=yes "
|
||||||
f"{VM_USER}@{VM_HOST} "
|
f"{VM_USER}@{VM_HOST} "
|
||||||
f"'ls -t /backups/cloudproject/myapps-backup-*.tar.gz 2>/dev/null | head -20'"
|
f"'ls -t /backups/cloudproject/myapps-backup-*.tar.gz "
|
||||||
|
f"/backups/cloudproject/myapps-k8s-backup-*.tar.gz 2>/dev/null | head -20'"
|
||||||
)
|
)
|
||||||
stdout, _ = _run(cmd, timeout=25)
|
stdout, _ = _run(cmd, timeout=25)
|
||||||
if stdout:
|
if stdout:
|
||||||
@@ -90,7 +95,10 @@ def get_vm_backups():
|
|||||||
else:
|
else:
|
||||||
backup_dir = '/backups/cloudproject'
|
backup_dir = '/backups/cloudproject'
|
||||||
if os.path.exists(backup_dir):
|
if os.path.exists(backup_dir):
|
||||||
files = glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz')
|
files = (
|
||||||
|
glob.glob(f'{backup_dir}/myapps-backup-*.tar.gz')
|
||||||
|
+ glob.glob(f'{backup_dir}/myapps-k8s-backup-*.tar.gz')
|
||||||
|
)
|
||||||
files.sort(key=os.path.getmtime, reverse=True)
|
files.sort(key=os.path.getmtime, reverse=True)
|
||||||
vm_backups = [os.path.basename(f) for f in files[:20]]
|
vm_backups = [os.path.basename(f) for f in files[:20]]
|
||||||
return vm_backups
|
return vm_backups
|
||||||
@@ -408,7 +416,11 @@ def _sha256_file(path):
|
|||||||
# ────────────────────────────────────────────────────────────────
|
# ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def delete_backup(backup_file, source='local'):
|
def delete_backup(backup_file, source='local'):
|
||||||
if not re.match(r'^myapps-backup-\d{8}_\d{6}\.tar\.gz$', backup_file):
|
# Accepts both the legacy Docker-era lineage (myapps-backup-*) and the
|
||||||
|
# k8s-native one (myapps-k8s-backup-*, from backup-k8s-apps.sh). Still a
|
||||||
|
# tight anchor match — this guards against path traversal in a filename
|
||||||
|
# that ends up interpolated into a shell/filesystem path below.
|
||||||
|
if not re.match(r'^myapps-(k8s-)?backup-\d{8}_\d{6}\.tar\.gz$', backup_file):
|
||||||
return False, f'Invalid backup filename: {backup_file}'
|
return False, f'Invalid backup filename: {backup_file}'
|
||||||
|
|
||||||
if source == 'local':
|
if source == 'local':
|
||||||
|
|||||||
Reference in New Issue
Block a user