Sync from main server - 2026-04-15 13:03:38
This commit is contained in:
219
platform/app.py
219
platform/app.py
@@ -4,10 +4,17 @@ import subprocess
|
||||
import threading
|
||||
import uuid
|
||||
import time
|
||||
from config import MAIN_SERVER_IP, VM_HOST, VM_PORT, VM_KEY, VM_USER, RUNNING_ON_MAIN_SERVER
|
||||
from config import MAIN_SERVER_IP, VM_HOST, VM_PORT, VM_KEY, VM_USER
|
||||
from modules.auth import login_required
|
||||
from modules.backups import get_containers, get_local_backups, get_vm_backups
|
||||
from modules.backups import (
|
||||
get_containers, get_all_root_containers, get_local_backups,
|
||||
get_vm_backups, get_all_stats, get_system_info
|
||||
)
|
||||
from modules.commands import run_command, run_ssh_to_vm
|
||||
from modules.users import (
|
||||
get_all_users, get_user_containers, get_all_users_containers,
|
||||
create_user, delete_user, get_user_disk_usage
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = 'navitrends-secret-key-2025'
|
||||
@@ -33,6 +40,9 @@ def _stream_restore(job_id, cmd):
|
||||
restore_jobs[job_id]['status'] = 'error'
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# DASHBOARD
|
||||
# ─────────────────────────────────────────────
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def dashboard():
|
||||
@@ -40,14 +50,121 @@ def dashboard():
|
||||
running_count = sum(1 for c in containers if 'Up' in c.get('status', ''))
|
||||
backups = get_local_backups()
|
||||
vm_backups = get_vm_backups()
|
||||
system = get_system_info()
|
||||
users = get_all_users()
|
||||
return render_template('dashboard.html',
|
||||
containers=containers,
|
||||
running_count=running_count,
|
||||
backups=backups,
|
||||
vm_backups=vm_backups,
|
||||
main_server=MAIN_SERVER_IP)
|
||||
main_server=MAIN_SERVER_IP,
|
||||
system=system,
|
||||
users=users)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# API — system info + stats (live poll)
|
||||
# ─────────────────────────────────────────────
|
||||
@app.route('/api/system')
|
||||
@login_required
|
||||
def api_system():
|
||||
return jsonify(get_system_info())
|
||||
|
||||
|
||||
@app.route('/api/stats')
|
||||
@login_required
|
||||
def api_stats():
|
||||
"""Container resource stats for ALL containers (root + rootless users)."""
|
||||
return jsonify(get_all_stats())
|
||||
|
||||
|
||||
@app.route('/api/containers')
|
||||
@login_required
|
||||
def api_containers():
|
||||
containers = get_all_root_containers()
|
||||
running_count = sum(1 for c in containers if 'Up' in c.get('status', ''))
|
||||
return jsonify({'containers': containers, 'running': running_count})
|
||||
|
||||
|
||||
@app.route('/api/containers/all')
|
||||
@login_required
|
||||
def api_containers_all():
|
||||
"""Root containers + all users' rootless containers combined."""
|
||||
root_ctrs = get_all_root_containers()
|
||||
user_ctrs = get_all_users_containers()
|
||||
all_ctrs = root_ctrs + user_ctrs
|
||||
running = sum(1 for c in all_ctrs if 'Up' in c.get('status', ''))
|
||||
return jsonify({'containers': all_ctrs, 'running': running})
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# API — backups
|
||||
# ─────────────────────────────────────────────
|
||||
@app.route('/api/backups')
|
||||
@login_required
|
||||
def api_backups():
|
||||
return jsonify({'local': get_local_backups(), 'vm': get_vm_backups()})
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# API — users management
|
||||
# ─────────────────────────────────────────────
|
||||
@app.route('/api/users')
|
||||
@login_required
|
||||
def api_users():
|
||||
return jsonify(get_all_users())
|
||||
|
||||
|
||||
@app.route('/api/users/<username>/containers')
|
||||
@login_required
|
||||
def api_user_containers(username):
|
||||
return jsonify(get_user_containers(username))
|
||||
|
||||
|
||||
@app.route('/api/users/<username>/disk')
|
||||
@login_required
|
||||
def api_user_disk(username):
|
||||
return jsonify(get_user_disk_usage(username))
|
||||
|
||||
|
||||
@app.route('/api/users/create', methods=['POST'])
|
||||
@login_required
|
||||
def api_create_user():
|
||||
data = request.get_json() or {}
|
||||
username = data.get('username', '').strip()
|
||||
password = data.get('password', '').strip()
|
||||
setup_docker = data.get('setup_docker', True)
|
||||
disk_quota_mb = data.get('disk_quota_mb')
|
||||
|
||||
if not username:
|
||||
return jsonify({'success': False, 'message': 'Username required'}), 400
|
||||
|
||||
success, message = create_user(
|
||||
username=username,
|
||||
password=password or None,
|
||||
setup_docker=setup_docker,
|
||||
disk_quota_mb=disk_quota_mb,
|
||||
)
|
||||
return jsonify({'success': success, 'message': message})
|
||||
|
||||
|
||||
@app.route('/api/users/delete', methods=['POST'])
|
||||
@login_required
|
||||
def api_delete_user():
|
||||
data = request.get_json() or {}
|
||||
username = data.get('username', '').strip()
|
||||
remove_home = data.get('remove_home', False)
|
||||
|
||||
if not username:
|
||||
return jsonify({'success': False, 'message': 'Username required'}), 400
|
||||
|
||||
success, message = delete_user(username, remove_home=remove_home)
|
||||
return jsonify({'success': success, 'message': message})
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# RESTORE
|
||||
# ─────────────────────────────────────────────
|
||||
@app.route('/restore/start', methods=['POST'])
|
||||
@login_required
|
||||
def restore_start():
|
||||
@@ -68,41 +185,22 @@ def restore_start():
|
||||
if not backup_file:
|
||||
return jsonify({'error': 'No backup file specified'}), 400
|
||||
|
||||
# ── Resolve backup archive path on THIS server ───────────────────────────
|
||||
# Resolve archive path on this server
|
||||
if backup_source == 'local':
|
||||
# Local path depends on which server we're running on
|
||||
if RUNNING_ON_MAIN_SERVER:
|
||||
backup_path = f"/root/backups/{backup_file}"
|
||||
else:
|
||||
backup_path = f"/backups/main-server/{backup_file}"
|
||||
|
||||
backup_path = f"/root/backups/{backup_file}"
|
||||
if not os.path.exists(backup_path):
|
||||
return jsonify({'error': f'Backup not found: {backup_path}'}), 400
|
||||
|
||||
else:
|
||||
# "Other server" backup source — pull it to /tmp/ first
|
||||
backup_path = f"/tmp/{backup_file}"
|
||||
if not os.path.exists(backup_path):
|
||||
if RUNNING_ON_MAIN_SERVER:
|
||||
# Original logic: pull from VM via SSH tunnel (port 2223)
|
||||
pull_cmd = (
|
||||
f"scp -i {VM_KEY} -P {VM_PORT} "
|
||||
f"-o StrictHostKeyChecking=no -o ConnectTimeout=15 "
|
||||
f"{VM_USER}@{VM_HOST}:/backups/main-server/{backup_file} "
|
||||
f"{backup_path}"
|
||||
)
|
||||
else:
|
||||
# On VM: pull from main server's /root/backups/ via port 22
|
||||
pull_cmd = (
|
||||
f"scp -i {VM_KEY} -P 22 "
|
||||
f"-o StrictHostKeyChecking=no -o ConnectTimeout=15 "
|
||||
f"{VM_USER}@{MAIN_SERVER_IP}:/root/backups/{backup_file} "
|
||||
f"{backup_path}"
|
||||
)
|
||||
|
||||
pull_cmd = (
|
||||
f"scp -i {VM_KEY} -P {VM_PORT} "
|
||||
f"-o StrictHostKeyChecking=no -o ConnectTimeout=15 "
|
||||
f"{VM_USER}@{VM_HOST}:/backups/main-server/{backup_file} {backup_path}"
|
||||
)
|
||||
result = subprocess.run(pull_cmd, shell=True, capture_output=True, text=True)
|
||||
if result.returncode != 0:
|
||||
return jsonify({'error': f'Failed to pull backup: {result.stderr}'}), 500
|
||||
return jsonify({'error': f'Failed to pull from VM: {result.stderr}'}), 500
|
||||
|
||||
restore_script_local = os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), 'restore-myapps.sh'
|
||||
@@ -110,60 +208,48 @@ def restore_start():
|
||||
if not os.path.exists(restore_script_local):
|
||||
return jsonify({'error': f'restore-myapps.sh not found at {restore_script_local}'}), 500
|
||||
|
||||
# ── Build command ────────────────────────────────────────────────────────
|
||||
if target == 'local':
|
||||
session_dir = f"/tmp/restore-session-{uuid.uuid4().hex[:8]}"
|
||||
cmd = (
|
||||
f"set -e && "
|
||||
f"mkdir -p {session_dir} && "
|
||||
f"set -e && mkdir -p {session_dir} && "
|
||||
f"echo '📂 Extracting backup locally...' && "
|
||||
f"tar -xzf {backup_path} -C {session_dir} --strip-components=1 && "
|
||||
f"cp {restore_script_local} {session_dir}/restore-myapps.sh && "
|
||||
f"chmod +x {session_dir}/restore-myapps.sh && "
|
||||
f"cd {session_dir} && "
|
||||
f"bash restore-myapps.sh ; "
|
||||
f"cd {session_dir} && bash restore-myapps.sh ; "
|
||||
f"EXIT=$? ; rm -rf {session_dir} ; exit $EXIT"
|
||||
)
|
||||
|
||||
else:
|
||||
if not remote_ip:
|
||||
return jsonify({'error': 'remote_ip is required for remote restore'}), 400
|
||||
|
||||
base_ssh_opts = f"-o StrictHostKeyChecking=no -o ConnectTimeout=15"
|
||||
return jsonify({'error': 'remote_ip required for remote restore'}), 400
|
||||
|
||||
base_opts = "-o StrictHostKeyChecking=no -o ConnectTimeout=15"
|
||||
if auth_method == 'key':
|
||||
if not ssh_key_path:
|
||||
return jsonify({'error': 'ssh_key_path is required'}), 400
|
||||
ssh_prefix = f"ssh -p {remote_port} -i {ssh_key_path} {base_ssh_opts}"
|
||||
scp_prefix = f"scp -P {remote_port} -i {ssh_key_path} {base_ssh_opts}"
|
||||
return jsonify({'error': 'ssh_key_path required'}), 400
|
||||
ssh_prefix = f"ssh -p {remote_port} -i {ssh_key_path} {base_opts}"
|
||||
scp_prefix = f"scp -P {remote_port} -i {ssh_key_path} {base_opts}"
|
||||
else:
|
||||
if not ssh_password:
|
||||
return jsonify({'error': 'ssh_password is required'}), 400
|
||||
ssh_prefix = f"sshpass -p '{ssh_password}' ssh -p {remote_port} {base_ssh_opts}"
|
||||
scp_prefix = f"sshpass -p '{ssh_password}' scp -P {remote_port} {base_ssh_opts}"
|
||||
return jsonify({'error': 'ssh_password required'}), 400
|
||||
ssh_prefix = f"sshpass -p '{ssh_password}' ssh -p {remote_port} {base_opts}"
|
||||
scp_prefix = f"sshpass -p '{ssh_password}' scp -P {remote_port} {base_opts}"
|
||||
|
||||
remote_dest = f"/backups/restore-session-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
cmd = (
|
||||
f"echo '🔗 Connecting to {remote_user}@{remote_ip}:{remote_port}...' && "
|
||||
f"{ssh_prefix} {remote_user}@{remote_ip} 'mkdir -p {remote_dest}' && "
|
||||
f"echo '✅ Connected.' && "
|
||||
|
||||
f"echo '📤 Copying backup archive to {remote_ip}:{remote_port}...' && "
|
||||
f"echo '📤 Copying backup archive...' && "
|
||||
f"{scp_prefix} {backup_path} {remote_user}@{remote_ip}:{remote_dest}/{backup_file} && "
|
||||
|
||||
f"echo '📤 Copying restore script...' && "
|
||||
f"{scp_prefix} {restore_script_local} {remote_user}@{remote_ip}:{remote_dest}/restore-myapps.sh && "
|
||||
|
||||
f"echo '🚀 Running restore on {remote_ip}:{remote_port}...' && "
|
||||
f"{ssh_prefix} {remote_user}@{remote_ip} "
|
||||
f"'set -e && "
|
||||
f"cd {remote_dest} && "
|
||||
f"'set -e && cd {remote_dest} && "
|
||||
f"echo \"📂 Extracting backup...\" && "
|
||||
f"tar -xzf {backup_file} --strip-components=1 && "
|
||||
f"chmod +x restore-myapps.sh && "
|
||||
f"bash restore-myapps.sh' ; "
|
||||
|
||||
f"chmod +x restore-myapps.sh && bash restore-myapps.sh' ; "
|
||||
f"EXIT=$? ; "
|
||||
f"{ssh_prefix} {remote_user}@{remote_ip} 'rm -rf {remote_dest}' 2>/dev/null ; "
|
||||
f"exit $EXIT"
|
||||
@@ -172,7 +258,6 @@ def restore_start():
|
||||
job_id = str(uuid.uuid4())
|
||||
t = threading.Thread(target=_stream_restore, args=(job_id, cmd), daemon=True)
|
||||
t.start()
|
||||
|
||||
return jsonify({'job_id': job_id, 'status': 'started'})
|
||||
|
||||
|
||||
@@ -189,22 +274,9 @@ def restore_status_poll(job_id):
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/backups')
|
||||
@login_required
|
||||
def api_backups():
|
||||
return jsonify({'local': get_local_backups(), 'vm': get_vm_backups()})
|
||||
|
||||
|
||||
@app.route('/api/containers')
|
||||
@login_required
|
||||
def api_containers():
|
||||
containers = get_containers()
|
||||
return jsonify({
|
||||
'containers': containers,
|
||||
'running': sum(1 for c in containers if 'Up' in c.get('status', ''))
|
||||
})
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# SERVER STATUS
|
||||
# ─────────────────────────────────────────────
|
||||
@app.route('/server/status')
|
||||
@login_required
|
||||
def server_status():
|
||||
@@ -214,6 +286,9 @@ def server_status():
|
||||
return jsonify({'status': 'online', 'info': stdout.strip()})
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# AUTH
|
||||
# ─────────────────────────────────────────────
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
error = ''
|
||||
|
||||
Reference in New Issue
Block a user