feat: OPNsense integration and widget (#3424)
Co-authored-by: Meier Lukas <meierschlumpf@gmail.com> Co-authored-by: deepsource-io[bot] <42547082+deepsource-io[bot]@users.noreply.github.com>
This commit is contained in:
@@ -31,6 +31,7 @@ import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
|
||||
import { NPMIntegration } from "../npm/npm-integration";
|
||||
import { NTFYIntegration } from "../ntfy/ntfy-integration";
|
||||
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
||||
import { OPNsenseIntegration } from "../opnsense/opnsense-integration";
|
||||
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
|
||||
import { createPiHoleIntegrationAsync } from "../pi-hole/pi-hole-integration-factory";
|
||||
import { PlexIntegration } from "../plex/plex-integration";
|
||||
@@ -102,6 +103,7 @@ export const integrationCreators = {
|
||||
emby: EmbyIntegration,
|
||||
nextcloud: NextcloudIntegration,
|
||||
unifiController: UnifiControllerIntegration,
|
||||
opnsense: OPNsenseIntegration,
|
||||
github: GithubIntegration,
|
||||
dockerHub: DockerHubIntegration,
|
||||
gitlab: GitlabIntegration,
|
||||
|
||||
@@ -21,13 +21,20 @@ export { PiHoleIntegrationV5 } from "./pi-hole/v5/pi-hole-integration-v5";
|
||||
export { PiHoleIntegrationV6 } from "./pi-hole/v6/pi-hole-integration-v6";
|
||||
export { PlexIntegration } from "./plex/plex-integration";
|
||||
export { ProwlarrIntegration } from "./prowlarr/prowlarr-integration";
|
||||
export { OPNsenseIntegration } from "./opnsense/opnsense-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 {
|
||||
FirewallInterface,
|
||||
FirewallCpuSummary,
|
||||
FirewallInterfacesSummary,
|
||||
FirewallVersionSummary,
|
||||
FirewallMemorySummary,
|
||||
} from "./interfaces/firewall-summary/firewall-summary-types";
|
||||
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
||||
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {
|
||||
FirewallCpuSummary,
|
||||
FirewallInterfacesSummary,
|
||||
FirewallMemorySummary,
|
||||
FirewallVersionSummary,
|
||||
} from "./firewall-summary-types";
|
||||
|
||||
export interface FirewallSummaryIntegration {
|
||||
getFirewallCpuAsync(): Promise<FirewallCpuSummary>;
|
||||
getFirewallMemoryAsync(): Promise<FirewallMemorySummary>;
|
||||
getFirewallInterfacesAsync(): Promise<FirewallInterfacesSummary[]>;
|
||||
getFirewallVersionAsync(): Promise<FirewallVersionSummary>;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
export interface FirewallInterfacesSummary {
|
||||
data: FirewallInterface[];
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
export interface FirewallInterface {
|
||||
name: string;
|
||||
receive: number;
|
||||
transmit: number;
|
||||
}
|
||||
|
||||
export interface FirewallVersionSummary {
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface FirewallCpuSummary {
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface FirewallMemorySummary {
|
||||
used: number;
|
||||
total: number;
|
||||
percent: number;
|
||||
}
|
||||
189
packages/integrations/src/opnsense/opnsense-integration.ts
Normal file
189
packages/integrations/src/opnsense/opnsense-integration.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { ParseError, ResponseError } from "@homarr/common/server";
|
||||
import { createChannelEventHistory } from "@homarr/redis";
|
||||
|
||||
import { HandleIntegrationErrors } from "../base/errors/decorator";
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { FirewallSummaryIntegration } from "../interfaces/firewall-summary/firewall-summary-integration";
|
||||
import type {
|
||||
FirewallCpuSummary,
|
||||
FirewallInterface,
|
||||
FirewallInterfacesSummary,
|
||||
FirewallMemorySummary,
|
||||
FirewallVersionSummary,
|
||||
} from "../interfaces/firewall-summary/firewall-summary-types";
|
||||
import {
|
||||
opnsenseCPUSchema,
|
||||
opnsenseInterfacesSchema,
|
||||
opnsenseMemorySchema,
|
||||
opnsenseSystemSummarySchema,
|
||||
} from "./opnsense-types";
|
||||
|
||||
@HandleIntegrationErrors([])
|
||||
export class OPNsenseIntegration extends Integration implements FirewallSummaryIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/api/diagnostics/system/system_information"), {
|
||||
headers: {
|
||||
Authorization: this.getAuthHeaders(),
|
||||
},
|
||||
});
|
||||
if (!response.ok) return TestConnectionError.StatusResult(response);
|
||||
|
||||
const result = await response.json();
|
||||
if (typeof result === "object" && result !== null) return { success: true };
|
||||
|
||||
return TestConnectionError.ParseResult(new ParseError("Expected object data"));
|
||||
}
|
||||
|
||||
private getAuthHeaders() {
|
||||
const username = super.getSecretValue("username");
|
||||
const password = super.getSecretValue("password");
|
||||
return `Basic ${btoa(`${username}:${password}`)}`;
|
||||
}
|
||||
|
||||
public async getFirewallVersionAsync(): Promise<FirewallVersionSummary> {
|
||||
const responseVersion = await fetchWithTrustedCertificatesAsync(
|
||||
this.url("/api/diagnostics/system/system_information"),
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.getAuthHeaders(),
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!responseVersion.ok) {
|
||||
throw new ResponseError(responseVersion);
|
||||
}
|
||||
const summary = opnsenseSystemSummarySchema.parse(await responseVersion.json());
|
||||
|
||||
return {
|
||||
version: summary.versions.at(0) ?? "Unknown",
|
||||
};
|
||||
}
|
||||
|
||||
private getInterfacesChannel() {
|
||||
return createChannelEventHistory<FirewallInterface[]>(`integration:${this.integration.id}:interfaces`, 15);
|
||||
}
|
||||
|
||||
public async getFirewallInterfacesAsync(): Promise<FirewallInterfacesSummary[]> {
|
||||
const channel = this.getInterfacesChannel();
|
||||
|
||||
const responseInterfaces = await fetchWithTrustedCertificatesAsync(this.url("/api/diagnostics/traffic/interface"), {
|
||||
headers: {
|
||||
Authorization: this.getAuthHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!responseInterfaces.ok) {
|
||||
throw new ResponseError(responseInterfaces);
|
||||
}
|
||||
const interfaces = opnsenseInterfacesSchema.parse(await responseInterfaces.json());
|
||||
|
||||
const returnValue: FirewallInterface[] = [];
|
||||
const interfaceKeys = Object.keys(interfaces.interfaces);
|
||||
|
||||
for (const key of interfaceKeys) {
|
||||
const inter = interfaces.interfaces[key];
|
||||
if (!inter) continue;
|
||||
|
||||
const bytesTransmitted = inter["bytes transmitted"];
|
||||
const bytesReceived = inter["bytes received"];
|
||||
const receiveValue = parseInt(bytesReceived, 10);
|
||||
const transmitValue = parseInt(bytesTransmitted, 10);
|
||||
|
||||
returnValue.push({
|
||||
name: inter.name,
|
||||
receive: receiveValue,
|
||||
transmit: transmitValue,
|
||||
});
|
||||
}
|
||||
|
||||
await channel.pushAsync(returnValue);
|
||||
|
||||
return await channel.getSliceAsync(0, 1);
|
||||
}
|
||||
|
||||
public async getFirewallMemoryAsync(): Promise<FirewallMemorySummary> {
|
||||
const responseMemory = await fetchWithTrustedCertificatesAsync(
|
||||
this.url("/api/diagnostics/system/systemResources"),
|
||||
{
|
||||
headers: {
|
||||
Authorization: this.getAuthHeaders(),
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!responseMemory.ok) {
|
||||
throw new ResponseError(responseMemory);
|
||||
}
|
||||
|
||||
const memory = opnsenseMemorySchema.parse(await responseMemory.json());
|
||||
|
||||
// Using parseInt for memoryTotal is normal, the api sends the total memory as a string
|
||||
const memoryTotal = parseInt(memory.memory.total);
|
||||
const memoryUsed = memory.memory.used;
|
||||
const memoryPercent = (100 * memoryUsed) / memoryTotal;
|
||||
return {
|
||||
total: memoryTotal,
|
||||
used: memoryUsed,
|
||||
percent: memoryPercent,
|
||||
};
|
||||
}
|
||||
|
||||
public async getFirewallCpuAsync(): Promise<FirewallCpuSummary> {
|
||||
const responseCpu = await fetchWithTrustedCertificatesAsync(this.url("/api/diagnostics/cpu_usage/stream"), {
|
||||
headers: {
|
||||
Authorization: this.getAuthHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!responseCpu.ok) {
|
||||
throw new ResponseError(responseCpu);
|
||||
}
|
||||
|
||||
if (!responseCpu.body) {
|
||||
throw new Error("ReadableStream not supported in this environment.");
|
||||
}
|
||||
|
||||
const reader = responseCpu.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let loopCounter = 0;
|
||||
try {
|
||||
while (loopCounter < 10) {
|
||||
loopCounter++;
|
||||
const result = await reader.read();
|
||||
if (result.done) {
|
||||
break;
|
||||
}
|
||||
if (!(result.value instanceof Uint8Array)) {
|
||||
throw new Error("Received value is not an Uint8Array.");
|
||||
}
|
||||
|
||||
const value: AllowSharedBufferSource = result.value;
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
const lines = chunk.split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data:")) {
|
||||
continue;
|
||||
}
|
||||
if (loopCounter < 2) {
|
||||
continue;
|
||||
}
|
||||
const data = line.substring(5).trim();
|
||||
const cpuValues = opnsenseCPUSchema.parse(JSON.parse(data));
|
||||
|
||||
return {
|
||||
...cpuValues,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error("No valid CPU data found.");
|
||||
} finally {
|
||||
await reader.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
30
packages/integrations/src/opnsense/opnsense-types.ts
Normal file
30
packages/integrations/src/opnsense/opnsense-types.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { z } from "zod";
|
||||
|
||||
// API documentation : https://docs.opnsense.org/development/api.html#core-api
|
||||
|
||||
export const opnsenseSystemSummarySchema = z.object({
|
||||
name: z.string(),
|
||||
versions: z.array(z.string()),
|
||||
});
|
||||
|
||||
export const opnsenseMemorySchema = z.object({
|
||||
memory: z.object({
|
||||
total: z.string(),
|
||||
used: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
const interfaceSchema = z.object({
|
||||
"bytes received": z.string(),
|
||||
"bytes transmitted": z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const opnsenseInterfacesSchema = z.object({
|
||||
interfaces: z.record(interfaceSchema),
|
||||
time: z.number(),
|
||||
});
|
||||
|
||||
export const opnsenseCPUSchema = z.object({
|
||||
total: z.number(),
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from "./interfaces/calendar/calendar-types";
|
||||
export * from "./interfaces/dns-hole-summary/dns-hole-summary-types";
|
||||
export * from "./interfaces/network-controller-summary/network-controller-summary-types";
|
||||
export * from "./interfaces/firewall-summary/firewall-summary-types";
|
||||
export * from "./interfaces/health-monitoring/health-monitoring-types";
|
||||
export * from "./interfaces/indexer-manager/indexer-manager-types";
|
||||
export * from "./interfaces/media-requests/media-request-types";
|
||||
@@ -8,4 +9,5 @@ export * from "./base/searchable-integration";
|
||||
export * from "./homeassistant/homeassistant-types";
|
||||
export * from "./proxmox/proxmox-types";
|
||||
export * from "./unifi-controller/unifi-controller-types";
|
||||
export * from "./opnsense/opnsense-types";
|
||||
export * from "./interfaces/media-releases";
|
||||
|
||||
Reference in New Issue
Block a user