Files
pvemanager/backend/app/api/proxmox/lxc.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

483 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
API endpoints for creating LXC containers from Proxmox CT templates (vztmpl files).
"""
from __future__ import annotations
import asyncio
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.responses import JSONResponse
from loguru import logger
from pydantic import BaseModel
from sqlalchemy.orm import Session
from ...auth import PermissionChecker
from ...db import get_db, SessionLocal
from ...ipam_service import IPAMService
from ...logging_service import LoggingService
from ...models import DeployTask, IPAMNetwork, ProxmoxServer, User, VMInstance
from ...proxmox import ProxmoxClient
from ...websocket_manager import broadcast_task_update
from ._helpers import _get_proxmox_client, get_next_vmid, save_vm_instance
router = APIRouter()
_lxc_deploy_executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="lxc_deploy")
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
# ── Pydantic schemas ─────────────────────────────────────────────────────────
class LXCDeployRequest(BaseModel):
server_id: int
ostemplate: str # volid, e.g. "local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst"
node: Optional[str] = None
name: str # hostname / display name
vmid: Optional[int] = None
cores: int = 1
memory: int = 512 # МБ
swap: int = 512 # МБ
disk: int = 8 # ГБ
storage: str = "local-lvm"
bridge: str = "vmbr0"
ip_address: Optional[str] = None
gateway: Optional[str] = None
ipam_network_id: Optional[int] = None
ipam_pool_id: Optional[int] = None
password: Optional[str] = None
ssh_keys: Optional[str] = None
ssh_key_ids: Optional[List[int]] = None
owner_id: Optional[int] = None
nameserver: Optional[str] = None
searchdomain: Optional[str] = None
unprivileged: bool = True
start_after_create: bool = True
onboot: bool = False
class LXCDeployResponse(BaseModel):
task_id: int
status: str
name: str
class LXCTemplateItem(BaseModel):
volid: str
storage: str
node: str
name: str
size: int
# ── Helpers ──────────────────────────────────────────────────────────────────
def _update_task(task_id: int, status: str, step: str, progress: int,
vmid: int = None, node: str = None, error: str = None):
db = SessionLocal()
try:
task = db.query(DeployTask).filter(DeployTask.id == task_id).first()
if not task:
return
task.status = status
task.step = step
task.progress = progress
if vmid is not None:
task.vmid = vmid
if node is not None:
task.node = node
if error is not None:
task.error_message = error
if status == "running" and not task.started_at:
task.started_at = _utcnow()
if status in ("completed", "failed"):
task.completed_at = _utcnow()
db.commit()
try:
broadcast_task_update(task.user_id, "deploy_task_update", task.to_dict())
except Exception:
pass
finally:
db.close()
def _do_lxc_deploy(task_id: int, req: LXCDeployRequest, user_id: int, username: str,
owner_id: Optional[int] = None):
"""Blocking LXC deploy — runs in thread pool."""
db = SessionLocal()
server = None
deploy_node = None
new_vmid = None
ipam_allocation = None
try:
_update_task(task_id, "running", "Подготовка...", 5)
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == req.server_id).first()
if not server:
_update_task(task_id, "failed", "Сервер не найден", 0, error="Сервер Proxmox не найден")
return
# Connect
_update_task(task_id, "running", "Подключение к Proxmox...", 10)
client = _get_proxmox_client(server)
if not client.is_connected():
_update_task(task_id, "failed", "Ошибка подключения к Proxmox", 10,
error="Не удалось подключиться к Proxmox серверу")
return
# Determine node
deploy_node = req.node
if not deploy_node:
nodes = client.get_nodes()
if not nodes:
_update_task(task_id, "failed", "Нет доступных нод", 15, error="Не найдено нод Proxmox")
return
# Try to find a node that has the template storage
for n in nodes:
try:
storages = client.proxmox.nodes(n["node"]).storage.get()
for s in storages:
if "vztmpl" in s.get("content", ""):
deploy_node = n["node"]
break
except Exception:
pass
if deploy_node:
break
if not deploy_node:
deploy_node = nodes[0]["node"]
# Allocate VMID
_update_task(task_id, "running", "Генерация VMID...", 20)
if req.vmid:
new_vmid = req.vmid
else:
new_vmid = get_next_vmid(db, server.id)
# IPAM
_update_task(task_id, "running", "Выделение IP-адреса...", 25)
allocated_ip = req.ip_address
gateway = req.gateway
network_mask = "24"
ipam_allocation = None
if req.ipam_network_id and not req.ip_address:
ipam = IPAMService(db)
ipam_allocation, error = ipam.auto_allocate_ip(
network_id=req.ipam_network_id,
pool_id=req.ipam_pool_id,
resource_type="ct",
resource_name=req.name,
proxmox_server_id=server.id,
proxmox_vmid=new_vmid,
proxmox_node=deploy_node,
allocated_by=username,
notes=f"Auto-allocated for LXC {req.name}",
)
if not error:
allocated_ip = ipam_allocation.ip_address
if not gateway:
net = db.query(IPAMNetwork).filter(IPAMNetwork.id == req.ipam_network_id).first()
if net and net.gateway:
gateway = net.gateway
elif req.ip_address and req.ipam_network_id:
ipam = IPAMService(db)
ipam_allocation, _ = ipam.allocate_ip(
ip_address=req.ip_address,
network_id=req.ipam_network_id,
pool_id=req.ipam_pool_id,
resource_type="ct",
resource_name=req.name,
proxmox_server_id=server.id,
proxmox_vmid=new_vmid,
proxmox_node=deploy_node,
allocated_by=username,
notes=f"Manually assigned for LXC {req.name}",
)
if allocated_ip and req.ipam_network_id:
try:
network_id = req.ipam_network_id
if ipam_allocation and hasattr(ipam_allocation, "network_id"):
network_id = ipam_allocation.network_id
if network_id:
net = db.query(IPAMNetwork).filter(IPAMNetwork.id == network_id).first()
if net and net.network and "/" in net.network:
network_mask = net.network.split("/")[1]
if not gateway and net and net.gateway:
gateway = net.gateway
except Exception as e:
logger.warning(f"Could not get network info from IPAM: {e}")
# Build ip_config string for LXC net0
if allocated_ip:
ip_config = f"ip={allocated_ip}/{network_mask},gw={gateway}" if gateway else f"ip={allocated_ip}/{network_mask}"
else:
ip_config = "ip=dhcp"
# Create LXC
_update_task(task_id, "running", f"Создание LXC контейнера VMID {new_vmid}...", 35,
vmid=new_vmid, node=deploy_node)
net0_str = f"name=eth0,bridge={req.bridge},{ip_config},firewall=1"
upid = client.create_lxc_container(
node=deploy_node,
vmid=new_vmid,
ostemplate=req.ostemplate,
hostname=req.name,
cores=req.cores,
memory=req.memory,
swap=req.swap,
storage=req.storage,
rootfs_size=req.disk,
net0=net0_str,
nameserver=req.nameserver,
searchdomain=req.searchdomain,
password=req.password,
ssh_public_keys=req.ssh_keys,
unprivileged=req.unprivileged,
start_after_create=False, # we start manually after config
onboot=req.onboot,
description=f"Created from {req.ostemplate} by {username}",
)
if not upid:
_update_task(task_id, "failed", "Ошибка создания контейнера", 35,
error="Proxmox не вернул UPID задачи создания контейнера")
return
# Wait for creation task
_update_task(task_id, "running", "Ожидание завершения создания...", 50,
vmid=new_vmid, node=deploy_node)
success = client.wait_for_task(deploy_node, upid, timeout=300)
if not success:
_update_task(task_id, "failed", "Таймаут создания контейнера", 50,
error="Создание контейнера заняло слишком долго (>5 мин)")
return
_update_task(task_id, "running", "Контейнер создан", 70, vmid=new_vmid, node=deploy_node)
# Start if requested
if req.start_after_create:
_update_task(task_id, "running", "Запуск контейнера...", 85,
vmid=new_vmid, node=deploy_node)
client.start_container(deploy_node, new_vmid)
# Save to DB
_update_task(task_id, "running", "Сохранение в базе данных...", 95,
vmid=new_vmid, node=deploy_node)
save_vm_instance(
db=db,
server_id=server.id,
vmid=new_vmid,
node=deploy_node,
vm_type="lxc",
name=req.name,
cores=req.cores,
memory=req.memory,
disk_size=req.disk,
ip_address=allocated_ip,
ip_prefix=int(network_mask),
gateway=gateway,
cloud_init_user="root",
cloud_init_password=req.password,
ssh_keys=req.ssh_keys,
template_id=None,
template_name=req.ostemplate,
description=f"Created from CT template {req.ostemplate}",
owner_id=owner_id or user_id,
)
LoggingService.log_proxmox_action(
db=db,
action="create",
resource_type="ct",
resource_id=new_vmid,
username=username,
resource_name=req.name,
server_id=server.id,
server_name=server.name,
node_name=deploy_node,
details={
"ostemplate": req.ostemplate,
"cores": req.cores,
"memory": req.memory,
"disk": req.disk,
"ip_address": allocated_ip,
},
success=True,
)
_update_task(task_id, "completed", "Контейнер успешно создан!", 100,
vmid=new_vmid, node=deploy_node)
logger.info(f"[LXC DEPLOY] Task #{task_id}: {req.name} (VMID {new_vmid}) deployed by {username}")
except Exception as e:
err = str(e)
logger.error(f"[LXC DEPLOY] Task #{task_id} failed: {err}")
# Release IPAM allocation on failure
try:
if ipam_allocation is not None:
ipam = IPAMService(db)
ipam.release_ip(ipam_allocation.id)
logger.info(f"[LXC DEPLOY] Task #{task_id}: Released IPAM IP {ipam_allocation.ip_address} on failure")
except Exception as ipam_err:
logger.warning(f"[LXC DEPLOY] Task #{task_id}: Could not release IPAM IP: {ipam_err}")
try:
if server:
LoggingService.log_proxmox_action(
db=db,
action="create",
resource_type="ct",
resource_id=req.vmid,
username=username,
resource_name=req.name,
server_id=server.id,
server_name=server.name,
node_name=deploy_node,
details={"ostemplate": req.ostemplate},
success=False,
error_message=err,
)
except Exception:
pass
_update_task(task_id, "failed", f"Ошибка: {err[:150]}", 0, error=err)
finally:
db.close()
# ── Endpoints ────────────────────────────────────────────────────────────────
@router.get("/api/lxc-templates/{server_id}")
def list_lxc_templates(
server_id: int,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("vm:view")),
):
"""
Список LXC CT-шаблонов (vztmpl) на хранилищах выбранного Proxmox сервера.
"""
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Proxmox сервер не найден")
client = _get_proxmox_client(server)
if not client.is_connected():
raise HTTPException(status_code=503, detail="Не удалось подключиться к Proxmox серверу")
node_filter = None
if server.hostname and server.hostname != server.ip_address:
node_filter = server.hostname
templates = client.get_lxc_storage_templates(node=node_filter)
return JSONResponse(content={"server_id": server_id, "templates": templates})
@router.get("/api/lxc-storages/{server_id}")
def list_lxc_storages(
server_id: int,
node: Optional[str] = None,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("vm:view")),
):
"""
Список хранилищ ноды, пригодных для rootfs LXC контейнера.
"""
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Proxmox сервер не найден")
client = _get_proxmox_client(server)
if not client.is_connected():
raise HTTPException(status_code=503, detail="Не удалось подключиться к Proxmox серверу")
target_node = node or server.hostname or None
if not target_node:
nodes = client.get_nodes()
target_node = nodes[0]["node"] if nodes else None
if not target_node:
raise HTTPException(status_code=400, detail="Не удалось определить ноду")
storages = client.get_node_storages(target_node)
result = []
for s in storages:
content = s.get("content", "")
if "rootdir" in content or "images" in content:
result.append({
"storage": s.get("storage"),
"type": s.get("type"),
"avail": s.get("avail"),
"total": s.get("total"),
"used": s.get("used"),
"active": s.get("active", 0),
})
return JSONResponse(content={"storages": result})
@router.post("/api/lxc/deploy", response_model=LXCDeployResponse, status_code=202)
async def deploy_lxc_from_template(
req: LXCDeployRequest,
db: Session = Depends(get_db),
current_user: User = Depends(PermissionChecker("vm:create")),
):
"""
Асинхронное создание LXC контейнера из CT-шаблона.
Возвращает task_id для отслеживания статуса.
"""
server = db.query(ProxmoxServer).filter(ProxmoxServer.id == req.server_id).first()
if not server:
raise HTTPException(status_code=404, detail="Proxmox сервер не найден")
if not req.ostemplate:
raise HTTPException(status_code=400, detail="Необходимо указать ostemplate")
if not req.name or len(req.name) < 1:
raise HTTPException(status_code=400, detail="Укажите имя контейнера")
# Resolve owner / SSH keys
is_admin = current_user.has_permission("user:manage") or current_user.is_admin
instance_owner_id = current_user.id
if req.owner_id and req.owner_id != current_user.id:
if not is_admin:
raise HTTPException(status_code=403, detail="Only admins can assign instance to another user")
owner = db.query(User).filter(User.id == req.owner_id).first()
if not owner:
raise HTTPException(status_code=404, detail="Owner user not found")
instance_owner_id = owner.id
if req.ssh_key_ids:
from ..ssh_keys import resolve_ssh_keys_for_deploy
resolved = resolve_ssh_keys_for_deploy(db, current_user, req.ssh_key_ids, instance_owner_id)
existing = (req.ssh_keys or "").strip()
req.ssh_keys = (existing + "\n" + resolved).strip() if existing else resolved
# Create task record
task = DeployTask(
status="pending",
step="В очереди...",
progress=0,
name=req.name,
user_id=current_user.id,
created_at=_utcnow(),
)
db.add(task)
db.commit()
db.refresh(task)
task_id = task.id
loop = asyncio.get_event_loop()
loop.run_in_executor(
_lxc_deploy_executor,
_do_lxc_deploy,
task_id,
req,
current_user.id,
current_user.username,
instance_owner_id,
)
return LXCDeployResponse(task_id=task_id, status="pending", name=req.name)