Files
proxmox-monitor/src/lib/proxmox.js
Rob Bond ad328efa3c fix: power control 502 error and add instant refresh
- Power operations use /api2/extjs endpoint (not /api2/json)
- Remove unprivileged parameter that causes 400 validation error
- Refetch VM/LXC/node/status after successful power action via SWR mutate

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:56:29 +01:00

187 lines
4.8 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;
}
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<void>}
*/
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 },
};
}