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

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

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

448 lines
16 KiB
Python

from fastapi import APIRouter, Depends, Request, HTTPException
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
from loguru import logger
from typing import Optional
from ...db import get_db
from ...models import ProxmoxServer, User, IPAMNetwork
from ...auth import PermissionChecker
from ...logging_service import LoggingService
from ._helpers import _get_proxmox_client
from ...proxmox import _run_in_executor
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("server: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("server: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("server: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 = await _run_in_executor(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("server: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("server: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("server: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 = await _run_in_executor(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("server: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("server: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("server: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')
create_ipam_network = data.get('create_ipam_network', False)
if not subnet:
raise HTTPException(status_code=400, detail="Subnet CIDR is required")
try:
client = _get_proxmox_client(server)
result = await _run_in_executor(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}")
response = dict(result)
# Optionally create a matching IPAMNetwork
if create_ipam_network:
existing = db.query(IPAMNetwork).filter(IPAMNetwork.network == subnet).first()
if existing:
response['ipam_network_id'] = existing.id
response['ipam_already_existed'] = True
else:
ipam_net = IPAMNetwork(
name=f"{vnet} — {subnet}",
network=subnet,
gateway=gateway or None,
proxmox_server_id=server_id,
proxmox_bridge=vnet,
is_active=True,
)
db.add(ipam_net)
db.commit()
db.refresh(ipam_net)
response['ipam_network_id'] = ipam_net.id
response['ipam_created'] = True
logger.info(
f"User {current_user.username} auto-created IPAM network "
f"{subnet} (id={ipam_net.id}) linked to vnet {vnet}"
)
return JSONResponse(content=response)
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("server: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))
@router.put("/api/servers/{server_id}/sdn/zones/{zone}")
async def update_sdn_zone(
server_id: int,
zone: str,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("server:manage"))
):
"""Update an existing 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()
try:
client = _get_proxmox_client(server)
result = await _run_in_executor(client.update_sdn_zone, zone, **data)
if result.get('success'):
logger.info(f"User {current_user.username} updated SDN zone: {zone}")
return JSONResponse(content=result)
else:
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to update zone'))
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating SDN zone {zone}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.put("/api/servers/{server_id}/sdn/vnets/{vnet}")
async def update_sdn_vnet(
server_id: int,
vnet: str,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("server:manage"))
):
"""Update an existing 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()
try:
client = _get_proxmox_client(server)
result = await _run_in_executor(client.update_sdn_vnet, vnet, **data)
if result.get('success'):
logger.info(f"User {current_user.username} updated SDN vnet: {vnet}")
return JSONResponse(content=result)
else:
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to update vnet'))
except HTTPException:
raise
except Exception as e:
logger.error(f"Error updating SDN vnet {vnet}: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/api/servers/{server_id}/sdn/vnets/{vnet}/subnets/{subnet_cidr:path}")
def delete_sdn_subnet(
server_id: int,
vnet: str,
subnet_cidr: str,
delete_ipam_network: bool = False,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("server:manage"))
):
"""Delete a subnet from a VNet. subnet_cidr uses '-' instead of '/' (e.g. 10.0.0.0-24)."""
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Proxmox server not found")
# Accept both dash-encoded and slash CIDR
subnet_slash = subnet_cidr.replace("-", "/") if "-" in subnet_cidr else subnet_cidr
try:
client = _get_proxmox_client(server)
result = client.delete_sdn_subnet(vnet, subnet_slash)
if result.get('success'):
logger.info(f"User {current_user.username} deleted subnet {subnet_slash} from vnet {vnet}")
response = dict(result)
if delete_ipam_network:
ipam_net = db.query(IPAMNetwork).filter(
IPAMNetwork.network == subnet_slash,
IPAMNetwork.proxmox_server_id == server_id,
).first()
if ipam_net:
db.delete(ipam_net)
db.commit()
response['ipam_network_deleted'] = True
logger.info(
f"User {current_user.username} deleted IPAM network "
f"{subnet_slash} (id={ipam_net.id})"
)
return JSONResponse(content=response)
else:
raise HTTPException(status_code=400, detail=result.get('error', 'Failed to delete subnet'))
except HTTPException:
raise
except Exception as e:
logger.error(f"Error deleting SDN subnet {subnet_slash}: {e}")
raise HTTPException(status_code=500, detail=str(e))