Merge pull request #6 from markmorado/claude/code-review-audit-cxCWB
Claude/code review audit cx cwb
This commit is contained in:
@@ -30,7 +30,7 @@ templates = Jinja2Templates(directory="app/templates")
|
||||
# ==================== HTML Pages ====================
|
||||
|
||||
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def ipam_dashboard(request: Request):
|
||||
def ipam_dashboard(request: Request):
|
||||
"""IPAM Dashboard page"""
|
||||
from ..i18n import t
|
||||
lang = request.cookies.get("language", "en")
|
||||
@@ -44,7 +44,7 @@ async def ipam_dashboard(request: Request):
|
||||
|
||||
|
||||
@router.get("/networks", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def ipam_networks_page(request: Request):
|
||||
def ipam_networks_page(request: Request):
|
||||
"""IPAM Networks management page"""
|
||||
from ..i18n import t
|
||||
lang = request.cookies.get("language", "en")
|
||||
@@ -58,7 +58,7 @@ async def ipam_networks_page(request: Request):
|
||||
|
||||
|
||||
@router.get("/network/{network_id}", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def ipam_network_detail(request: Request, network_id: int):
|
||||
def ipam_network_detail(request: Request, network_id: int):
|
||||
"""IPAM Network detail page with IP grid"""
|
||||
from ..i18n import t
|
||||
lang = request.cookies.get("language", "en")
|
||||
@@ -73,7 +73,7 @@ async def ipam_network_detail(request: Request, network_id: int):
|
||||
|
||||
|
||||
@router.get("/allocations", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def ipam_allocations_page(request: Request):
|
||||
def ipam_allocations_page(request: Request):
|
||||
"""IPAM Allocations page"""
|
||||
from ..i18n import t
|
||||
lang = request.cookies.get("language", "en")
|
||||
@@ -87,7 +87,7 @@ async def ipam_allocations_page(request: Request):
|
||||
|
||||
|
||||
@router.get("/history", response_class=HTMLResponse, include_in_schema=False)
|
||||
async def ipam_history_page(request: Request):
|
||||
def ipam_history_page(request: Request):
|
||||
"""IPAM History page"""
|
||||
from ..i18n import t
|
||||
lang = request.cookies.get("language", "en")
|
||||
@@ -103,7 +103,7 @@ async def ipam_history_page(request: Request):
|
||||
# ==================== Network API ====================
|
||||
|
||||
@router.get("/api/networks", response_model=List[IPAMNetworkResponse])
|
||||
async def get_networks(
|
||||
def get_networks(
|
||||
is_active: Optional[bool] = None,
|
||||
proxmox_server_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -146,7 +146,7 @@ async def get_networks(
|
||||
|
||||
|
||||
@router.get("/api/networks/{network_id}", response_model=IPAMNetworkResponse)
|
||||
async def get_network(
|
||||
def get_network(
|
||||
network_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.view"))
|
||||
@@ -169,7 +169,7 @@ async def get_network(
|
||||
|
||||
|
||||
@router.get("/api/networks/{network_id}/stats", response_model=IPAMNetworkStats)
|
||||
async def get_network_stats(
|
||||
def get_network_stats(
|
||||
network_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.view"))
|
||||
@@ -185,7 +185,7 @@ async def get_network_stats(
|
||||
|
||||
|
||||
@router.get("/api/networks/{network_id}/ip-map")
|
||||
async def get_network_ip_map(
|
||||
def get_network_ip_map(
|
||||
network_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.view"))
|
||||
@@ -203,7 +203,7 @@ async def get_network_ip_map(
|
||||
|
||||
|
||||
@router.post("/api/networks", response_model=IPAMNetworkResponse)
|
||||
async def create_network(
|
||||
def create_network(
|
||||
network_data: IPAMNetworkCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.manage"))
|
||||
@@ -224,7 +224,7 @@ async def create_network(
|
||||
|
||||
|
||||
@router.put("/api/networks/{network_id}", response_model=IPAMNetworkResponse)
|
||||
async def update_network(
|
||||
def update_network(
|
||||
network_id: int,
|
||||
network_data: IPAMNetworkUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -247,7 +247,7 @@ async def update_network(
|
||||
|
||||
|
||||
@router.delete("/api/networks/{network_id}")
|
||||
async def delete_network(
|
||||
def delete_network(
|
||||
network_id: int,
|
||||
force: bool = False,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -280,7 +280,7 @@ async def delete_network(
|
||||
# ==================== Pool API ====================
|
||||
|
||||
@router.get("/api/pools", response_model=List[IPAMPoolResponse])
|
||||
async def get_pools(
|
||||
def get_pools(
|
||||
network_id: Optional[int] = None,
|
||||
is_active: Optional[bool] = None,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -314,7 +314,7 @@ async def get_pools(
|
||||
|
||||
|
||||
@router.post("/api/pools", response_model=IPAMPoolResponse)
|
||||
async def create_pool(
|
||||
def create_pool(
|
||||
pool_data: IPAMPoolCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.manage"))
|
||||
@@ -345,7 +345,7 @@ async def create_pool(
|
||||
|
||||
|
||||
@router.put("/api/pools/{pool_id}", response_model=IPAMPoolResponse)
|
||||
async def update_pool(
|
||||
def update_pool(
|
||||
pool_id: int,
|
||||
pool_data: IPAMPoolUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -380,7 +380,7 @@ async def update_pool(
|
||||
|
||||
|
||||
@router.delete("/api/pools/{pool_id}")
|
||||
async def delete_pool(
|
||||
def delete_pool(
|
||||
pool_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.manage"))
|
||||
@@ -402,7 +402,7 @@ async def delete_pool(
|
||||
# ==================== Allocation API ====================
|
||||
|
||||
@router.get("/api/allocations", response_model=List[IPAMAllocationResponse])
|
||||
async def get_allocations(
|
||||
def get_allocations(
|
||||
network_id: Optional[int] = None,
|
||||
pool_id: Optional[int] = None,
|
||||
status: Optional[str] = None,
|
||||
@@ -471,7 +471,7 @@ async def get_allocations(
|
||||
|
||||
|
||||
@router.get("/api/allocations/{allocation_id}", response_model=IPAMAllocationResponse)
|
||||
async def get_allocation(
|
||||
def get_allocation(
|
||||
allocation_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.view"))
|
||||
@@ -492,7 +492,7 @@ async def get_allocation(
|
||||
|
||||
|
||||
@router.post("/api/allocations", response_model=IPAMAllocationResponse)
|
||||
async def create_allocation(
|
||||
def create_allocation(
|
||||
alloc_data: IPAMAllocationCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.manage"))
|
||||
@@ -525,7 +525,7 @@ async def create_allocation(
|
||||
|
||||
|
||||
@router.post("/api/allocations/auto", response_model=IPAMAllocationResponse)
|
||||
async def auto_allocate_ip(
|
||||
def auto_allocate_ip(
|
||||
request: IPAMAutoAllocateRequest,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.manage"))
|
||||
@@ -550,7 +550,7 @@ async def auto_allocate_ip(
|
||||
|
||||
|
||||
@router.put("/api/allocations/{allocation_id}", response_model=IPAMAllocationResponse)
|
||||
async def update_allocation(
|
||||
def update_allocation(
|
||||
allocation_id: int,
|
||||
alloc_data: IPAMAllocationUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -575,7 +575,7 @@ async def update_allocation(
|
||||
|
||||
|
||||
@router.delete("/api/allocations/{allocation_id}")
|
||||
async def delete_allocation(
|
||||
def delete_allocation(
|
||||
allocation_id: int,
|
||||
reason: Optional[str] = None,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -600,7 +600,7 @@ async def delete_allocation(
|
||||
|
||||
|
||||
@router.get("/api/allocations/next-available/{network_id}")
|
||||
async def get_next_available_ip(
|
||||
def get_next_available_ip(
|
||||
network_id: int,
|
||||
pool_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
@@ -619,7 +619,7 @@ async def get_next_available_ip(
|
||||
# ==================== History API ====================
|
||||
|
||||
@router.get("/api/history", response_model=List[IPAMHistoryResponse])
|
||||
async def get_history(
|
||||
def get_history(
|
||||
network_id: Optional[int] = None,
|
||||
ip_address: Optional[str] = None,
|
||||
action: Optional[str] = None,
|
||||
@@ -643,7 +643,7 @@ async def get_history(
|
||||
|
||||
|
||||
@router.get("/api/history/ip/{ip_address}", response_model=List[IPAMHistoryResponse])
|
||||
async def get_ip_history(
|
||||
def get_ip_history(
|
||||
ip_address: str,
|
||||
limit: int = Query(default=50, le=200),
|
||||
db: Session = Depends(get_db),
|
||||
@@ -657,7 +657,7 @@ async def get_ip_history(
|
||||
# ==================== Utility API ====================
|
||||
|
||||
@router.get("/api/conflicts")
|
||||
async def detect_conflicts(
|
||||
def detect_conflicts(
|
||||
network_id: Optional[int] = None,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.view"))
|
||||
@@ -669,7 +669,7 @@ async def detect_conflicts(
|
||||
|
||||
|
||||
@router.get("/api/summary")
|
||||
async def get_ipam_summary(
|
||||
def get_ipam_summary(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.view"))
|
||||
):
|
||||
@@ -712,7 +712,7 @@ async def get_ipam_summary(
|
||||
# ==================== Maintenance API ====================
|
||||
|
||||
@router.get("/api/orphans")
|
||||
async def get_orphan_allocations(
|
||||
def get_orphan_allocations(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.manage"))
|
||||
):
|
||||
@@ -815,7 +815,7 @@ async def cleanup_orphan_allocations(
|
||||
|
||||
|
||||
@router.get("/api/unlinked")
|
||||
async def get_unlinked_allocations(
|
||||
def get_unlinked_allocations(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.manage"))
|
||||
):
|
||||
@@ -860,7 +860,7 @@ async def get_unlinked_allocations(
|
||||
|
||||
|
||||
@router.post("/api/link-allocations")
|
||||
async def link_allocations_to_vms(
|
||||
def link_allocations_to_vms(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("ipam.manage"))
|
||||
):
|
||||
|
||||
13
backend/app/api/proxmox/__init__.py
Normal file
13
backend/app/api/proxmox/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
from fastapi import APIRouter
|
||||
from .servers import router as servers_router
|
||||
from .sdn import router as sdn_router
|
||||
from .snapshots import router as snapshots_router
|
||||
from .vms import router as vms_router
|
||||
from .tasks import router as tasks_router
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(servers_router)
|
||||
router.include_router(sdn_router)
|
||||
router.include_router(snapshots_router)
|
||||
router.include_router(vms_router)
|
||||
router.include_router(tasks_router)
|
||||
416
backend/app/api/proxmox/_helpers.py
Normal file
416
backend/app/api/proxmox/_helpers.py
Normal file
@@ -0,0 +1,416 @@
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
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
|
||||
from ..schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerResponse
|
||||
from ..proxmox_client import ProxmoxClient, get_proxmox_resources
|
||||
from ..auth import get_current_user, PermissionChecker, require_permission, check_permission
|
||||
from ..logging_service import LoggingService
|
||||
from ..template_helpers import add_i18n_context
|
||||
from ..ipam_service import IPAMService
|
||||
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
|
||||
# ==================== Helper Functions for User Isolation ====================
|
||||
|
||||
def check_vm_access(db: Session, current_user: User, server_id: int, vmid: int) -> bool:
|
||||
"""
|
||||
Check if user has access to a specific VM instance.
|
||||
|
||||
Admins and users with 'vms:view' permission can access all VMs.
|
||||
Users with 'user' role can only access VMs where they are the owner.
|
||||
|
||||
Returns True if access is allowed, False otherwise.
|
||||
"""
|
||||
# Admin role has full access
|
||||
if current_user.role and current_user.role.name == 'admin':
|
||||
return True
|
||||
|
||||
# Check permissions
|
||||
if current_user.role and current_user.role.permissions:
|
||||
perms = current_user.role.permissions
|
||||
# If user has full vms:view, they can access all VMs
|
||||
has_full_view = perms.get('vms.view', False) or perms.get('vms:view', False)
|
||||
has_own_only = perms.get('vms:view:own', False) or perms.get('vms.view.own', False)
|
||||
|
||||
if has_full_view and not has_own_only:
|
||||
return True
|
||||
|
||||
# For limited users, check ownership
|
||||
if current_user.role and current_user.role.name == 'user':
|
||||
instance = db.query(VMInstance).filter(
|
||||
VMInstance.server_id == server_id,
|
||||
VMInstance.vmid == vmid,
|
||||
VMInstance.deleted_at.is_(None)
|
||||
).first()
|
||||
|
||||
if instance and instance.owner_id == current_user.id:
|
||||
return True
|
||||
return False
|
||||
|
||||
# Default: allow access (for backwards compatibility)
|
||||
return True
|
||||
|
||||
|
||||
def require_vm_access(db: Session, current_user: User, server_id: int, vmid: int):
|
||||
"""
|
||||
Require user to have access to VM, raise 403 if not.
|
||||
"""
|
||||
if not check_vm_access(db, current_user, server_id, vmid):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="You do not have access to this virtual machine"
|
||||
)
|
||||
|
||||
|
||||
def _get_proxmox_client(server: ProxmoxServer) -> ProxmoxClient:
|
||||
"""Build a ProxmoxClient from a ProxmoxServer model (password or token auth)."""
|
||||
if server.use_password:
|
||||
return ProxmoxClient(
|
||||
host=server.ip_address,
|
||||
user=server.api_user,
|
||||
password=server.password,
|
||||
verify_ssl=server.verify_ssl
|
||||
)
|
||||
return ProxmoxClient(
|
||||
host=server.ip_address,
|
||||
user=server.api_user,
|
||||
token_name=server.api_token_name,
|
||||
token_value=server.api_token_value,
|
||||
verify_ssl=server.verify_ssl
|
||||
)
|
||||
|
||||
|
||||
def get_next_vmid(db: Session, server_id: int) -> int:
|
||||
"""Get next available VMID from Proxmox server (sequential)"""
|
||||
|
||||
# Get server from DB
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Server not found")
|
||||
|
||||
try:
|
||||
# Create Proxmox client and get next VMID from Proxmox API
|
||||
client = _get_proxmox_client(server)
|
||||
|
||||
# Get next VMID from Proxmox (it returns sequential IDs)
|
||||
vmid = client.get_next_vmid()
|
||||
if vmid:
|
||||
return vmid
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get VMID from Proxmox, falling back to DB: {e}")
|
||||
|
||||
# Fallback: get from DB if Proxmox not available
|
||||
used_vmids = set(
|
||||
row[0] for row in db.query(VMInstance.vmid)
|
||||
.filter(VMInstance.server_id == server_id, VMInstance.deleted_at == None)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Find first available VMID starting from 100
|
||||
for vmid in range(100, 999999):
|
||||
if vmid not in used_vmids:
|
||||
return vmid
|
||||
|
||||
raise HTTPException(status_code=500, detail="No available VMID")
|
||||
|
||||
|
||||
def archive_and_delete_snapshots(
|
||||
db: Session,
|
||||
client: ProxmoxClient,
|
||||
server_id: int,
|
||||
server_name: str,
|
||||
vmid: int,
|
||||
vm_name: str,
|
||||
vm_type: str,
|
||||
node: str,
|
||||
deleted_by: str,
|
||||
deletion_reason: str = None
|
||||
) -> dict:
|
||||
"""
|
||||
Archive all snapshots to database before deleting them.
|
||||
|
||||
This function:
|
||||
1. Gets list of all snapshots for the VM/container
|
||||
2. Saves each snapshot's configuration to vm_snapshot_archives table
|
||||
3. Explicitly deletes each snapshot one by one
|
||||
4. Logs each deletion
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
client: ProxmoxClient instance
|
||||
server_id: Proxmox server ID
|
||||
server_name: Proxmox server name for logging
|
||||
vmid: VM/container ID
|
||||
vm_name: VM/container name
|
||||
vm_type: 'qemu' for VM, 'lxc' for container
|
||||
node: Proxmox node name
|
||||
deleted_by: Username who initiated deletion
|
||||
deletion_reason: Reason for deletion
|
||||
|
||||
Returns:
|
||||
Dict with archived count, deleted count, and any errors
|
||||
"""
|
||||
result = {
|
||||
"archived": 0,
|
||||
"deleted": 0,
|
||||
"errors": [],
|
||||
"snapshots": []
|
||||
}
|
||||
|
||||
try:
|
||||
# Get all snapshots
|
||||
if vm_type == 'lxc':
|
||||
snapshots = client.get_container_snapshots(node, vmid)
|
||||
else:
|
||||
snapshots = client.get_vm_snapshots(node, vmid)
|
||||
|
||||
# Filter out 'current' state which is not a real snapshot
|
||||
snapshots = [s for s in snapshots if s.get('name') != 'current']
|
||||
|
||||
if not snapshots:
|
||||
logger.info(f"No snapshots found for {vm_type} {vmid}")
|
||||
return result
|
||||
|
||||
logger.info(f"Found {len(snapshots)} snapshots to archive for {vm_type} {vmid}")
|
||||
|
||||
# Archive and delete each snapshot
|
||||
for snap in snapshots:
|
||||
snapname = snap.get('name')
|
||||
if not snapname:
|
||||
continue
|
||||
|
||||
# Create archive record
|
||||
try:
|
||||
archive = VMSnapshotArchive(
|
||||
server_id=server_id,
|
||||
server_name=server_name,
|
||||
vmid=vmid,
|
||||
vm_name=vm_name,
|
||||
vm_type=vm_type,
|
||||
node=node,
|
||||
snapname=snapname,
|
||||
description=snap.get('description'),
|
||||
snaptime=snap.get('snaptime'),
|
||||
parent=snap.get('parent'),
|
||||
vmstate=bool(snap.get('vmstate')),
|
||||
snapshot_config=snap,
|
||||
deleted_by=deleted_by,
|
||||
deletion_reason=deletion_reason
|
||||
)
|
||||
db.add(archive)
|
||||
db.flush() # Get ID immediately
|
||||
result["archived"] += 1
|
||||
result["snapshots"].append(snapname)
|
||||
logger.info(f"Archived snapshot {snapname} for {vm_type} {vmid}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error archiving snapshot {snapname}: {e}")
|
||||
result["errors"].append(f"Archive {snapname}: {str(e)}")
|
||||
|
||||
# Delete snapshot from Proxmox
|
||||
try:
|
||||
if vm_type == 'lxc':
|
||||
delete_result = client.delete_container_snapshot(node, vmid, snapname, force=True)
|
||||
else:
|
||||
delete_result = client.delete_vm_snapshot(node, vmid, snapname, force=True)
|
||||
|
||||
if delete_result.get('success'):
|
||||
result["deleted"] += 1
|
||||
|
||||
# Log the deletion
|
||||
LoggingService.log_proxmox_action(
|
||||
db=db,
|
||||
action="snapshot_delete",
|
||||
resource_type=vm_type,
|
||||
resource_id=vmid,
|
||||
username=deleted_by,
|
||||
resource_name=snapname,
|
||||
server_id=server_id,
|
||||
server_name=server_name,
|
||||
node_name=node,
|
||||
details={
|
||||
"reason": f"Pre-deletion cleanup for {vm_type} {vmid}",
|
||||
"cascade_delete": True
|
||||
},
|
||||
success=True
|
||||
)
|
||||
logger.info(f"Deleted snapshot {snapname} for {vm_type} {vmid}")
|
||||
else:
|
||||
error_msg = delete_result.get('error', 'Unknown error')
|
||||
result["errors"].append(f"Delete {snapname}: {error_msg}")
|
||||
logger.warning(f"Failed to delete snapshot {snapname}: {error_msg}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting snapshot {snapname}: {e}")
|
||||
result["errors"].append(f"Delete {snapname}: {str(e)}")
|
||||
|
||||
# Commit archive records
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Snapshot cleanup for {vm_type} {vmid}: archived={result['archived']}, deleted={result['deleted']}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in archive_and_delete_snapshots for {vm_type} {vmid}: {e}")
|
||||
result["errors"].append(f"General error: {str(e)}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def save_vm_instance(
|
||||
db: Session,
|
||||
server_id: int,
|
||||
vmid: int,
|
||||
node: str,
|
||||
vm_type: str,
|
||||
name: str,
|
||||
cores: int = None,
|
||||
memory: int = None,
|
||||
disk_size: int = None,
|
||||
ip_address: str = None,
|
||||
ip_prefix: int = 24,
|
||||
gateway: str = None,
|
||||
nameserver: str = None,
|
||||
cloud_init_user: str = None,
|
||||
cloud_init_password: str = None,
|
||||
ssh_keys: str = None,
|
||||
template_id: int = None,
|
||||
template_name: str = None,
|
||||
description: str = None,
|
||||
extra_config: dict = None,
|
||||
owner_id: int = None
|
||||
) -> VMInstance:
|
||||
"""Save or update VM instance configuration"""
|
||||
|
||||
# Проверяем, существует ли уже активная запись
|
||||
existing = db.query(VMInstance).filter(
|
||||
VMInstance.server_id == server_id,
|
||||
VMInstance.vmid == vmid,
|
||||
VMInstance.deleted_at == None
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
# Обновляем существующую запись
|
||||
existing.node = node
|
||||
existing.vm_type = vm_type
|
||||
existing.name = name
|
||||
existing.cores = cores
|
||||
existing.memory = memory
|
||||
existing.disk_size = disk_size
|
||||
existing.ip_address = ip_address
|
||||
existing.ip_prefix = ip_prefix
|
||||
existing.gateway = gateway
|
||||
existing.nameserver = nameserver
|
||||
existing.cloud_init_user = cloud_init_user
|
||||
existing.cloud_init_password = cloud_init_password
|
||||
existing.ssh_keys = ssh_keys
|
||||
existing.template_id = template_id
|
||||
existing.template_name = template_name
|
||||
existing.description = description
|
||||
existing.extra_config = extra_config
|
||||
existing.updated_at = func.now()
|
||||
db.commit()
|
||||
db.refresh(existing)
|
||||
return existing
|
||||
|
||||
# Проверяем, есть ли удалённая запись с тем же vmid (soft-deleted)
|
||||
deleted_existing = db.query(VMInstance).filter(
|
||||
VMInstance.server_id == server_id,
|
||||
VMInstance.vmid == vmid,
|
||||
VMInstance.deleted_at != None
|
||||
).first()
|
||||
|
||||
if deleted_existing:
|
||||
# Восстанавливаем и обновляем удалённую запись
|
||||
deleted_existing.node = node
|
||||
deleted_existing.vm_type = vm_type
|
||||
deleted_existing.name = name
|
||||
deleted_existing.cores = cores
|
||||
deleted_existing.memory = memory
|
||||
deleted_existing.disk_size = disk_size
|
||||
deleted_existing.ip_address = ip_address
|
||||
deleted_existing.ip_prefix = ip_prefix
|
||||
deleted_existing.gateway = gateway
|
||||
deleted_existing.nameserver = nameserver
|
||||
deleted_existing.cloud_init_user = cloud_init_user
|
||||
deleted_existing.cloud_init_password = cloud_init_password
|
||||
deleted_existing.ssh_keys = ssh_keys
|
||||
deleted_existing.template_id = template_id
|
||||
deleted_existing.template_name = template_name
|
||||
deleted_existing.description = description
|
||||
deleted_existing.extra_config = extra_config
|
||||
deleted_existing.owner_id = owner_id
|
||||
deleted_existing.status = 'unknown'
|
||||
deleted_existing.deleted_at = None # Восстанавливаем запись
|
||||
deleted_existing.updated_at = func.now()
|
||||
db.commit()
|
||||
db.refresh(deleted_existing)
|
||||
return deleted_existing
|
||||
|
||||
# Создаем новую запись
|
||||
instance = VMInstance(
|
||||
server_id=server_id,
|
||||
vmid=vmid,
|
||||
node=node,
|
||||
vm_type=vm_type,
|
||||
name=name,
|
||||
cores=cores,
|
||||
memory=memory,
|
||||
disk_size=disk_size,
|
||||
ip_address=ip_address,
|
||||
ip_prefix=ip_prefix,
|
||||
gateway=gateway,
|
||||
nameserver=nameserver,
|
||||
cloud_init_user=cloud_init_user,
|
||||
cloud_init_password=cloud_init_password,
|
||||
ssh_keys=ssh_keys,
|
||||
template_id=template_id,
|
||||
template_name=template_name,
|
||||
description=description,
|
||||
extra_config=extra_config,
|
||||
owner_id=owner_id
|
||||
)
|
||||
db.add(instance)
|
||||
db.commit()
|
||||
db.refresh(instance)
|
||||
return instance
|
||||
|
||||
|
||||
def get_vm_instance(db: Session, server_id: int, vmid: int) -> VMInstance:
|
||||
"""Get VM instance configuration"""
|
||||
return db.query(VMInstance).filter(
|
||||
VMInstance.server_id == server_id,
|
||||
VMInstance.vmid == vmid,
|
||||
VMInstance.deleted_at == None
|
||||
).first()
|
||||
|
||||
|
||||
def soft_delete_vm_instance(db: Session, server_id: int, vmid: int):
|
||||
"""Soft delete VM instance (mark as deleted) and release IPAM"""
|
||||
instance = get_vm_instance(db, server_id, vmid)
|
||||
if instance:
|
||||
# Release IPAM allocation
|
||||
try:
|
||||
ipam = IPAMService(db)
|
||||
released = ipam.release_ip_by_vmid(
|
||||
proxmox_server_id=server_id,
|
||||
proxmox_vmid=vmid,
|
||||
released_by="system",
|
||||
reason="VM/Container soft deleted"
|
||||
)
|
||||
if released:
|
||||
logger.info(f"Released IPAM for soft-deleted instance {vmid} on server {server_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to release IPAM for soft-deleted instance: {e}")
|
||||
|
||||
instance.deleted_at = func.now()
|
||||
db.commit()
|
||||
326
backend/app/api/proxmox/sdn.py
Normal file
326
backend/app/api/proxmox/sdn.py
Normal file
@@ -0,0 +1,326 @@
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
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
|
||||
from ..schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerResponse
|
||||
from ..proxmox_client import ProxmoxClient, get_proxmox_resources
|
||||
from ..auth import get_current_user, PermissionChecker, require_permission, check_permission
|
||||
from ..logging_service import LoggingService
|
||||
from ..template_helpers import add_i18n_context
|
||||
from ..ipam_service import IPAMService
|
||||
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,
|
||||
templates)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ==================== SDN (Software Defined Networking) ====================
|
||||
|
||||
@router.get("/api/servers/{server_id}/sdn/status")
|
||||
def get_sdn_status(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""Check if SDN is available on the server"""
|
||||
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)
|
||||
|
||||
is_available = client.sdn_is_available()
|
||||
pending = client.get_sdn_pending() if is_available else []
|
||||
|
||||
return JSONResponse(content={
|
||||
"server_id": server_id,
|
||||
"sdn_available": is_available,
|
||||
"pending_changes": len(pending) > 0,
|
||||
"pending": pending
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking SDN status for server {server_id}: {e}")
|
||||
return JSONResponse(content={
|
||||
"server_id": server_id,
|
||||
"sdn_available": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
|
||||
@router.get("/api/servers/{server_id}/sdn/zones")
|
||||
def get_sdn_zones(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""Get all SDN zones"""
|
||||
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)
|
||||
|
||||
zones = client.get_sdn_zones()
|
||||
return JSONResponse(content={"zones": zones})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting SDN zones for server {server_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/servers/{server_id}/sdn/zones")
|
||||
async def create_sdn_zone(
|
||||
server_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.manage"))
|
||||
):
|
||||
"""Create a new SDN zone"""
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
data = await request.json()
|
||||
zone_name = data.get('zone')
|
||||
zone_type = data.get('type', 'simple')
|
||||
|
||||
if not zone_name:
|
||||
raise HTTPException(status_code=400, detail="Zone name is required")
|
||||
|
||||
try:
|
||||
client = _get_proxmox_client(server)
|
||||
|
||||
# Pass additional options from request
|
||||
kwargs = {k: v for k, v in data.items() if k not in ['zone', 'type']}
|
||||
result = client.create_sdn_zone(zone_name, zone_type, **kwargs)
|
||||
|
||||
if result.get('success'):
|
||||
logger.info(f"User {current_user.username} created SDN zone: {zone_name}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to create zone'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating SDN zone: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/api/servers/{server_id}/sdn/zones/{zone}")
|
||||
def delete_sdn_zone(
|
||||
server_id: int,
|
||||
zone: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.manage"))
|
||||
):
|
||||
"""Delete an SDN zone"""
|
||||
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)
|
||||
|
||||
result = client.delete_sdn_zone(zone)
|
||||
|
||||
if result.get('success'):
|
||||
logger.info(f"User {current_user.username} deleted SDN zone: {zone}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to delete zone'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting SDN zone: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/servers/{server_id}/sdn/vnets")
|
||||
def get_sdn_vnets(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""Get all SDN VNets"""
|
||||
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)
|
||||
|
||||
vnets = client.get_sdn_vnets()
|
||||
return JSONResponse(content={"vnets": vnets})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting SDN vnets for server {server_id}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/servers/{server_id}/sdn/vnets")
|
||||
async def create_sdn_vnet(
|
||||
server_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.manage"))
|
||||
):
|
||||
"""Create a new SDN VNet"""
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
data = await request.json()
|
||||
vnet_name = data.get('vnet')
|
||||
zone = data.get('zone')
|
||||
tag = data.get('tag')
|
||||
alias = data.get('alias')
|
||||
vlanaware = data.get('vlanaware', False)
|
||||
|
||||
if not vnet_name or not zone:
|
||||
raise HTTPException(status_code=400, detail="VNet name and zone are required")
|
||||
|
||||
try:
|
||||
client = _get_proxmox_client(server)
|
||||
|
||||
result = client.create_sdn_vnet(vnet_name, zone, tag=tag, alias=alias, vlanaware=vlanaware)
|
||||
|
||||
if result.get('success'):
|
||||
logger.info(f"User {current_user.username} created SDN vnet: {vnet_name} in zone {zone}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to create vnet'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating SDN vnet: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/api/servers/{server_id}/sdn/vnets/{vnet}")
|
||||
def delete_sdn_vnet(
|
||||
server_id: int,
|
||||
vnet: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.manage"))
|
||||
):
|
||||
"""Delete an SDN VNet"""
|
||||
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)
|
||||
|
||||
result = client.delete_sdn_vnet(vnet)
|
||||
|
||||
if result.get('success'):
|
||||
logger.info(f"User {current_user.username} deleted SDN vnet: {vnet}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to delete vnet'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting SDN vnet: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/servers/{server_id}/sdn/vnets/{vnet}/subnets")
|
||||
def get_sdn_subnets(
|
||||
server_id: int,
|
||||
vnet: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""Get subnets for a VNet"""
|
||||
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)
|
||||
|
||||
subnets = client.get_sdn_subnets(vnet)
|
||||
return JSONResponse(content={"subnets": subnets})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting SDN subnets for vnet {vnet}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/servers/{server_id}/sdn/vnets/{vnet}/subnets")
|
||||
async def create_sdn_subnet(
|
||||
server_id: int,
|
||||
vnet: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.manage"))
|
||||
):
|
||||
"""Create a subnet in a VNet"""
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
data = await request.json()
|
||||
subnet = data.get('subnet')
|
||||
gateway = data.get('gateway')
|
||||
snat = data.get('snat', False)
|
||||
dnszoneprefix = data.get('dnszoneprefix')
|
||||
|
||||
if not subnet:
|
||||
raise HTTPException(status_code=400, detail="Subnet CIDR is required")
|
||||
|
||||
try:
|
||||
client = _get_proxmox_client(server)
|
||||
|
||||
result = client.create_sdn_subnet(vnet, subnet, gateway=gateway, snat=snat, dnszoneprefix=dnszoneprefix)
|
||||
|
||||
if result.get('success'):
|
||||
logger.info(f"User {current_user.username} created subnet {subnet} in vnet {vnet}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to create subnet'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating SDN subnet: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/servers/{server_id}/sdn/apply")
|
||||
def apply_sdn_changes(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.manage"))
|
||||
):
|
||||
"""Apply pending SDN changes"""
|
||||
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)
|
||||
|
||||
result = client.apply_sdn_changes()
|
||||
|
||||
if result.get('success'):
|
||||
logger.info(f"User {current_user.username} applied SDN changes on server {server.name}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to apply SDN changes'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error applying SDN changes: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
774
backend/app/api/proxmox/servers.py
Normal file
774
backend/app/api/proxmox/servers.py
Normal file
@@ -0,0 +1,774 @@
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
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
|
||||
from ..schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerResponse
|
||||
from ..proxmox_client import ProxmoxClient, get_proxmox_resources
|
||||
from ..auth import get_current_user, PermissionChecker, require_permission, check_permission
|
||||
from ..logging_service import LoggingService
|
||||
from ..template_helpers import add_i18n_context
|
||||
from ..ipam_service import IPAMService
|
||||
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,
|
||||
templates)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ==================== HTML Pages ====================
|
||||
|
||||
@router.get("/vms", response_class=HTMLResponse, include_in_schema=False)
|
||||
def vms_page(request: Request, db: Session = Depends(get_db)):
|
||||
"""Страница управления Proxmox серверами, VM и LXC"""
|
||||
from ..i18n import t
|
||||
lang = request.cookies.get("language", "en")
|
||||
|
||||
proxmox_servers = db.query(ProxmoxServer).all()
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"proxmox_servers": proxmox_servers,
|
||||
"page_title": t('nav_proxmox', lang),
|
||||
}
|
||||
context = add_i18n_context(request, context)
|
||||
return templates.TemplateResponse("proxmox_vms.html", context)
|
||||
|
||||
|
||||
@router.get("/server/{server_id}", response_class=HTMLResponse, include_in_schema=False)
|
||||
def server_detail_page(request: Request, server_id: int, db: Session = Depends(get_db)):
|
||||
"""Страница детального просмотра VM/LXC конкретного Proxmox сервера"""
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"server": server,
|
||||
"page_title": server.name,
|
||||
}
|
||||
context = add_i18n_context(request, context)
|
||||
return templates.TemplateResponse("proxmox_server_detail.html", context)
|
||||
|
||||
|
||||
@router.get("/server/{server_id}/instance/{vmid}", response_class=HTMLResponse, include_in_schema=False)
|
||||
def instance_detail_page(request: Request, server_id: int, vmid: int, type: str = "qemu", node: str = "", db: Session = Depends(get_db)):
|
||||
"""Страница детального просмотра конкретной VM или LXC"""
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
context = {
|
||||
"request": request,
|
||||
"server": server,
|
||||
"vmid": vmid,
|
||||
"type": type,
|
||||
"node": node,
|
||||
"page_title": f"VM {vmid}",
|
||||
}
|
||||
context = add_i18n_context(request, context)
|
||||
return templates.TemplateResponse("instance_detail.html", context)
|
||||
|
||||
|
||||
# ==================== Proxmox Server CRUD ====================
|
||||
|
||||
@router.get("/api/servers", response_model=List[ProxmoxServerResponse])
|
||||
def list_proxmox_servers(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""Получить список всех Proxmox серверов"""
|
||||
servers = db.query(ProxmoxServer).all()
|
||||
return servers
|
||||
|
||||
|
||||
@router.post("/api/servers", response_model=ProxmoxServerResponse, status_code=status.HTTP_201_CREATED)
|
||||
def create_proxmox_server(
|
||||
server_data: ProxmoxServerCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.servers.add"))
|
||||
):
|
||||
"""Добавить новый Proxmox сервер"""
|
||||
# Разрешаем несколько серверов с одинаковым IP (например, разные порты или кластеры)
|
||||
# Проверяем только дубликаты имён
|
||||
existing = db.query(ProxmoxServer).filter(ProxmoxServer.name == server_data.name).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Server with name '{server_data.name}' already exists"
|
||||
)
|
||||
|
||||
server = ProxmoxServer(**server_data.model_dump())
|
||||
db.add(server)
|
||||
db.commit()
|
||||
db.refresh(server)
|
||||
|
||||
logger.info(f"User {current_user.username} added Proxmox server: {server.name} ({server.ip_address})")
|
||||
return server
|
||||
|
||||
|
||||
@router.post("/api/servers/auto-setup", status_code=status.HTTP_201_CREATED)
|
||||
async def auto_setup_proxmox_server(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.servers.add"))
|
||||
):
|
||||
"""
|
||||
Автоматическая настройка Proxmox сервера:
|
||||
1. Проверяем подключение по логину/паролю
|
||||
2. Создаем API Token автоматически
|
||||
3. Сохраняем сервер в базу с токеном и паролем (для VNC)
|
||||
"""
|
||||
import requests as http_requests
|
||||
import uuid
|
||||
|
||||
# Получаем данные из body
|
||||
data = await request.json()
|
||||
|
||||
name = data.get('name')
|
||||
hostname = data.get('hostname')
|
||||
ip_address = data.get('ip_address')
|
||||
port = data.get('port', 8006)
|
||||
api_user = data.get('api_user', 'root@pam')
|
||||
password = data.get('password')
|
||||
verify_ssl = data.get('verify_ssl', True)
|
||||
description = data.get('description')
|
||||
|
||||
if not all([name, ip_address, password]):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Необходимо указать: name, ip_address, password"
|
||||
)
|
||||
|
||||
# Разрешаем несколько серверов с одинаковым IP
|
||||
# Проверяем только дубликаты имён
|
||||
existing = db.query(ProxmoxServer).filter(ProxmoxServer.name == name).first()
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Сервер с именем '{name}' уже существует"
|
||||
)
|
||||
|
||||
base_url = f"https://{ip_address}:{port}"
|
||||
|
||||
# 1. Получить auth ticket
|
||||
logger.info(f"Attempting to connect to Proxmox at {base_url}")
|
||||
try:
|
||||
auth_response = http_requests.post(
|
||||
f"{base_url}/api2/json/access/ticket",
|
||||
data={"username": api_user, "password": password},
|
||||
verify=verify_ssl,
|
||||
timeout=15
|
||||
)
|
||||
|
||||
if auth_response.status_code != 200:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Ошибка авторизации в Proxmox: неверный логин или пароль"
|
||||
)
|
||||
|
||||
auth_data = auth_response.json().get("data", {})
|
||||
ticket = auth_data.get("ticket")
|
||||
csrf_token = auth_data.get("CSRFPreventionToken")
|
||||
|
||||
if not ticket:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Не удалось получить auth ticket от Proxmox"
|
||||
)
|
||||
|
||||
logger.info(f"Successfully authenticated to Proxmox {ip_address}")
|
||||
|
||||
except http_requests.exceptions.RequestException as e:
|
||||
logger.error(f"Connection error to Proxmox: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Не удалось подключиться к Proxmox: {str(e)}"
|
||||
)
|
||||
|
||||
# 2. Создать API Token
|
||||
# Извлекаем username из api_user (root@pam -> root)
|
||||
username_part = api_user.split('@')[0] if '@' in api_user else api_user
|
||||
realm = api_user.split('@')[1] if '@' in api_user else 'pam'
|
||||
|
||||
token_name = f"panel-{uuid.uuid4().hex[:8]}"
|
||||
|
||||
headers = {
|
||||
"Cookie": f"PVEAuthCookie={ticket}",
|
||||
"CSRFPreventionToken": csrf_token
|
||||
}
|
||||
|
||||
try:
|
||||
# Создаем токен через API
|
||||
token_response = http_requests.post(
|
||||
f"{base_url}/api2/json/access/users/{api_user}/token/{token_name}",
|
||||
headers=headers,
|
||||
data={"privsep": "0"}, # Полные права как у пользователя
|
||||
verify=verify_ssl,
|
||||
timeout=15
|
||||
)
|
||||
|
||||
if token_response.status_code not in [200, 201]:
|
||||
logger.warning(f"Failed to create token: {token_response.status_code} - {token_response.text}")
|
||||
# Если не удалось создать токен, работаем только с паролем
|
||||
token_value = None
|
||||
token_name = None
|
||||
logger.info("Will use password authentication only")
|
||||
else:
|
||||
token_data = token_response.json().get("data", {})
|
||||
token_value = token_data.get("value")
|
||||
logger.info(f"Created API token: {token_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error creating token: {e}, will use password auth")
|
||||
token_value = None
|
||||
token_name = None
|
||||
|
||||
# 3. Получить список нод кластера и их IP адреса
|
||||
cluster_nodes = []
|
||||
node_ips = {} # node_name -> ip_address
|
||||
|
||||
try:
|
||||
# Получаем список нод
|
||||
nodes_response = http_requests.get(
|
||||
f"{base_url}/api2/json/nodes",
|
||||
headers=headers,
|
||||
cookies={"PVEAuthCookie": ticket},
|
||||
verify=verify_ssl,
|
||||
timeout=15
|
||||
)
|
||||
if nodes_response.status_code == 200:
|
||||
nodes_data = nodes_response.json().get("data", [])
|
||||
cluster_nodes = [n for n in nodes_data if n.get("node")]
|
||||
logger.info(f"Found {len(cluster_nodes)} nodes in cluster")
|
||||
|
||||
# Получаем IP адреса нод из cluster/status
|
||||
if len(cluster_nodes) > 1:
|
||||
cluster_status_response = http_requests.get(
|
||||
f"{base_url}/api2/json/cluster/status",
|
||||
headers=headers,
|
||||
cookies={"PVEAuthCookie": ticket},
|
||||
verify=verify_ssl,
|
||||
timeout=15
|
||||
)
|
||||
if cluster_status_response.status_code == 200:
|
||||
cluster_status = cluster_status_response.json().get("data", [])
|
||||
for item in cluster_status:
|
||||
if item.get("type") == "node" and item.get("name") and item.get("ip"):
|
||||
node_ips[item.get("name")] = item.get("ip")
|
||||
logger.info(f"Node {item.get('name')} IP: {item.get('ip')}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get cluster nodes: {e}")
|
||||
|
||||
# 4. Сохранить серверы в базу
|
||||
created_servers = []
|
||||
|
||||
if len(cluster_nodes) > 1:
|
||||
# Это кластер - добавляем все ноды
|
||||
for node_info in cluster_nodes:
|
||||
node_name = node_info.get("node")
|
||||
# Используем IP ноды из cluster/status, или IP через который подключились
|
||||
node_ip = node_ips.get(node_name, ip_address)
|
||||
|
||||
# Имя сервера: "BaseName - NodeName" или просто NodeName
|
||||
if name:
|
||||
server_name = f"{name} - {node_name}"
|
||||
else:
|
||||
server_name = node_name
|
||||
|
||||
# Проверяем дубликаты
|
||||
existing = db.query(ProxmoxServer).filter(ProxmoxServer.name == server_name).first()
|
||||
if existing:
|
||||
logger.info(f"Server '{server_name}' already exists, skipping")
|
||||
continue
|
||||
|
||||
# Для каждой ноды создаём токен (используем тот же, так как токен кластерный)
|
||||
server = ProxmoxServer(
|
||||
name=server_name,
|
||||
hostname=node_name,
|
||||
ip_address=node_ip, # Используем реальный IP ноды
|
||||
port=port,
|
||||
api_user=api_user,
|
||||
api_token_name=token_name,
|
||||
api_token_value=token_value,
|
||||
use_password=token_value is None,
|
||||
password=password,
|
||||
verify_ssl=verify_ssl,
|
||||
description=f"{description or ''} (Node: {node_name})".strip(),
|
||||
is_online=node_info.get("status") == "online"
|
||||
)
|
||||
|
||||
db.add(server)
|
||||
created_servers.append({
|
||||
"name": server_name,
|
||||
"node": node_name,
|
||||
"ip": node_ip,
|
||||
"status": node_info.get("status", "unknown")
|
||||
})
|
||||
|
||||
db.commit()
|
||||
logger.info(f"User {current_user.username} auto-setup Proxmox cluster with {len(created_servers)} nodes")
|
||||
|
||||
return {
|
||||
"cluster": True,
|
||||
"nodes_count": len(created_servers),
|
||||
"servers": created_servers,
|
||||
"message": f"Добавлено {len(created_servers)} нод кластера"
|
||||
}
|
||||
else:
|
||||
# Одиночный сервер
|
||||
server = ProxmoxServer(
|
||||
name=name,
|
||||
hostname=hostname or ip_address,
|
||||
ip_address=ip_address,
|
||||
port=port,
|
||||
api_user=api_user,
|
||||
api_token_name=token_name,
|
||||
api_token_value=token_value,
|
||||
use_password=token_value is None,
|
||||
password=password,
|
||||
verify_ssl=verify_ssl,
|
||||
description=description,
|
||||
is_online=True
|
||||
)
|
||||
|
||||
db.add(server)
|
||||
db.commit()
|
||||
db.refresh(server)
|
||||
|
||||
logger.info(f"User {current_user.username} auto-setup Proxmox server: {server.name} ({server.ip_address})")
|
||||
|
||||
return {
|
||||
"cluster": False,
|
||||
"id": server.id,
|
||||
"name": server.name,
|
||||
"hostname": server.hostname,
|
||||
"ip_address": server.ip_address,
|
||||
"port": server.port,
|
||||
"api_user": server.api_user,
|
||||
"api_token_name": token_name or "(пароль)",
|
||||
"use_password": server.use_password,
|
||||
"verify_ssl": server.verify_ssl,
|
||||
"description": server.description,
|
||||
"is_online": server.is_online
|
||||
}
|
||||
|
||||
|
||||
@router.get("/api/servers/{server_id}", response_model=ProxmoxServerResponse)
|
||||
def get_proxmox_server(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""Получить информацию о конкретном Proxmox сервере"""
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
return server
|
||||
|
||||
|
||||
@router.get("/api/servers/{server_id}/cluster-info")
|
||||
def get_server_cluster_info(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""
|
||||
Получить информацию о кластерном режиме сервера.
|
||||
Возвращает is_cluster=true если сервер в кластере (HA доступна).
|
||||
"""
|
||||
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():
|
||||
return JSONResponse(content={
|
||||
"server_id": server_id,
|
||||
"server_name": server.name,
|
||||
"is_cluster": False,
|
||||
"error": "Cannot connect to server"
|
||||
})
|
||||
|
||||
is_cluster = client.is_cluster()
|
||||
nodes = client.get_nodes() if is_cluster else []
|
||||
|
||||
return JSONResponse(content={
|
||||
"server_id": server_id,
|
||||
"server_name": server.name,
|
||||
"is_cluster": is_cluster,
|
||||
"node_count": len(nodes),
|
||||
"nodes": [n.get('node') for n in nodes] if nodes else []
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking cluster info for server {server_id}: {e}")
|
||||
return JSONResponse(content={
|
||||
"server_id": server_id,
|
||||
"server_name": server.name,
|
||||
"is_cluster": False,
|
||||
"error": str(e)
|
||||
})
|
||||
|
||||
|
||||
@router.put("/api/servers/{server_id}", response_model=ProxmoxServerResponse)
|
||||
def update_proxmox_server(
|
||||
server_id: int,
|
||||
server_data: ProxmoxServerUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.servers.edit"))
|
||||
):
|
||||
"""Обновить Proxmox сервер"""
|
||||
from ..proxmox_client import clear_server_cache
|
||||
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
# Если изменились учётные данные - очищаем кеш
|
||||
update_data = server_data.model_dump(exclude_unset=True)
|
||||
if any(key in update_data for key in ['password', 'api_token_name', 'api_token_value', 'api_user']):
|
||||
clear_server_cache(server.ip_address)
|
||||
logger.info(f"Cleared cache for server {server.ip_address} due to credential update")
|
||||
|
||||
# Update only provided fields
|
||||
for field, value in update_data.items():
|
||||
setattr(server, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(server)
|
||||
|
||||
logger.info(f"User {current_user.username} updated Proxmox server: {server.name}")
|
||||
return server
|
||||
|
||||
|
||||
@router.delete("/api/servers/{server_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_proxmox_server(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.servers.delete"))
|
||||
):
|
||||
"""Удалить Proxmox сервер и связанные OS Templates"""
|
||||
from ..proxmox_client import clear_server_cache
|
||||
from ..models import OSTemplate, OSTemplateGroup, VMInstance
|
||||
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
server_name = server.name
|
||||
server_ip = server.ip_address
|
||||
|
||||
# Release ALL IPAM allocations for this server
|
||||
try:
|
||||
ipam = IPAMService(db)
|
||||
released_count = ipam.release_all_by_server(
|
||||
proxmox_server_id=server_id,
|
||||
released_by=current_user.username
|
||||
)
|
||||
if released_count > 0:
|
||||
logger.info(f"Released {released_count} IPAM allocations for server {server_name}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to release IPAM allocations for server: {e}")
|
||||
|
||||
# Удаляем связанные OS Templates
|
||||
templates_deleted = db.query(OSTemplate).filter(OSTemplate.server_id == server_id).delete()
|
||||
if templates_deleted > 0:
|
||||
logger.info(f"Deleted {templates_deleted} OS templates for server {server_name}")
|
||||
|
||||
# Удаляем кэш VM/контейнеров для этого сервера
|
||||
vms_deleted = db.query(VMInstance).filter(VMInstance.server_id == server_id).delete()
|
||||
if vms_deleted > 0:
|
||||
logger.info(f"Deleted {vms_deleted} cached VM instances for server {server_name}")
|
||||
|
||||
# Удаляем пустые группы шаблонов (без шаблонов)
|
||||
empty_groups = db.query(OSTemplateGroup).filter(
|
||||
~OSTemplateGroup.id.in_(
|
||||
db.query(OSTemplate.group_id).filter(OSTemplate.group_id != None).distinct()
|
||||
)
|
||||
).all()
|
||||
for group in empty_groups:
|
||||
# Проверяем что группа действительно пуста
|
||||
template_count = db.query(OSTemplate).filter(OSTemplate.group_id == group.id).count()
|
||||
if template_count == 0:
|
||||
logger.info(f"Deleting empty template group: {group.name}")
|
||||
db.delete(group)
|
||||
|
||||
logger.info(f"User {current_user.username} deleted Proxmox server: {server_name} ({server_ip})")
|
||||
|
||||
# Очищаем кеш подключений для этого сервера
|
||||
clear_server_cache(server_ip)
|
||||
|
||||
db.delete(server)
|
||||
db.commit()
|
||||
return None
|
||||
|
||||
|
||||
@router.post("/api/servers/{server_id}/test", status_code=status.HTTP_200_OK)
|
||||
def test_proxmox_connection(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""Проверить подключение к Proxmox серверу"""
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
try:
|
||||
# Determine auth method
|
||||
client = _get_proxmox_client(server)
|
||||
|
||||
if client.is_connected():
|
||||
server.update_status(True)
|
||||
db.commit()
|
||||
return {"status": "success", "message": "Connection successful"}
|
||||
else:
|
||||
server.update_status(False, "Failed to connect")
|
||||
db.commit()
|
||||
raise HTTPException(status_code=503, detail="Failed to connect to Proxmox server")
|
||||
|
||||
except Exception as e:
|
||||
server.update_status(False, str(e))
|
||||
db.commit()
|
||||
logger.error(f"Error testing Proxmox connection to {server.name}: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ==================== Proxmox Resources ====================
|
||||
|
||||
@router.get("/api/{server_id}/resources")
|
||||
def get_server_resources(
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""API для получения всех ресурсов (VM + LXC) с Proxmox сервера"""
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
# Если hostname указан (нода в кластере), фильтруем по ней
|
||||
# Проверяем что hostname не пустой и не равен IP адресу (автономный сервер)
|
||||
node_filter = None
|
||||
if server.hostname and server.hostname != server.ip_address:
|
||||
node_filter = server.hostname
|
||||
|
||||
try:
|
||||
if server.use_password:
|
||||
resources = get_proxmox_resources(
|
||||
host=server.ip_address,
|
||||
user=server.api_user,
|
||||
password=server.password,
|
||||
verify_ssl=server.verify_ssl,
|
||||
node=node_filter
|
||||
)
|
||||
else:
|
||||
resources = get_proxmox_resources(
|
||||
host=server.ip_address,
|
||||
user=server.api_user,
|
||||
token_name=server.api_token_name,
|
||||
token_value=server.api_token_value,
|
||||
verify_ssl=server.verify_ssl,
|
||||
node=node_filter
|
||||
)
|
||||
|
||||
# Фильтруем шаблоны - они не должны считаться как VM
|
||||
vms = [vm for vm in resources.get('vms', []) if not vm.get('template', 0)]
|
||||
containers = [ct for ct in resources.get('containers', []) if not ct.get('template', 0)]
|
||||
|
||||
server.update_status(True)
|
||||
db.commit()
|
||||
|
||||
return JSONResponse(content={
|
||||
"server_id": server_id,
|
||||
"server_name": server.name,
|
||||
"vms": vms,
|
||||
"containers": containers
|
||||
})
|
||||
except Exception as e:
|
||||
server.update_status(False, str(e))
|
||||
db.commit()
|
||||
raise HTTPException(status_code=500, detail=f"Error getting resources: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/api/resources/all")
|
||||
def get_all_resources(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""API для получения всех ресурсов со всех Proxmox серверов"""
|
||||
proxmox_servers = db.query(ProxmoxServer).all()
|
||||
|
||||
all_resources = {
|
||||
"servers": [],
|
||||
"total_vms": 0,
|
||||
"total_containers": 0
|
||||
}
|
||||
|
||||
for server in proxmox_servers:
|
||||
try:
|
||||
logger.info(f"Getting resources from server {server.name} ({server.ip_address}), use_password={server.use_password}")
|
||||
# Создаём клиента для каждого сервера независимо
|
||||
if server.use_password:
|
||||
resources = get_proxmox_resources(
|
||||
host=server.ip_address,
|
||||
user=server.api_user,
|
||||
password=server.password,
|
||||
verify_ssl=server.verify_ssl
|
||||
)
|
||||
else:
|
||||
logger.info(f"Using API token for {server.name}: user={server.api_user}, token_name={server.api_token_name}")
|
||||
resources = get_proxmox_resources(
|
||||
host=server.ip_address,
|
||||
user=server.api_user,
|
||||
token_name=server.api_token_name,
|
||||
token_value=server.api_token_value,
|
||||
verify_ssl=server.verify_ssl
|
||||
)
|
||||
|
||||
vms = resources.get('vms', [])
|
||||
containers = resources.get('containers', [])
|
||||
|
||||
logger.info(f"Got {len(vms)} VMs and {len(containers)} containers from {server.name}")
|
||||
|
||||
server.update_status(True)
|
||||
db.commit()
|
||||
|
||||
all_resources["servers"].append({
|
||||
"id": server.id,
|
||||
"name": server.name,
|
||||
"ip": server.ip_address,
|
||||
"vms": vms,
|
||||
"containers": containers,
|
||||
"vms_count": len(vms),
|
||||
"containers_count": len(containers)
|
||||
})
|
||||
|
||||
all_resources["total_vms"] += len(vms)
|
||||
all_resources["total_containers"] += len(containers)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting resources from server {server.name} ({server.ip_address}): {e}")
|
||||
server.update_status(False, str(e))
|
||||
db.commit()
|
||||
all_resources["servers"].append({
|
||||
"id": server.id,
|
||||
"name": server.name,
|
||||
"ip": server.ip_address,
|
||||
"error": str(e),
|
||||
"vms": [],
|
||||
"containers": [],
|
||||
"vms_count": 0,
|
||||
"containers_count": 0
|
||||
})
|
||||
# Продолжаем обработку следующего сервера
|
||||
continue
|
||||
|
||||
return JSONResponse(content=all_resources)
|
||||
|
||||
|
||||
# ==================== Node (Host) Status ====================
|
||||
|
||||
@router.get("/api/{server_id}/storages")
|
||||
def get_storages(
|
||||
server_id: int,
|
||||
node: str = Query(..., description="Node name"),
|
||||
content_type: str = Query(None, description="Filter by content type (images, rootdir, vztmpl, etc)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("proxmox.view"))
|
||||
):
|
||||
"""Получить список хранилищ на ноде Proxmox"""
|
||||
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")
|
||||
|
||||
storages = client.get_storages(node)
|
||||
|
||||
# Фильтрация по типу контента если указано
|
||||
if content_type and storages:
|
||||
filtered = []
|
||||
for storage in storages:
|
||||
content = storage.get('content', '')
|
||||
if content_type in content.split(','):
|
||||
filtered.append(storage)
|
||||
storages = filtered
|
||||
|
||||
# Сортировка: сначала активные, потом по имени
|
||||
storages.sort(key=lambda x: (not x.get('active', 1), x.get('storage', '')))
|
||||
|
||||
return JSONResponse(content={"storages": storages})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting storages: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/{server_id}/node/status")
|
||||
def get_node_status(
|
||||
server_id: int,
|
||||
node: str = Query(..., description="Node name"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""Получить текущий статус ноды Proxmox (хоста)"""
|
||||
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_node_status(node)
|
||||
if not status:
|
||||
raise HTTPException(status_code=404, detail="Node status not found")
|
||||
|
||||
return JSONResponse(content=status)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting node {node} status: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/{server_id}/node/rrddata")
|
||||
def get_node_rrddata(
|
||||
server_id: int,
|
||||
node: str = Query(..., description="Node name"),
|
||||
timeframe: str = Query("hour", regex="^(hour|day|week|month|year)$"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""Получить исторические данные ноды для графиков"""
|
||||
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")
|
||||
|
||||
rrddata = client.get_node_rrddata(node, timeframe)
|
||||
return JSONResponse(content=rrddata)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting node {node} RRD data: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
462
backend/app/api/proxmox/snapshots.py
Normal file
462
backend/app/api/proxmox/snapshots.py
Normal file
@@ -0,0 +1,462 @@
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
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
|
||||
from ..schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerResponse
|
||||
from ..proxmox_client import ProxmoxClient, get_proxmox_resources
|
||||
from ..auth import get_current_user, PermissionChecker, require_permission, check_permission
|
||||
from ..logging_service import LoggingService
|
||||
from ..template_helpers import add_i18n_context
|
||||
from ..ipam_service import IPAMService
|
||||
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,
|
||||
templates)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
# ==================== Snapshots ====================
|
||||
|
||||
@router.get("/api/{server_id}/vm/{vmid}/snapshots")
|
||||
def get_vm_snapshots(
|
||||
server_id: int,
|
||||
vmid: int,
|
||||
node: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""Get all snapshots for a VM"""
|
||||
# VPS-style user isolation
|
||||
require_vm_access(db, current_user, server_id, vmid)
|
||||
|
||||
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)
|
||||
|
||||
snapshots = client.get_vm_snapshots(node, vmid)
|
||||
# Filter out 'current' pseudo-snapshot if present
|
||||
snapshots = [s for s in snapshots if s.get('name') != 'current']
|
||||
return JSONResponse(content={"snapshots": snapshots})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting VM {vmid} snapshots: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/{server_id}/vm/{vmid}/snapshots")
|
||||
async def create_vm_snapshot(
|
||||
server_id: int,
|
||||
vmid: int,
|
||||
node: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""Create a VM snapshot"""
|
||||
# VPS-style user isolation
|
||||
require_vm_access(db, current_user, server_id, vmid)
|
||||
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
data = await request.json()
|
||||
snapname = data.get('snapname')
|
||||
description = data.get('description')
|
||||
vmstate = data.get('vmstate', False)
|
||||
|
||||
if not snapname:
|
||||
raise HTTPException(status_code=400, detail="Snapshot name is required")
|
||||
|
||||
try:
|
||||
client = _get_proxmox_client(server)
|
||||
|
||||
result = client.create_vm_snapshot(node, vmid, snapname, description, vmstate)
|
||||
|
||||
if result.get('success'):
|
||||
LoggingService.log_proxmox_action(
|
||||
db=db,
|
||||
action="snapshot_create",
|
||||
resource_type="vm",
|
||||
resource_id=vmid,
|
||||
username=current_user.username,
|
||||
resource_name=snapname,
|
||||
server_id=server_id,
|
||||
server_name=server.name,
|
||||
node_name=node,
|
||||
success=True
|
||||
)
|
||||
logger.info(f"User {current_user.username} created snapshot {snapname} for VM {vmid}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to create snapshot'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating VM {vmid} snapshot: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/api/{server_id}/vm/{vmid}/snapshots/{snapname}")
|
||||
def delete_vm_snapshot(
|
||||
server_id: int,
|
||||
vmid: int,
|
||||
snapname: str,
|
||||
node: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""Delete a VM snapshot"""
|
||||
# VPS-style user isolation
|
||||
require_vm_access(db, current_user, server_id, vmid)
|
||||
|
||||
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)
|
||||
|
||||
result = client.delete_vm_snapshot(node, vmid, snapname)
|
||||
|
||||
if result.get('success'):
|
||||
LoggingService.log_proxmox_action(
|
||||
db=db,
|
||||
action="snapshot_delete",
|
||||
resource_type="vm",
|
||||
resource_id=vmid,
|
||||
username=current_user.username,
|
||||
resource_name=snapname,
|
||||
server_id=server_id,
|
||||
server_name=server.name,
|
||||
node_name=node,
|
||||
success=True
|
||||
)
|
||||
logger.info(f"User {current_user.username} deleted snapshot {snapname} for VM {vmid}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to delete snapshot'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting VM {vmid} snapshot: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/{server_id}/vm/{vmid}/snapshots/{snapname}/rollback")
|
||||
def rollback_vm_snapshot(
|
||||
server_id: int,
|
||||
vmid: int,
|
||||
snapname: str,
|
||||
node: str,
|
||||
start: bool = False,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""Rollback a VM to a snapshot"""
|
||||
# VPS-style user isolation
|
||||
require_vm_access(db, current_user, server_id, vmid)
|
||||
|
||||
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)
|
||||
|
||||
result = client.rollback_vm_snapshot(node, vmid, snapname, start)
|
||||
|
||||
if result.get('success'):
|
||||
LoggingService.log_proxmox_action(
|
||||
db=db,
|
||||
action="snapshot_rollback",
|
||||
resource_type="vm",
|
||||
resource_id=vmid,
|
||||
username=current_user.username,
|
||||
resource_name=snapname,
|
||||
server_id=server_id,
|
||||
server_name=server.name,
|
||||
node_name=node,
|
||||
success=True
|
||||
)
|
||||
logger.info(f"User {current_user.username} rolled back VM {vmid} to snapshot {snapname}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to rollback snapshot'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error rolling back VM {vmid} to snapshot: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# Container Snapshots
|
||||
|
||||
@router.get("/api/{server_id}/container/{vmid}/snapshots")
|
||||
def get_container_snapshots(
|
||||
server_id: int,
|
||||
vmid: int,
|
||||
node: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""Get all snapshots for a container"""
|
||||
# VPS-style user isolation
|
||||
require_vm_access(db, current_user, server_id, vmid)
|
||||
|
||||
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)
|
||||
|
||||
snapshots = client.get_container_snapshots(node, vmid)
|
||||
# Filter out 'current' pseudo-snapshot if present
|
||||
snapshots = [s for s in snapshots if s.get('name') != 'current']
|
||||
return JSONResponse(content={"snapshots": snapshots})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting container {vmid} snapshots: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/{server_id}/container/{vmid}/snapshots")
|
||||
async def create_container_snapshot(
|
||||
server_id: int,
|
||||
vmid: int,
|
||||
node: str,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""Create a container snapshot"""
|
||||
# VPS-style user isolation
|
||||
require_vm_access(db, current_user, server_id, vmid)
|
||||
|
||||
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
|
||||
if not server:
|
||||
raise HTTPException(status_code=404, detail="Proxmox server not found")
|
||||
|
||||
data = await request.json()
|
||||
snapname = data.get('snapname')
|
||||
description = data.get('description')
|
||||
|
||||
if not snapname:
|
||||
raise HTTPException(status_code=400, detail="Snapshot name is required")
|
||||
|
||||
try:
|
||||
client = _get_proxmox_client(server)
|
||||
|
||||
result = client.create_container_snapshot(node, vmid, snapname, description)
|
||||
|
||||
if result.get('success'):
|
||||
LoggingService.log_proxmox_action(
|
||||
db=db,
|
||||
action="snapshot_create",
|
||||
resource_type="container",
|
||||
resource_id=vmid,
|
||||
username=current_user.username,
|
||||
resource_name=snapname,
|
||||
server_id=server_id,
|
||||
server_name=server.name,
|
||||
node_name=node,
|
||||
success=True
|
||||
)
|
||||
logger.info(f"User {current_user.username} created snapshot {snapname} for container {vmid}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to create snapshot'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating container {vmid} snapshot: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.delete("/api/{server_id}/container/{vmid}/snapshots/{snapname}")
|
||||
def delete_container_snapshot(
|
||||
server_id: int,
|
||||
vmid: int,
|
||||
snapname: str,
|
||||
node: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""Delete a container snapshot"""
|
||||
# VPS-style user isolation
|
||||
require_vm_access(db, current_user, server_id, vmid)
|
||||
|
||||
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)
|
||||
|
||||
result = client.delete_container_snapshot(node, vmid, snapname)
|
||||
|
||||
if result.get('success'):
|
||||
LoggingService.log_proxmox_action(
|
||||
db=db,
|
||||
action="snapshot_delete",
|
||||
resource_type="container",
|
||||
resource_id=vmid,
|
||||
username=current_user.username,
|
||||
resource_name=snapname,
|
||||
server_id=server_id,
|
||||
server_name=server.name,
|
||||
node_name=node,
|
||||
success=True
|
||||
)
|
||||
logger.info(f"User {current_user.username} deleted snapshot {snapname} for container {vmid}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to delete snapshot'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting container {vmid} snapshot: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/api/{server_id}/container/{vmid}/snapshots/{snapname}/rollback")
|
||||
def rollback_container_snapshot(
|
||||
server_id: int,
|
||||
vmid: int,
|
||||
snapname: str,
|
||||
node: str,
|
||||
start: bool = False,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("vms.view"))
|
||||
):
|
||||
"""Rollback a container to a snapshot"""
|
||||
# VPS-style user isolation
|
||||
require_vm_access(db, current_user, server_id, vmid)
|
||||
|
||||
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)
|
||||
|
||||
result = client.rollback_container_snapshot(node, vmid, snapname, start)
|
||||
|
||||
if result.get('success'):
|
||||
LoggingService.log_proxmox_action(
|
||||
db=db,
|
||||
action="snapshot_rollback",
|
||||
resource_type="container",
|
||||
resource_id=vmid,
|
||||
username=current_user.username,
|
||||
resource_name=snapname,
|
||||
server_id=server_id,
|
||||
server_name=server.name,
|
||||
node_name=node,
|
||||
success=True
|
||||
)
|
||||
logger.info(f"User {current_user.username} rolled back container {vmid} to snapshot {snapname}")
|
||||
return JSONResponse(content=result)
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to rollback snapshot'))
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Error rolling back container {vmid} to snapshot: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
# ==================== Snapshot Archives ====================
|
||||
|
||||
@router.get("/api/snapshot-archives")
|
||||
def get_snapshot_archives(
|
||||
server_id: int = None,
|
||||
vmid: int = None,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("logs.view"))
|
||||
):
|
||||
"""
|
||||
Get archived snapshots from deleted VMs/containers.
|
||||
Admin-only endpoint for audit and recovery purposes.
|
||||
"""
|
||||
query = db.query(VMSnapshotArchive).order_by(VMSnapshotArchive.archived_at.desc())
|
||||
|
||||
if server_id:
|
||||
query = query.filter(VMSnapshotArchive.server_id == server_id)
|
||||
if vmid:
|
||||
query = query.filter(VMSnapshotArchive.vmid == vmid)
|
||||
|
||||
total = query.count()
|
||||
archives = query.offset(offset).limit(limit).all()
|
||||
|
||||
return JSONResponse(content={
|
||||
"total": total,
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"archives": [
|
||||
{
|
||||
"id": a.id,
|
||||
"server_id": a.server_id,
|
||||
"server_name": a.server_name,
|
||||
"vmid": a.vmid,
|
||||
"vm_name": a.vm_name,
|
||||
"vm_type": a.vm_type,
|
||||
"node": a.node,
|
||||
"snapname": a.snapname,
|
||||
"description": a.description,
|
||||
"snaptime": a.snaptime,
|
||||
"parent": a.parent,
|
||||
"vmstate": a.vmstate,
|
||||
"deleted_by": a.deleted_by,
|
||||
"deletion_reason": a.deletion_reason,
|
||||
"archived_at": a.archived_at.isoformat() if a.archived_at else None
|
||||
}
|
||||
for a in archives
|
||||
]
|
||||
})
|
||||
|
||||
|
||||
@router.get("/api/snapshot-archives/{archive_id}")
|
||||
def get_snapshot_archive_detail(
|
||||
archive_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(PermissionChecker("logs.view"))
|
||||
):
|
||||
"""Get full details of an archived snapshot including config"""
|
||||
archive = db.query(VMSnapshotArchive).filter(VMSnapshotArchive.id == archive_id).first()
|
||||
if not archive:
|
||||
raise HTTPException(status_code=404, detail="Archive not found")
|
||||
|
||||
return JSONResponse(content={
|
||||
"id": archive.id,
|
||||
"server_id": archive.server_id,
|
||||
"server_name": archive.server_name,
|
||||
"vmid": archive.vmid,
|
||||
"vm_name": archive.vm_name,
|
||||
"vm_type": archive.vm_type,
|
||||
"node": archive.node,
|
||||
"snapname": archive.snapname,
|
||||
"description": archive.description,
|
||||
"snaptime": archive.snaptime,
|
||||
"parent": archive.parent,
|
||||
"vmstate": archive.vmstate,
|
||||
"snapshot_config": archive.snapshot_config,
|
||||
"deleted_by": archive.deleted_by,
|
||||
"deletion_reason": archive.deletion_reason,
|
||||
"archived_at": archive.archived_at.isoformat() if archive.archived_at else None
|
||||
})
|
||||
148
backend/app/api/proxmox/tasks.py
Normal file
148
backend/app/api/proxmox/tasks.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from fastapi import APIRouter, Depends, Request, HTTPException, Query, Form, status, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
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
|
||||
from ..schemas import ProxmoxServerCreate, ProxmoxServerUpdate, ProxmoxServerResponse
|
||||
from ..proxmox_client import ProxmoxClient, get_proxmox_resources
|
||||
from ..auth import get_current_user, PermissionChecker, require_permission, check_permission
|
||||
from ..logging_service import LoggingService
|
||||
from ..template_helpers import add_i18n_context
|
||||
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,
|
||||
templates)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@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("vms.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("vms.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))
|
||||
|
||||
|
||||
@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 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_tasks(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user)
|
||||
):
|
||||
"""Get all active (pending/running) 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 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 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"}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user