implement pcie device add and delete endpoints,

change getDeviceInfo to return primary device with sun devices listed,
fix bug in approveResources when checking list type resources
This commit is contained in:
Arthur Lu 2023-06-22 00:23:34 +00:00
parent 49192daac6
commit 19f38fa25d
5 changed files with 179 additions and 32 deletions

View File

@ -22,14 +22,17 @@ In Proxmox VE, follow the following steps:
## Installation - API ## Installation - API
1. Clone this repo onto `Client Host` 1. Clone this repo onto `Client Host`
2. Run `npm install` to initiaze the package requirements 2. Run `npm install` to initiaze the package requirements
3. Copy `localdb.json.template` as `localdb.json` and modify the following values: 3. Copy `localdb.json.template` as `localdb.json` and modify the following values under `pveAPIToken`:
- pveAPI - the URI to the Proxmox API, ie `<proxmoxhost>:8006/api2/json` or `<proxmox URL>/api2/json` if Proxmox VE is behind a reverse proxy. - pveAPI - the URI to the Proxmox API, ie `<proxmoxhost>:8006/api2/json` or `<proxmox URL>/api2/json` if Proxmox VE is behind a reverse proxy.
- hostname - the ProxmoxAAS-Client URL, ie `host.domain.tld` - hostname - the ProxmoxAAS-Client URL, ie `host.domain.tld`
- domain - the base domain for the client and proxmox, ie `domain.tld` - domain - the base domain for the client and proxmox, ie `domain.tld`
- listenPort - the port you want the API to listen on, ie `8080` - listenPort - the port you want the API to listen on, ie `8080`
- pveAPIToken - the user(name), authentication realm, token id, and token secrey key (uuid) - pveAPIToken - the user(name), authentication realm, token id, and token secrey key (uuid)
4. You may also wish to confuigure users at this point as well. An example user config is shown in the template. 4. (Optional) In order to allow users to customize instance pcie devices, the API must use the root credentials for privilege elevation. Modify the following values under `pveroot` in order to use this feature:
5. Start the service using `node .`, or call the provided shell script, or use the provided systemctl service script - username: root user, typically `root@pam`
- password: root user password
5. You may also wish to configure users at this point as well. An example user config is shown in the template.
6. Start the service using `node .`, or call the provided shell script, or use the provided systemctl service script
## Installation - Reverse Proxy ## Installation - Reverse Proxy
1. Configure nginx or preferred reverse proxy to reverse proxy the client. The configuration should include at least the following: 1. Configure nginx or preferred reverse proxy to reverse proxy the client. The configuration should include at least the following:

View File

@ -7,6 +7,10 @@
"id": "token", "id": "token",
"uuid": "token-secret-value" "uuid": "token-secret-value"
}, },
"pveroot": {
"username": "root@pam",
"password": "rootpassword"
},
"listenPort": 80, "listenPort": 80,
"hostname": "client.mydomain", "hostname": "client.mydomain",
"domain": "mydomain" "domain": "mydomain"
@ -93,7 +97,8 @@
"swap": 0, "swap": 0,
"local": 0, "local": 0,
"cephpl": 0, "cephpl": 0,
"network": 0 "network": 0,
"pci": ["[GeForce GTX 1070]", "[GeForce GTX 1080 Ti]"]
} }
}, },
"nodes": [ "nodes": [

View File

@ -5,7 +5,7 @@ import cors from "cors";
import morgan from "morgan"; import morgan from "morgan";
import api from "../package.json" assert {type: "json"}; import api from "../package.json" assert {type: "json"};
import { requestPVE, handleResponse, getDiskInfo, getDeviceInfo, getUsedResources } from "./pve.js"; import { requestPVE, handleResponse, getDiskInfo, getDeviceInfo, getNodeAvailDevices } from "./pve.js";
import { checkAuth, approveResources, getUserResources } from "./utils.js"; import { checkAuth, approveResources, getUserResources } from "./utils.js";
import { db, pveAPIToken, listenPort, hostname, domain } from "./db.js"; import { db, pveAPIToken, listenPort, hostname, domain } from "./db.js";
@ -614,7 +614,7 @@ app.delete("/api/instance/network/delete", async (req, res) => {
* - vmid: Number - vm id number to destroy * - vmid: Number - vm id number to destroy
* - hostpci: String - hostpci number * - hostpci: String - hostpci number
* responses: * responses:
* - 200: {device_name: PVE PCI Device Object} * - 200: PVE PCI Device Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {error: String} * - 500: {error: String}
*/ */
@ -638,7 +638,7 @@ app.get("/api/instance/pci", async (req, res) => {
res.end(); res.end();
return; return;
} }
res.status(200).send({ device_name: deviceData }); res.status(200).send(deviceData);
res.end(); res.end();
return; return;
}); });
@ -648,7 +648,7 @@ app.get("/api/instance/pci", async (req, res) => {
* request: * request:
* - node: String - vm host node id * - node: String - vm host node id
* responses: * responses:
* - 200: [PVE PCI Device Object] * - 200: PVE PCI Device Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {error: String} * - 500: {error: String}
*/ */
@ -658,32 +658,151 @@ app.get("/api/nodes/pci", async (req, res) => {
if (!auth) { return; } if (!auth) { return; }
// get remaining user resources // get remaining user resources
let userAvailPci = (await getUserResources(req, req.cookies.username)).avail.pci; let userAvailPci = (await getUserResources(req, req.cookies.username)).avail.pci;
// get node pci devices // get node avail devices
let nodeAvailPci = (await requestPVE(`/nodes/${req.query.node}/hardware/pci`, "GET", req.body.cookies, null, pveAPIToken)).data.data; let nodeAvailPci = await getNodeAvailDevices(req.query.node, req.cookies);
// for each node container, get its config and remove devices which are already used nodeAvailPci = nodeAvailPci.filter(nodeAvail => userAvailPci.some((userAvail) => { return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail); }));
let vms = (await requestPVE(`/nodes/${req.query.node}/qemu`, "GET", req.body.cookies, null, pveAPIToken)).data.data;
for (let vm of vms) {
let config = (await requestPVE(`/nodes/${req.query.node}/qemu/${vm.vmid}/config`, "GET", req.body.cookies, null, pveAPIToken)).data.data;
Object.keys(config).forEach((key) => {
if (key.startsWith("hostpci")) {
let device_id = config[key].split(",")[0];
let allfn = !device_id.includes(".");
if (allfn) { // if allfn, remove all devices which include the same id as already allocated device
nodeAvailPci = nodeAvailPci.filter(element => !element.id.includes(device_id));
}
else { // if not allfn, remove only device with exact id match
nodeAvailPci = nodeAvailPci.filter(element => !element.id === device_id);
}
}
});
}
nodeAvailPci = nodeAvailPci.filter(nodeAvail => userAvailPci.some((userAvail) => { return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail) }));
res.status(200).send(nodeAvailPci); res.status(200).send(nodeAvailPci);
res.end(); res.end();
return; return;
}); });
/**
* POST - modify existing instance pci device
* request:
* - node: String - vm host node id
* - type: String - vm type (lxc, qemu)
* - vmid: Number - vm id number to destroy
* - hostpci: String - hostpci number
* - device: String - new device id
* - pcie: Boolean - whether to use pci express or pci
* response:
* - 200: PVE Task Object
* - 401: {auth: false, path: String}
* - 500: {request: Object, error: String}
* - 500: PVE Task Object
*/
app.post("/api/instance/pci/modify", async (req, res) => {
});
/**
* POST - add new instance pci device
* request:
* - node: String - vm host node id
* - type: String - vm type (lxc, qemu)
* - vmid: Number - vm id number to destroy
* - device: String - new device id
* - pcie: Boolean - whether to use pci express or pci
* response:
* - 200: PVE Task Object
* - 401: {auth: false, path: String}
* - 500: {request: Object, error: String}
* - 500: PVE Task Object
*/
app.post("/api/instance/pci/create", async (req, res) => {
// check if type is qemu
if (req.body.type !== "qemu") {
res.status(500).send({ error: `Type must be qemu (vm).` });
res.end();
return;
}
// check auth for specific instance
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
let auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { return; }
// force all functions
req.body.device = req.body.device.split(".")[0];
// get instance config to find next available hostpci slot
let config = requestPVE(`/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}/config`, "GET", req.body.cookies, null, null);
let hostpci = 0;
while (config[`hostpci${hostpci}`]) {
hostpci++;
}
// setup request
let deviceData = await getDeviceInfo(req.body.node, req.body.type, req.body.vmid, req.body.device);
let request = {
pci: deviceData.device_name
};
// check resource approval
if (!await approveResources(req, req.cookies.username, request)) {
res.status(500).send({ request: request, error: `Could not fulfil request for ${deviceData.device_name}.` });
res.end();
return;
}
// check node availability
let nodeAvailPci = await getNodeAvailDevices(req.body.node, req.cookies);
if (!nodeAvailPci.some(element => element.id.split(".")[0] === req.body.device)) {
res.status(500).send({ error: `Device ${req.body.device} is already in use on ${req.body.node}.` });
res.end();
return;
}
// setup action
let action = {};
action[`hostpci${hostpci}`] = `${req.body.device},pcie=${req.body.pcie}`;
action = JSON.stringify(action);
// commit action
let rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getApplicationConfig().pveroot), null);
if (!(rootauth.status === 200)) {
res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." });
res.end();
return;
}
let rootcookies = {
PVEAuthCookie: rootauth.data.data.ticket,
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
};
let result = await requestPVE(`${vmpath}/config`, "POST", rootcookies, action, null);
await handleResponse(req.body.node, result, res);
});
/**
* DELETE - delete instance pci device
* request:
* - node: String - vm host node id
* - type: String - vm type (lxc, qemu)
* - vmid: Number - vm id number to destroy
* - hostpci: String - hostpci number
* response:
* - 200: PVE Task Object
* - 401: {auth: false, path: String}
* - 500: {request: Object, error: String}
* - 500: PVE Task Object
*/
app.delete("/api/instance/pci/delete", async (req, res) => {
// check if type is qemu
if (req.body.type !== "qemu") {
res.status(500).send({ error: `Type must be qemu (vm).` });
res.end();
return;
}
// check auth for specific instance
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
let auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { return; }
// check device is in instance config
let config = (await requestPVE(`${vmpath}/config`, "GET", req.cookies)).data.data;
if (!config[`hostpci${req.body.hostpci}`]) {
res.status(500).send({ error: `Could not find hostpci${req.body.hostpci} in ${req.body.vmid}.` });
res.end();
return;
}
// setup action
let action = JSON.stringify({ delete: `hostpci${req.body.hostpci}`});
// commit action, need to use root user here because proxmox api only allows root to modify hostpci for whatever reason
let rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getApplicationConfig().pveroot), null);
if (!(rootauth.status === 200)) {
res.status(response.status).send({ auth: false, error: "API could not authenticate as root user." });
res.end();
return;
}
let rootcookies = {
PVEAuthCookie: rootauth.data.data.ticket,
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
};
let result = await requestPVE(`${vmpath}/config`, "POST", rootcookies, action, null);
await handleResponse(req.body.node, result, res);
});
/** /**
* POST - set basic resources for vm * POST - set basic resources for vm
* request: * request:

View File

@ -104,7 +104,7 @@ export async function getUsedResources(req, resourceMeta) {
else if (key.startsWith("hostpci")) { else if (key.startsWith("hostpci")) {
let deviceInfo = await getDeviceInfo(instance.node, instance.type, instance.vmid, config[key].split(",")[0]); let deviceInfo = await getDeviceInfo(instance.node, instance.type, instance.vmid, config[key].split(",")[0]);
if (deviceInfo) { // only count if device exists if (deviceInfo) { // only count if device exists
used.pci.push(deviceInfo); used.pci.push(deviceInfo.device_name);
} }
} }
} }
@ -137,9 +137,28 @@ export async function getDeviceInfo(node, type, vmid, qid) {
} }
}); });
deviceData.sort((a, b) => { return a.id < b.id }) deviceData.sort((a, b) => { return a.id < b.id })
return deviceData[0].device_name; let device = deviceData[0];
device.subfn = structuredClone(deviceData.slice(1));
return device;
} }
catch { catch {
return null; return null;
} }
} }
export async function getNodeAvailDevices(node, cookies) {
// get node pci devices
let nodeAvailPci = (await requestPVE(`/nodes/${node}/hardware/pci`, "GET", cookies, null, pveAPIToken)).data.data;
// for each node container, get its config and remove devices which are already used
let vms = (await requestPVE(`/nodes/${node}/qemu`, "GET", cookies, null, pveAPIToken)).data.data;
for (let vm of vms) {
let config = (await requestPVE(`/nodes/${node}/qemu/${vm.vmid}/config`, "GET", cookies, null, pveAPIToken)).data.data;
Object.keys(config).forEach((key) => {
if (key.startsWith("hostpci")) {
let device_id = config[key].split(",")[0];
nodeAvailPci = nodeAvailPci.filter(element => !element.id.includes(device_id));
}
});
}
return nodeAvailPci;
}

View File

@ -58,7 +58,8 @@ export async function approveResources(req, username, request) {
approved = false; approved = false;
} }
else if (resources[key].type === "list") { else if (resources[key].type === "list") {
if (avail[key].includes(request[key]) != resources[key].whitelist) { let inAvail = avail[key].some(availElem => request[key].includes(availElem));
if (inAvail != resources[key].whitelist) {
approved = false; approved = false;
} }
} }