feat(backups): restore target node/storage selection + task progress tracking
All checks were successful
Backend Tests / pytest (Python 3.12) (push) Successful in 1m6s
Deploy to Production / Trigger production update (push) Successful in 8s

Add target node and storage overrides to single and bulk restore flows, and
track Proxmox task progress (parse_task_progress in the monitoring worker,
new ProxmoxTask progress column + migration, enriched task API responses).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 21:26:01 +05:00
parent fbf6d174b6
commit 681aa2f303
10 changed files with 234 additions and 25 deletions

View File

@@ -19,6 +19,7 @@ from ...auth import PermissionChecker
from ...logging_service import LoggingService
from ._helpers import _get_proxmox_client
from ...proxmox import _run_in_executor
from .tasks import ProxmoxTaskService
router = APIRouter()
@@ -322,16 +323,18 @@ async def restore_backup(
node = data.get("node")
vmid = data.get("vmid")
archive = data.get("archive") # volume id
storage = data.get("storage")
# Target storage for the restored disks. Empty => "from backup configuration"
# (disks go to the storage recorded in the backup). NOT the backup storage.
storage = data.get("storage") or None
vm_type = data.get("vm_type", "qemu")
new_vmid = data.get("new_vmid")
start = bool(data.get("start", False))
unique = bool(data.get("unique", True))
if not all([server_id, node, vmid, archive, storage]):
if not all([server_id, node, vmid, archive]):
raise HTTPException(
status_code=400,
detail="server_id, node, vmid, archive, storage are required",
detail="server_id, node, vmid, archive are required",
)
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
@@ -356,6 +359,21 @@ async def restore_backup(
)
if result.get("success"):
target_vmid = int(new_vmid) if new_vmid else int(vmid)
upid = result.get("upid")
# Track the restore as a Proxmox UPID task so it shows up on the Tasks
# page and the poller can report live progress / final status.
if upid:
try:
ptask = ProxmoxTaskService.register(
db=db, upid=upid, server_id=server_id, user_id=current_user.id,
action="backup_restore", node=node, vmid=target_vmid,
vm_type=vm_type, description=f"Восстановление {vm_type.upper()} {target_vmid}",
)
ptask.progress = 0
db.commit()
except Exception as _te:
logger.warning(f"Could not register restore task: {_te}")
LoggingService.log_proxmox_action(
db=db, action="backup_restore", resource_type="backup",
resource_id=str(vmid), username=current_user.username,
@@ -363,7 +381,7 @@ async def restore_backup(
node_name=node, success=True,
details={"archive": archive, "storage": storage, "vm_type": vm_type},
)
return JSONResponse(content={"success": True, "upid": result.get("upid")})
return JSONResponse(content={"success": True, "upid": upid})
raise HTTPException(status_code=400, detail=result.get("error", "Failed"))
except HTTPException:
raise
@@ -389,13 +407,14 @@ async def restore_backup_bulk(
"""
data = await request.json()
server_id = data.get("server_id")
storage = data.get("storage")
# Target storage for restored disks. Empty => "from backup configuration".
storage = data.get("storage") or None
mode = data.get("mode", "in_place")
start = bool(data.get("start", False))
items = data.get("items", [])
if not server_id or not storage or not items:
raise HTTPException(status_code=400, detail="server_id, storage and items are required")
if not server_id or not items:
raise HTTPException(status_code=400, detail="server_id and items are required")
if mode not in ("in_place", "new_vmid"):
raise HTTPException(status_code=400, detail="mode must be 'in_place' or 'new_vmid'")
@@ -466,6 +485,18 @@ async def restore_backup_bulk(
if r.get("success"):
results.append({"vmid": vmid, "target_vmid": target, "archive": archive,
"success": True, "upid": r.get("upid")})
upid = r.get("upid")
if upid:
try:
ptask = ProxmoxTaskService.register(
db=db, upid=upid, server_id=server_id, user_id=current_user.id,
action="backup_restore", node=node, vmid=target,
vm_type=vm_type, description=f"Восстановление {vm_type.upper()} {target}",
)
ptask.progress = 0
db.commit()
except Exception as _te:
logger.warning(f"Could not register bulk restore task: {_te}")
LoggingService.log_proxmox_action(
db=db, action="backup_restore", resource_type="backup",
resource_id=str(target), username=current_user.username,

View File

@@ -223,8 +223,28 @@ def get_all_tasks(
proxmox_tasks = proxmox_q.order_by(ProxmoxTask.created_at.desc()).limit(limit).all()
deploy_tasks = deploy_q.order_by(DeployTask.created_at.desc()).limit(limit).all()
# Resolve server_id -> name once for enrichment (proxmox/deploy tasks carry server_id).
server_ids = {t.server_id for t in proxmox_tasks if t.server_id} | {t.server_id for t in deploy_tasks if t.server_id}
server_names = {}
if server_ids:
for sid, sname in db.query(ProxmoxServer.id, ProxmoxServer.name).filter(ProxmoxServer.id.in_(server_ids)).all():
server_names[sid] = sname
def _enrich(d: dict) -> dict:
# All listed tasks belong to the current user.
if not d.get("user"):
d["user"] = current_user.username
if d.get("server_id") and not d.get("server_name"):
d["server_name"] = server_names.get(d["server_id"])
# Тип column reads `type` first; fall back to action/task_type/kind.
if not d.get("type"):
d["type"] = d.get("action") or d.get("task_type") or d.get("kind")
return d
all_tasks = sorted(
[t.to_dict() for t in bulk_tasks] + [t.to_dict() for t in proxmox_tasks] + [t.to_dict() for t in deploy_tasks],
[_enrich(t.to_dict()) for t in bulk_tasks]
+ [_enrich(t.to_dict()) for t in proxmox_tasks]
+ [_enrich(t.to_dict()) for t in deploy_tasks],
key=lambda t: t.get("created_at") or "",
reverse=True,
)[:limit]