feat: add Proxmox integration/widget (#1903)
This commit is contained in:
@@ -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
|
||||
|
||||
127
src/server/api/routers/health-monitoring/openmediavault.ts
Normal file
127
src/server/api/routers/health-monitoring/openmediavault.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
109
src/server/api/routers/health-monitoring/proxmox.ts
Normal file
109
src/server/api/routers/health-monitoring/proxmox.ts
Normal 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;
|
||||
}
|
||||
77
src/server/api/routers/health-monitoring/router.ts
Normal file
77
src/server/api/routers/health-monitoring/router.ts
Normal 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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user