Files
pvemanager/backend/app/api/proxmox/tasks.py
Dilshod Rasulov de967c35e0 refactor(rbac): migrate API permission checks to resource:action format
Replace all legacy dotted permission codes (vms.view, proxmox.manage, ...)
in PermissionChecker/has_permission across app/api with the new
resource:action format (vm:view, server:manage, ...), per LEGACY_PERMISSION_MAP.
All 33 codes used by the API are now registered in the permission registry.

Also switch the /api/auth/me admin branch to get_all_permissions_v2 so the
endpoint emits permissions in the new format only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 02:07:48 +05:00

330 lines
12 KiB
Python

from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from sqlalchemy import func
from loguru import logger
from typing import List
import ssl
import asyncio
import httpx
import websockets
from ...db import get_db
from ...models import ProxmoxServer, VMInstance, User, IPAMAllocation, IPAMNetwork, VMSnapshotArchive, ProxmoxTask, DeployTask
from ...schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerResponse
from ...proxmox import ProxmoxClient, get_proxmox_resources
from ...auth import get_current_user, PermissionChecker, require_permission, check_permission
from ...logging_service import LoggingService
from ...ipam_service import IPAMService
from ...models import TaskQueue
from ...services.task_queue_service import TaskQueueService, process_task_queue
from ._helpers import (check_vm_access, require_vm_access, _get_proxmox_client,
get_next_vmid, archive_and_delete_snapshots,
save_vm_instance, get_vm_instance, soft_delete_vm_instance)
router = APIRouter()
# ==================== ProxmoxTaskService ====================
class ProxmoxTaskService:
"""Service for managing Proxmox UPID tasks stored in DB."""
@staticmethod
def register(
db: Session,
upid: str,
server_id: int,
user_id: int,
action: str,
node: str = None,
vmid: int = None,
vm_type: str = None,
description: str = None,
) -> ProxmoxTask:
task = ProxmoxTask(
upid=upid,
server_id=server_id,
user_id=user_id,
node=node,
vmid=vmid,
vm_type=vm_type,
action=action,
description=description,
status="running",
)
db.add(task)
db.commit()
db.refresh(task)
logger.info(f"[PROXMOX TASK] Registered UPID task #{task.id}: {action} on {node}/{vmid}")
return task
@staticmethod
def get_user_tasks(db: Session, user_id: int, limit: int = 50) -> list:
return (
db.query(ProxmoxTask)
.filter(ProxmoxTask.user_id == user_id)
.order_by(ProxmoxTask.created_at.desc())
.limit(limit)
.all()
)
@staticmethod
def get_active(db: Session, user_id: int) -> list:
return (
db.query(ProxmoxTask)
.filter(ProxmoxTask.user_id == user_id, ProxmoxTask.status == "running")
.order_by(ProxmoxTask.created_at.desc())
.all()
)
@router.get("/api/{server_id}/task/{upid}/status")
def get_task_status(
server_id: int,
upid: str,
node: str,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("vm:view"))
):
"""Получить статус задачи по UPID"""
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Proxmox server not found")
try:
client = _get_proxmox_client(server)
if not client.is_connected():
raise HTTPException(status_code=503, detail="Failed to connect to Proxmox server")
status = client.get_task_status(node, upid)
return JSONResponse(content=status)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting task status {upid}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/{server_id}/task/{upid}/log")
def get_task_log(
server_id: int,
upid: str,
node: str,
start: int = 0,
limit: int = 50,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("vm:view"))
):
"""Получить лог задачи по UPID"""
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Proxmox server not found")
try:
client = _get_proxmox_client(server)
if not client.is_connected():
raise HTTPException(status_code=503, detail="Failed to connect to Proxmox server")
logs = client.get_task_log(node, upid, start, limit)
return JSONResponse(content={"logs": logs})
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting task log {upid}: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ─── Bulk (TaskQueue) endpoints ─────────────────────────────────────────────
@router.get("/api/tasks")
def get_user_tasks(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
limit: int = Query(20, ge=1, le=100)
):
"""Get recent bulk-operation tasks for current user"""
tasks = TaskQueueService.get_user_tasks(db, current_user.id, limit)
return {"tasks": [task.to_dict() for task in tasks]}
@router.get("/api/tasks/active")
def get_active_bulk_tasks(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get all active (pending/running) bulk tasks for current user"""
tasks = db.query(TaskQueue).filter(
TaskQueue.user_id == current_user.id,
TaskQueue.status.in_(['pending', 'running'])
).order_by(TaskQueue.created_at.desc()).all()
return {"tasks": [task.to_dict() for task in tasks]}
@router.get("/api/tasks/{task_id}")
def get_task_by_id(
task_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Get status of a specific bulk task"""
task = db.query(TaskQueue).filter(
TaskQueue.id == task_id,
TaskQueue.user_id == current_user.id
).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return task.to_dict()
@router.post("/api/tasks/{task_id}/cancel")
def cancel_task(
task_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user)
):
"""Cancel a pending bulk task"""
success = TaskQueueService.cancel_task(db, task_id, current_user.id)
if not success:
raise HTTPException(status_code=400, detail="Task cannot be cancelled (not found, not pending, or not yours)")
return {"success": True, "message": "Task cancelled"}
# ─── All tasks (bulk + proxmox UPID) combined ──────────────────────────────
@router.get("/api/all-tasks")
def get_all_tasks(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
limit: int = Query(30, ge=1, le=200),
status_filter: str = Query(None, alias="status"),
):
"""
Return both TaskQueue (bulk ops) and ProxmoxTask (UPID) records for the
current user, merged and sorted by created_at descending.
"""
bulk_q = db.query(TaskQueue).filter(TaskQueue.user_id == current_user.id)
proxmox_q = db.query(ProxmoxTask).filter(ProxmoxTask.user_id == current_user.id)
deploy_q = db.query(DeployTask).filter(DeployTask.user_id == current_user.id)
if status_filter:
# Normalise statuses: proxmox uses running/completed/failed; bulk uses the same + pending/cancelled
bulk_q = bulk_q.filter(TaskQueue.status == status_filter)
proxmox_q = proxmox_q.filter(ProxmoxTask.status == status_filter)
deploy_q = deploy_q.filter(DeployTask.status == status_filter)
bulk_tasks = bulk_q.order_by(TaskQueue.created_at.desc()).limit(limit).all()
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(
[_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]
return {"tasks": all_tasks}
@router.get("/api/all-tasks/active-count")
def get_active_count(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Return count of currently active tasks (pending/running) for navbar badge."""
bulk_count = db.query(func.count(TaskQueue.id)).filter(
TaskQueue.user_id == current_user.id,
TaskQueue.status.in_(["pending", "running"]),
).scalar() or 0
proxmox_count = db.query(func.count(ProxmoxTask.id)).filter(
ProxmoxTask.user_id == current_user.id,
ProxmoxTask.status == "running",
).scalar() or 0
deploy_count = db.query(func.count(DeployTask.id)).filter(
DeployTask.user_id == current_user.id,
DeployTask.status.in_(["pending", "running"]),
).scalar() or 0
return {"count": bulk_count + proxmox_count + deploy_count}
# ─── Proxmox UPID task log from DB ──────────────────────────────────────────
@router.get("/api/proxmox-tasks")
def list_proxmox_tasks(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
limit: int = Query(20, ge=1, le=100),
):
"""List ProxmoxTask records for current user."""
tasks = ProxmoxTaskService.get_user_tasks(db, current_user.id, limit)
return {"tasks": [t.to_dict() for t in tasks]}
@router.get("/api/proxmox-tasks/{task_id}/log")
def get_proxmox_task_log_db(
task_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Get the cached log text for a ProxmoxTask stored in DB."""
task = db.query(ProxmoxTask).filter(
ProxmoxTask.id == task_id,
ProxmoxTask.user_id == current_user.id,
).first()
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return {"log_text": task.log_text or "", "status": task.status, "exit_status": task.exit_status}
@router.delete("/api/all-tasks/completed")
def clear_completed_tasks(
db: Session = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Delete all completed/failed/cancelled tasks for the current user."""
done_statuses = ["completed", "failed", "cancelled"]
bulk_deleted = db.query(TaskQueue).filter(
TaskQueue.user_id == current_user.id,
TaskQueue.status.in_(done_statuses),
).delete(synchronize_session=False)
proxmox_deleted = db.query(ProxmoxTask).filter(
ProxmoxTask.user_id == current_user.id,
ProxmoxTask.status.in_(done_statuses),
).delete(synchronize_session=False)
deploy_deleted = db.query(DeployTask).filter(
DeployTask.user_id == current_user.id,
DeployTask.status.in_(done_statuses),
).delete(synchronize_session=False)
db.commit()
return {"deleted": bulk_deleted + proxmox_deleted + deploy_deleted}