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:
parent
49192daac6
commit
19f38fa25d
@ -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:
|
||||||
|
@ -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": [
|
||||||
|
169
src/main.js
169
src/main.js
@ -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:
|
||||||
|
23
src/pve.js
23
src/pve.js
@ -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;
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user