diff --git a/backend/app/routers/templates.py b/backend/app/routers/templates.py index 3136fbd..8d48535 100644 --- a/backend/app/routers/templates.py +++ b/backend/app/routers/templates.py @@ -22,9 +22,67 @@ def _safe_name(value: str, suffix: str = "") -> str: 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()))) @@ -40,48 +98,17 @@ def list_proxmox_templates(_: models.User = Depends(require_admin)): @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: + imported = _import_discovered(db, discover_templates()) db.commit() except IntegrityError: db.rollback() - raise HTTPException(status_code=409, detail="Имена импортируемых шаблонов пересекаются с каталогом панели") + 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