refactor(api): remove duplicate/superseded endpoints
All checks were successful
Backend Tests / pytest (Python 3.12) (push) Successful in 49s
Deploy to Production / Trigger production update (push) Successful in 8s

Drop dead route handlers that have an active replacement and zero callers
(frontend, backend, tests):
- create-lxc, create-lxc-smart  -> superseded by POST /lxc/deploy
- clone-lxc                      -> superseded by container/{vmid}/clone
- {server_id}/lxc-templates      -> duplicate of /lxc-templates/{server_id}
- dashboard/stats               -> dashboard uses resources/all + alerts
- permissions (v1)              -> superseded by permissions/v2

The identically-named ProxmoxClient/SecurityService methods are kept (they
back the surviving endpoints).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 00:05:41 +05:00
parent 2886326353
commit 0529e902f5
4 changed files with 62 additions and 588 deletions

View File

@@ -11,78 +11,6 @@ from ..config import utcnow
router = APIRouter()
@router.get("/api/dashboard/stats")
def dashboard_stats(
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("dashboard.view"))
):
"""Dashboard statistics API"""
from .workspaces import get_workspace_server_ids
server_ids = get_workspace_server_ids(request, db, current_user)
from sqlalchemy import func, distinct
base_q = db.query(ProxmoxServer)
if server_ids is not None:
base_q = base_q.filter(ProxmoxServer.id.in_(server_ids))
total_servers = base_q.count()
online_servers = base_q.filter(ProxmoxServer.is_online == True).count()
offline_servers = base_q.filter(ProxmoxServer.is_online == False).count()
cluster_q = db.query(func.count(distinct(ProxmoxServer.cluster_name))).filter(
ProxmoxServer.cluster_name.isnot(None),
ProxmoxServer.cluster_name != ''
)
if server_ids is not None:
cluster_q = cluster_q.filter(ProxmoxServer.id.in_(server_ids))
total_clusters = cluster_q.scalar() or 0
since_24h = utcnow() - timedelta(hours=24)
total_alerts = db.query(AuditLog).filter(
AuditLog.level.in_(['error', 'critical']),
AuditLog.created_at >= since_24h
).count()
critical_alerts = db.query(AuditLog).filter(
AuditLog.level == 'critical',
AuditLog.created_at >= since_24h
).count()
recent_audit = db.query(AuditLog).order_by(
AuditLog.created_at.desc()
).limit(5).all()
recent_servers_q = db.query(ProxmoxServer)
if server_ids is not None:
recent_servers_q = recent_servers_q.filter(ProxmoxServer.id.in_(server_ids))
recent_servers = recent_servers_q.order_by(ProxmoxServer.id.desc()).limit(6).all()
return {
"stats": {
"total_servers": total_servers,
"online_servers": online_servers,
"offline_servers": offline_servers,
"total_clusters": total_clusters,
"total_alerts": total_alerts,
"critical_alerts": critical_alerts,
},
"recent_servers": [
{"id": s.id, "name": s.name, "ip_address": s.ip_address, "is_online": s.is_online}
for s in recent_servers
],
"recent_audit": [
{
"id": a.id, "level": a.level, "category": a.category,
"action": a.action, "message": a.message, "username": a.username,
"created_at": a.created_at.isoformat() if a.created_at else None,
}
for a in recent_audit
],
}
@router.get("/api/dashboard/alerts")
def get_dashboard_alerts(
db: Session = Depends(get_db),

View File

@@ -2186,36 +2186,6 @@ def get_saved_vm_config(
# ==================== LXC Container Creation ====================
@router.get("/api/{server_id}/lxc-templates")
def get_lxc_templates(
server_id: int,
node: str,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("templates.view"))
):
"""Получить список доступных шаблонов LXC"""
logger.info(f"[LXC API] Request for templates: server_id={server_id}, node={node}")
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 not client.is_connected():
raise HTTPException(status_code=503, detail="Cannot connect to Proxmox server")
templates = client.get_lxc_templates(node)
logger.info(f"[LXC API] Found {len(templates)} templates")
return JSONResponse(content=templates)
except HTTPException:
raise
except Exception as e:
logger.error(f"Error getting LXC templates: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/api/{server_id}/all-lxc-templates")
def get_all_lxc_templates(
server_id: int,
@@ -2340,440 +2310,6 @@ async def download_lxc_template(
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/{server_id}/create-lxc")
async def create_lxc_container(
server_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("vms.create"))
):
"""Создать новый LXC контейнер"""
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()
node = data.get('node')
vmid = data.get('vmid')
ostemplate = data.get('ostemplate') # storage:vztmpl/template.tar.gz
hostname = data.get('hostname')
password = data.get('password')
ssh_public_keys = data.get('ssh_public_keys')
storage = data.get('storage', 'local-lvm')
rootfs_size = data.get('rootfs_size', 8)
memory = data.get('memory', 512)
swap = data.get('swap', 512)
cores = data.get('cores', 1)
net0 = data.get('net0')
unprivileged = data.get('unprivileged', True)
start_after_create = data.get('start_after_create', False)
onboot = data.get('onboot', False)
description = data.get('description')
features = data.get('features')
if not all([node, ostemplate, hostname]):
raise HTTPException(status_code=400, detail="Missing required parameters: node, ostemplate, hostname")
if not password and not ssh_public_keys:
raise HTTPException(status_code=400, detail="Either password or SSH keys must be provided")
try:
# Determine auth method
client = _get_proxmox_client(server)
if not client.is_connected():
raise HTTPException(status_code=503, detail="Cannot connect to Proxmox server")
# Получить следующий VMID если не указан
if not vmid:
vmid = client.get_next_vmid()
if not vmid:
raise HTTPException(status_code=500, detail="Failed to get next VMID")
# Combine user's SSH key with request SSH keys
combined_ssh_keys = ssh_public_keys or ""
if current_user.ssh_public_key:
if combined_ssh_keys:
combined_ssh_keys = f"{combined_ssh_keys}\n{current_user.ssh_public_key}"
else:
combined_ssh_keys = current_user.ssh_public_key
# Deduplicate SSH keys while preserving order
_seen_ssh: set = set()
_uniq_ssh = []
for _k in combined_ssh_keys.splitlines():
_k = _k.strip()
if _k and _k not in _seen_ssh:
_seen_ssh.add(_k)
_uniq_ssh.append(_k)
combined_ssh_keys = "\n".join(_uniq_ssh)
upid = client.create_lxc_container(
node=node,
vmid=vmid,
ostemplate=ostemplate,
hostname=hostname,
password=password,
ssh_public_keys=combined_ssh_keys if combined_ssh_keys else None,
storage=storage,
rootfs_size=rootfs_size,
memory=memory,
swap=swap,
cores=cores,
net0=net0,
unprivileged=unprivileged,
start_after_create=start_after_create,
onboot=onboot,
description=description,
features=features
)
if not upid:
raise HTTPException(status_code=500, detail="Failed to create LXC container")
# Register UPID task for real-time tracking
try:
ProxmoxTaskService.register(
db=db,
upid=upid,
server_id=server_id,
user_id=current_user.id,
action="create_lxc",
node=node,
vmid=vmid,
vm_type="lxc",
description=f"Create LXC container: {hostname} (ID: {vmid})",
)
except Exception as _e:
logger.warning(f"Failed to register UPID task for LXC creation: {_e}")
LoggingService.log_proxmox_action(
db=db,
action="create",
resource_type="container",
resource_id=vmid,
username=current_user.username,
resource_name=hostname,
server_id=server_id,
server_name=server.name,
node_name=node,
details={"template": ostemplate, "memory": memory, "cores": cores},
success=True,
ip_address=request.client.host if request.client else None
)
return JSONResponse(content={
"success": True,
"vmid": vmid,
"upid": upid,
"message": f"LXC container {hostname} (ID: {vmid}) creation started"
})
except HTTPException:
raise
except Exception as e:
logger.error(f"Error creating LXC container: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/{server_id}/create-lxc-smart")
async def create_lxc_container_smart(
server_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("vms.create"))
):
"""
Умное создание LXC контейнера с поддержкой кросс-нодного деплоя.
Если шаблон на shared storage или на целевой ноде - создаём напрямую.
Если шаблон на локальном хранилище другой ноды - создаём там и мигрируем.
"""
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()
template_node = data.get('template_node') # Нода где находится шаблон
target_node = data.get('target_node') # Целевая нода для контейнера
ostemplate = data.get('ostemplate')
hostname = data.get('hostname')
password = data.get('password')
ssh_public_keys = data.get('ssh_public_keys') # SSH keys from request
storage = data.get('storage', 'local-lvm')
rootfs_size = data.get('rootfs_size', 8)
memory = data.get('memory', 512)
swap = data.get('swap', 512)
cores = data.get('cores', 1)
start_after_create = data.get('start_after_create', False)
onboot = data.get('onboot', False)
enable_ha = data.get('enable_ha', False)
is_shared = data.get('is_shared', False)
net0 = data.get('net0') # Network configuration from IPAM
ipam_allocation_id = data.get('ipam_allocation_id') # IPAM allocation ID for tracking
if not all([template_node, target_node, ostemplate, hostname]):
raise HTTPException(status_code=400, detail="Missing required parameters")
if not password and not ssh_public_keys and not current_user.ssh_public_key:
raise HTTPException(status_code=400, detail="Password or SSH keys are required")
try:
client = _get_proxmox_client(server)
if not client.is_connected():
raise HTTPException(status_code=503, detail="Cannot connect to Proxmox server")
vmid = client.get_next_vmid()
if not vmid:
raise HTTPException(status_code=500, detail="Failed to get next VMID")
# Определяем стратегию создания
needs_migration = not is_shared and template_node != target_node
create_node = template_node if needs_migration else target_node
logger.info(f"[LXC Smart Create] Template on {template_node}, target {target_node}, shared={is_shared}, needs_migration={needs_migration}, net0={net0}")
# Combine user's SSH key with request SSH keys
combined_ssh_keys = ssh_public_keys or ""
if current_user.ssh_public_key:
if combined_ssh_keys:
combined_ssh_keys = f"{combined_ssh_keys}\n{current_user.ssh_public_key}"
else:
combined_ssh_keys = current_user.ssh_public_key
# Deduplicate SSH keys while preserving order
_seen_ssh: set = set()
_uniq_ssh = []
for _k in combined_ssh_keys.splitlines():
_k = _k.strip()
if _k and _k not in _seen_ssh:
_seen_ssh.add(_k)
_uniq_ssh.append(_k)
combined_ssh_keys = "\n".join(_uniq_ssh)
# Создаём контейнер
upid = client.create_lxc_container(
node=create_node,
vmid=vmid,
ostemplate=ostemplate,
hostname=hostname,
password=password,
ssh_public_keys=combined_ssh_keys if combined_ssh_keys else None,
storage=storage,
rootfs_size=rootfs_size,
memory=memory,
swap=swap,
cores=cores,
net0=net0, # Network configuration from IPAM
unprivileged=True,
start_after_create=False, # Не запускаем до миграции
onboot=onboot
)
if not upid:
raise HTTPException(status_code=500, detail="Failed to create LXC container")
# Ждём завершения создания
create_success = client.wait_for_task(create_node, upid, timeout=300)
if not create_success:
raise HTTPException(status_code=500, detail="Container creation timed out")
# Если нужна миграция
if needs_migration:
logger.info(f"[LXC Smart Create] Migrating container {vmid} from {create_node} to {target_node}")
migrate_upid = client.migrate_container(create_node, vmid, target_node, target_storage=storage)
if migrate_upid:
migrate_success = client.wait_for_task(create_node, migrate_upid, timeout=600)
if not migrate_success:
logger.warning(f"Migration of container {vmid} may have timed out")
else:
logger.warning(f"Failed to start migration for container {vmid}")
# Запускаем если нужно
if start_after_create:
final_node = target_node
client.start_container(final_node, vmid)
# Enable High Availability if requested (cluster only)
ha_enabled = False
if enable_ha:
try:
if client.is_cluster():
ha_result = client.add_to_ha(vmid=vmid, vm_type='ct', max_restart=3, max_relocate=3)
if ha_result.get('success'):
ha_enabled = True
logger.info(f"HA enabled for LXC container {vmid}")
else:
logger.warning(f"Failed to enable HA for LXC {vmid}: {ha_result.get('error')}")
else:
logger.warning(f"HA requested but server {server.name} is not in a cluster")
except Exception as ha_err:
logger.warning(f"Failed to enable HA for LXC {vmid}: {ha_err}")
# Update IPAM allocation with VMID and server info
if ipam_allocation_id:
try:
from ...models import IPAMAllocation
allocation = db.query(IPAMAllocation).filter(IPAMAllocation.id == ipam_allocation_id).first()
if allocation:
allocation.resource_id = vmid
allocation.proxmox_vmid = vmid
allocation.proxmox_server_id = server.id
allocation.proxmox_node = target_node
allocation.resource_type = "lxc"
allocation.resource_name = hostname
db.commit()
logger.info(f"[IPAM] Updated allocation {ipam_allocation_id} with VMID {vmid}, server_id {server.id}")
except Exception as ipam_err:
logger.warning(f"[IPAM] Failed to update allocation {ipam_allocation_id}: {ipam_err}")
LoggingService.log_proxmox_action(
db=db,
action="create",
resource_type="container",
resource_id=vmid,
username=current_user.username,
resource_name=hostname,
server_id=server_id,
server_name=server.name,
node_name=target_node,
details={
"template_node": template_node,
"template": ostemplate,
"migrated": needs_migration
},
success=True,
ip_address=request.client.host if request.client else None
)
migration_msg = f" (migrated from {template_node})" if needs_migration else ""
return JSONResponse(content={
"success": True,
"vmid": vmid,
"node": target_node,
"message": f"LXC container {hostname} (ID: {vmid}) created on {target_node}{migration_msg}"
})
except HTTPException:
raise
except Exception as e:
logger.error(f"Error in smart LXC creation: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/api/{server_id}/clone-lxc")
async def clone_lxc_container(
server_id: int,
request: Request,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("vms.create"))
):
"""Клонировать LXC контейнер"""
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()
node = data.get('node')
source_vmid = data.get('source_vmid')
new_vmid = data.get('new_vmid')
hostname = data.get('hostname')
full_clone = data.get('full_clone', True)
target_storage = data.get('target_storage')
description = data.get('description')
disk_size = data.get('disk_size') # Disk size in GB for resize after clone
if not all([node, source_vmid, hostname]):
raise HTTPException(status_code=400, detail="Missing required parameters")
try:
# Determine auth method
client = _get_proxmox_client(server)
if not client.is_connected():
raise HTTPException(status_code=503, detail="Cannot connect to Proxmox server")
if not new_vmid:
new_vmid = client.get_next_vmid()
if not new_vmid:
raise HTTPException(status_code=500, detail="Failed to get next VMID")
upid = client.clone_lxc_container(
node=node,
source_vmid=source_vmid,
new_vmid=new_vmid,
hostname=hostname,
full_clone=full_clone,
target_storage=target_storage,
description=description
)
if not upid:
raise HTTPException(status_code=500, detail="Failed to clone LXC container")
# Wait for clone to complete and resize disk if requested
if disk_size:
clone_success = client.wait_for_task(node, upid, timeout=300)
if clone_success:
# Resize rootfs to requested size
try:
client.resize_container_disk(node, new_vmid, 'rootfs', f'{disk_size}G')
logger.info(f"Resized LXC {new_vmid} rootfs to {disk_size}G")
except Exception as e:
logger.warning(f"Failed to resize LXC {new_vmid} disk: {e}")
# Get source template name for display
source_template_name = None
template_id = data.get('template_id')
if template_id:
template = db.query(OSTemplate).filter(OSTemplate.id == template_id).first()
if template:
source_template_name = template.name
# Save LXC instance to database with owner
save_vm_instance(
db=db,
server_id=server_id,
vmid=new_vmid,
node=node,
vm_type='lxc',
name=hostname,
disk_size=disk_size,
template_id=template_id,
template_name=source_template_name,
description=description or f"Cloned from container {source_vmid}",
owner_id=current_user.id # VPS-style user isolation
)
LoggingService.log_proxmox_action(
db=db,
action="clone",
resource_type="container",
resource_id=new_vmid,
username=current_user.username,
resource_name=hostname,
server_id=server_id,
server_name=server.name,
node_name=node,
details={"source_vmid": source_vmid, "full_clone": full_clone},
success=True,
ip_address=request.client.host if request.client else None
)
return JSONResponse(content={
"success": True,
"vmid": new_vmid,
"upid": upid,
"message": f"LXC container clone started"
})
except HTTPException:
raise
except Exception as e:
logger.error(f"Error cloning LXC container: {e}")
raise HTTPException(status_code=500, detail=str(e))
# ==================== Terminal Proxy (xterm.js) ====================
@router.get("/api/{server_id}/vm/{vmid}/terminal")

View File

@@ -273,14 +273,6 @@ async def delete_role(
return {"message": "Role deleted"}
@router.get("/api/permissions")
async def get_all_permissions(
current_user: User = Depends(PermissionChecker("roles.view"))
):
"""Get all available permissions (legacy format for backwards compatibility)"""
return SecurityService.get_all_permissions()
@router.get("/api/permissions/v2")
async def get_all_permissions_v2(
current_user: User = Depends(PermissionChecker("roles.view"))

View File

@@ -1376,36 +1376,10 @@ class ProxmoxClient(VmMixin, LxcMixin, ClusterMixin, StorageMixin, NetworkMixin,
}
pid = exec_result['pid']
# Ждем завершения команды
start_time = time.time()
while time.time() - start_time < timeout:
try:
status = self.proxmox.nodes(node).qemu(vmid).agent('exec-status').get(pid=pid)
if status.get('exited'):
# Команда завершена
return {
'success': True,
'stdout': status.get('out-data', ''),
'stderr': status.get('err-data', ''),
'exit_code': status.get('exitcode', 0)
}
except Exception as e:
logger.debug(f"Error checking command status: {e}")
# Ждем немного перед следующей проверкой
time.sleep(0.5)
# Таймаут
return {
'success': False,
'error': f'Command execution timeout ({timeout}s)',
'stdout': '',
'stderr': '',
'exit_code': -1
}
# Ждем завершения команды и возвращаем результат
return self._wait_exec_status(node, vmid, pid, timeout)
except Exception as e:
logger.error(f"Failed to execute command on VM {vmid}: {e}")
return {
@@ -1416,6 +1390,41 @@ class ProxmoxClient(VmMixin, LxcMixin, ClusterMixin, StorageMixin, NetworkMixin,
'exit_code': -1
}
def _wait_exec_status(self, node: str, vmid: int, pid: int, timeout: int) -> Dict:
"""
Опросить статус выполнения команды guest agent по pid до завершения.
Returns:
Dict с ключами: success, stdout, stderr, exit_code
"""
start_time = time.time()
while time.time() - start_time < timeout:
try:
status = self.proxmox.nodes(node).qemu(vmid).agent('exec-status').get(pid=pid)
if status.get('exited'):
# Команда завершена
return {
'success': True,
'stdout': status.get('out-data', ''),
'stderr': status.get('err-data', ''),
'exit_code': status.get('exitcode', 0)
}
except Exception as e:
logger.debug(f"Error checking command status: {e}")
# Ждем немного перед следующей проверкой
time.sleep(0.5)
# Таймаут
return {
'success': False,
'error': f'Command execution timeout ({timeout}s)',
'stdout': '',
'stderr': '',
'exit_code': -1
}
def execute_script(self, node: str, vmid: int, script_content: str,
interpreter: str = "/bin/bash", timeout: int = 60) -> Dict:
"""
@@ -1441,20 +1450,29 @@ class ProxmoxClient(VmMixin, LxcMixin, ClusterMixin, StorageMixin, NetworkMixin,
}
try:
# Вместо создания файла, передаем скрипт напрямую через stdin
# Это более надежно и не требует записи файлов
# Кодируем скрипт в base64 для безопасной передачи
script_b64 = base64.b64encode(script_content.encode('utf-8')).decode('utf-8')
# Выполняем скрипт через sh -c с heredoc
# Это позволяет избежать проблем с кавычками и специальными символами
exec_cmd = f"{interpreter} -c \"$(echo '{script_b64}' | base64 -d)\""
exec_result = self.execute_command(node, vmid, exec_cmd, timeout=timeout)
return exec_result
# Передаем скрипт напрямую интерпретатору через stdin (input-data).
# Интерпретатор запускается без -c, читая тело скрипта со stdin,
# поэтому многострочные скрипты, кавычки и shebang (#!...) в первой
# строке обрабатываются корректно без дополнительного слоя кавычек.
#
# Proxmox API сам кодирует input-data в base64 перед передачей
# guest agent'у, поэтому передаем сырое содержимое скрипта.
exec_result = self.proxmox.nodes(node).qemu(vmid).agent.exec.post(
command=[interpreter],
**{'input-data': script_content}
)
if 'pid' not in exec_result:
return {
'success': False,
'error': 'Failed to start script execution',
'stdout': '',
'stderr': '',
'exit_code': -1
}
return self._wait_exec_status(node, vmid, exec_result['pid'], timeout)
except Exception as e:
logger.error(f"Failed to execute script on VM {vmid}: {e}")
return {