- 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>
187 lines
4.8 KiB
JavaScript
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 },
|
|
};
|
|
}
|