feat: add Proxmox integration/widget (#1903)

This commit is contained in:
Dylan Slattery
2024-03-23 10:19:38 -05:00
committed by GitHub
parent 4a8b7377a8
commit 06772713ce
13 changed files with 1181 additions and 217 deletions

View File

@@ -8,13 +8,13 @@ import { dashDotRouter } from './routers/dash-dot';
import { dnsHoleRouter } from './routers/dns-hole/router';
import { dockerRouter } from './routers/docker/router';
import { downloadRouter } from './routers/download';
import { healthMonitoringRouter } from './routers/health-monitoring/router';
import { iconRouter } from './routers/icon';
import { indexerManagerRouter } from './routers/indexer-manager';
import { inviteRouter } from './routers/invite/invite-router';
import { mediaRequestsRouter } from './routers/media-request';
import { mediaServerRouter } from './routers/media-server';
import { notebookRouter } from './routers/notebook';
import { openmediavaultRouter } from './routers/openmediavault';
import { overseerrRouter } from './routers/overseerr';
import { passwordRouter } from './routers/password';
import { rssRouter } from './routers/rss';
@@ -50,7 +50,7 @@ export const rootRouter = createTRPCRouter({
password: passwordRouter,
notebook: notebookRouter,
smartHomeEntityState: smartHomeEntityStateRouter,
openmediavault: openmediavaultRouter,
healthMonitoring: healthMonitoringRouter,
});
// export type definition of API

View File

@@ -0,0 +1,127 @@
import axios from 'axios';
import Consola from 'consola';
import { checkIntegrationsType, findAppProperty } from '~/tools/client/app-properties';
import { getConfig } from '~/tools/config/getConfig';
import { ConfigAppType } from '~/types/app';
let sessionId: string | null = null;
let loginToken: string | null = null;
async function makeOpenMediaVaultRPCCall(
serviceName: string,
method: string,
params: Record<string, any>,
headers: Record<string, string>,
input: { configName: string }
) {
const config = getConfig(input.configName);
const app = config.apps.find((app) => checkIntegrationsType(app.integration, ['openmediavault']));
if (!app) {
Consola.error(`App 'openmediavault' not found for configName '${input.configName}'`);
return null;
}
const appUrl = new URL(app.url);
const response = await axios.post(
`${appUrl.origin}/rpc.php`,
{
service: serviceName,
method: method,
params: params,
},
{
headers: {
'Content-Type': 'application/json',
...headers,
},
}
);
return response;
}
export async function makeOpenMediaVaultCalls(app: ConfigAppType, input: any) {
let authResponse: any = null;
if (!sessionId || !loginToken) {
if (!app) {
Consola.error(
`Failed to process request to app 'openmediavault'. Please check username & password`
);
return null;
}
authResponse = await makeOpenMediaVaultRPCCall(
'session',
'login',
{
username: findAppProperty(app, 'username'),
password: findAppProperty(app, 'password'),
},
{},
input
);
if (authResponse.data.response.sessionid) {
sessionId = authResponse.data.response.sessionid;
} else {
const cookies = authResponse.headers['set-cookie'] || [];
sessionId = cookies
.find((cookie: any) => cookie.includes('X-OPENMEDIAVAULT-SESSIONID'))
?.split(';')[0];
loginToken = cookies
.find((cookie: any) => cookie.includes('X-OPENMEDIAVAULT-LOGIN'))
?.split(';')[0];
}
const responses = await Promise.allSettled([
makeOpenMediaVaultRPCCall(
'system',
'getInformation',
{},
loginToken
? { Cookie: `${loginToken};${sessionId}` }
: { 'X-OPENMEDIAVAULT-SESSIONID': sessionId as string },
input
),
makeOpenMediaVaultRPCCall(
'filesystemmgmt',
'enumerateMountedFilesystems',
{ includeroot: true },
loginToken
? { Cookie: `${loginToken};${sessionId}` }
: { 'X-OPENMEDIAVAULT-SESSIONID': sessionId as string },
input
),
makeOpenMediaVaultRPCCall(
'cputemp',
'get',
{},
loginToken
? { Cookie: `${loginToken};${sessionId}` }
: { 'X-OPENMEDIAVAULT-SESSIONID': sessionId as string },
input
),
]);
const systemInfoResponse =
responses[0].status === 'fulfilled' && responses[0].value
? responses[0].value.data?.response
: null;
const fileSystemResponse =
responses[1].status === 'fulfilled' && responses[1].value
? responses[1].value.data?.response
: null;
const cpuTempResponse =
responses[2].status === 'fulfilled' && responses[2].value
? responses[2].value.data?.response
: null;
return {
systemInfo: systemInfoResponse,
fileSystem: fileSystemResponse,
cpuTemp: cpuTempResponse,
};
}
}

View File

@@ -0,0 +1,109 @@
import axios from 'axios';
import Consola from 'consola';
import https from 'https';
import { findAppProperty } from '~/tools/client/app-properties';
import { ConfigAppType } from '~/types/app';
import { ResourceData, ResourceSummary } from '~/widgets/health-monitoring/cluster/types';
export async function makeProxmoxStatusAPICall(app: ConfigAppType, input: any) {
if (!app) {
Consola.error(`App 'proxmox' not found for configName '${input.configName}'`);
return null;
}
const apiKey = findAppProperty(app, 'apiKey');
if (!apiKey) {
Consola.error(`'proxmox': Missing API key. Please check the configuration.`);
return null;
}
const appUrl = new URL('api2/json/cluster/resources', app.url);
const agent = input.ignoreCerts
? new https.Agent({ rejectUnauthorized: false, requestCert: false })
: new https.Agent();
const result = await axios
.get(appUrl.toString(), {
headers: {
Authorization: `PVEAPIToken=${apiKey}`,
},
httpsAgent: agent,
})
.catch((error) => {
Consola.error(
`'proxmox': Error accessing service API: '${appUrl}'. Please check the configuration.`
);
return null;
})
.then((res) => {
let resources: ResourceSummary = { vms: [], lxcs: [], nodes: [], storage: [] };
if (!res) return null;
res.data.data.forEach((item: any) => {
if (input.filterNode === '' || input.filterNode === item.node) {
let resource: ResourceData = {
id: item.id,
cpu: item.cpu ? item.cpu : 0,
maxCpu: item.maxcpu ? item.maxcpu : 0,
maxMem: item.maxmem ? item.maxmem : 0,
mem: item.mem ? item.mem : 0,
name: item.name,
node: item.node,
status: item.status,
running: false,
type: item.type,
uptime: item.uptime,
vmId: item.vmid,
netIn: item.netin,
netOut: item.netout,
diskRead: item.diskread,
diskWrite: item.diskwrite,
disk: item.disk,
maxDisk: item.maxdisk,
haState: item.hastate,
storagePlugin: item.plugintype,
storageShared: item.shared == 1,
};
if (item.template == 0) {
if (item.type === 'qemu') {
resource.running = resource.status === 'running';
resources.vms.push(resource);
} else if (item.type === 'lxc') {
resource.running = resource.status === 'running';
resources.lxcs.push(resource);
}
} else if (item.type === 'node') {
resource.name = item.node;
resource.running = resource.status === 'online';
resources.nodes.push(resource);
} else if (item.type === 'storage') {
resource.name = item.storage;
resource.running = resource.status === 'available';
resources.storage.push(resource);
}
}
});
// results must be sorted; proxmox api result order can change dynamically,
// so sort the data to keep the item positions consistent
const sorter = (a: ResourceData, b: ResourceData) => {
if (a.id < b.id) {
return -1;
}
if (a.id > b.id) {
return 1;
}
return 0;
};
resources.nodes.sort(sorter);
resources.lxcs.sort(sorter);
resources.storage.sort(sorter);
resources.vms.sort(sorter);
return resources;
});
return result;
}

View File

@@ -0,0 +1,77 @@
import Consola from 'consola';
import { z } from 'zod';
import { checkIntegrationsType } from '~/tools/client/app-properties';
import { getConfig } from '~/tools/config/getConfig';
import { createTRPCRouter, publicProcedure } from '../../trpc';
import { makeOpenMediaVaultCalls } from './openmediavault';
import { makeProxmoxStatusAPICall } from './proxmox';
export const healthMonitoringRouter = createTRPCRouter({
integrations: publicProcedure
.input(
z.object({
configName: z.string(),
})
)
.query(async ({ input }) => {
const config = getConfig(input.configName);
const apps = config.apps.map((app) => {
if (checkIntegrationsType(app.integration, ['proxmox', 'openmediavault'])) {
return app.integration.type;
}
});
return apps;
}),
fetchData: publicProcedure
.input(
z.object({
configName: z.string(),
filterNode: z.string(),
ignoreCerts: z.boolean(),
})
)
.query(async ({ input }) => {
const config = getConfig(input.configName);
const omvApp = config.apps.find((app) =>
checkIntegrationsType(app.integration, ['openmediavault'])
);
const proxApp = config.apps.find((app) =>
checkIntegrationsType(app.integration, ['proxmox'])
);
if (!omvApp && !proxApp) {
Consola.error(`No valid integrations found for health monitoring in '${input.configName}'`);
return null;
}
let systemData: any;
let clusterData: any;
try {
const results = await Promise.all([
omvApp ? makeOpenMediaVaultCalls(omvApp, input) : null,
proxApp ? makeProxmoxStatusAPICall(proxApp, input) : null,
]);
for (const response of results) {
if (response) {
if ('systemInfo' in response && response.systemInfo != null) {
systemData = response;
} else if ('nodes' in response) {
clusterData = response;
}
}
}
} catch (error) {
Consola.error(`Error executing health monitoring requests(s): ${error}`);
return null;
}
return {
system: systemData,
cluster: clusterData,
};
}),
});

View File

@@ -1,142 +0,0 @@
import axios from 'axios';
import Consola from 'consola';
import { z } from 'zod';
import { checkIntegrationsType, findAppProperty } from '~/tools/client/app-properties';
import { getConfig } from '~/tools/config/getConfig';
import { createTRPCRouter, publicProcedure } from '../trpc';
let sessionId: string | null = null;
let loginToken: string | null = null;
async function makeOpenMediaVaultRPCCall(
serviceName: string,
method: string,
params: Record<string, any>,
headers: Record<string, string>,
input: { configName: string }
) {
const config = getConfig(input.configName);
const app = config.apps.find((app) => checkIntegrationsType(app.integration, ['openmediavault']));
if (!app) {
Consola.error(`App not found for configName '${input.configName}'`);
return null;
}
const appUrl = new URL(app.url);
const response = await axios.post(
`${appUrl.origin}/rpc.php`,
{
service: serviceName,
method: method,
params: params,
},
{
headers: {
'Content-Type': 'application/json',
...headers,
},
}
);
return response;
}
export const openmediavaultRouter = createTRPCRouter({
fetchData: publicProcedure
.input(
z.object({
configName: z.string(),
})
)
.query(async ({ input }) => {
let authResponse: any = null;
let app: any;
if (!sessionId || !loginToken) {
app = getConfig(input.configName)?.apps.find((app) =>
checkIntegrationsType(app.integration, ['openmediavault'])
);
if (!app) {
Consola.error(
`Failed to process request to app '${app.integration}' (${app.id}). Please check username & password`
);
return null;
}
authResponse = await makeOpenMediaVaultRPCCall(
'session',
'login',
{
username: findAppProperty(app, 'username'),
password: findAppProperty(app, 'password'),
},
{},
input
);
if (authResponse.data.response.sessionid) {
sessionId = authResponse.data.response.sessionid;
} else {
const cookies = authResponse.headers['set-cookie'] || [];
sessionId = cookies
.find((cookie: any) => cookie.includes('X-OPENMEDIAVAULT-SESSIONID'))
?.split(';')[0];
loginToken = cookies
.find((cookie: any) => cookie.includes('X-OPENMEDIAVAULT-LOGIN'))
?.split(';')[0];
}
}
const responses = await Promise.allSettled([
makeOpenMediaVaultRPCCall(
'system',
'getInformation',
{},
loginToken
? { Cookie: `${loginToken};${sessionId}` }
: { 'X-OPENMEDIAVAULT-SESSIONID': sessionId as string },
input
),
makeOpenMediaVaultRPCCall(
'filesystemmgmt',
'enumerateMountedFilesystems',
{ includeroot: true },
loginToken
? { Cookie: `${loginToken};${sessionId}` }
: { 'X-OPENMEDIAVAULT-SESSIONID': sessionId as string },
input
),
makeOpenMediaVaultRPCCall(
'cputemp',
'get',
{},
loginToken
? { Cookie: `${loginToken};${sessionId}` }
: { 'X-OPENMEDIAVAULT-SESSIONID': sessionId as string },
input
),
]);
const systemInfoResponse =
responses[0].status === 'fulfilled' && responses[0].value
? responses[0].value.data?.response
: null;
const fileSystemResponse =
responses[1].status === 'fulfilled' && responses[1].value
? responses[1].value.data?.response
: null;
const cpuTempResponse =
responses[2].status === 'fulfilled' && responses[2].value
? responses[2].value.data?.response
: null;
return {
systemInfo: systemInfoResponse,
fileSystem: fileSystemResponse,
cpuTemp: cpuTempResponse,
};
}),
});