From cbfe4139e37d5c14741e70a6e8f3c9dbf343cf6f Mon Sep 17 00:00:00 2001 From: host Date: Sun, 9 Aug 2026 22:56:21 +0300 Subject: [PATCH] =?UTF-8?q?fix(delete):=20=D1=81=D1=87=D0=B8=D1=82=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20=D1=83=D0=B6=D0=B5=20=D0=BE=D1=82=D1=81=D1=83?= =?UTF-8?q?=D1=82=D1=81=D1=82=D0=B2=D1=83=D1=8E=D1=89=D0=B8=D0=B9=20=D0=B2?= =?UTF-8?q?=20Proxmox=20=D1=80=D0=B5=D1=81=D1=83=D1=80=D1=81=20=D1=83?= =?UTF-8?q?=D1=81=D0=BF=D0=B5=D1=88=D0=BD=D0=BE=20=D1=83=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D1=91=D0=BD=D0=BD=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/proxmox_client.py | 72 +++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/backend/app/proxmox_client.py b/backend/app/proxmox_client.py index 5453511..dfb7f88 100644 --- a/backend/app/proxmox_client.py +++ b/backend/app/proxmox_client.py @@ -7,6 +7,7 @@ import string import time from proxmoxer import ProxmoxAPI +from proxmoxer.core import ResourceException from .config import settings @@ -26,7 +27,7 @@ def _client() -> ProxmoxAPI: def gen_password(length: int = 14) -> str: - """Генерирует пароль из букв и цифр (для VM-cloud-init, если пользователь не задал).""" + """Генерирует пароль из букв и цифр.""" alphabet = string.ascii_letters + string.digits return "".join(random.choice(alphabet) for _ in range(length)) @@ -64,25 +65,9 @@ def create_lxc( storage: str = "local-lvm", node: str = None, ) -> str: - """Создаёт LXC из архивного шаблона и запускает его. - - Шаги (каждый — отдельный запрос к Proxmox API): - 1. Создаёт LXC через ``ostemplate`` (без запуска). - 2. Задаёт root-пароль через ``/passwd``. - 3. Создаёт rootfs указанного размера через ``/resize``. - 4. Запускает контейнер через ``/status/start``. - - ``password`` приходит из формы (клиент задаёт сам) и сохраняется в БД - для одноразового показа в карточке инстанса. - - LXC создаётся ТОЛЬКО из архивного шаблона (vztmpl на storage). - Клонирование существующего LXC по VMID не поддерживается. - """ + """Создаёт LXC из архива: ostemplate → passwd → resize → start.""" node = node or settings.pve_node px = _client() - - # 1. Создаём LXC через ostemplate. start=0 — не запускаем, чтобы успеть - # задать пароль и диск нужного размера до старта. upid = px.nodes(node).lxc.post( vmid=new_vmid, hostname=name, @@ -96,23 +81,14 @@ def create_lxc( start=0, ) _wait_task(px, node, upid) - - # 2. Задаём пароль через отдельный endpoint /passwd. - # Поле password в POST /lxc на некоторых версиях Proxmox игнорируется, - # поэтому дублируем через /passwd — это идемпотентная операция. px.nodes(node).lxc(new_vmid).passwd.post(password=password) - - # 3. Создаём rootfs указанного размера (storage:size_G). px.nodes(node).lxc(new_vmid).resize.put( disk="rootfs", size=f"{storage}:{disk_gb}", ) - - # 4. Запускаем контейнер. upid = px.nodes(node).lxc(new_vmid).status.start.post() if upid: _wait_task(px, node, upid) - return password @@ -137,8 +113,7 @@ _DISK_KEYS = ("scsi0", "virtio0", "ide0", "sata0") def _disk_size_from_config(cfg: dict) -> int: """Извлекает размер диска VM в ГБ.""" for key in _DISK_KEYS: - value = cfg.get(key, "") - match = re.search(r"size=(\d+)G", value) + match = re.search(r"size=(\d+)G", cfg.get(key, "")) if match: return int(match.group(1)) return 10 @@ -163,7 +138,7 @@ def list_vm_templates(node: str = None) -> list: def list_lxc_templates(node: str = None) -> list: - """Возвращает архивные LXC-шаблоны (vztmpl) из доступных хранилищ.""" + """Возвращает архивные LXC-шаблоны (vztmpl).""" node = node or settings.pve_node px = _client() result = [] @@ -276,6 +251,16 @@ def guest_action(guest_type: str, vmid: int, action: str, node: str = None) -> N getattr(endpoint.status, action).post() +def _is_missing_guest_error(exc: Exception) -> bool: + """Проверяет, что Proxmox сообщает об отсутствии VM/LXC.""" + message = str(exc).lower() + return ( + "does not exist" in message + or "not found" in message + or "configuration file" in message and "does not exist" in message + ) + + def _wait_guest_stopped(px: ProxmoxAPI, guest_type: str, vmid: int, node: str, timeout: int = 180) -> None: """Ожидает, пока VM или LXC перейдёт в состояние stopped.""" endpoint = px.nodes(node).qemu(vmid) if guest_type == "vm" else px.nodes(node).lxc(vmid) @@ -289,23 +274,44 @@ def _wait_guest_stopped(px: ProxmoxAPI, guest_type: str, vmid: int, node: str, t def delete_guest(guest_type: str, vmid: int, node: str = None) -> None: - """Останавливает работающий инстанс и удаляет его.""" + """Удаляет VM/LXC. + + Если ресурс уже удалён вручную в Proxmox, операция считается успешной. + Это позволяет backend удалить устаревшую запись из собственной БД. + """ node = node or settings.pve_node px = _client() endpoint = px.nodes(node).qemu(vmid) if guest_type == "vm" else px.nodes(node).lxc(vmid) - status = endpoint.status.current.get().get("status") + + try: + status = endpoint.status.current.get().get("status") + except ResourceException as exc: + if _is_missing_guest_error(exc): + logger.info("%s %s уже отсутствует в Proxmox — считаем удалённым", guest_type, vmid) + return + raise if status == "running": try: shutdown_upid = endpoint.status.shutdown.post(timeout=60) if shutdown_upid: _wait_task(px, node, shutdown_upid, timeout=120) + except ResourceException as exc: + if not _is_missing_guest_error(exc): + logger.warning("Мягкая остановка %s %s не удалась: %s", guest_type, vmid, exc) + endpoint.status.stop.post() except Exception as exc: logger.warning("Мягкая остановка %s %s не удалась: %s", guest_type, vmid, exc) endpoint.status.stop.post() _wait_guest_stopped(px, guest_type, vmid, node) - upid = endpoint.delete() + try: + upid = endpoint.delete() + except ResourceException as exc: + if _is_missing_guest_error(exc): + logger.info("%s %s исчез во время удаления — считаем удалённым", guest_type, vmid) + return + raise if upid: _wait_task(px, node, upid)