feat: OMV integration & health monitoring widget (#1142)

This commit is contained in:
Yossi Hillali
2024-09-30 14:05:13 +03:00
committed by GitHub
parent 6ce466d38e
commit 0f56dc1ecd
19 changed files with 802 additions and 8 deletions

View File

@@ -14,6 +14,7 @@ import { JellyfinIntegration } from "../jellyfin/jellyfin-integration";
import { JellyseerrIntegration } from "../jellyseerr/jellyseerr-integration";
import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration";
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
import { PiHoleIntegration } from "../pi-hole/pi-hole-integration";
import { ProwlarrIntegration } from "../prowlarr/prowlarr-integration";
@@ -60,4 +61,5 @@ export const integrationCreators = {
jellyseerr: JellyseerrIntegration,
overseerr: OverseerrIntegration,
prowlarr: ProwlarrIntegration,
openmediavault: OpenMediaVaultIntegration,
} satisfies Partial<Record<IntegrationKind, new (integration: IntegrationInput) => Integration>>;

View File

@@ -6,23 +6,25 @@ export { DownloadClientIntegration } from "./interfaces/downloads/download-clien
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
export { RadarrIntegration } from "./media-organizer/radarr/radarr-integration";
export { SonarrIntegration } from "./media-organizer/sonarr/sonarr-integration";
export { OpenMediaVaultIntegration } from "./openmediavault/openmediavault-integration";
export { OverseerrIntegration } from "./overseerr/overseerr-integration";
export { PiHoleIntegration } from "./pi-hole/pi-hole-integration";
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
export { NzbGetIntegration } from "./download-client/nzbget/nzbget-integration";
export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorrent-integration";
export { DelugeIntegration } from "./download-client/deluge/deluge-integration";
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
export { OverseerrIntegration } from "./overseerr/overseerr-integration";
export { PiHoleIntegration } from "./pi-hole/pi-hole-integration";
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
// Types
export type { IntegrationInput } from "./base/integration";
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring";
export { MediaRequestStatus } from "./interfaces/media-requests/media-request";
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request";
export type { StreamSession } from "./interfaces/media-server/session";
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
export type { IntegrationInput } from "./base/integration";
// Schemas
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";

View File

@@ -0,0 +1,27 @@
export interface HealthMonitoring {
version: string;
cpuModelName: string;
cpuUtilization: number;
memUsed: string;
memAvailable: string;
uptime: number;
loadAverage: {
"1min": number;
"5min": number;
"15min": number;
};
rebootRequired: boolean;
availablePkgUpdates: number;
cpuTemp: number;
fileSystem: {
deviceName: string;
used: string;
available: string;
percentage: number;
}[];
smart: {
deviceName: string;
temperature: number;
overallStatus: string;
}[];
}

View File

@@ -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;
}
}

View File

@@ -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(),
}),
});

View File

@@ -1,5 +1,6 @@
export * from "./calendar-types";
export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
export * from "./interfaces/health-monitoring/healt-monitoring";
export * from "./interfaces/indexer-manager/indexer";
export * from "./interfaces/media-requests/media-request";
export * from "./pi-hole/pi-hole-types";