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=@!= * @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} */ 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} */ 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} */ 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} */ 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 }, }; }