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("-") def _unique_name(db: Session, desired: str, source_key: str) -> str: """Возвращает свободное имя, не ломая существующий каталог.""" existing = db.scalar(select(models.Template).where(models.Template.name == desired)) if existing is None or source_key in {str(existing.source_vmid), existing.source_template}: return desired for index in range(1, 100): candidate = f"{desired}-{index}"[:64] if db.scalar(select(models.Template).where(models.Template.name == candidate)) is None: return candidate raise RuntimeError("Не удалось подобрать уникальное имя шаблона") def _import_discovered(db: Session, discovered: dict) -> list[models.Template]: """Обновляет каталог панели данными Proxmox без конфликтов имён.""" imported: list[models.Template] = [] for item in discovered.get("vm_templates", []): source_vmid = item["vmid"] template = db.scalar(select(models.Template).where( models.Template.source_vmid == source_vmid, models.Template.guest_type == models.GuestType.vm, )) if template is None: name = _unique_name(db, _safe_name(item["name"], str(source_vmid)), str(source_vmid)) template = models.Template(name=name, guest_type=models.GuestType.vm, source_vmid=source_vmid, description=f"Импортировано из Proxmox: {item['name']}") db.add(template) template.cores = item.get("cores", 1) template.memory_mb = item.get("memory_mb", 1024) template.disk_gb = template.disk_gb or 10 template.is_active = True imported.append(template) for item in discovered.get("lxc_templates", []): source = item.get("volid", "") template = db.scalar(select(models.Template).where( models.Template.guest_type == models.GuestType.lxc, models.Template.source_template == source, )) if template is None: base_name = PurePosixPath(item.get("name", "lxc")).name base_name = re.sub(r"\.tar\.(zst|gz|xz)$", "", base_name) suffix = f"ct{item['vmid']}" if item.get("source_kind") == "vmid" else "" desired = _safe_name(base_name, suffix) template = models.Template(name=_unique_name(db, desired, source), guest_type=models.GuestType.lxc, source_template=source, description=f"Импортировано из Proxmox: {item.get('name', source)}") db.add(template) template.cores = item.get("cores", template.cores or 1) template.memory_mb = item.get("memory_mb", template.memory_mb or 1024) template.disk_gb = template.disk_gb or 10 template.is_active = True imported.append(template) return imported @router.get("", response_model=list[schemas.TemplateOut]) def list_templates(db: Session = Depends(get_db), _: models.User = Depends(get_current_user)): """Автоматически обновляет каталог из Proxmox и возвращает активные шаблоны.""" try: _import_discovered(db, discover_templates()) db.commit() except Exception as error: db.rollback() logger.warning("Не удалось автоматически обновить шаблоны из Proxmox: %s", error) 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)): """Принудительно обновляет каталог; повторный запуск безопасен.""" try: imported = _import_discovered(db, discover_templates()) db.commit() except IntegrityError: db.rollback() raise HTTPException(status_code=409, detail="Не удалось сохранить шаблоны") except Exception: db.rollback() logger.exception("Не удалось синхронизировать шаблоны Proxmox") raise HTTPException(status_code=502, detail="Proxmox временно недоступен") 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}