feat: OMV integration & health monitoring widget (#1142)
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import { Integration } from "../base/integration";
|
||||
import { IntegrationTestConnectionError } from "../base/test-connection-error";
|
||||
import type { HealthMonitoring } from "../types";
|
||||
import { cpuTempSchema, fileSystemSchema, smartSchema, systemInformationSchema } from "./openmediavault-types";
|
||||
|
||||
export class OpenMediaVaultIntegration extends Integration {
|
||||
static extractSessionIdFromCookies(headers: Headers): string {
|
||||
const cookies = headers.get("set-cookie") ?? "";
|
||||
const sessionId = cookies
|
||||
.split(";")
|
||||
.find((cookie) => cookie.includes("X-OPENMEDIAVAULT-SESSIONID") || cookie.includes("OPENMEDIAVAULT-SESSIONID"));
|
||||
|
||||
if (sessionId) {
|
||||
return sessionId;
|
||||
} else {
|
||||
throw new Error("Session ID not found in cookies");
|
||||
}
|
||||
}
|
||||
|
||||
static extractLoginTokenFromCookies(headers: Headers): string {
|
||||
const cookies = headers.get("set-cookie") ?? "";
|
||||
const loginToken = cookies
|
||||
.split(";")
|
||||
.find((cookie) => cookie.includes("X-OPENMEDIAVAULT-LOGIN") || cookie.includes("OPENMEDIAVAULT-LOGIN"));
|
||||
|
||||
if (loginToken) {
|
||||
return loginToken;
|
||||
} else {
|
||||
throw new Error("Login token not found in cookies");
|
||||
}
|
||||
}
|
||||
|
||||
public async getSystemInfoAsync(): Promise<HealthMonitoring> {
|
||||
if (!this.headers) {
|
||||
await this.authenticateAndConstructSessionInHeaderAsync();
|
||||
}
|
||||
|
||||
const systemResponses = await this.makeOpenMediaVaultRPCCallAsync("system", "getInformation", {}, this.headers);
|
||||
const fileSystemResponse = await this.makeOpenMediaVaultRPCCallAsync(
|
||||
"filesystemmgmt",
|
||||
"enumerateMountedFilesystems",
|
||||
{ includeroot: true },
|
||||
this.headers,
|
||||
);
|
||||
const smartResponse = await this.makeOpenMediaVaultRPCCallAsync("smart", "enumerateDevices", {}, this.headers);
|
||||
const cpuTempResponse = await this.makeOpenMediaVaultRPCCallAsync("cputemp", "get", {}, this.headers);
|
||||
|
||||
const systemResult = systemInformationSchema.safeParse(await systemResponses.json());
|
||||
const fileSystemResult = fileSystemSchema.safeParse(await fileSystemResponse.json());
|
||||
const smartResult = smartSchema.safeParse(await smartResponse.json());
|
||||
const cpuTempResult = cpuTempSchema.safeParse(await cpuTempResponse.json());
|
||||
|
||||
if (!systemResult.success) {
|
||||
throw new Error("Invalid system information response");
|
||||
}
|
||||
if (!fileSystemResult.success) {
|
||||
throw new Error("Invalid file system response");
|
||||
}
|
||||
if (!smartResult.success) {
|
||||
throw new Error("Invalid SMART information response");
|
||||
}
|
||||
if (!cpuTempResult.success) {
|
||||
throw new Error("Invalid CPU temperature response");
|
||||
}
|
||||
|
||||
const fileSystem = fileSystemResult.data.response.map((fileSystem) => ({
|
||||
deviceName: fileSystem.devicename,
|
||||
used: fileSystem.used,
|
||||
available: fileSystem.available,
|
||||
percentage: fileSystem.percentage,
|
||||
}));
|
||||
|
||||
const smart = smartResult.data.response.map((smart) => ({
|
||||
deviceName: smart.devicename,
|
||||
temperature: smart.temperature,
|
||||
overallStatus: smart.overallstatus,
|
||||
}));
|
||||
|
||||
return {
|
||||
version: systemResult.data.response.version,
|
||||
cpuModelName: systemResult.data.response.cpuModelName,
|
||||
cpuUtilization: systemResult.data.response.cpuUtilization,
|
||||
memUsed: systemResult.data.response.memUsed,
|
||||
memAvailable: systemResult.data.response.memAvailable,
|
||||
uptime: systemResult.data.response.uptime,
|
||||
loadAverage: {
|
||||
"1min": systemResult.data.response.loadAverage["1min"],
|
||||
"5min": systemResult.data.response.loadAverage["5min"],
|
||||
"15min": systemResult.data.response.loadAverage["15min"],
|
||||
},
|
||||
rebootRequired: systemResult.data.response.rebootRequired,
|
||||
availablePkgUpdates: systemResult.data.response.availablePkgUpdates,
|
||||
cpuTemp: cpuTempResult.data.response.cputemp,
|
||||
fileSystem,
|
||||
smart,
|
||||
};
|
||||
}
|
||||
|
||||
public async testConnectionAsync(): Promise<void> {
|
||||
const response = await this.makeOpenMediaVaultRPCCallAsync("session", "login", {
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new IntegrationTestConnectionError("invalidCredentials");
|
||||
}
|
||||
const result = (await response.json()) as unknown;
|
||||
if (typeof result !== "object" || result === null || !("response" in result)) {
|
||||
throw new IntegrationTestConnectionError("invalidJson");
|
||||
}
|
||||
}
|
||||
|
||||
private async makeOpenMediaVaultRPCCallAsync(
|
||||
serviceName: string,
|
||||
method: string,
|
||||
params: Record<string, unknown>,
|
||||
headers: Record<string, string> = {},
|
||||
): Promise<Response> {
|
||||
return await fetch(`${this.integration.url}/rpc.php`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
service: serviceName,
|
||||
method,
|
||||
params,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private headers: Record<string, string> | undefined = undefined;
|
||||
|
||||
private async authenticateAndConstructSessionInHeaderAsync() {
|
||||
const authResponse = await this.makeOpenMediaVaultRPCCallAsync("session", "login", {
|
||||
username: this.getSecretValue("username"),
|
||||
password: this.getSecretValue("password"),
|
||||
});
|
||||
const authResult = (await authResponse.json()) as Response;
|
||||
const response = (authResult as { response?: { sessionid?: string } }).response;
|
||||
let sessionId;
|
||||
const headers: Record<string, string> = {};
|
||||
if (response?.sessionid) {
|
||||
sessionId = response.sessionid;
|
||||
headers["X-OPENMEDIAVAULT-SESSIONID"] = sessionId;
|
||||
} else {
|
||||
sessionId = OpenMediaVaultIntegration.extractSessionIdFromCookies(authResponse.headers);
|
||||
const loginToken = OpenMediaVaultIntegration.extractLoginTokenFromCookies(authResponse.headers);
|
||||
headers.Cookie = `${loginToken};${sessionId}`;
|
||||
}
|
||||
this.headers = headers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// Schema for system information
|
||||
export const systemInformationSchema = z.object({
|
||||
response: z.object({
|
||||
version: z.string(),
|
||||
cpuModelName: z.string(),
|
||||
cpuUtilization: z.number(),
|
||||
memUsed: z.string(),
|
||||
memAvailable: z.string(),
|
||||
uptime: z.number(),
|
||||
loadAverage: z.object({
|
||||
"1min": z.number(),
|
||||
"5min": z.number(),
|
||||
"15min": z.number(),
|
||||
}),
|
||||
rebootRequired: z.boolean(),
|
||||
availablePkgUpdates: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
// Schema for file systems
|
||||
export const fileSystemSchema = z.object({
|
||||
response: z.array(
|
||||
z.object({
|
||||
devicename: z.string(),
|
||||
used: z.string(),
|
||||
available: z.string(),
|
||||
percentage: z.number(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
// Schema for SMART information
|
||||
export const smartSchema = z.object({
|
||||
response: z.array(
|
||||
z.object({
|
||||
devicename: z.string(),
|
||||
temperature: z.union([z.string(), z.number()]).transform((val) => {
|
||||
// Convert string to number if necessary
|
||||
const temp = typeof val === "string" ? parseFloat(val) : val;
|
||||
if (isNaN(temp)) {
|
||||
throw new Error("Invalid temperature value");
|
||||
}
|
||||
return temp;
|
||||
}),
|
||||
overallstatus: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
// Schema for CPU temperature
|
||||
export const cpuTempSchema = z.object({
|
||||
response: z.object({
|
||||
cputemp: z.number(),
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user