212 lines
5.3 KiB
JavaScript
212 lines
5.3 KiB
JavaScript
import axios from 'axios';
|
|
|
|
function getEnv(key) {
|
|
const val = process.env[key];
|
|
if (!val) {
|
|
throw new Error(`Missing ${key} in environment variables.`);
|
|
}
|
|
return val;
|
|
}
|
|
|
|
/**
|
|
* Generates the Proxmox API2 Authorization header.
|
|
* Format: PVEAPIToken=<user>@<realm>!<token-id>=<secret>
|
|
* @param {string} _method
|
|
* @param {string} _path
|
|
* @param {string} _body
|
|
* @returns {{ Authorization: string }}
|
|
*/
|
|
function createAuthHeaders(_method, _path, _body) {
|
|
const TOKEN_ID = getEnv('PROXMOX_TOKEN_ID'); // e.g. "root@pam!monitorapp"
|
|
const TOKEN_SECRET = getEnv('PROXMOX_TOKEN_SECRET');
|
|
return { Authorization: `PVEAPIToken=${TOKEN_ID}=${TOKEN_SECRET}` };
|
|
}
|
|
|
|
/**
|
|
* Fetches the Proxmox API with authentication.
|
|
* @param {string} path
|
|
* @param {'GET'|'POST'|'PUT'|'DELETE'} [method='GET']
|
|
* @param {string} [body]
|
|
* @returns {Promise<*>}
|
|
*/
|
|
async function proxmoxFetch(path, method = 'GET', body) {
|
|
const headers = {
|
|
...createAuthHeaders(method, path, body),
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
const baseUrl = getEnv('PROXMOX_API_URL');
|
|
const base = baseUrl.replace(/\/+$/, '');
|
|
const cleanPath = path.replace(/^\/+/, '');
|
|
const fullUrl = `${base}/${cleanPath}`;
|
|
|
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
|
|
try {
|
|
const res = await axios({
|
|
url: fullUrl,
|
|
method,
|
|
headers,
|
|
data: body,
|
|
});
|
|
|
|
return res.data;
|
|
} catch (err) {
|
|
const status = err.response?.status || 'Unknown Status';
|
|
const message = err.response?.data?.message || err.response?.data || err.message;
|
|
throw new Error(`Proxmox API error ${status}: ${message}`);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} ProxmoxNode
|
|
* @property {string} name
|
|
* @property {'node'} type
|
|
* @property {number} cpu
|
|
* @property {{ total: number; used: number }} memory
|
|
* @property {string} status
|
|
* @property {number} uptime
|
|
* @property {number} load
|
|
*/
|
|
|
|
/**
|
|
* Fetches all Proxmox nodes and returns mapped stats.
|
|
* @returns {Promise<ProxmoxNode[]>}
|
|
*/
|
|
export async function getNodes() {
|
|
const data = await proxmoxFetch('/nodes');
|
|
return (data.data || []).map((node) => ({
|
|
name: node.node,
|
|
type: 'node',
|
|
cpu: node.cpu ?? 0,
|
|
memory: {
|
|
total: node.maxmem ?? 0,
|
|
used: node.mem ?? 0,
|
|
},
|
|
status: node.status,
|
|
uptime: node.uptime ?? 0,
|
|
load: node.load ?? 0,
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} ProxmoxVM
|
|
* @property {number} vmid
|
|
* @property {string} name
|
|
* @property {string} type
|
|
* @property {string} status
|
|
* @property {number} cpu
|
|
* @property {{ total: number; used: number }} memory
|
|
* @property {{ total: number; used: number }} disk
|
|
*/
|
|
|
|
/**
|
|
* Fetches QEMU VMs for a given node.
|
|
* @param {string} nodeName
|
|
* @returns {Promise<ProxmoxVM[]>}
|
|
*/
|
|
export async function getVMs(nodeName) {
|
|
const data = await proxmoxFetch(`/nodes/${nodeName}/qemu`);
|
|
return (data.data || []).map((vm) => ({
|
|
vmid: vm.vmid,
|
|
name: vm.name || `VM ${vm.vmid}`,
|
|
type: 'qemu',
|
|
status: vm.status,
|
|
cpu: vm.cpu ?? 0,
|
|
memory: {
|
|
total: vm.maxmem ?? vm.maxmem ?? 0,
|
|
used: vm.mem ?? 0,
|
|
},
|
|
disk: {
|
|
total: 0,
|
|
used: vm.disk ?? 0,
|
|
},
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Fetches LXC containers for a given node.
|
|
* @param {string} nodeName
|
|
* @returns {Promise<ProxmoxVM[]>}
|
|
*/
|
|
export async function getLXC(nodeName) {
|
|
const data = await proxmoxFetch(`/nodes/${nodeName}/lxc`);
|
|
return (data.data || []).map((vm) => ({
|
|
vmid: vm.vmid,
|
|
name: vm.name || `LXC ${vm.vmid}`,
|
|
type: 'lxc',
|
|
status: vm.status,
|
|
cpu: vm.cpu ?? 0,
|
|
memory: {
|
|
total: vm.maxmem ?? vm.maxmem ?? 0,
|
|
used: vm.mem ?? 0,
|
|
},
|
|
disk: {
|
|
total: 0,
|
|
used: vm.disk ?? 0,
|
|
},
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* @typedef {Object} SummaryStat
|
|
* @property {number} totalNodes
|
|
* @property {number} runningVMs
|
|
* @property {number} stoppedVMs
|
|
* @property {number} totalCPUs
|
|
* @property {{ total: number; used: number }} totalMemory
|
|
*/
|
|
|
|
/**
|
|
* Aggregated summary across all nodes.
|
|
* @returns {Promise<SummaryStat>}
|
|
*/
|
|
/**
|
|
* Sends a power action to a VM or LXC.
|
|
* @param {string} nodeName
|
|
* @param {number} vmid
|
|
* @param {'qemu'|'lxc'} type
|
|
* @param {'start'|'stop'|'restart'} action
|
|
* @returns {Promise<void>}
|
|
*/
|
|
export async function powerControl(nodeName, vmid, type, action) {
|
|
await proxmoxFetch(`/nodes/${nodeName}/${type}/${vmid}/status/${action}`, 'POST');
|
|
}
|
|
|
|
/**
|
|
* Aggregated summary across all nodes.
|
|
* @returns {Promise<SummaryStat>}
|
|
*/
|
|
export async function getSummary() {
|
|
const nodes = await getNodes();
|
|
let totalVMs = [];
|
|
let totalLXC = [];
|
|
for (const node of nodes) {
|
|
try {
|
|
const vms = await getVMs(node.name);
|
|
totalVMs = totalVMs.concat(vms);
|
|
} catch { /* skip */ }
|
|
try {
|
|
const lxcs = await getLXC(node.name);
|
|
totalLXC = totalLXC.concat(lxcs);
|
|
} catch { /* skip */ }
|
|
}
|
|
|
|
const allVMs = [...totalVMs, ...totalLXC];
|
|
const runningVMs = allVMs.filter((vm) => vm.status === 'running').length;
|
|
const stoppedVMs = allVMs.filter((vm) => vm.status !== 'running').length;
|
|
|
|
const totalMemTotal = nodes.reduce((s, n) => s + (n.memory?.total || 0), 0);
|
|
const totalMemUsed = nodes.reduce((s, n) => s + (n.memory?.used || 0), 0);
|
|
|
|
return {
|
|
totalNodes: nodes.length,
|
|
runningVMs,
|
|
stoppedVMs,
|
|
totalCPUs: nodes.reduce((s, n) => s + n.cpu, 0),
|
|
totalMemory: { total: totalMemTotal, used: totalMemUsed },
|
|
};
|
|
}
|