From 1e114b200f153b9c8c5dd382de011e2cae30edc4 Mon Sep 17 00:00:00 2001 From: host Date: Sat, 1 Aug 2026 13:15:47 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=8D=D0=BA=D1=81=D0=BF=D0=BE=D1=80=D1=82=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=81=D1=82=D1=80=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=20?= =?UTF-8?q?Proxmox=20Tools=20(Kybinka)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tool-proxmox_tools_kybinka-export-1785579047349.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 tool-proxmox_tools_kybinka-export-1785579047349.json diff --git a/tool-proxmox_tools_kybinka-export-1785579047349.json b/tool-proxmox_tools_kybinka-export-1785579047349.json new file mode 100644 index 0000000..1fd89ef --- /dev/null +++ b/tool-proxmox_tools_kybinka-export-1785579047349.json @@ -0,0 +1 @@ +[{"id":"proxmox_tools_kybinka","user_id":"a9b3bc2c-071d-44d2-b6a3-7188033e0a66","name":"Proxmox Tools (Kybinka)","meta":{"description":"Инструменты для управления Proxmox VE (сервер Kybinka, 192.168.1.10): LXC (создание, старт, стоп, удаление), VM (клонирование из шаблона, создание из ISO, управление), мониторинг (ноды, хранилище, ресурсы, задачи).","manifest":{"title":"Proxmox Tools (Kybinka)","author":"(your name)","description":"Инструменты для управления Proxmox VE (сервер Kybinka, 192.168.1.10): LXC (создание, старт, стоп, удаление), VM (клонирование из шаблона, создание из ISO, управление), мониторинг (ноды, хранилище, ресурсы, задачи).","version":"1.0.0"},"has_user_valves":false},"access_grants":[],"updated_at":1785573012,"created_at":1785572990,"user":null,"write_access":true,"content":"\"\"\"\ntitle: Proxmox Tools (Kybinka)\nauthor: (your name)\ndescription: Инструменты для управления Proxmox VE (сервер Kybinka, 192.168.1.10): LXC (создание, старт, стоп, удаление), VM (клонирование из шаблона, создание из ISO, управление), мониторинг (ноды, хранилище, ресурсы, задачи).\nversion: 1.0.0\n\"\"\"\n\nimport time\nimport requests\nimport urllib3\nfrom pydantic import BaseModel, Field\nfrom typing import Optional\n\n\nclass Tools:\n class Valves(BaseModel):\n proxmox_host: str = Field(\n default=\"https://192.168.1.10:8006\",\n description=\"Базовый URL API Proxmox (например, https://192.168.1.10:8006)\",\n )\n proxmox_user: str = Field(\n default=\"root@pam\",\n description=\"Пользователь API Proxmox (например, root@pam)\",\n )\n proxmox_token_name: str = Field(\n default=\"open-webui\", description=\"Имя/API-токена Proxmox\"\n )\n proxmox_token_value: str = Field(\n default=\"\",\n description=\"Секрет API-токена Proxmox (задаётся в интерфейсе Valves, никогда не коммитьте в код)\",\n )\n verify_ssl: bool = Field(\n default=False, description=\"Проверять TLS-сертификат хоста Proxmox\"\n )\n default_node: str = Field(\n default=\"kybinka\", description=\"Имя ноды Proxmox по умолчанию\"\n )\n default_storage: str = Field(\n default=\"local\", description=\"Хранилище по умолчанию для rootfs/шаблонов LXC\",\n )\n default_bridge: str = Field(\n default=\"vmbr0\", description=\"Сетевой мост по умолчанию\"\n )\n request_timeout: int = Field(\n default=30, description=\"Таймаут HTTP-запроса в секундах\"\n )\n\n def __init__(self):\n self.valves = self.Valves()\n # Гасим спам \"InsecureRequestWarning\" при verify_ssl=False\n urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)\n\n # ============================================================\n # ВНУТРЕННИЕ ХЕЛПЕРЫ (не отдаются LLM)\n # ============================================================\n def _base_url(self) -> str:\n \"\"\"Базовый URL API Proxmox.\"\"\"\n return self.valves.proxmox_host.rstrip(\"/\") + \"/api2/json\"\n\n def _auth_header(self) -> dict:\n \"\"\"Заголовок авторизации по API-токену.\"\"\"\n return {\n \"Authorization\": (\n f\"PVEAPIToken={self.valves.proxmox_user}!\"\n f\"{self.valves.proxmox_token_name}={self.valves.proxmox_token_value}\"\n )\n }\n\n def _api(\n self,\n method: str,\n path: str,\n params: Optional[dict] = None,\n data: Optional[dict] = None,\n ):\n \"\"\"Выполнить запрос к API Proxmox.\"\"\"\n url = self._base_url() + path\n resp = requests.request(\n method=method,\n url=url,\n headers=self._auth_header(),\n params=params,\n json=data,\n verify=self.valves.verify_ssl,\n timeout=self.valves.request_timeout,\n )\n resp.raise_for_status()\n return resp.json().get(\"data\", {})\n\n @staticmethod\n def _fmt(b) -> str:\n \"\"\"Отформатировать байты в человекочитаемый вид.\"\"\"\n if b is None:\n return \"N/A\"\n b = float(b)\n for u in [\"B\", \"KB\", \"MB\", \"GB\", \"TB\"]:\n if b < 1024:\n return f\"{b:.1f} {u}\"\n b /= 1024\n return f\"{b:.1f} PB\"\n\n @classmethod\n def _pct(cls, u, t) -> str:\n \"\"\"Отформатировать used/total как '1.2 GB / 4.0 GB (30.0%)'.\"\"\"\n if u is None or t is None or t == 0:\n return \"N/A\"\n return f\"{cls._fmt(u)} / {cls._fmt(t)} ({u/t*100:.1f}%)\"\n\n @staticmethod\n def _uptime(s) -> str:\n \"\"\"Отформатировать uptime в секундах как '1d 2h 3m 4s'.\"\"\"\n if s is None:\n return \"N/A\"\n d, r = divmod(int(s), 86400)\n h, r = divmod(r, 3600)\n m, s2 = divmod(r, 60)\n parts = []\n if d:\n parts.append(f\"{d}d\")\n if h:\n parts.append(f\"{h}h\")\n if m:\n parts.append(f\"{m}m\")\n parts.append(f\"{s2}s\")\n return \" \".join(parts)\n\n def _wait_for_task(self, upid: str, node: str, timeout: int = 60):\n \"\"\"Дождаться завершения задачи Proxmox (по UPID) на указанной ноде.\"\"\"\n if not upid or not isinstance(upid, str) or not upid.startswith(\"UPID:\"):\n return True, \"no UPID to wait\"\n start = time.time()\n while time.time() - start < timeout:\n try:\n tasks = self._api(\"GET\", f\"/nodes/{node}/tasks\")\n for t in tasks:\n if t.get(\"upid\") == upid:\n status = t.get(\"status\", \"\")\n if status == \"stopped\":\n return (\n (True, \"OK\")\n if t.get(\"exitstatus\") == \"OK\"\n else (False, str(t))\n )\n break\n except Exception:\n pass\n time.sleep(2)\n return False, \"timeout waiting for task\"\n\n # ============================================================\n # ИНСТРУМЕНТЫ (отдаются LLM)\n # ============================================================\n def kybinka_connection(self) -> str:\n \"\"\"Проверить подключение к API Proxmox VE (Kybinka) и вернуть версию.\"\"\"\n try:\n v = self._api(\"GET\", \"/version\")\n return (\n \"OK Proxmox VE \"\n + v.get(\"version\", \"?\")\n + f\" | node: {self.valves.default_node} | host: {self.valves.proxmox_host}\"\n )\n except Exception as e:\n return \"ERR \" + str(e)\n\n def kybinka_nodes(self) -> str:\n \"\"\"Список всех нод кластера с загрузкой CPU/RAM и аптаймом.\"\"\"\n try:\n items = self._api(\"GET\", \"/nodes\")\n out = [\"Ноды:\"]\n for n in items:\n out.append(f\"- {n.get('node', '?')} ({n.get('status', '?')})\")\n out.append(\n f\" CPU: {n.get('cpu', 0)*100:.1f}% | RAM: {self._pct(n.get('mem'), n.get('maxmem'))}\"\n )\n out.append(f\" Uptime: {self._uptime(n.get('uptime'))}\")\n return \"\\n\".join(out)\n except Exception as e:\n return \"ERR \" + str(e)\n\n def kybinka_node_status(self, node: Optional[str] = None) -> str:\n \"\"\"Подробный статус ноды Proxmox: модель CPU, нагрузка, RAM, swap, диск, аптайм, ядро и версия PVE.\n Args:\n node: Имя ноды. По умолчанию — нода из настроек.\n \"\"\"\n node = node or self.valves.default_node\n try:\n s = self._api(\"GET\", f\"/nodes/{node}/status\")\n out = [f\"Нода {node}:\"]\n ci = s.get(\"cpuinfo\", {})\n out.append(f\"CPU: {ci.get('model', '?')} ({s.get('cpus', 0)} cores)\")\n out.append(f\"Load: {s.get('cpu', 0)*100:.1f}%\")\n la = s.get(\"loadavg\", [])\n if la:\n out.append(f\"Loadavg: {' / '.join(f'{float(v):.2f}' for v in la)}\")\n m = s.get(\"memory\", {})\n out.append(f\"RAM: {self._pct(m.get('used'), m.get('total'))}\")\n sw = s.get(\"swap\", {})\n if sw.get(\"total\", 0):\n out.append(f\"SWAP: {self._pct(sw.get('used'), sw.get('total'))}\")\n r = s.get(\"rootfs\", {})\n if r:\n out.append(f\"Disk: {self._pct(r.get('used'), r.get('total'))}\")\n out.append(f\"Uptime: {self._uptime(s.get('uptime'))}\")\n out.append(f\"Kernel: {s.get('kversion', '?')}\")\n if s.get(\"pveversion\"):\n out.append(f\"PVE: {s['pveversion']}\")\n return \"\\n\".join(out)\n except Exception as e:\n return \"ERR \" + str(e)\n\n def kybinka_cluster(self) -> str:\n \"\"\"Статус кластера: кворум и участники.\"\"\"\n try:\n items = self._api(\"GET\", \"/cluster/status\")\n out = [\"Кластер:\"]\n has_cluster_info = False\n for item in items:\n t = item.get(\"type\", \"\")\n if t == \"cluster\":\n has_cluster_info = True\n out.append(\n f\"Кластер: {item.get('name', '?')} | Кворум: {'OK' if item.get('quorate') else 'FAIL'}\"\n )\n elif t == \"node\":\n out.append(\n f\" {item.get('name', '?')} - {item.get('status', '?')} ({item.get('ip', '-')})\"\n )\n if not has_cluster_info:\n node_"] \ No newline at end of file