From 400ad3da2adcd23e0f7dd6a5c5a967d4e36b1bba Mon Sep 17 00:00:00 2001 From: host Date: Sun, 9 Aug 2026 16:39:24 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=20Proxmox=20=D1=81=20=D0=BA=D0=B0=D1=82?= =?UTF-8?q?=D0=B0=D0=BB=D0=BE=D0=B3=D0=BE=D0=BC=20=D0=BF=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/templates.py | 88 +++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 24 deletions(-) diff --git a/backend/app/routers/templates.py b/backend/app/routers/templates.py index a7f4904..3136fbd 100644 --- a/backend/app/routers/templates.py +++ b/backend/app/routers/templates.py @@ -1,4 +1,6 @@ import logging +import re +from pathlib import PurePosixPath from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select @@ -14,24 +16,21 @@ logger = logging.getLogger(__name__) 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]) -def list_templates( - 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()) - ) - ) +def list_templates(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()))) @router.get("/from-proxmox") def list_proxmox_templates(_: models.User = Depends(require_admin)): - """Находит VM templates и CT templates на настроенной ноде Proxmox.""" + """Возвращает найденные на Proxmox VM- и CT-шаблоны.""" try: return discover_templates() except Exception: @@ -39,13 +38,58 @@ def list_proxmox_templates(_: models.User = Depends(require_admin)): 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) -def create_template( - payload: schemas.TemplateCreate, - db: Session = Depends(get_db), - _: models.User = Depends(require_admin), -): - """Создаёт шаблон только от имени администратора.""" +def create_template(payload: schemas.TemplateCreate, db: Session = Depends(get_db), _: models.User = Depends(require_admin)): + """Создаёт шаблон вручную.""" template = models.Template(**payload.model_dump()) db.add(template) try: @@ -58,11 +102,7 @@ def create_template( @router.delete("/{template_id}") -def deactivate_template( - template_id: int, - db: Session = Depends(get_db), - _: models.User = Depends(require_admin), -): +def deactivate_template(template_id: int, db: Session = Depends(get_db), _: models.User = Depends(require_admin)): """Деактивирует шаблон без удаления связанных инстансов.""" template = db.get(models.Template, template_id) if template is None: