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>
330 lines
12 KiB
Python
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}
|
|
|