Восстановлена read-only версия truenas_tools.py v2.0
This commit is contained in:
+180
-39
@@ -1,39 +1,180 @@
|
|||||||
# v2.1.2: проверяем реально работающие эндпоинты TrueNAS 25.10
|
"""Read-only инструменты мониторинга TrueNAS v2.0."""
|
||||||
# (многие получили новые имена — перебираем варианты)
|
import json
|
||||||
checks = [
|
import time
|
||||||
("system/info", "GET", None, "базовая информация о системе"),
|
import requests
|
||||||
("alert/list", "GET", None, "список алертов"),
|
from typing import Any, Optional
|
||||||
("catalog", "GET", None, "список каталогов приложений"),
|
from pydantic import BaseModel, Field
|
||||||
("pool/query", "GET", None, "список пулов ZFS (v2)"),
|
|
||||||
("pool", "GET", None, "список пулов ZFS (v1)"),
|
|
||||||
("disk/query", "GET", None, "список дисков (v2)"),
|
class Tools:
|
||||||
("disk", "GET", None, "список дисков (v1)"),
|
"""Мониторинг TrueNAS через REST API. Изменяющие операции отсутствуют."""
|
||||||
("app/query", "GET", None, "список приложений (v2)"),
|
|
||||||
("app", "GET", None, "список приложений (v1)"),
|
class Valves(BaseModel):
|
||||||
("chart/release/query", "GET", None, "helm-релизы"),
|
TRUENAS_URL: str = Field(default="http://192.168.31.100", description="URL TrueNAS")
|
||||||
]
|
TRUENAS_API_KEY: str = Field(default="", description="API-ключ TrueNAS")
|
||||||
ok = 0
|
CACHE_TTL: int = Field(default=30, description="TTL кэша GET-запросов")
|
||||||
checked: set = set()
|
MEM_CACHE_TTL: int = Field(default=60, description="TTL кэша RAM")
|
||||||
for endpoint, method, payload, descr in checks:
|
verify_ssl: bool = Field(default=False, description="Проверять TLS")
|
||||||
if endpoint in checked:
|
request_timeout: int = Field(default=15, description="Таймаут запроса")
|
||||||
continue
|
|
||||||
checked.add(endpoint)
|
def __init__(self):
|
||||||
url = f"{self._base}/api/v2.0/{endpoint}"
|
self.valves = self.Valves()
|
||||||
try:
|
self._cache = {}
|
||||||
if method == "GET":
|
self._last_error = "нет данных"
|
||||||
r = requests.get(url, headers=self._headers, params=payload, timeout=self.valves.request_timeout, verify=self.valves.verify_ssl)
|
|
||||||
else:
|
@property
|
||||||
r = requests.post(url, headers=self._headers, json=payload or {}, timeout=self.valves.request_timeout, verify=self.valves.verify_ssl)
|
def _base(self):
|
||||||
if r.status_code == 200:
|
return self.valves.TRUENAS_URL.rstrip("/")
|
||||||
lines.append(f"✅ {endpoint:30s} — {descr}")
|
|
||||||
ok += 1
|
@property
|
||||||
elif r.status_code in (401, 403):
|
def _headers(self):
|
||||||
lines.append(f"🔑 {endpoint:30s} — 401/403 (нет прав)")
|
return {"Authorization": f"Bearer {self.valves.TRUENAS_API_KEY}", "Content-Type": "application/json"}
|
||||||
elif r.status_code == 404:
|
|
||||||
lines.append(f"❌ {endpoint:30s} — HTTP 404 (нет такого пути)")
|
def clear_cache(self) -> str:
|
||||||
elif r.status_code == 405:
|
"""Очищает кэш запросов."""
|
||||||
lines.append(f"⚠️ {endpoint:30s} — HTTP 405 (другой метод)")
|
n = len(self._cache)
|
||||||
else:
|
self._cache.clear()
|
||||||
lines.append(f"⚠️ {endpoint:30s} — HTTP {r.status_code}")
|
return f"🧹 Кэш очищен ({n} записей удалено)."
|
||||||
except Exception as e:
|
|
||||||
lines.append(f"💥 {endpoint:30s} — ошибка: {type(e).__name__}")
|
def _get(self, key: str, endpoint: str, ttl: Optional[int] = None):
|
||||||
|
"""Выполняет кэшируемый GET-запрос."""
|
||||||
|
ttl = self.valves.CACHE_TTL if ttl is None else ttl
|
||||||
|
now = time.time()
|
||||||
|
if key in self._cache and ttl > 0 and now - self._cache[key][0] < ttl:
|
||||||
|
return self._cache[key][1]
|
||||||
|
try:
|
||||||
|
r = requests.get(f"{self._base}/api/v2.0/{endpoint}", headers=self._headers, timeout=self.valves.request_timeout, verify=self.valves.verify_ssl)
|
||||||
|
if r.status_code == 200:
|
||||||
|
value = r.json()
|
||||||
|
if ttl > 0:
|
||||||
|
self._cache[key] = (now, value)
|
||||||
|
return value
|
||||||
|
self._last_error = f"HTTP {r.status_code} на {endpoint}: {r.text[:300]}"
|
||||||
|
except Exception as e:
|
||||||
|
self._last_error = f"{type(e).__name__}: {e}"
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _first(self, name, endpoints):
|
||||||
|
for ep in endpoints:
|
||||||
|
value = self._get(f"{name}_{ep.replace('/', '_')}", ep)
|
||||||
|
if value is not None:
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _fmt(value: Any) -> str:
|
||||||
|
if value is None:
|
||||||
|
return "Н/Д"
|
||||||
|
try:
|
||||||
|
n = float(value)
|
||||||
|
for unit in ("B", "KiB", "MiB", "GiB", "TiB"):
|
||||||
|
if abs(n) < 1024:
|
||||||
|
return f"{n:.1f} {unit}"
|
||||||
|
n /= 1024
|
||||||
|
return f"{n:.1f} PiB"
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
def get_debug_info(self) -> str:
|
||||||
|
"""Проверяет соединение с TrueNAS."""
|
||||||
|
try:
|
||||||
|
r = requests.get(f"{self._base}/api/v2.0/system/info", headers=self._headers, timeout=self.valves.request_timeout, verify=False)
|
||||||
|
return f"URL: {self._base}\nКод ответа: {r.status_code}\nПоследняя ошибка: {self._last_error}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"URL: {self._base}\nОшибка: {type(e).__name__}: {e}"
|
||||||
|
|
||||||
|
def validate_api_key(self) -> str:
|
||||||
|
"""Проверяет read-only доступ API-ключа."""
|
||||||
|
if not self.valves.TRUENAS_API_KEY:
|
||||||
|
return "❌ API-ключ не задан"
|
||||||
|
checks = [("system/info", "система"), ("pool/query", "пулы"), ("disk/query", "диски"), ("app/query", "приложения"), ("alert/list", "алерты")]
|
||||||
|
out, ok = ["🔐 ВАЛИДАЦИЯ API-КЛЮЧА TRUENAS", ""], 0
|
||||||
|
for ep, title in checks:
|
||||||
|
value = self._get(f"validate_{ep}", ep, 0)
|
||||||
|
if value is not None:
|
||||||
|
out.append(f"✅ {ep:15s} — {title}")
|
||||||
|
ok += 1
|
||||||
|
else:
|
||||||
|
out.append(f"⚠️ {ep:15s} — недоступен")
|
||||||
|
out.append(f"\nИтого: {ok}/{len(checks)}")
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
def get_memory_info(self) -> str:
|
||||||
|
"""Показывает RAM через доступные API-эндпоинты."""
|
||||||
|
data = self._first("memory", ["system/memory", "system/info", "system/get_info"])
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return "💾 RAM: Н/Д (TrueNAS REST API не возвращает память в этой версии)"
|
||||||
|
total = data.get("total_memory") or data.get("memory_size") or data.get("memory") or data.get("total")
|
||||||
|
used = data.get("used")
|
||||||
|
return f"💾 RAM\n Всего: {self._fmt(total)}\n Использовано: {self._fmt(used)}"
|
||||||
|
|
||||||
|
def get_system_status(self) -> str:
|
||||||
|
"""Показывает системную информацию."""
|
||||||
|
d = self._get("system", "system/info")
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return f"⚠️ Ошибка получения системы: {self._last_error}"
|
||||||
|
load = d.get("loadavg") or []
|
||||||
|
return "\n".join([f"🖥️ TrueNAS: {d.get('version', 'N/A')}", f"🔧 Процессор: {d.get('model', 'N/A')}", f"📊 Load Average: {load}", self.get_memory_info()])
|
||||||
|
|
||||||
|
def get_pools_status(self) -> str:
|
||||||
|
"""Показывает пулы ZFS."""
|
||||||
|
pools = self._first("pools", ["pool/query", "pool", "storage/pool"])
|
||||||
|
if not isinstance(pools, list):
|
||||||
|
return f"⚠️ Пулы недоступны: {self._last_error}"
|
||||||
|
rows = []
|
||||||
|
for p in pools:
|
||||||
|
size, free = p.get("size", 0), p.get("free", 0)
|
||||||
|
used = (size - free) / size * 100 if size else 0
|
||||||
|
rows.append(f"{'✅' if p.get('healthy') else '⚠️'} '{p.get('name', '?')}' | {p.get('status', '?')} | {used:.1f}% занято")
|
||||||
|
return "\n".join(rows) or "Пулы не найдены."
|
||||||
|
|
||||||
|
def get_disks_temp_and_info(self) -> str:
|
||||||
|
"""Показывает диски и температуры."""
|
||||||
|
disks = self._first("disks", ["disk/query", "disk", "storage/disk"])
|
||||||
|
if not isinstance(disks, list):
|
||||||
|
return f"⚠️ Диски недоступны: {self._last_error}"
|
||||||
|
return "\n".join(f"💾 {d.get('name', 'N/A')} | {d.get('model', '')} | {self._fmt(d.get('size'))} | {d.get('temperature', 'N/A')}°C" for d in disks)
|
||||||
|
|
||||||
|
def get_apps_status(self) -> str:
|
||||||
|
"""Показывает установленные приложения. Установка отсутствует."""
|
||||||
|
apps = self._first("apps", ["app/query", "app", "chart/release/query"])
|
||||||
|
if not isinstance(apps, list):
|
||||||
|
return f"⚠️ Приложения недоступны: {self._last_error}"
|
||||||
|
icons = {"RUNNING": "🟢", "STOPPED": "⚪", "DEPLOYING": "🟡", "CRASHED": "🔴"}
|
||||||
|
return "\n".join(f"{icons.get(str(a.get('state', a.get('status', 'N/A'))).upper(), '⚪')} {a.get('name', a.get('id', '?'))} | статус: {a.get('state', a.get('status', 'N/A'))} | версия: {a.get('human_version', a.get('version', 'N/A'))}" for a in apps) or "Приложения не установлены."
|
||||||
|
|
||||||
|
def get_cpu_temperature(self) -> str:
|
||||||
|
"""Показывает температуру CPU, если она есть в API."""
|
||||||
|
d = self._get("temperature", "system/info") or {}
|
||||||
|
value = d.get("cpu_temperature") or d.get("cpu_temp") or d.get("temperature")
|
||||||
|
return f"🌡️ CPU: {value} °C" if value is not None else "🌡️ Температура CPU недоступна через API."
|
||||||
|
|
||||||
|
def get_arc_stats(self) -> str:
|
||||||
|
"""Показывает ZFS ARC, если он доступен через API."""
|
||||||
|
d = self._first("arc", ["stats", "zfs/arc", "zfs/pool/stats", "pool/statistics"])
|
||||||
|
return json.dumps(d, ensure_ascii=False, indent=2, default=str) if d else "📊 ARC-статистика недоступна через API."
|
||||||
|
|
||||||
|
def get_vms_status(self) -> str:
|
||||||
|
"""Показывает виртуальные машины."""
|
||||||
|
vms = self._first("vms", ["vm/query", "vm"])
|
||||||
|
if not isinstance(vms, list):
|
||||||
|
return f"⚠️ VM недоступны: {self._last_error}"
|
||||||
|
return "\n".join(f"{v.get('name', '?')} | {v.get('status', 'N/A')}" for v in vms) or "Виртуальные машины не найдены."
|
||||||
|
|
||||||
|
def get_containers_status(self) -> str:
|
||||||
|
"""Показывает контейнеры virt/incus."""
|
||||||
|
items = self._first("containers", ["virt/instance", "virt/instance/query", "container/query"])
|
||||||
|
if not isinstance(items, list):
|
||||||
|
return f"⚠️ Контейнеры недоступны: {self._last_error}"
|
||||||
|
return "\n".join(f"{c.get('name', c.get('id', '?'))} | {c.get('status', c.get('state', 'N/A'))}" for c in items) or "Контейнеры не найдены."
|
||||||
|
|
||||||
|
def get_active_alerts(self) -> str:
|
||||||
|
"""Показывает активные алерты."""
|
||||||
|
alerts = self._get("alerts", "alert/list") or []
|
||||||
|
active = [a for a in alerts if not a.get("dismissed", False)]
|
||||||
|
return "✅ Все системы работают штатно." if not active else "\n".join(f"[{a.get('level', 'INFO')}] {a.get('formatted', a.get('message', '?'))}" for a in active)
|
||||||
|
|
||||||
|
def get_full_report(self) -> str:
|
||||||
|
"""Формирует полный read-only отчёт."""
|
||||||
|
sections = [("СИСТЕМА", self.get_system_status()), ("RAM", self.get_memory_info()), ("ТЕМПЕРАТУРА", self.get_cpu_temperature()), ("ARC", self.get_arc_stats()), ("ПУЛЫ", self.get_pools_status()), ("ДИСКИ", self.get_disks_temp_and_info()), ("ПРИЛОЖЕНИЯ", self.get_apps_status()), ("VM", self.get_vms_status()), ("КОНТЕЙНЕРЫ", self.get_containers_status()), ("АЛЕРТЫ", self.get_active_alerts())]
|
||||||
|
return "\n\n".join(f"--- {title} ---\n{content}" for title, content in sections)
|
||||||
|
|||||||
Reference in New Issue
Block a user