[{"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_"]