diff --git a/README.md b/README.md index 9351ef1..e69cfee 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,17 @@ In Proxmox VE, follow the following steps: ## Installation - API 1. Clone this repo onto `Client Host` 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 `:8006/api2/json` or `/api2/json` if Proxmox VE is behind a reverse proxy. - hostname - the ProxmoxAAS-Client URL, ie `host.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` - 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. -5. Start the service using `node .`, or call the provided shell script, or use the provided systemctl service script +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: + - 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 1. Configure nginx or preferred reverse proxy to reverse proxy the client. The configuration should include at least the following: diff --git a/config/localdb.json.template b/config/localdb.json.template index 81a587e..1a38689 100644 --- a/config/localdb.json.template +++ b/config/localdb.json.template @@ -7,6 +7,10 @@ "id": "token", "uuid": "token-secret-value" }, + "pveroot": { + "username": "root@pam", + "password": "rootpassword" + }, "listenPort": 80, "hostname": "client.mydomain", "domain": "mydomain" @@ -93,7 +97,8 @@ "swap": 0, "local": 0, "cephpl": 0, - "network": 0 + "network": 0, + "pci": ["[GeForce GTX 1070]", "[GeForce GTX 1080 Ti]"] } }, "nodes": [ diff --git a/src/main.js b/src/main.js index b9ff474..c6c470b 100644 --- a/src/main.js +++ b/src/main.js @@ -5,7 +5,7 @@ import cors from "cors"; import morgan from "morgan"; 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 { 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 * - hostpci: String - hostpci number * responses: - * - 200: {device_name: PVE PCI Device Object} + * - 200: PVE PCI Device Object * - 401: {auth: false, path: String} * - 500: {error: String} */ @@ -638,7 +638,7 @@ app.get("/api/instance/pci", async (req, res) => { res.end(); return; } - res.status(200).send({ device_name: deviceData }); + res.status(200).send(deviceData); res.end(); return; }); @@ -648,7 +648,7 @@ app.get("/api/instance/pci", async (req, res) => { * request: * - node: String - vm host node id * responses: - * - 200: [PVE PCI Device Object] + * - 200: PVE PCI Device Object * - 401: {auth: false, path: String} * - 500: {error: String} */ @@ -658,32 +658,151 @@ app.get("/api/nodes/pci", async (req, res) => { if (!auth) { return; } // get remaining user resources let userAvailPci = (await getUserResources(req, req.cookies.username)).avail.pci; - // get node pci devices - let nodeAvailPci = (await requestPVE(`/nodes/${req.query.node}/hardware/pci`, "GET", req.body.cookies, null, pveAPIToken)).data.data; - // for each node container, get its config and remove devices which are already used - 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) })); + // get node avail devices + let nodeAvailPci = await getNodeAvailDevices(req.query.node, req.cookies); + nodeAvailPci = nodeAvailPci.filter(nodeAvail => userAvailPci.some((userAvail) => { return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail); })); res.status(200).send(nodeAvailPci); res.end(); 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 * request: diff --git a/src/pve.js b/src/pve.js index 910adc9..cf28874 100644 --- a/src/pve.js +++ b/src/pve.js @@ -104,7 +104,7 @@ export async function getUsedResources(req, resourceMeta) { else if (key.startsWith("hostpci")) { let deviceInfo = await getDeviceInfo(instance.node, instance.type, instance.vmid, config[key].split(",")[0]); 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 }) - return deviceData[0].device_name; + let device = deviceData[0]; + device.subfn = structuredClone(deviceData.slice(1)); + return device; } catch { 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; } \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index 492cadc..62609db 100644 --- a/src/utils.js +++ b/src/utils.js @@ -58,7 +58,8 @@ export async function approveResources(req, username, request) { approved = false; } 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; } }