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>
448 lines
16 KiB
Python
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))
|