longhorn support

* longhorn widget for showing storage stats as "disks"
pull/448/head
James Wynn 2 years ago
parent c4333fd2dc
commit 8887fcc3ee

@ -0,0 +1,57 @@
import useSWR from "swr";
import { BiError } from "react-icons/bi";
import { i18n, useTranslation } from "next-i18next";
import Node from "./node";
export default function Longhorn({ options }) {
const { expanded, total, labels, include, nodes } = options;
const { t } = useTranslation();
const { data, error } = useSWR(`/api/widgets/longhorn`, {
refreshInterval: 1500
});
if (error || data?.error) {
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
<BiError className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left">
<span className="text-theme-800 dark:text-theme-200 text-xs">{t("widget.api_error")}</span>
</div>
</div>
);
}
if (!data) {
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
<div className="flex flex-row self-center flex-wrap justify-between" />
</div>
);
}
return (
<div className="flex flex-col max-w:full sm:basis-auto self-center grow-0 flex-wrap">
<div className="flex flex-row self-center flex-wrap justify-between">
{data.nodes
.filter((node) => {
if (node.id === 'total' && total) {
return true;
}
if (!nodes) {
return false;
}
if (include && !include.includes(node.id)) {
return false;
}
return true;
})
.map((node) =>
<div key={node.id}>
<Node data={{ node }} expanded={expanded} labels={labels} />
</div>
)}
</div>
</div>
);
}

@ -0,0 +1,32 @@
import { FiHardDrive } from "react-icons/fi";
import { useTranslation } from "next-i18next";
import UsageBar from "../resources/usage-bar";
export default function Node({ data, expanded, labels }) {
const { t } = useTranslation();
return (
<>
<div className="flex-none flex flex-row items-center mr-3 py-1.5">
<FiHardDrive className="text-theme-800 dark:text-theme-200 w-5 h-5" />
<div className="flex flex-col ml-3 text-left min-w-[85px]">
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">{t("common.bytes", { value: data.node.available })}</div>
<div className="pr-1">{t("resources.free")}</div>
</span>
{expanded && (
<span className="text-theme-800 dark:text-theme-200 text-xs flex flex-row justify-between">
<div className="pl-0.5">{t("common.bytes", { value: data.node.maximum })}</div>
<div className="pr-1">{t("resources.total")}</div>
</span>
)}
<UsageBar percent={Math.round((data.node.available / data.node.maximum) * 100)} />
</div>
</div>
{labels && (
<div className="ml-6 pt-1 text-center text-theme-800 dark:text-theme-200 text-xs">{data.node.id}</div>
)}
</>
);
}

@ -13,6 +13,7 @@ const widgetMappings = {
unifi_console: dynamic(() => import("components/widgets/unifi_console/unifi_console")),
glances: dynamic(() => import("components/widgets/glances/glances")),
openmeteo: dynamic(() => import("components/widgets/openmeteo/openmeteo")),
longhorn: dynamic(() => import("components/widgets/longhorn/longhorn")),
};
export default function Widget({ widget }) {

@ -39,19 +39,7 @@ export default async function handler(req, res) {
}
});
}
// Maybe Storage CSI can provide this information
// if (type === "disk") {
// if (!existsSync(target)) {
// return res.status(404).json({
// error: "Target not found",
// });
// }
//
// return res.status(200).json({
// drive: await drive.info(target || "/"),
// });
// }
//
if (type === "memory") {
const SCALE_MB = 1024 * 1024;
const usedMemMb = memUsage / SCALE_MB;

@ -0,0 +1,82 @@
import { httpProxy } from "../../../utils/proxy/http";
import createLogger from "../../../utils/logger";
import { getSettings } from "../../../utils/config/config";
const logger = createLogger("longhorn");
function parseLonghornData(data) {
const json = JSON.parse(data);
if (!json) {
return null;
}
const nodes = json.data.map((node) => {
let available = 0;
let maximum = 0;
let reserved = 0;
let scheduled = 0;
Object.keys(node.disks).forEach((diskKey) => {
const disk = node.disks[diskKey];
available += disk.storageAvailable;
maximum += disk.storageMaximum;
reserved += disk.storageReserved;
scheduled += disk.storageScheduled;
});
return {
id: node.id,
available,
maximum,
reserved,
scheduled,
};
});
const total = nodes.reduce((summary, node) => ({
available: summary.available + node.available,
maximum: summary.maximum + node.maximum,
reserved: summary.reserved + node.reserved,
scheduled: summary.scheduled + node.scheduled,
}));
total.id = "total";
nodes.push(total);
return nodes;
}
export default async function handler(req, res) {
const settings = getSettings();
const longhornSettings = settings?.providers?.longhorn;
const {url, username, password} = longhornSettings;
if (!url) {
const errorMessage = "Missing Longhorn URL";
logger.error(errorMessage);
return res.status(400).json({ error: errorMessage });
}
const apiUrl = `${url}/v1/nodes`;
const headers = {
"Accept-Encoding": "application/json"
};
if (username && password) {
headers.Authorization = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`
}
const params = { method: "GET", headers };
const [status, contentType, data] = await httpProxy(apiUrl, params);
if (status === 401) {
logger.error("Authorization failure getting data from Longhorn API. Data: %s", data);
}
if (status !== 200) {
logger.error("HTTP %d getting data from Longhorn API. Data: %s", status, data);
}
if (contentType) res.setHeader("Content-Type", contentType);
const nodes = parseLonghornData(data);
return res.status(200).json({
nodes,
});
}
Loading…
Cancel
Save