refactor(api): remove duplicate/superseded endpoints
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:
@@ -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),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user