import axios from 'axios'; function getEnv(key) { const val = process.env[key]; if (!val) { throw new Error(`Missing ${key} in environment variables.`); } return val; } function createAuthHeaders(_method, _path, _body) { const TOKEN_ID = getEnv('PROXMOX_TOKEN_ID'); const TOKEN_SECRET = getEnv('PROXMOX_TOKEN_SECRET'); return { Authorization: `PVEAPIToken=${TOKEN_ID}=${TOKEN_SECRET}` }; } async function proxmoxFetch(path, method = 'GET', body) { 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: { ...createAuthHeaders(method, path, body), 'Content-Type': 'application/json', }, 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}`); } } /** * Sends a power action to a VM or LXC. * Uses /api2/extjs endpoint (different from /api2/json used by other routes). * @param {string} nodeName * @param {number} vmid * @param {'qemu'|'lxc'} type * @param {'start'|'stop'|'restart'} action * @returns {Promise} */ export async function powerControl(nodeName, vmid, type, action) { const baseUrl = getEnv('PROXMOX_API_URL'); const extjsUrl = baseUrl.replace('/api2/json', '/api2/extjs'); const res = await axios({ url: `${extjsUrl}/nodes/${nodeName}/${type}/${vmid}/status/${action}`, method: 'POST', headers: { ...createAuthHeaders('POST', '', ''), 'Content-Type': 'application/json', }, data: '{}', }); const data = res.data?.data; if (res.data?.success === 0 || !data) { throw new Error(res.data?.message || 'Power operation failed'); } } /** * @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 */ 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 */ 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, }, })); } 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 */ 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 }, }; }