From 9dfd567f29d3fa33f30052638fd1bec119f3256e Mon Sep 17 00:00:00 2001 From: Derek Stotz Date: Mon, 29 Jan 2024 18:39:25 -0600 Subject: [PATCH] Add TrueNAS info widget for resource monitoring --- docs/widgets/info/truenas.md | 19 +++++ src/components/widgets/truenas/truenas.jsx | 38 +++++++++ src/components/widgets/widget.jsx | 1 + src/pages/api/widgets/truenas.js | 90 ++++++++++++++++++++++ 4 files changed, 148 insertions(+) create mode 100644 docs/widgets/info/truenas.md create mode 100644 src/components/widgets/truenas/truenas.jsx create mode 100644 src/pages/api/widgets/truenas.js diff --git a/docs/widgets/info/truenas.md b/docs/widgets/info/truenas.md new file mode 100644 index 00000000..eb08118f --- /dev/null +++ b/docs/widgets/info/truenas.md @@ -0,0 +1,19 @@ +--- +title: TrueNAS +description: TrueNAS Information Widget Configuration +--- + +_(Find the TrueNAS service widget [here](../services/truenas.md))_ + +The TrueNAS widget allows you to monitor the resources (CPU/memory) of your TrueNAS hosts, and is designed to match the `kubernetes` info widget. You can have multiple instances by adding another configuration block. + +```yaml +- truenas: + url: http://host.or.ip:port + username: user # not required if using api key + password: pass # not required if using api key + key: yourtruenasapikey # not required if using username / password + label: My TrueNAS # optional + icon: si-truenas # optional, defaults to si-truenas + refresh: 5000 # optional, in ms +``` diff --git a/src/components/widgets/truenas/truenas.jsx b/src/components/widgets/truenas/truenas.jsx new file mode 100644 index 00000000..d934f001 --- /dev/null +++ b/src/components/widgets/truenas/truenas.jsx @@ -0,0 +1,38 @@ +import useSWR from "swr"; +import { useTranslation } from "next-i18next"; + +import Error from "../widget/error"; +import ServiceResource from "../resources/serviceResource"; + +import ResolvedIcon from "components/resolvedicon"; + +export default function Widget({ options }) { + const { i18n } = useTranslation(); + const { icon: iconVal, label, refresh } = options; + + const { data, error } = useSWR( + `/api/widgets/truenas?${new URLSearchParams({ lang: i18n.language, ...options }).toString()}`, + { + refreshInterval: refresh ?? 5000, + }, + ); + + const icon = ; + + if (error || data?.error) { + return ; + } + + const memUsagePercent = Math.round(((data?.memory?.used ?? 0) / (data?.memory?.total ?? 1)) * 100); + + return ( + + ); +} diff --git a/src/components/widgets/widget.jsx b/src/components/widgets/widget.jsx index b4fdb143..4eceba43 100644 --- a/src/components/widgets/widget.jsx +++ b/src/components/widgets/widget.jsx @@ -15,6 +15,7 @@ const widgetMappings = { openmeteo: dynamic(() => import("components/widgets/openmeteo/openmeteo")), longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")), kubernetes: dynamic(() => import("components/widgets/kubernetes/kubernetes")), + truenas: dynamic(() => import("components/widgets/truenas/truenas")), }; export default function Widget({ widget, style }) { diff --git a/src/pages/api/widgets/truenas.js b/src/pages/api/widgets/truenas.js new file mode 100644 index 00000000..83b2d1d3 --- /dev/null +++ b/src/pages/api/widgets/truenas.js @@ -0,0 +1,90 @@ +import { httpProxy } from "utils/proxy/http"; +import createLogger from "utils/logger"; +import { getPrivateWidgetOptions } from "utils/config/widget-helpers"; + +const logger = createLogger("truenas"); + +async function retrieveFromTruenasAPI(privateWidgetOptions, endpoint, method, body) { + let errorMessage; + const url = privateWidgetOptions?.url; + if (!url) { + errorMessage = "Missing Truenas URL"; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + const apiUrl = `${url}/api/v2.0/${endpoint}`; + const headers = { + "Accept-Encoding": "application/json", + }; + if (privateWidgetOptions.username && privateWidgetOptions.password) { + headers.Authorization = `Basic ${Buffer.from( + `${privateWidgetOptions.username}:${privateWidgetOptions.password}`, + ).toString("base64")}`; + } else if (privateWidgetOptions.key) { + headers.Authorization = `Bearer ${privateWidgetOptions.key}`; + } else { + errorMessage = "Missing TrueNAS credentials"; + logger.error(errorMessage); + throw new Error(errorMessage); + } + const params = { method, headers, body }; + + const [status, , data] = await httpProxy(apiUrl, params); + + if (status === 401) { + errorMessage = `Authorization failure getting data from TrueNAS API. Data: ${data.toString()}`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (status !== 200) { + errorMessage = `HTTP ${status} getting data from TrueNAS API. Data: ${data.toString()}`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + return JSON.parse(Buffer.from(data).toString()); +} + +export default async function handler(req, res) { + const { index } = req.query; + + const privateWidgetOptions = await getPrivateWidgetOptions("truenas", index); + + try { + const systemInfo = await retrieveFromTruenasAPI(privateWidgetOptions, "system/info"); + const memoryInfo = await retrieveFromTruenasAPI( + privateWidgetOptions, + "reporting/get_data", + "POST", + JSON.stringify({ + graphs: [{ name: "memory" }], + reporting_query: { + start: Math.round((new Date() - 30000) / 1000), // 30 seconds ago + end: "NOW", + aggregate: true, + }, + }), + ); + + const [used, free, cached, buffered] = memoryInfo?.[0]?.aggregations?.mean || []; + + const data = { + cpu: { + load: systemInfo?.loadavg?.[0], + }, + memory: { + used, + free, + cached, + buffered, + total: used + free, // Using used + free instead of total memory to match TrueNAS dashboard + }, + }; + + return res.status(200).send(data); + } catch (e) { + return res.status(400).json({ error: e.message }); + } +}