270 lines
8.0 KiB
JavaScript
270 lines
8.0 KiB
JavaScript
import axios from "axios";
|
|
import { PVE_BACKEND } from "./backends.js";
|
|
|
|
export default class PVE extends PVE_BACKEND {
|
|
#pveAPIURL = null;
|
|
#pveAPIToken = null;
|
|
#pveRoot = null;
|
|
#paasFabric = null;
|
|
|
|
constructor (config) {
|
|
super();
|
|
this.#pveAPIURL = config.url;
|
|
this.#pveAPIToken = config.token;
|
|
this.#pveRoot = config.root;
|
|
this.#paasFabric = config.fabric;
|
|
}
|
|
|
|
async openSession (user, password) {
|
|
const credentials = { username: `${user.id}@${user.realm}`, password };
|
|
const response = await global.pve.requestPVE("/access/ticket", "POST", null, credentials);
|
|
if (!(response.status === 200)) {
|
|
return {
|
|
ok: false,
|
|
status: response.status,
|
|
message: "Authorization failed",
|
|
cookies: []
|
|
};
|
|
}
|
|
const ticket = response.data.ticket;
|
|
const csrftoken = response.data.CSRFPreventionToken;
|
|
return {
|
|
ok: true,
|
|
status: response.status,
|
|
cookies: [
|
|
{
|
|
name: "PVEAuthCookie",
|
|
value: ticket,
|
|
expiresMSFromNow: 2 * 60 * 60 * 1000
|
|
},
|
|
{
|
|
name: "CSRFPreventionToken",
|
|
value: csrftoken,
|
|
expiresMSFromNow: 2 * 60 * 60 * 1000
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Send HTTP request to proxmox API. Allows requests to be made with user cookie credentials or an API token for controlled priviledge elevation.
|
|
* @param {string} path HTTP path, prepended with the proxmox API base url.
|
|
* @param {string} method HTTP method.
|
|
* @param {Object} auth authentication method. Set auth.cookies with user cookies or auth.token with PVE API Token. Optional.
|
|
* @param {string} body body parameters and data to be sent. Optional.
|
|
* @returns {Object} HTTP response object or HTTP error object.
|
|
*/
|
|
async requestPVE (path, method, auth = null, body = null) {
|
|
const url = `${this.#pveAPIURL}${path}`;
|
|
const content = {
|
|
method,
|
|
mode: "cors",
|
|
credentials: "include",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
},
|
|
data: body
|
|
};
|
|
|
|
if (auth && auth.cookies) { // user cookie credentials
|
|
content.headers.CSRFPreventionToken = auth.cookies.CSRFPreventionToken;
|
|
content.headers.Cookie = `PVEAuthCookie=${auth.cookies.PVEAuthCookie}; CSRFPreventionToken=${auth.cookies.CSRFPreventionToken}`;
|
|
}
|
|
else if (auth && auth.token) { // upgraded request as api
|
|
const token = this.#pveAPIToken;
|
|
content.headers.Authorization = `PVEAPIToken=${token.user}@${token.realm}!${token.id}=${token.uuid}`;
|
|
}
|
|
else if (auth && auth.root) { // upgraded request as root
|
|
const rootauth = await this.requestPVE("/access/ticket", "POST", null, this.#pveRoot);
|
|
if (!(rootauth.status === 200)) {
|
|
return rootauth.response;
|
|
}
|
|
const rootcookie = rootauth.data.ticket;
|
|
const rootcsrf = rootauth.data.CSRFPreventionToken;
|
|
content.headers.CSRFPreventionToken = rootcsrf;
|
|
content.headers.Cookie = `PVEAuthCookie=${rootcookie}; CSRFPreventionToken=${rootcsrf}`;
|
|
}
|
|
|
|
try {
|
|
const result = await axios.request(url, content);
|
|
return {
|
|
ok: result.ok,
|
|
status: result.status,
|
|
data: result.data.data, // pve returns {data: {data: {...}}}, unwrap here to conform to standard {data: {...}} format
|
|
headers: result.headers
|
|
};
|
|
}
|
|
catch (error) {
|
|
console.log(`pve: error ocuured in pve.requestPVE: ${method} ${path} resulted in ${error}`);
|
|
const result = error.response;
|
|
result.ok = result.status === 200;
|
|
return result;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Send HTTP request to PAAS Fabric
|
|
* @param {string} path HTTP path, prepended with the proxmox API base url.
|
|
* @param {string} method HTTP method.
|
|
* @param {Object} auth authentication method. Set auth.cookies with user cookies or auth.token with PVE API Token. Optional.
|
|
* @param {string} body body parameters and data to be sent. Optional.
|
|
* @returns {Object} HTTP response object or HTTP error object.
|
|
*/
|
|
async requestFabric (path, method, body = null) {
|
|
const url = `${this.#paasFabric}${path}`;
|
|
const content = {
|
|
method,
|
|
mode: "cors",
|
|
credentials: "include",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded"
|
|
},
|
|
data: body
|
|
};
|
|
|
|
try {
|
|
return await axios.request(url, content);
|
|
}
|
|
catch (error) {
|
|
console.log(`pve: error ocuured in pve.requestFabric: ${method} ${path} resulted in ${error}`);
|
|
return error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle various proxmox API responses. Handles sync and async responses.
|
|
* In sync responses, responses are completed when the response arrives. Method returns the response directly.
|
|
* In async responses, proxmox sends responses with a UPID to track process completion. Method returns the status of the proxmox process once it completes.
|
|
* @param {string} node response originates from.
|
|
* @param {Object} result response from proxmox.
|
|
* @param {Object} res response object of ProxmoxAAS API call.
|
|
*/
|
|
async handleResponse (node, result, res) {
|
|
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
|
|
if (result.status !== 200) {
|
|
res.status(result.status).send({ error: result.statusText });
|
|
res.end();
|
|
}
|
|
else if (result.data && typeof (result.data) === "string" && result.data.startsWith("UPID:")) {
|
|
const upid = result.data;
|
|
let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
|
|
while (taskStatus.data.status !== "stopped") {
|
|
await waitFor(100);
|
|
taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
|
|
}
|
|
if (taskStatus.data.exitstatus === "OK") {
|
|
const result = taskStatus.data;
|
|
const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true });
|
|
result.log = taskLog.data;
|
|
res.status(200).send(result);
|
|
res.end();
|
|
}
|
|
else {
|
|
const result = taskStatus.data;
|
|
const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true });
|
|
result.log = taskLog.data;
|
|
res.status(500).send(result);
|
|
res.end();
|
|
}
|
|
}
|
|
else {
|
|
res.status(result.status).send(result.data);
|
|
res.end();
|
|
}
|
|
}
|
|
|
|
async getNode (node) {
|
|
const res = await this.requestFabric(`/nodes/${node}`, "GET");
|
|
if (res.status !== 200) {
|
|
console.error(res);
|
|
return null;
|
|
}
|
|
|
|
return res.data.node;
|
|
}
|
|
|
|
async syncNode (node) {
|
|
this.requestFabric(`/nodes/${node}/sync`, "POST");
|
|
}
|
|
|
|
async getInstance (node, instance) {
|
|
const res = await this.requestFabric(`/nodes/${node}/instances/${instance}`, "GET");
|
|
if (res.status !== 200) {
|
|
console.error(res);
|
|
return null;
|
|
}
|
|
|
|
return res.data.instance;
|
|
}
|
|
|
|
async syncInstance (node, vmid) {
|
|
this.requestFabric(`/nodes/${node}/instances/${vmid}/sync`, "POST");
|
|
}
|
|
|
|
async getDisk (node, instance, disk) {
|
|
const config = await this.getInstance(node, instance);
|
|
if (config !== null && config.volumes[disk] !== null) {
|
|
return config.volumes[disk];
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async getNet (node, instance, netid) {
|
|
const config = await this.getInstance(node, instance);
|
|
if (config !== null && config.nets[netid] !== null) {
|
|
return config.nets[netid];
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async getDevice (node, instance, deviceid) {
|
|
const config = await this.getInstance(node, instance);
|
|
if (config !== null && config.devices[deviceid] !== null) {
|
|
return config.devices[deviceid];
|
|
}
|
|
else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async getPoolResources (cookies, pool) {
|
|
// get pool resources
|
|
const res = await this.requestPVE(`/pools/?poolid=${pool}`, "GET", { cookies });
|
|
if (res.status !== 200) {
|
|
return null;
|
|
}
|
|
const data = res.data;
|
|
if (data.length !== 1) {
|
|
return null;
|
|
}
|
|
const poolPVE = data[0];
|
|
if (poolPVE.poolid !== pool) {
|
|
return null;
|
|
}
|
|
|
|
const poolPVEResources = poolPVE.members;
|
|
const resources = {};
|
|
|
|
// for each resource, add to the object
|
|
for (const resource of poolPVEResources) {
|
|
// only add type if it is vm or ct (ie has vmid)
|
|
if (resource.vmid) {
|
|
const instance = await this.getInstance(resource.node, resource.vmid);
|
|
if (instance === null) {
|
|
return null;
|
|
}
|
|
else {
|
|
instance.node = resource.node;
|
|
resources[resource.vmid] = instance;
|
|
}
|
|
}
|
|
}
|
|
|
|
return resources;
|
|
}
|
|
}
|