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,