Add server user/protocol migrate export-import for connect_domain moves.

This commit is contained in:
orohi
2026-08-09 17:31:08 +03:00
parent 58aad90dee
commit f9b9857460
8 changed files with 768 additions and 2 deletions
+99 -2
View File
@@ -23,7 +23,7 @@ import time
import urllib.request
import zipfile
import signal
from datetime import datetime
from datetime import datetime, timezone
import io
from fastapi.responses import JSONResponse, RedirectResponse, HTMLResponse, StreamingResponse, FileResponse
from starlette.background import BackgroundTask
@@ -103,7 +103,7 @@ else:
application_path = os.path.dirname(__file__)
DATA_FILE = os.path.join(application_path, 'data.json') # legacy JSON; used only for one-shot import / export
CURRENT_VERSION = "v2.7.0"
CURRENT_VERSION = "v2.7.1"
RELEASES_REPO_URL = repo_url()
RELEASES_API_LATEST = api_latest_url()
BIN_DIR = os.environ.get('TUNNEL_BIN_DIR', os.path.join(application_path, 'bin'))
@@ -4159,6 +4159,103 @@ async def api_protocol_backup_restore(request: Request, server_id: int, req: Bac
return JSONResponse({'error': str(e)}, status_code=500)
@app.get('/api/servers/{server_id}/migrate/export', tags=["Servers"])
async def api_server_migrate_export(
request: Request,
server_id: int,
include_protocols: bool = True,
):
"""Export users/connections (+ protocol state) for domain-preserving server migration."""
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
try:
data = await load_data_async()
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
server = data['servers'][server_id]
from managers.migrate_manager import export_migrate_zip
def _do_export():
ssh = None
try:
if include_protocols:
ssh = get_ssh(server)
ssh.connect()
return export_migrate_zip(
ssh,
data,
server_id,
include_protocol_backups=include_protocols,
protocol_container_name_fn=protocol_container_name,
)
finally:
if ssh:
ssh.disconnect()
zip_bytes, summary = await asyncio.to_thread(_do_export)
stamp = datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')
safe_name = re.sub(r'[^\w.-]+', '_', server.get('name') or server.get('host') or 'server')
filename = f'amnezia-migrate-{safe_name}-{stamp}.zip'
headers = {
'Content-Disposition': f'attachment; filename="{filename}"',
'X-Migrate-Users': str(summary.get('users', 0)),
'X-Migrate-Connections': str(summary.get('connections', 0)),
'X-Migrate-Protocols': ','.join(summary.get('protocols') or []),
}
return StreamingResponse(io.BytesIO(zip_bytes), media_type='application/zip', headers=headers)
except Exception as e:
logger.exception('Server migrate export failed')
return JSONResponse({'error': str(e)}, status_code=500)
@app.post('/api/servers/{server_id}/migrate/import', tags=["Servers"])
async def api_server_migrate_import(
request: Request,
server_id: int,
file: UploadFile = File(...),
restore_protocols: bool = True,
):
"""Import migrate ZIP onto this server (remap users/connections, restore protocol state)."""
if not _check_admin(request):
return JSONResponse({'error': 'Forbidden'}, status_code=403)
try:
content = await file.read()
if not content:
return JSONResponse({'error': 'Empty file'}, status_code=400)
from managers.migrate_manager import import_migrate_zip
async with DATA_LOCK:
data = load_data()
if server_id >= len(data['servers']):
return JSONResponse({'error': 'Server not found'}, status_code=404)
server = data['servers'][server_id]
def _do_import():
ssh = None
try:
if restore_protocols:
ssh = get_ssh(server)
ssh.connect()
return import_migrate_zip(
ssh,
data,
server_id,
content,
restore_protocols=restore_protocols,
protocol_container_name_fn=protocol_container_name,
)
finally:
if ssh:
ssh.disconnect()
result = await asyncio.to_thread(_do_import)
save_data(data)
return {'status': 'success', **result}
except Exception as e:
logger.exception('Server migrate import failed')
return JSONResponse({'error': str(e)}, status_code=400)
@app.post('/api/servers/{server_id}/backups/export-clients', tags=["Protocols"])
async def api_protocol_export_clients(request: Request, server_id: int, req: ProtocolRequest):
"""Download a ZIP with all reconstructable WireGuard/AWG client .conf files."""