This repository has been archived on 2026-08-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Proxmox-VPS-Panel-secure/backend/app/routers/templates.py
T

140 lines
6.7 KiB
Python

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}