refactor: split proxmox.py (4676 lines) into package with 7 modules

- _helpers.py: shared helpers (check_vm_access, _get_proxmox_client, etc.)
- servers.py: HTML pages, server CRUD, resources, node status (16 routes)
- sdn.py: SDN zones/vnets/subnets (10 routes)
- snapshots.py: VM/container snapshots + archives (10 routes)
- vms.py: VM/LXC control, HA, monitoring, VNC, terminal, bulk ops (42 routes)
- tasks.py: Proxmox task management (6 routes)
- __init__.py: re-exports unified router (84 routes total, no public API change)

https://claude.ai/code/session_019rK2wvaWg5zXFwmAPPEB7R
This commit is contained in:
Claude
2026-03-07 17:35:17 +00:00
parent 7bd2dd5b8f
commit 6fb7406d27
7 changed files with 2156 additions and 2013 deletions

View 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
})