From 6bcaec8003e737901319528c694f1363b642d047 Mon Sep 17 00:00:00 2001 From: jrad88 Date: Wed, 10 Apr 2024 15:51:24 -0700 Subject: [PATCH] ceph widget --- docs/widgets/services/ceph.md | 20 ++++++++ public/locales/en/common.json | 11 +++++ src/widgets/ceph/component.jsx | 68 +++++++++++++++++++++++++ src/widgets/ceph/proxy.js | 90 ++++++++++++++++++++++++++++++++++ src/widgets/ceph/widget.js | 7 +++ src/widgets/components.js | 1 + src/widgets/widgets.js | 2 + 7 files changed, 199 insertions(+) create mode 100644 docs/widgets/services/ceph.md create mode 100644 src/widgets/ceph/component.jsx create mode 100644 src/widgets/ceph/proxy.js create mode 100644 src/widgets/ceph/widget.js diff --git a/docs/widgets/services/ceph.md b/docs/widgets/services/ceph.md new file mode 100644 index 000000000..c5fe0f3ad --- /dev/null +++ b/docs/widgets/services/ceph.md @@ -0,0 +1,20 @@ +--- +title: Ceph +description: Ceph Widget Configuration +--- + +Learn more about [Ceph API](https://docs.ceph.com/en/latest/api/). + +The username and password are the same as used to login to the web interface. + +Allowed fields: `["status", "alerts", "freespace", "usedspace", "free", "used", "read", "write", "recovering"]`. + + +```yaml +widget: + type: ceph + url: http://ceph.host.or.ip:port + username: user1 + password: password1 + fields: ["status", "alerts", "used"] +``` diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 3ac3ed0d2..0f6cd57cf 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -876,5 +876,16 @@ "crowdsec": { "alerts": "Alerts", "bans": "Bans" + }, + "ceph": { + "status": "Status", + "alerts": "Alerts", + "freespace": "Free Space", + "usedspace": "Used Space", + "free": "Free", + "used": "Used", + "read": "Read", + "write": "Write", + "recovering": "Recovering" } } diff --git a/src/widgets/ceph/component.jsx b/src/widgets/ceph/component.jsx new file mode 100644 index 000000000..19089ea42 --- /dev/null +++ b/src/widgets/ceph/component.jsx @@ -0,0 +1,68 @@ +import { useTranslation } from "next-i18next"; + +import Container from "components/services/widget/container"; +import Block from "components/services/widget/block"; +import useWidgetAPI from "utils/proxy/use-widget-api"; + +export default function Component({ service }) { + const { t } = useTranslation(); + const { widget } = service; + + const { data: infoData, error: infoError } = useWidgetAPI(widget, "ceph/proxy-hosts"); + + if (infoError) { + return ; + } + + // Provide a default if not set in the config + if (!widget.fields) { + widget.fields = ["status", "alerts", "used"]; + } + + // Limit to a maximum of 4 at a time + if (widget.fields.length > 4) { + widget.fields = widget.fields.slice(0, 4); + } +/* + "status": "Status", + "alerts": "Alerts", + "freespace": "Free Space", + "usedspace": "Used Space", + "free": "Free", + "used": "Used", + "read": "Read", + "write": "Write", + "recovering": "Recovering" + + */ + + if (!infoData) { + return ( + + + + + + + + + + + + ); + } + + return ( + + + + + + + + + + + + ); +} diff --git a/src/widgets/ceph/proxy.js b/src/widgets/ceph/proxy.js new file mode 100644 index 000000000..9121d0013 --- /dev/null +++ b/src/widgets/ceph/proxy.js @@ -0,0 +1,90 @@ +import cache from "memory-cache"; + +import { httpProxy } from "utils/proxy/http"; +import { formatApiCall } from "utils/proxy/api-helpers"; +import getServiceWidget from "utils/config/service-helpers"; +import createLogger from "utils/logger"; + +const proxyName = "cephProxyHandler"; +const sessionTokenCacheKey = `${proxyName}__sessionToken`; +const logger = createLogger(proxyName); + +async function login(widget) { + + const loginUrl = new URL(formatApiCall("{url}/api/auth", widget)); + + const [status, , data] = await httpProxy(loginUrl, { + method: "POST", + body: JSON.stringify({ username: widget.username, password: widget.password }), + headers: { + "accept": "application/vnd.ceph.api.v1.0+json", + "Content-Type": "application/json", + }, + }); + + // try to avoid parsing errors that are not from ceph + if (status >= 500) + { + logger.error("Failed to connect to %s, status: %d, detail: %s", loginUrl, status, data?.error?.message ?? "-- Unable to read error message from request"); + return [status, false ]; + } + const dataParsed = JSON.parse(data); + + if (!(status === 201) || !dataParsed.token) { + logger.error("Failed to login to Ceph, status: %d, detail: %s", status, dataParsed?.detail); + return [status, false ]; + } + + return [ status, dataParsed.token ]; +} + +export default async function cephProxyHandler(req, res) { + const { group, service } = req.query; + + if (!group || !service) { + logger.debug("Invalid or missing service '%s' or group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + const widget = await getServiceWidget(group, service); + if (!widget) { + logger.debug("Invalid or missing widget for service '%s' in group '%s'", service, group); + return res.status(400).json({ error: "Invalid proxy service type" }); + } + + if (!widget.url) { + return res.status(500).json({ error: { message: "Service widget url not configured" } }); + } + + let token = cache.get(`${sessionTokenCacheKey}.${service}`); + + const url = new URL(formatApiCall("{url}/api/health/full", widget)); + const params = { + method: "GET", + headers: { + "accept": "application/vnd.ceph.api.v1.0+json", + "Authorization": `Bearer ${token}` + } + }; + + let [status, , data] = await httpProxy(url, params); + + if (status === 401) { + [status, token] = await login(widget); + + if (status !== 201) { + logger.error("HTTP %d logging in to ceph", status); + return res.status(status).end(data); + } + + cache.put(`${sessionTokenCacheKey}.${service}`, token); + params.headers.Authorization = `Bearer ${token}`; + [status, , data] = await httpProxy(url, params); + } + + if (status !== 200) { + logger.error("HTTP %d getting data from ceph. Data: %s", status, data); + } + + return res.status(status).send(data); +} diff --git a/src/widgets/ceph/widget.js b/src/widgets/ceph/widget.js new file mode 100644 index 000000000..af9da1f38 --- /dev/null +++ b/src/widgets/ceph/widget.js @@ -0,0 +1,7 @@ +import cephProxyHandler from "./proxy"; + +const widget = { + proxyHandler: cephProxyHandler, +}; + +export default widget; diff --git a/src/widgets/components.js b/src/widgets/components.js index 500fe0ce7..171c23197 100644 --- a/src/widgets/components.js +++ b/src/widgets/components.js @@ -11,6 +11,7 @@ const components = { caddy: dynamic(() => import("./caddy/component")), calendar: dynamic(() => import("./calendar/component")), calibreweb: dynamic(() => import("./calibreweb/component")), + ceph: dynamic(() => import("./ceph/component")), changedetectionio: dynamic(() => import("./changedetectionio/component")), channelsdvrserver: dynamic(() => import("./channelsdvrserver/component")), cloudflared: dynamic(() => import("./cloudflared/component")), diff --git a/src/widgets/widgets.js b/src/widgets/widgets.js index 7ed98bfb9..c8e142e6c 100644 --- a/src/widgets/widgets.js +++ b/src/widgets/widgets.js @@ -8,6 +8,7 @@ import bazarr from "./bazarr/widget"; import caddy from "./caddy/widget"; import calendar from "./calendar/widget"; import calibreweb from "./calibreweb/widget"; +import ceph from "./ceph/widget"; import changedetectionio from "./changedetectionio/widget"; import channelsdvrserver from "./channelsdvrserver/widget"; import cloudflared from "./cloudflared/widget"; @@ -122,6 +123,7 @@ const widgets = { bazarr, caddy, calibreweb, + ceph, changedetectionio, channelsdvrserver, cloudflared,