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
+35 -21
+35 -21
View File
@@ -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})"
# ============================================================