diff --git a/proxmox_tools_kybinka_v2.py b/proxmox_tools_kybinka_v2.py index 74f63b0..0720660 100644 --- a/proxmox_tools_kybinka_v2.py +++ b/proxmox_tools_kybinka_v2.py @@ -1,8 +1,8 @@ """ title: Proxmox Tools (Kybinka) v2 author: (your name) -description: Инструменты для управления Proxmox VE (сервер Kybinka, 192.168.1.20): LXC (создание, старт, стоп, удаление), VM (клонирование из шаблона, создание из ISO, управление), мониторинг (ноды, хранилище, ресурсы, задачи). v2 — исправлено ожидание задач (_wait_for_task: отдельный endpoint статуса + видимость ошибок), default_node=px, config/start не пропускаются при таймауте, таймаут ожидания вынесен в Valves. -version: 2.0.0 +description: Инструменты для управления Proxmox VE (сервер Kybinka, 192.168.1.20): LXC (создание, старт, стоп, удаление), VM (клонирование из шаблона, создание из ISO, управление), мониторинг (ноды, хранилище, ресурсы, задачи). v2 — исправлено ожидание задач (_wait_for_task: сканирование списка задач с нормализацией upid, не 400 на токены), default_node=px, config/start не пропускаются при таймауте, таймаут ожидания вынесен в Valves. +version: 2.0.1 """ import time import requests @@ -129,40 +129,54 @@ class Tools: parts.append(f"{s2}s") return " ".join(parts) + @staticmethod + def _task_key(upid: str) -> str: + """Нормализованный ключ задачи: без префикса 'UPID:' и без последнего + поля userid (оно может нести !token и ломать точное сравнение/regex). + Остальные поля node:pid:time:inc:func:arg уникальны для задачи.""" + u = upid + if u.startswith("UPID:"): + u = u[len("UPID:"):] + parts = u.split(":") + return ":".join(parts[:-1]) if len(parts) > 1 else u + def _wait_for_task( self, upid: str, node: str, timeout: Optional[int] = None, poll: float = 1.5 ): """Дождаться завершения задачи Proxmox (по UPID) на указанной ноде. - Использует отдельный endpoint статуса задачи - (GET /nodes/{node}/tasks/{task}/status) вместо сканирования списка задач — - так нет хрупкого сравнения полных строк UPID и ошибок не глотается молча. + Ищет задачу в списке GET /nodes/{node}/tasks и сравнивает UPID по + нормализованному ключу (без поля userid). Это обходит 400 + 'Parameter verification failed' от endpoint tasks/{task}/status на + userid'ах с токеном (root@pam!token), и ошибки не глотаются молча. """ if not upid or not isinstance(upid, str) or not upid.startswith("UPID:"): return True, "no UPID to wait" if timeout is None: timeout = self.valves.task_wait_timeout - # task id = часть ПОСЛЕ "UPID:" (само имя задачи в API Proxmox) - task = urllib.parse.quote(upid[len("UPID:"):], safe=":") - url = f"/nodes/{node}/tasks/{task}/status" + target = self._task_key(upid) + url = f"/nodes/{node}/tasks" start, last_err = time.time(), "" while time.time() - start < timeout: try: - # Ответ: {"status": "running"|"stopped", "exitstatus": "OK"|"ERROR", ...} - data = self._api("GET", url) - status = data.get("status") - if status == "stopped": - if data.get("exitstatus") == "OK": - return True, "OK" - return ( - False, - f"task failed: {data.get('exitmsg', '')} exit={data.get('exitstatus')}", - ) - # status == "running" — ждём дальше - time.sleep(poll) + tasks = self._api("GET", url) + if isinstance(tasks, list): + for t in tasks: + if self._task_key(str(t.get("upid", ""))) == target: + status = t.get("status") + if status == "stopped": + if t.get("exitstatus") == "OK": + return True, "OK" + return ( + False, + f"task failed: {t.get('exitmsg', '')} " + f"exit={t.get('exitstatus')}", + ) + # status == "running" — ждём дальше + break except Exception as e: last_err = str(e) # НЕ глотаем — покажем в финальном сообщении - time.sleep(poll) + time.sleep(poll) return False, f"timeout waiting for task (last_err={last_err})" # ============================================================