import logging import re from pathlib import PurePosixPath from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from .. import models, schemas from ..database import get_db from ..deps import get_current_user, require_admin from ..proxmox_templates import list_templates as discover_templates 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()))) @router.get("/from-proxmox") def list_proxmox_templates(_: models.User = Depends(require_admin)): """Возвращает найденные на Proxmox VM- и CT-шаблоны.""" try: return discover_templates() except Exception: logger.exception("Не удалось получить список шаблонов 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) 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: db.commit() except IntegrityError: db.rollback() raise HTTPException(status_code=409, detail="Шаблон с таким именем уже существует") db.refresh(template) return template @router.delete("/{template_id}") 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: raise HTTPException(status_code=404, detail="Шаблон не найден") template.is_active = False db.commit() return {"ok": True}