From 3e110c4e30fcbfb88525c97627e92b5d8df8bcdf Mon Sep 17 00:00:00 2001 From: host Date: Sun, 9 Aug 2026 22:48:08 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/routers/instances.py | 70 ++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/backend/app/routers/instances.py b/backend/app/routers/instances.py index 49af52b..e6ba333 100644 --- a/backend/app/routers/instances.py +++ b/backend/app/routers/instances.py @@ -102,7 +102,18 @@ def create_instance( db: Session = Depends(get_db), user: models.User = Depends(get_current_user), ): - """Регистрирует один инстанс и запускает его создание в фоне.""" + """Регистрирует один инстанс и запускает его создание в фоне. + + Бизнес-логика VMID: + 1. Запрашиваем ``nextid`` у Proxmox (он возвращает следующий свободный VMID + в кластере — то есть тот, что реально не занят ни в Proxmox). + 2. Проверяем в БД, что этот VMID не используется активным инстансом + (``status != deleted``). Если есть запись со статусом deleted — это + «мусор» от старых удалений, который мы НЕ должны учитывать (nextid + от Proxmox пришёл, значит в Proxmox этот VMID свободен). + 3. Если конфликт — запрашиваем новый VMID через ``nextid`` + (онлайн-цикл до 5 попыток). + """ db.query(models.User).filter(models.User.id == user.id).with_for_update().one() pending = ( db.query(models.Instance) @@ -123,19 +134,36 @@ def create_instance( if not template: raise HTTPException(status_code=404, detail="Шаблон не найден") - try: - vmid = pve.get_next_vmid() - except Exception as exc: - logger.error("Proxmox недоступен при запросе nextid: %s", exc) - raise HTTPException(status_code=502, detail=f"Proxmox недоступен: {exc}") - - active = ( - db.query(models.Instance) - .filter(models.Instance.vmid == vmid, models.Instance.status != models.InstanceStatus.deleted) - .first() - ) - if active: - raise HTTPException(status_code=409, detail=f"VMID {vmid} уже используется в панели") + # Получаем VMID, который точно свободен: nextid из Proxmox + проверка БД. + # Если в БД висит «удалённая» запись с этим VMID (мусор), пропускаем — + # запрашиваем следующий. Proxmox при повторном nextid может вернуть тот же + # VMID, потому что он сам не знает про нашу БД. Поэтому цикл по Proxmox + # ограничен сверху, а грязные записи в БД лучше чистить заранее + # (разовая операция, см. README). + vmid = None + for _ in range(5): + try: + candidate = pve.get_next_vmid() + except Exception as exc: + logger.error("Proxmox недоступен при запросе nextid: %s", exc) + raise HTTPException(status_code=502, detail=f"Proxmox недоступен: {exc}") + active = ( + db.query(models.Instance) + .filter( + models.Instance.vmid == candidate, + models.Instance.status != models.InstanceStatus.deleted, + ) + .first() + ) + if not active: + vmid = candidate + break + if vmid is None: + raise HTTPException( + status_code=409, + detail="Не удалось получить свободный VMID — несколько попыток подряд вернули занятый VMID. " + "Очистите БД от записей со статусом 'deleted' или повторите позже.", + ) instance = models.Instance( name=_sanitize_name(payload.name), @@ -231,15 +259,19 @@ def instance_action(instance_id: int, payload: schemas.InstanceAction, db: Sessi @router.delete("/{instance_id}") def delete_instance(instance_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)): - """Останавливает работающую VM/LXC и удаляет её из Proxmox.""" + """Останавливает работающую VM/LXC и удаляет её из Proxmox. + + После успешного удаления в Proxmox — физически удаляет запись из БД, + чтобы VMID можно было использовать повторно (``nextid`` от Proxmox всегда + вернёт свободный ID, а наша БД не должна блокировать его из-за мусора). + """ instance = db.query(models.Instance).filter(models.Instance.id == instance_id).first() if not instance: raise HTTPException(status_code=404, detail="VPS не найден") _ensure_owner(instance, user) if instance.status == models.InstanceStatus.deleting: raise HTTPException(status_code=409, detail="Инстанс уже удаляется") - instance.status = models.InstanceStatus.deleting - db.commit() + vmid_for_log = instance.vmid # сохраняем для логов до удаления ORM-объекта try: pve.delete_guest(instance.guest_type.value, instance.vmid, instance.node) except Exception as exc: @@ -247,6 +279,8 @@ def delete_instance(instance_id: int, db: Session = Depends(get_db), user: model instance.status = models.InstanceStatus.error db.commit() raise HTTPException(status_code=502, detail=f"Ошибка удаления: {exc}") - instance.status = models.InstanceStatus.deleted + # Физическое удаление записи — VMID освобождается для повторного использования. + db.delete(instance) db.commit() + logger.info("Инстанс vmid=%s удалён из БД (был id=%s)", vmid_for_log, instance_id) return {"ok": True}