Добавлена синхронизация шаблонов Proxmox с каталогом панели

This commit is contained in:
2026-08-09 16:39:24 +03:00
parent 43f5a37920
commit 400ad3da2a
+64 -24
View File
@@ -1,4 +1,6 @@
import logging import logging
import re
from pathlib import PurePosixPath
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import select from sqlalchemy import select
@@ -14,24 +16,21 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/templates", tags=["templates"]) router = APIRouter(prefix="/templates", tags=["templates"])
def _safe_name(value: str, suffix: str = "") -> str:
"""Преобразует имя Proxmox в допустимое имя шаблона панели."""
name = re.sub(r"[^a-zA-Z0-9-]+", "-", value.lower()).strip("-") or "template"
return f"{name}-{suffix}"[:64].strip("-")
@router.get("", response_model=list[schemas.TemplateOut]) @router.get("", response_model=list[schemas.TemplateOut])
def list_templates( def list_templates(db: Session = Depends(get_db), _: models.User = Depends(get_current_user)):
db: Session = Depends(get_db), """Возвращает активные шаблоны панели."""
_: models.User = Depends(get_current_user), return list(db.scalars(select(models.Template).where(models.Template.is_active.is_(True)).order_by(models.Template.created_at.desc())))
):
"""Возвращает только активные шаблоны панели."""
return list(
db.scalars(
select(models.Template)
.where(models.Template.is_active.is_(True))
.order_by(models.Template.created_at.desc())
)
)
@router.get("/from-proxmox") @router.get("/from-proxmox")
def list_proxmox_templates(_: models.User = Depends(require_admin)): def list_proxmox_templates(_: models.User = Depends(require_admin)):
"""Находит VM templates и CT templates на настроенной ноде Proxmox.""" """Возвращает найденные на Proxmox VM- и CT-шаблоны."""
try: try:
return discover_templates() return discover_templates()
except Exception: except Exception:
@@ -39,13 +38,58 @@ def list_proxmox_templates(_: models.User = Depends(require_admin)):
raise HTTPException(status_code=502, detail="Proxmox временно недоступен") raise HTTPException(status_code=502, detail="Proxmox временно недоступен")
@router.post("/sync", response_model=list[schemas.TemplateOut])
def sync_proxmox_templates(db: Session = Depends(get_db), _: models.User = Depends(require_admin)):
"""Импортирует найденные Proxmox-шаблоны в каталог панели.
Повторный запуск обновляет существующие записи по source_vmid/source_template
и не создаёт дубликаты.
"""
try:
discovered = discover_templates()
except Exception:
logger.exception("Не удалось синхронизировать шаблоны Proxmox")
raise HTTPException(status_code=502, detail="Proxmox временно недоступен")
imported: list[models.Template] = []
for item in discovered["vm_templates"]:
template = db.scalar(select(models.Template).where(models.Template.source_vmid == item["vmid"], models.Template.guest_type == models.GuestType.vm))
if template is None:
template = models.Template(name=_safe_name(item["name"], str(item["vmid"])), guest_type=models.GuestType.vm, source_vmid=item["vmid"], description=f"Импортировано из Proxmox: {item['name']}")
db.add(template)
template.cores = item["cores"]
template.memory_mb = item["memory_mb"]
template.disk_gb = template.disk_gb or 10
template.is_active = True
imported.append(template)
for item in discovered["lxc_templates"]:
source = item["volid"]
template = db.scalar(select(models.Template).where(models.Template.source_template == source))
if template is None:
base_name = PurePosixPath(item["name"]).name
base_name = re.sub(r"\.tar\.(zst|gz|xz)$", "", base_name)
template = models.Template(name=_safe_name(base_name), guest_type=models.GuestType.lxc, source_template=source, description=f"Импортировано из Proxmox: {item['name']}")
db.add(template)
template.cores = template.cores or 1
template.memory_mb = template.memory_mb or 1024
template.disk_gb = template.disk_gb or 10
template.is_active = True
imported.append(template)
try:
db.commit()
except IntegrityError:
db.rollback()
raise HTTPException(status_code=409, detail="Имена импортируемых шаблонов пересекаются с каталогом панели")
for template in imported:
db.refresh(template)
return imported
@router.post("", response_model=schemas.TemplateOut, status_code=status.HTTP_201_CREATED) @router.post("", response_model=schemas.TemplateOut, status_code=status.HTTP_201_CREATED)
def create_template( def create_template(payload: schemas.TemplateCreate, db: Session = Depends(get_db), _: models.User = Depends(require_admin)):
payload: schemas.TemplateCreate, """Создаёт шаблон вручную."""
db: Session = Depends(get_db),
_: models.User = Depends(require_admin),
):
"""Создаёт шаблон только от имени администратора."""
template = models.Template(**payload.model_dump()) template = models.Template(**payload.model_dump())
db.add(template) db.add(template)
try: try:
@@ -58,11 +102,7 @@ def create_template(
@router.delete("/{template_id}") @router.delete("/{template_id}")
def deactivate_template( def deactivate_template(template_id: int, db: Session = Depends(get_db), _: models.User = Depends(require_admin)):
template_id: int,
db: Session = Depends(get_db),
_: models.User = Depends(require_admin),
):
"""Деактивирует шаблон без удаления связанных инстансов.""" """Деактивирует шаблон без удаления связанных инстансов."""
template = db.get(models.Template, template_id) template = db.get(models.Template, template_id)
if template is None: if template is None: