Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Proxmox integration/widget #1903

Merged
merged 12 commits into from
Mar 23, 2024
145 changes: 113 additions & 32 deletions public/locales/en/modules/health-monitoring.json
Original file line number Diff line number Diff line change
@@ -1,37 +1,118 @@
{
"descriptor": {
"name": "System Health Monitoring",
"description": "Information about your NAS",
"settings": {
"title": "System Health Monitoring",
"fahrenheit": {
"label": "Fahrenheit"
"descriptor": {
"name": "System Health Monitoring",
"description": "Displays information showing the health and status of your system(s).",
"settings": {
"title": "System Health Monitoring",
"fahrenheit": {
"label": "Fahrenheit"
},
"proxmox": {
"node": {
"label": "Filter by node name",
"info": "Enter your Proxmox node name to only show metrics for that node. By default, the entire cluster is shown."
},
"defaultViewState": {
"label": "Section open by default",
"data": {
"none": "None",
"node": "Nodes",
"vm": "VMs",
"lxc": "LXCs",
"storage": "Storage"
}
},
"summary": {
"label": "Show summary section"
},
"showNode": {
"label": "Show nodes section"
},
"showVM": {
"label": "Show VMs section"
},
"showLXCs": {
"label": "Show LXCs section"
},
"showStorage": {
"label": "Show storage section"
},
"sectionIndicatorColor": {
"label": "Requirement for section status indicator to be 'OK'",
"info": "'All' requires that all items be online for the indicator to be green. 'Any' requires at least one item to be online.",
"data": {
"any": "Any Active",
"all": "All Active"
}
},
"ignoreCert": {
"label": "Ignore Certificate Errors",
"info": "If enabled, the widget will ignore certificate errors when accessing the Proxmox API. This can be helpful when accessing Proxmox through HTTPS."
}
}
}
},
"cpu": {
"label": "CPU",
"load": "Load Average",
"minute": "{{minute}} minute"
},
"memory": {
"label": "Memory",
"totalMem": "Total memory: {{total}}GB",
"available": "Available: {{available}}GB - {{percentage}}%"
},
"fileSystem": {
"label": "File System",
"available": "Available: {{available}} - {{percentage}}%"
},
"info": {
"uptime": "Uptime",
"updates": "Updates",
"reboot": "Reboot"
},
"errors": {
"general": {
"title": "Unable to find your system(s).",
"text": "There was a problem connecting to your system. Please verify your configuration/integration(s)."
}
},
"cluster": {
"summary": {
"cpu": "CPU",
"ram": "RAM"
},
"cpu": {
"label": "CPU",
"load": "Load Average",
"minute": "{{minute}} minute"
},
"memory": {
"label": "Memory",
"totalMem": "Total memory: {{total}}GB",
"available": "Available: {{available}}GB - {{percentage}}%"
},
"fileSystem": {
"label": "File System",
"available": "Available: {{available}} - {{percentage}}%"
},
"info": {
"uptime": "Uptime",
"updates": "Updates",
"reboot": "Reboot"
},
"errors": {
"general": {
"title": "Unable to find your NAS",
"text": "There was a problem connecting to your NAS. Please verify your configuration/integration(s)."
}
"accordion": {
"title": {
"nodes": "Nodes",
"vms": "VMs",
"lxcs": "LXCs",
"storage": "Storage"
}
},
"table": {
"header": {
"name": "Name",
"cpu": "CPU",
"ram": "RAM",
"node": "Node"
}
}
},
"popover": {
"node": "Node",
"vmid": "VMID",
"details": "Details",
"cores": "Cores - {{maxCpu}}",
"memSize": "Memory - {{maxMem}}",
"memRatio": "Memory - {{usedMem}} / {{maxMem}}",
"diskSize": "Disk - {{maxDisk}}",
"diskRatio": "Disk - {{usedDisk}} / {{maxDisk}}",
"uptime": "Uptime - {{uptime}}",
"plugin": "Plugin",
"ha": "HA State - {{haState}}",
"sharedStorage": "Shared Storage",
"localStorage": "Local Storage",
"na": "N/A"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -198,4 +198,9 @@ export const availableIntegrations = [
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/openmediavault.png',
label: 'OpenMediaVault',
},
{
value: 'proxmox',
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/proxmox.png',
label: 'Proxmox',
}
] as const satisfies Readonly<SelectItem[]>;
4 changes: 2 additions & 2 deletions src/server/api/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,7 +50,7 @@ export const rootRouter = createTRPCRouter({
password: passwordRouter,
notebook: notebookRouter,
smartHomeEntityState: smartHomeEntityStateRouter,
openmediavault: openmediavaultRouter,
healthMonitoring: healthMonitoringRouter,
});

// export type definition of API
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
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 { ConfigAppType } from '~/types/app';

import { createTRPCRouter, publicProcedure } from '../trpc';

let sessionId: string | null = null;
let loginToken: string | null = null;
Expand All @@ -20,7 +19,7 @@ async function makeOpenMediaVaultRPCCall(
const app = config.apps.find((app) => checkIntegrationsType(app.integration, ['openmediavault']));

if (!app) {
Consola.error(`App not found for configName '${input.configName}'`);
Consola.error(`App 'openmediavault' not found for configName '${input.configName}'`);
return null;
}

Expand All @@ -42,25 +41,13 @@ async function makeOpenMediaVaultRPCCall(
return response;
}

export const openmediavaultRouter = createTRPCRouter({
fetchData: publicProcedure
.input(
z.object({
configName: z.string(),
})
)
.query(async ({ input }) => {
export async function makeOpenMediaVaultCalls(app: ConfigAppType, input: any) {
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`
`Failed to process request to app 'openmediavault'. Please check username & password`
);
return null;
}
Expand Down Expand Up @@ -115,5 +102,4 @@ export const openmediavaultRouter = createTRPCRouter({
fileSystem: fileSystemResponse?.data.response,
cpuTemp: cpuTempResponse?.data.response,
};
}),
});
}
109 changes: 109 additions & 0 deletions src/server/api/routers/health-monitoring/proxmox.ts
Original file line number Diff line number Diff line change
@@ -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 or 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 })
Dismissed Show dismissed Hide dismissed
: new https.Agent();

const result = await axios
.get(appUrl.toString(), {
headers: {
Authorization: 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;
}
Loading
Loading