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:
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
|
||||
})
|
||||
Reference in New Issue
Block a user