Fix _wait_for_task: match task via /nodes/{node}/tasks list with normalized upid (drop userid field) to avoid 400 from token userids

This commit is contained in:
host committed 2026-10-07 00:46:11 +03:00
1 parent 35417d2622
commit 6a1085572d
1 file changed
+28 -14
+28 -14
View File
@@ -1,8 +1,8 @@
""" """
title: Proxmox Tools (Kybinka) v2 title: Proxmox Tools (Kybinka) v2
author: (your name) 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. 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.0 version: 2.0.1
""" """
import time import time
import requests import requests
@@ -129,37 +129,51 @@ class Tools:
parts.append(f"{s2}s") parts.append(f"{s2}s")
return " ".join(parts) 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( def _wait_for_task(
self, upid: str, node: str, timeout: Optional[int] = None, poll: float = 1.5 self, upid: str, node: str, timeout: Optional[int] = None, poll: float = 1.5
): ):
"""Дождаться завершения задачи Proxmox (по UPID) на указанной ноде. """Дождаться завершения задачи Proxmox (по UPID) на указанной ноде.
Использует отдельный endpoint статуса задачи Ищет задачу в списке GET /nodes/{node}/tasks и сравнивает UPID по
(GET /nodes/{node}/tasks/{task}/status) вместо сканирования списка задач — нормализованному ключу (без поля userid). Это обходит 400
так нет хрупкого сравнения полных строк UPID и ошибок не глотается молча. 'Parameter verification failed' от endpoint tasks/{task}/status на
userid'ах с токеном (root@pam!token), и ошибки не глотаются молча.
""" """
if not upid or not isinstance(upid, str) or not upid.startswith("UPID:"): if not upid or not isinstance(upid, str) or not upid.startswith("UPID:"):
return True, "no UPID to wait" return True, "no UPID to wait"
if timeout is None: if timeout is None:
timeout = self.valves.task_wait_timeout timeout = self.valves.task_wait_timeout
# task id = часть ПОСЛЕ "UPID:" (само имя задачи в API Proxmox) target = self._task_key(upid)
task = urllib.parse.quote(upid[len("UPID:"):], safe=":") url = f"/nodes/{node}/tasks"
url = f"/nodes/{node}/tasks/{task}/status"
start, last_err = time.time(), "" start, last_err = time.time(), ""
while time.time() - start < timeout: while time.time() - start < timeout:
try: try:
# Ответ: {"status": "running"|"stopped", "exitstatus": "OK"|"ERROR", ...} tasks = self._api("GET", url)
data = self._api("GET", url) if isinstance(tasks, list):
status = data.get("status") for t in tasks:
if self._task_key(str(t.get("upid", ""))) == target:
status = t.get("status")
if status == "stopped": if status == "stopped":
if data.get("exitstatus") == "OK": if t.get("exitstatus") == "OK":
return True, "OK" return True, "OK"
return ( return (
False, False,
f"task failed: {data.get('exitmsg', '')} exit={data.get('exitstatus')}", f"task failed: {t.get('exitmsg', '')} "
f"exit={t.get('exitstatus')}",
) )
# status == "running" — ждём дальше # status == "running" — ждём дальше
time.sleep(poll) break
except Exception as e: except Exception as e:
last_err = str(e) # НЕ глотаем — покажем в финальном сообщении last_err = str(e) # НЕ глотаем — покажем в финальном сообщении
time.sleep(poll) time.sleep(poll)