diff --git a/.gitignore b/.gitignore index b727747..e8142b2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ **/package-lock.json **/node_modules **/localdb.json -**/docs \ No newline at end of file +**/docs +**/config.json \ No newline at end of file diff --git a/config/config.json.template b/config/config.json.template new file mode 100644 index 0000000..74d8255 --- /dev/null +++ b/config/config.json.template @@ -0,0 +1,223 @@ +{ + "backends": { + "pve": { + "import": "pve.js", + "config": { + "url": "https://pve.mydomain.example/api2/json", + "token": { + "user": "proxmoxaas-api", + "realm": "pam", + "id": "token", + "uuid": "token-secret-value" + }, + "root": { + "username": "root@pam", + "password": "rootpassword" + } + } + }, + "localdb": { + "import": "localdb.js", + "config": { + "dbfile": "localdb.json" + } + }, + "paasldap": { + "import": "paasldap.js", + "config": { + "url": "http://paasldap.mydomain.example" + } + } + }, + "handlers": { + "pve": "pve", + "db": "localdb", + "auth": { + "pve": "pve", + "ldap": "paasldap" + } + }, + "application": { + "hostname": "paas.mydomain.example", + "domain": "mydomain.example", + "listenPort": 8081 + }, + "useriso": { + "node": "examplenode1", + "storage": "cephfs" + }, + "resources": { + "cpu": { + "type": "list", + "whitelist": true, + "display": false + }, + "cores": { + "name": "vCPU", + "type": "numeric", + "multiplier": 1, + "base": 1024, + "compact": false, + "unit": "Cores", + "display": true + }, + "memory": { + "name": "RAM", + "type": "numeric", + "multiplier": 1048576, + "base": 1024, + "compact": true, + "unit": "B", + "display": true + }, + "swap": { + "name": "SWAP", + "type": "numeric", + "multiplier": 1048576, + "base": 1024, + "compact": true, + "unit": "B", + "display": true + }, + "local": { + "name": "local", + "type": "storage", + "multiplier": 1, + "base": 1024, + "compact": true, + "unit": "B", + "disks": [ + "rootfs", + "mp", + "sata", + "unused" + ], + "display": true + }, + "cephpl": { + "name": "cephpl", + "type": "storage", + "multiplier": 1, + "base": 1024, + "compact": true, + "unit": "B", + "disks": [ + "rootfs", + "mp", + "sata", + "unused" + ], + "display": true + }, + "network": { + "name": "Network", + "type": "numeric", + "multiplier": 1000000, + "base": 1000, + "compact": true, + "unit": "B/s", + "display": true + }, + "pci": { + "type": "list", + "whitelist": true, + "display": true + } + }, + "clientsync": { + "resourcetypes": [ + "lxc", + "qemu", + "node" + ], + "schemes": { + "always": { + "enabled": true + }, + "hash": { + "enabled": true + }, + "interrupt": { + "min-rate": 1, + "max-rate": 60, + "enabled": true + } + } + }, + "defaultuser": { + "resources": { + "cpu": { + "global": [], + "nodes": {} + }, + "cores": { + "global": { + "max": 0 + }, + "nodes": {} + }, + "memory": { + "global": { + "max": 0 + }, + "nodes": {} + }, + "swap": { + "global": { + "max": 0 + }, + "nodes": {} + }, + "local": { + "global": { + "max": 0 + }, + "nodes": {} + }, + "cephpl": { + "global": { + "max": 0 + }, + "nodes": {} + }, + "network": { + "global": { + "max": 0 + }, + "nodes": {} + }, + "pci": { + "global": [], + "nodes": {} + } + }, + "nodes": [], + "cluster": { + "vmid": { + "min": -1, + "max": -1 + }, + "pool": "" + }, + "templates": { + "instances": { + "lxc": {}, + "qemu": {} + } + }, + "network": { + "lxc": { + "type": "veth", + "bridge": "vmbr0", + "vlan": 10, + "ip": "dhcp", + "ip6": "dhcp" + }, + "qemu": { + "type": "virtio", + "bridge": "vmbr0", + "vlan": 10 + } + } + } +} \ No newline at end of file diff --git a/config/template.localdb.json b/config/template.localdb.json deleted file mode 100644 index 80416af..0000000 --- a/config/template.localdb.json +++ /dev/null @@ -1,332 +0,0 @@ -{ - "static": { - "types": { - "auth": { - "pve": "pve", - "ldap": "ldap" - } - } - }, - "global": { - "application": { - "pveAPI": "https://pve.mydomain.example/api2/json", - "pveAPIToken": { - "user": "proxmoxaas-api", - "realm": "pve", - "id": "token", - "uuid": "token-secret-value" - }, - "pveroot": { - "username": "root@pam", - "password": "rootpassword" - }, - "hostname": "paas.mydomain.example", - "domain": "mydomain.example" - }, - "resources": { - "cpu": { - "type": "list", - "whitelist": true, - "display": false - }, - "cores": { - "name": "vCPU", - "type": "numeric", - "multiplier": 1, - "base": 1024, - "compact": false, - "unit": "Cores", - "display": true - }, - "memory": { - "name": "RAM", - "type": "numeric", - "multiplier": 1048576, - "base": 1024, - "compact": true, - "unit": "B", - "display": true - }, - "swap": { - "name": "SWAP", - "type": "numeric", - "multiplier": 1048576, - "base": 1024, - "compact": true, - "unit": "B", - "display": true - }, - "local": { - "name": "local", - "type": "storage", - "multiplier": 1, - "base": 1024, - "compact": true, - "unit": "B", - "disks": [ - "rootfs", - "mp", - "sata", - "unused" - ], - "display": true - }, - "cephpl": { - "name": "cephpl", - "type": "storage", - "multiplier": 1, - "base": 1024, - "compact": true, - "unit": "B", - "disks": [ - "rootfs", - "mp", - "sata", - "unused" - ], - "display": true - }, - "network": { - "name": "Network", - "type": "numeric", - "multiplier": 1000000, - "base": 1000, - "compact": true, - "unit": "B/s", - "display": true - }, - "pci": { - "type": "list", - "whitelist": true, - "display": true - } - }, - "clientsync": { - "resourcetypes": [ - "lxc", - "qemu", - "node" - ], - "schemes": { - "always": { - "enabled": true - }, - "hash": { - "enabled": true - }, - "interrupt": { - "min-rate": 1, - "max-rate": 60, - "enabled": true - } - } - }, - "useriso": { - "node": "examplenode1", - "storage": "cephfs" - }, - "defaultuser": { - "resources": { - "cpu": { - "global": [], - "nodes": {} - }, - "cores": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "memory": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "swap": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "local": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "cephpl": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "network": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "pci": { - "global": [], - "nodes": {} - } - }, - "nodes": [], - "cluster": { - "vmid": { - "min": -1, - "max": -1 - }, - "pool": "" - }, - "templates": { - "instances": { - "lxc": {}, - "qemu": {} - } - }, - "network": { - "lxc": { - "type": "veth", - "bridge": "vmbr0", - "vlan": 10, - "ip": "dhcp", - "ip6": "dhcp" - }, - "qemu": { - "type": "virtio", - "bridge": "vmbr0", - "vlan": 10 - } - } - } - }, - "users": { - "exampleuser@examplepool": { - "resources": { - "cpu": { - "global": [ - { - "match": "kvm64", - "name": "kvm64", - "max": 1 - }, - { - "match": "host", - "name": "host", - "max": 1 - } - ], - "nodes": {} - }, - "cores": { - "global": { - "max": 128 - }, - "nodes": {} - }, - "memory": { - "global": { - "max": 131072 - }, - "nodes": {} - }, - "swap": { - "global": { - "max": 131072 - }, - "nodes": {} - }, - "local": { - "global": { - "max": 1099511627776 - }, - "nodes": {} - }, - "cephpl": { - "global": { - "max": 1099511627776 - }, - "nodes": {} - }, - "network": { - "global": { - "max": 100000 - }, - "nodes": {} - }, - "pci": { - "global": [], - "nodes": { - "examplenode1": [ - { - "match": "[exampledevice1]", - "name": "exampledevice1", - "max": 1 - } - ], - "examplenode2": [ - { - "match": "[exampledevice2]", - "name": "exampledevice2", - "max": 1 - } - ] - } - } - }, - "nodes": [ - "examplenode1", - "examplenode2" - ], - "cluster": { - "vmid": { - "min": 100, - "max": 199 - }, - "pool": "examplepool" - }, - "templates": { - "instances": { - "lxc": { - "net0": { - "value": "name=eth0,bridge=vmbr0,ip=dhcp,ip6=dhcp,tag=10,type=veth,rate=1000", - "resource": { - "name": "network", - "amount": 1000 - } - } - }, - "qemu": { - "cpu": { - "value": "host", - "resource": null - }, - "net0": { - "value": "virtio,bridge=vmbr0,tag=10,rate=1000", - "resource": { - "name": "network", - "amount": 1000 - } - } - } - }, - "network": { - "lxc": { - "type": "veth", - "bridge": "vmbr0", - "vlan": 10, - "ip": "dhcp", - "ip6": "dhcp" - }, - "qemu": { - "type": "virtio", - "bridge": "vmbr0", - "vlan": 10 - } - } - } - } - } -} \ No newline at end of file diff --git a/src/backends/backends.js b/src/backends/backends.js new file mode 100644 index 0000000..fcada40 --- /dev/null +++ b/src/backends/backends.js @@ -0,0 +1,28 @@ +import path from "path"; +import url from "url"; + +export default async () => { + const backends = {}; + for (const name in global.config.backends) { + // get files and config + const target = global.config.backends[name].import; + const config = global.config.backends[name].config; + // get import path + const thisPath = path.dirname(url.fileURLToPath(import.meta.url)); + const fromPath = path.relative(".", path.dirname(url.fileURLToPath(import.meta.url))); + const targetPath = path.relative(".", `${fromPath}/${target}`); + const importPath = `./${path.relative(thisPath, targetPath)}`; + // import and add to list of imported handlers + const Backend = (await import(importPath)).default; + backends[name] = new Backend(config); + console.log(`backends: initialized backend ${name} from ${importPath}`); + } + // assign backends to handlers depending + const handlers = global.config.handlers; + global.pve = backends[handlers.pve]; + global.db = backends[handlers.db]; + global.auth = handlers.auth; + Object.keys(global.auth).forEach((e) => { + global.auth[e] = backends[global.auth[e]]; + }); +}; diff --git a/src/localdb.js b/src/backends/localdb.js similarity index 67% rename from src/localdb.js rename to src/backends/localdb.js index 8fa2baa..c26aa10 100644 --- a/src/localdb.js +++ b/src/backends/localdb.js @@ -1,18 +1,16 @@ import { readFileSync, writeFileSync } from "fs"; import { exit } from "process"; -class LocalDB { +export default class LocalDB { #path = null; #data = null; - constructor (path) { + #defaultuser = null; + constructor (config) { + const path = config.dbfile; try { this.#path = path; this.#load(); - this.pveAPI = this.getGlobal().application.pveAPI; - this.pveAPIToken = this.getGlobal().application.pveAPIToken; - this.listenPort = this.getGlobal().application.listenPort; - this.hostname = this.getGlobal().application.hostname; - this.domain = this.getGlobal().application.domain; + this.#defaultuser = global.config.defaultuser; } catch { console.log(`Error: ${path} was not found. Please follow the directions in the README to initialize localdb.json.`); @@ -34,21 +32,8 @@ class LocalDB { writeFileSync(this.#path, JSON.stringify(this.#data)); } - getStatic () { - return this.#data.static; - } - - getGlobal () { - return this.#data.global; - } - - setGloal (config) { - this.#data.global = config; - this.#save(); - } - addUser (username, config = null) { - config = config || this.#data.global.defaultuser; + config = config || this.#defaultuser; this.#data.users[username] = config; this.#save(); } @@ -84,5 +69,3 @@ class LocalDB { } } } - -export default LocalDB; diff --git a/src/backends/paasldap.js b/src/backends/paasldap.js new file mode 100644 index 0000000..38239a0 --- /dev/null +++ b/src/backends/paasldap.js @@ -0,0 +1,3 @@ +export default class PAASLDAP { + +} diff --git a/src/backends/pve.js b/src/backends/pve.js new file mode 100644 index 0000000..f13f5b8 --- /dev/null +++ b/src/backends/pve.js @@ -0,0 +1,171 @@ +import axios from "axios"; + +export default class PVE { + #pveAPIURL = null; + #pveAPIToken = null; + #pveRoot = null; + + constructor (config) { + this.#pveAPIURL = config.url; + this.#pveAPIToken = config.token; + this.#pveRoot = config.root; + } + + /** + * 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 path. + * @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" + } + }; + + if (auth && auth.cookies) { + content.headers.CSRFPreventionToken = auth.cookies.CSRFPreventionToken; + content.headers.Cookie = `PVEAuthCookie=${auth.cookies.PVEAuthCookie}; CSRFPreventionToken=${auth.cookies.CSRFPreventionToken}`; + } + else if (auth && auth.token) { + auth.token = this.#pveAPIToken; + content.headers.Authorization = `PVEAPIToken=${auth.token.user}@${auth.token.realm}!${auth.token.id}=${auth.token.uuid}`; + } + + if (body) { + content.data = JSON.parse(body); + } + + try { + return await axios.request(url, content); + } + catch (error) { + return error.response; + } + } + + /** + * 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.data.data && typeof (result.data.data) === "string" && result.data.data.startsWith("UPID:")) { + const upid = result.data.data; + let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true }); + while (taskStatus.data.data.status !== "stopped") { + await waitFor(1000); + taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true }); + } + if (taskStatus.data.data.exitstatus === "OK") { + const result = taskStatus.data.data; + const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true }); + result.log = taskLog.data.data; + res.status(200).send(result); + res.end(); + } + else { + const result = taskStatus.data.data; + const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true }); + result.log = taskLog.data.data; + res.status(500).send(result); + res.end(); + } + } + else { + res.status(result.status).send(result.data); + res.end(); + } + } + + /** + * Get meta data for a specific disk. Adds info that is not normally available in a instance's config. + * @param {string} node containing the query disk. + * @param {string} config of instance with query disk. + * @param {string} disk name of the query disk, ie. sata0. + * @returns {Objetc} k-v pairs of specific disk data, including storage and size of unused disks. + */ + async getDiskInfo (node, config, disk) { + try { + const storageID = config[disk].split(":")[0]; + const volID = config[disk].split(",")[0]; + const volInfo = await this.requestPVE(`/nodes/${node}/storage/${storageID}/content/${volID}`, "GET", { token: true }); + volInfo.data.data.storage = storageID; + return volInfo.data.data; + } + catch { + return null; + } + } + + /** + * Get meta data for a specific pci device. Adds info that is not normally available in a instance's config. + * @param {string} node containing the query device. + * @param {string} qid pci bus id number of the query device, ie. 89ab:cd:ef.0. + * @returns {Object} k-v pairs of specific device data, including device name and manufacturer. + */ + async getDeviceInfo (node, qid) { + try { + const result = (await this.requestPVE(`/nodes/${node}/hardware/pci`, "GET", { token: true })).data.data; + const deviceData = []; + result.forEach((element) => { + if (element.id.startsWith(qid)) { + deviceData.push(element); + } + }); + deviceData.sort((a, b) => { + return a.id < b.id; + }); + const device = deviceData[0]; + device.subfn = structuredClone(deviceData.slice(1)); + return device; + } + catch { + return null; + } + } + + /** + * Get available devices on specific node. + * @param {string} node to get devices from. + * @returns {Array.} array of k-v pairs of specific device data, including device name and manufacturer, which are available on the specified node. + */ + async getNodeAvailDevices (node) { + // get node pci devices + let nodeAvailPci = this.requestPVE(`/nodes/${node}/hardware/pci`, "GET", { token: true }); + // for each node container, get its config and remove devices which are already used + const vms = (await this.requestPVE(`/nodes/${node}/qemu`, "GET", { token: true })).data.data; + + const promises = []; + for (const vm of vms) { + promises.push(this.requestPVE(`/nodes/${node}/qemu/${vm.vmid}/config`, "GET", { token: true })); + } + const configs = await Promise.all(promises); + configs.forEach((e, i) => { + configs[i] = e.data.data; + }); + + nodeAvailPci = (await nodeAvailPci).data.data; + + for (const config of configs) { + Object.keys(config).forEach((key) => { + if (key.startsWith("hostpci")) { + const deviceID = config[key].split(",")[0]; + nodeAvailPci = nodeAvailPci.filter(element => !element.id.includes(deviceID)); + } + }); + } + return nodeAvailPci; + } +} diff --git a/src/main.js b/src/main.js index 48a7b32..f3c3154 100644 --- a/src/main.js +++ b/src/main.js @@ -3,39 +3,35 @@ import bodyParser from "body-parser"; import cookieParser from "cookie-parser"; import cors from "cors"; import morgan from "morgan"; - -import _package from "./package.js"; -import * as pve from "./pve.js"; -import * as utils from "./utils.js"; - import parseArgs from "minimist"; + +import * as utils from "./utils.js"; +import _backends from "./backends/backends.js"; + global.argv = parseArgs(process.argv.slice(2), { default: { package: "package.json", - listenPort: 8081, - db: "./localdb.js", // relative to main.js - dbconfig: "config/localdb.json" + config: "config/config.json" } }); -global.package = _package(global.argv.package); -global.pve = pve; global.utils = utils; -const DB = (await import(global.argv.db)).default; -global.db = new DB(global.argv.dbconfig); +global.package = global.utils.readJSONFile(global.argv.package); +global.config = global.utils.readJSONFile(global.argv.config); +await _backends(); const app = express(); global.app = app; app.use(bodyParser.urlencoded({ extended: true })); app.use(cookieParser()); -app.use(cors({ origin: global.db.hostname })); +app.use(cors({ origin: global.config.application.hostname })); app.use(morgan("combined")); -global.server = app.listen(global.argv.listenPort, () => { - console.log(`proxmoxaas-api v${global.package.version} listening on port ${global.argv.listenPort}`); +global.server = app.listen(global.config.application.listenPort, () => { + console.log(`proxmoxaas-api v${global.package.version} listening on port ${global.config.application.listenPort}`); }); -global.utils.recursiveImport(app, "/api", "routes"); +global.utils.recursiveImportRoutes(app, "/api", "routes"); /** * GET - get API version diff --git a/src/package.js b/src/package.js deleted file mode 100644 index fa00582..0000000 --- a/src/package.js +++ /dev/null @@ -1,11 +0,0 @@ -import { readFileSync } from "fs"; -import { exit } from "process"; -export default (path) => { - try { - return JSON.parse(readFileSync(path)); - } - catch (e) { - console.log(`Error: ${path} was not found.`); - exit(1); - } -}; diff --git a/src/pve.js b/src/pve.js deleted file mode 100644 index fdbfe72..0000000 --- a/src/pve.js +++ /dev/null @@ -1,163 +0,0 @@ -import axios from "axios"; - -/** - * 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 path. - * @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. - */ -export async function requestPVE (path, method, auth = null, body = null) { - const pveAPI = global.db.pveAPI; - const url = `${pveAPI}${path}`; - const content = { - method, - mode: "cors", - credentials: "include", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - } - }; - - if (auth && auth.cookies) { - content.headers.CSRFPreventionToken = auth.cookies.CSRFPreventionToken; - content.headers.Cookie = `PVEAuthCookie=${auth.cookies.PVEAuthCookie}; CSRFPreventionToken=${auth.cookies.CSRFPreventionToken}`; - } - else if (auth && auth.token) { - content.headers.Authorization = `PVEAPIToken=${auth.token.user}@${auth.token.realm}!${auth.token.id}=${auth.token.uuid}`; - } - - if (body) { - content.data = JSON.parse(body); - } - - try { - return await axios.request(url, content); - } - catch (error) { - return error.response; - } -} - -/** - * 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. - */ -export async function handleResponse (node, result, res) { - const pveAPIToken = global.db.pveAPIToken; - const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); - if (result.data.data && typeof (result.data.data) === "string" && result.data.data.startsWith("UPID:")) { - const upid = result.data.data; - let taskStatus = await requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: pveAPIToken }); - while (taskStatus.data.data.status !== "stopped") { - await waitFor(1000); - taskStatus = await requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: pveAPIToken }); - } - if (taskStatus.data.data.exitstatus === "OK") { - const result = taskStatus.data.data; - const taskLog = await requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: pveAPIToken }); - result.log = taskLog.data.data; - res.status(200).send(result); - res.end(); - } - else { - const result = taskStatus.data.data; - const taskLog = await requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: pveAPIToken }); - result.log = taskLog.data.data; - res.status(500).send(result); - res.end(); - } - } - else { - res.status(result.status).send(result.data); - res.end(); - } -} - -/** - * Get meta data for a specific disk. Adds info that is not normally available in a instance's config. - * @param {string} node containing the query disk. - * @param {string} config of instance with query disk. - * @param {string} disk name of the query disk, ie. sata0. - * @returns {Objetc} k-v pairs of specific disk data, including storage and size of unused disks. - */ -export async function getDiskInfo (node, config, disk) { - const pveAPIToken = global.db.pveAPIToken; - try { - const storageID = config[disk].split(":")[0]; - const volID = config[disk].split(",")[0]; - const volInfo = await requestPVE(`/nodes/${node}/storage/${storageID}/content/${volID}`, "GET", { token: pveAPIToken }); - volInfo.data.data.storage = storageID; - return volInfo.data.data; - } - catch { - return null; - } -} - -/** - * Get meta data for a specific pci device. Adds info that is not normally available in a instance's config. - * @param {string} node containing the query device. - * @param {string} qid pci bus id number of the query device, ie. 89ab:cd:ef.0. - * @returns {Object} k-v pairs of specific device data, including device name and manufacturer. - */ -export async function getDeviceInfo (node, qid) { - const pveAPIToken = global.db.pveAPIToken; - try { - const result = (await requestPVE(`/nodes/${node}/hardware/pci`, "GET", { token: pveAPIToken })).data.data; - const deviceData = []; - result.forEach((element) => { - if (element.id.startsWith(qid)) { - deviceData.push(element); - } - }); - deviceData.sort((a, b) => { - return a.id < b.id; - }); - const device = deviceData[0]; - device.subfn = structuredClone(deviceData.slice(1)); - return device; - } - catch { - return null; - } -} - -/** - * Get available devices on specific node. - * @param {string} node to get devices from. - * @returns {Array.} array of k-v pairs of specific device data, including device name and manufacturer, which are available on the specified node. - */ -export async function getNodeAvailDevices (node) { - const pveAPIToken = global.db.pveAPIToken; - // get node pci devices - let nodeAvailPci = requestPVE(`/nodes/${node}/hardware/pci`, "GET", { token: pveAPIToken }); - // for each node container, get its config and remove devices which are already used - const vms = (await requestPVE(`/nodes/${node}/qemu`, "GET", { token: pveAPIToken })).data.data; - - const promises = []; - for (const vm of vms) { - promises.push(requestPVE(`/nodes/${node}/qemu/${vm.vmid}/config`, "GET", { token: pveAPIToken })); - } - const configs = await Promise.all(promises); - configs.forEach((e, i) => { - configs[i] = e.data.data; - }); - - nodeAvailPci = (await nodeAvailPci).data.data; - - for (const config of configs) { - Object.keys(config).forEach((key) => { - if (key.startsWith("hostpci")) { - const deviceID = config[key].split(",")[0]; - nodeAvailPci = nodeAvailPci.filter(element => !element.id.includes(deviceID)); - } - }); - } - return nodeAvailPci; -} diff --git a/src/routes/auth.js b/src/routes/auth.js index ad18995..67c23f7 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -1,11 +1,7 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); ; -const db = global.db; -const domain = global.db.domain; const checkAuth = global.utils.checkAuth; -const requestPVE = global.pve.requestPVE; -const pveAPIToken = global.db.pveAPIToken; /** * GET - check authentication @@ -31,12 +27,13 @@ router.get("/", async (req, res) => { * - 401: {auth: false} */ router.post("/ticket", async (req, res) => { - const response = await requestPVE("/access/ticket", "POST", null, JSON.stringify(req.body)); + const response = await global.pve.requestPVE("/access/ticket", "POST", null, JSON.stringify(req.body)); if (!(response.status === 200)) { res.status(response.status).send({ auth: false }); res.end(); return; } + const domain = global.config.application.domain; const ticket = response.data.data.ticket; const csrftoken = response.data.data.CSRFPreventionToken; const username = response.data.data.username; @@ -55,6 +52,7 @@ router.post("/ticket", async (req, res) => { */ router.delete("/ticket", async (req, res) => { const expire = new Date(0); + const domain = global.config.application.domain; res.cookie("PVEAuthCookie", "", { domain, path: "/", httpOnly: true, secure: true, expires: expire }); res.cookie("CSRFPreventionToken", "", { domain, path: "/", httpOnly: true, secure: true, expires: expire }); res.cookie("username", "", { domain, path: "/", httpOnly: true, secure: true, expires: expire }); @@ -69,16 +67,19 @@ router.post("/password", async (req, res) => { }; const userRealm = params.userid.split("@").at(-1); - const domains = (await requestPVE("/access/domains", "GET", pveAPIToken)).data.data; + const domains = (await global.pve.requestPVE("/access/domains", "GET", { token: true })).data.data; const realm = domains.find((e) => e.realm === userRealm); - const authTypes = db.getStatic().types.auth; - const realmType = authTypes[realm.type]; + const authHandlers = global.config.handlers.auth; + const handlerType = authHandlers[realm.type]; - if (realmType === "pve") { - const response = await requestPVE("/access/password", "PUT", { cookies: req.cookies }, JSON.stringify(params)); + if (handlerType === "pve") { + const response = await global.pve.requestPVE("/access/password", "PUT", { cookies: req.cookies }, JSON.stringify(params)); res.status(response.status).send(response.data); } + else if (handlerType === "paasldap") { + res.status(501).send({ error: `Auth type ${handlerType} not implemented yet.` }); + } else { - res.status(501).send({ error: `Auth type ${realmType} not implemented yet.` }); + res.status(501).send({ error: `Auth type ${handlerType} not implemented yet.` }); } }); diff --git a/src/routes/cluster.js b/src/routes/cluster.js index 68a4ab2..4e7df76 100644 --- a/src/routes/cluster.js +++ b/src/routes/cluster.js @@ -2,12 +2,8 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); const db = global.db; -const requestPVE = global.pve.requestPVE; -const handleResponse = global.pve.handleResponse; const checkAuth = global.utils.checkAuth; const approveResources = global.utils.approveResources; -const pveAPIToken = global.db.pveAPIToken; -const getNodeAvailDevices = global.pve.getNodeAvailDevices; const getUserResources = global.utils.getUserResources; const nodeRegexP = "[\\w-]+"; @@ -16,7 +12,7 @@ const vmidRegexP = "\\d+"; const basePath = `/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP})`; -global.utils.recursiveImport(router, basePath, "cluster", import.meta.url); +global.utils.recursiveImportRoutes(router, basePath, "cluster", import.meta.url); /** * GET - get available pcie devices given node and user @@ -46,7 +42,7 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => { // get remaining user resources const userAvailPci = (await getUserResources(req, req.cookies.username)).pci.nodes[params.node]; // get node avail devices - let nodeAvailPci = await getNodeAvailDevices(params.node, req.cookies); + let nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies); nodeAvailPci = nodeAvailPci.filter(nodeAvail => userAvailPci.some((userAvail) => { return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0; })); @@ -88,7 +84,7 @@ router.post(`${basePath}/resources`, async (req, res) => { return; } // get current config - const currentConfig = await requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: pveAPIToken }); + const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true }); const request = { cores: Number(params.cores) - Number(currentConfig.data.data.cores), memory: Number(params.memory) - Number(currentConfig.data.data.memory) @@ -117,8 +113,8 @@ router.post(`${basePath}/resources`, async (req, res) => { action = JSON.stringify(action); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action - const result = await requestPVE(`${vmpath}/config`, method, { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -229,8 +225,8 @@ router.post(`${basePath}/create`, async (req, res) => { } action = JSON.stringify(action); // commit action - const result = await requestPVE(`/nodes/${params.node}/${params.type}`, "POST", { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}`, "POST", { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -257,6 +253,6 @@ router.delete(`${basePath}/delete`, async (req, res) => { return; } // commit action - const result = await requestPVE(vmpath, "DELETE", { token: pveAPIToken }); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(vmpath, "DELETE", { token: true }); + await global.pve.handleResponse(params.node, result, res); }); diff --git a/src/routes/cluster/disk.js b/src/routes/cluster/disk.js index 006f424..c708f18 100644 --- a/src/routes/cluster/disk.js +++ b/src/routes/cluster/disk.js @@ -1,13 +1,8 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); -const db = global.db; -const requestPVE = global.pve.requestPVE; -const handleResponse = global.pve.handleResponse; -const getDiskInfo = global.pve.getDiskInfo; const checkAuth = global.utils.checkAuth; const approveResources = global.utils.approveResources; -const pveAPIToken = global.db.pveAPIToken; /** * POST - detach mounted disk from instance @@ -37,7 +32,7 @@ router.post("/:disk/detach", async (req, res) => { return; } // get current config - const config = (await requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; + const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; // disk must exist if (!config[params.disk]) { res.status(500).send({ error: `Disk ${params.disk} does not exist.` }); @@ -52,8 +47,8 @@ router.post("/:disk/detach", async (req, res) => { } const action = JSON.stringify({ delete: params.disk }); const method = params.type === "qemu" ? "POST" : "PUT"; - const result = await requestPVE(`${vmpath}/config`, method, { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -86,7 +81,7 @@ router.post("/:disk/attach", async (req, res) => { return; } // get current config - const config = (await requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; + const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; // disk must exist if (!config[`unused${params.source}`]) { res.status(403).send({ error: `Requested disk unused${params.source} does not exist.` }); @@ -94,8 +89,8 @@ router.post("/:disk/attach", async (req, res) => { return; } // target disk must be allowed according to source disk's storage options - const diskConfig = await getDiskInfo(params.node, config, `unused${params.source}`); // get target disk - const resourceConfig = db.getGlobal().resources; + const diskConfig = await global.pve.getDiskInfo(params.node, config, `unused${params.source}`); // get target disk + const resourceConfig = global.config.resources; if (!resourceConfig[diskConfig.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) { res.status(500).send({ error: `Requested target ${params.disk} is not in allowed list [${resourceConfig[diskConfig.storage].disks}].` }); res.end(); @@ -107,8 +102,8 @@ router.post("/:disk/attach", async (req, res) => { action = JSON.stringify(action); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action - const result = await requestPVE(`${vmpath}/config`, method, { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -142,9 +137,9 @@ router.post("/:disk/resize", async (req, res) => { return; } // get current config - const config = (await requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; + const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; // check disk existence - const diskConfig = await getDiskInfo(params.node, config, params.disk); // get target disk + const diskConfig = await global.pve.getDiskInfo(params.node, config, params.disk); // get target disk if (!diskConfig) { // exit if disk does not exist res.status(500).send({ error: `requested disk ${params.disk} does not exist.` }); res.end(); @@ -162,8 +157,8 @@ router.post("/:disk/resize", async (req, res) => { } // action approved, commit to action const action = JSON.stringify({ disk: params.disk, size: `+${params.size}G` }); - const result = await requestPVE(`${vmpath}/resize`, "PUT", { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/resize`, "PUT", { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -199,9 +194,9 @@ router.post("/:disk/move", async (req, res) => { return; } // get current config - const config = (await requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; + const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; // check disk existence - const diskConfig = await getDiskInfo(params.node, config, params.disk); // get target disk + const diskConfig = await global.pve.getDiskInfo(params.node, config, params.disk); // get target disk if (!diskConfig) { // exit if disk does not exist res.status(500).send({ error: `requested disk ${params.disk} does not exist.` }); res.end(); @@ -231,8 +226,8 @@ router.post("/:disk/move", async (req, res) => { action = JSON.stringify(action); const route = params.type === "qemu" ? "move_disk" : "move_volume"; // commit action - const result = await requestPVE(`${vmpath}/${route}`, "POST", { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/${route}`, "POST", { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -263,7 +258,7 @@ router.delete("/:disk/delete", async (req, res) => { return; } // get current config - const config = (await requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; + const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; // disk must exist if (!config[params.disk]) { res.status(403).send({ error: `Requested disk unused${params.source} does not exist.` }); @@ -280,8 +275,8 @@ router.delete("/:disk/delete", async (req, res) => { const action = JSON.stringify({ delete: params.disk }); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action - const result = await requestPVE(`${vmpath}/config`, method, { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -318,7 +313,7 @@ router.post("/:disk/create", async (req, res) => { return; } // get current config - const config = (await requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; + const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; // disk must not exist if (config[params.disk]) { res.status(403).send({ error: `Requested disk ${params.disk} already exists.` }); @@ -337,7 +332,7 @@ router.post("/:disk/create", async (req, res) => { return; } // target disk must be allowed according to storage options - const resourceConfig = db.getGlobal().resources; + const resourceConfig = global.config.resources; if (!resourceConfig[params.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) { res.status(500).send({ error: `Requested target ${params.disk} is not in allowed list [${resourceConfig[params.storage].disks}].` }); res.end(); @@ -358,6 +353,6 @@ router.post("/:disk/create", async (req, res) => { action = JSON.stringify(action); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action - const result = await requestPVE(`${vmpath}/config`, method, { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); diff --git a/src/routes/cluster/net.js b/src/routes/cluster/net.js index 0dc2e07..2153357 100644 --- a/src/routes/cluster/net.js +++ b/src/routes/cluster/net.js @@ -2,11 +2,8 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); ; const db = global.db; -const requestPVE = global.pve.requestPVE; -const handleResponse = global.pve.handleResponse; const checkAuth = global.utils.checkAuth; const approveResources = global.utils.approveResources; -const pveAPIToken = global.db.pveAPIToken; /** * POST - create new virtual network interface @@ -41,7 +38,7 @@ router.post("/:netid/create", async (req, res) => { return; } // get current config - const currentConfig = await requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: pveAPIToken }); + const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true }); // net interface must not exist if (currentConfig.data.data[`net${params.netid}`]) { res.status(500).send({ error: `Network interface net${params.netid} already exists.` }); @@ -74,8 +71,8 @@ router.post("/:netid/create", async (req, res) => { action = JSON.stringify(action); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action - const result = await requestPVE(`${vmpath}/config`, method, { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -109,7 +106,7 @@ router.post("/:netid/modify", async (req, res) => { return; } // get current config - const currentConfig = await requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: pveAPIToken }); + const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true }); // net interface must already exist if (!currentConfig.data.data[`net${params.netid}`]) { res.status(500).send({ error: `Network interface net${params.netid} does not exist.` }); @@ -133,8 +130,8 @@ router.post("/:netid/modify", async (req, res) => { action = JSON.stringify(action); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action - const result = await requestPVE(`${vmpath}/config`, method, { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -165,7 +162,7 @@ router.delete("/:netid/delete", async (req, res) => { return; } // get current config - const currentConfig = await requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: pveAPIToken }); + const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true }); // net interface must already exist if (!currentConfig.data.data[`net${params.netid}`]) { res.status(500).send({ error: `Network interface net${params.netid} does not exist.` }); @@ -176,6 +173,6 @@ router.delete("/:netid/delete", async (req, res) => { const action = JSON.stringify({ delete: `net${params.netid}` }); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action - const result = await requestPVE(`${vmpath}/config`, method, { token: pveAPIToken }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); + await global.pve.handleResponse(params.node, result, res); }); diff --git a/src/routes/cluster/pci.js b/src/routes/cluster/pci.js index a5b3d27..135b083 100644 --- a/src/routes/cluster/pci.js +++ b/src/routes/cluster/pci.js @@ -1,14 +1,8 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); ; -const db = global.db; -const requestPVE = global.pve.requestPVE; -const handleResponse = global.pve.handleResponse; -const getDeviceInfo = global.pve.getDeviceInfo; -const getNodeAvailDevices = global.pve.getNodeAvailDevices; const checkAuth = global.utils.checkAuth; const approveResources = global.utils.approveResources; -const pveAPIToken = global.db.pveAPIToken; /** * GET - get instance pcie device data @@ -37,7 +31,7 @@ router.get("/:hostpci", async (req, res) => { return; } // check device is in instance config - const config = (await requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; + const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; if (!config[`hostpci${params.hostpci}`]) { res.status(500).send({ error: `Could not find hostpci${params.hostpci} in ${params.vmid}.` }); res.end(); @@ -45,7 +39,7 @@ router.get("/:hostpci", async (req, res) => { } const device = config[`hostpci${params.hostpci}`].split(",")[0]; // get node's pci devices - const deviceData = await getDeviceInfo(params.node, device); + const deviceData = await global.pve.getDeviceInfo(params.node, device); if (!deviceData) { res.status(500).send({ error: `Could not find hostpci${params.hostpci}=${device} in ${params.node}.` }); res.end(); @@ -95,8 +89,8 @@ router.post("/:hostpci/modify", async (req, res) => { // force all functions params.device = params.device.split(".")[0]; // get instance config to check if device has not changed - const config = (await requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: pveAPIToken })).data.data; - const currentDeviceData = await getDeviceInfo(params.node, config[`hostpci${params.hostpci}`].split(",")[0]); + const config = (await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true })).data.data; + const currentDeviceData = await global.pve.getDeviceInfo(params.node, config[`hostpci${params.hostpci}`].split(",")[0]); if (!currentDeviceData) { res.status(500).send({ error: `No device in hostpci${params.hostpci}.` }); res.end(); @@ -105,7 +99,7 @@ router.post("/:hostpci/modify", async (req, res) => { // only check user and node availability if base id is different if (currentDeviceData.id.split(".")[0] !== params.device) { // setup request - const deviceData = await getDeviceInfo(params.node, params.device); + const deviceData = await global.pve.getDeviceInfo(params.node, params.device); const request = { pci: deviceData.device_name }; // check resource approval if (!await approveResources(req, req.cookies.username, request, params.node)) { @@ -114,7 +108,7 @@ router.post("/:hostpci/modify", async (req, res) => { return; } // check node availability - const nodeAvailPci = await getNodeAvailDevices(params.node, req.cookies); + const nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies); if (!nodeAvailPci.some(element => element.id.split(".")[0] === params.device)) { res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` }); res.end(); @@ -126,7 +120,7 @@ router.post("/:hostpci/modify", async (req, res) => { action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`; action = JSON.stringify(action); // commit action - const rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getGlobal().application.pveroot)); + const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, JSON.stringify(global.config.backends.pve.config.root)); if (!(rootauth.status === 200)) { res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." }); res.end(); @@ -136,8 +130,8 @@ router.post("/:hostpci/modify", async (req, res) => { PVEAuthCookie: rootauth.data.data.ticket, CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken }; - const result = await requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -178,13 +172,13 @@ router.post("/create", async (req, res) => { // force all functions params.device = params.device.split(".")[0]; // get instance config to find next available hostpci slot - const config = requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { cookies: params.cookies }); + const config = global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { cookies: params.cookies }); let hostpci = 0; while (config[`hostpci${hostpci}`]) { hostpci++; } // setup request - const deviceData = await getDeviceInfo(params.node, params.device); + const deviceData = await global.pve.getDeviceInfo(params.node, params.device); const request = { pci: deviceData.device_name }; @@ -195,7 +189,7 @@ router.post("/create", async (req, res) => { return; } // check node availability - const nodeAvailPci = await getNodeAvailDevices(params.node, req.cookies); + const nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies); if (!nodeAvailPci.some(element => element.id.split(".")[0] === params.device)) { res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` }); res.end(); @@ -206,7 +200,7 @@ router.post("/create", async (req, res) => { action[`hostpci${hostpci}`] = `${params.device},pcie=${params.pcie}`; action = JSON.stringify(action); // commit action - const rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getGlobal().application.pveroot)); + const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, JSON.stringify(global.config.backends.pve.config.root)); if (!(rootauth.status === 200)) { res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." }); res.end(); @@ -216,8 +210,8 @@ router.post("/create", async (req, res) => { PVEAuthCookie: rootauth.data.data.ticket, CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken }; - const result = await requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action); + await global.pve.handleResponse(params.node, result, res); }); /** @@ -254,7 +248,7 @@ router.delete("/:hostpci/delete", async (req, res) => { return; } // check device is in instance config - const config = (await requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; + const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data; if (!config[`hostpci${params.hostpci}`]) { res.status(500).send({ error: `Could not find hostpci${params.hostpci} in ${params.vmid}.` }); res.end(); @@ -263,7 +257,7 @@ router.delete("/:hostpci/delete", async (req, res) => { // setup action const action = JSON.stringify({ delete: `hostpci${params.hostpci}` }); // commit action, need to use root user here because proxmox api only allows root to modify hostpci for whatever reason - const rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getGlobal().application.pveroot)); + const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, JSON.stringify(global.config.backends.pve.config.root)); if (!(rootauth.status === 200)) { res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." }); res.end(); @@ -273,6 +267,6 @@ router.delete("/:hostpci/delete", async (req, res) => { PVEAuthCookie: rootauth.data.data.ticket, CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken }; - const result = await requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action); - await handleResponse(params.node, result, res); + const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action); + await global.pve.handleResponse(params.node, result, res); }); diff --git a/src/routes/cluster/user.js b/src/routes/cluster/user.js new file mode 100644 index 0000000..151c22c --- /dev/null +++ b/src/routes/cluster/user.js @@ -0,0 +1,75 @@ +import { Router } from "express"; +export const router = Router({ mergeParams: true }); ; + +const config = global.config; +const checkAuth = global.utils.checkAuth; +const getUserResources = global.utils.getUserResources; + +/** + * GET - get db user resource information including allocated, free, and maximum resource values along with resource metadata + * responses: + * - 200: {avail: Object, max: Object, used: Object, resources: Object} + * - 401: {auth: false} + */ +router.get("/dynamic/resources", async (req, res) => { + // check auth + const auth = await checkAuth(req.cookies, res); + if (!auth) { + return; + } + const resources = await getUserResources(req, req.cookies.username); + res.status(200).send(resources); +}); + +/** + * GET - get db user configuration by key + * request: + * - key: string - user config key + * responses: + * - 200: Object + * - 401: {auth: false} + * - 401: {auth: false, error: string} + */ +router.get("/config/:key", async (req, res) => { + const params = { + key: req.params.key + }; + // check auth + const auth = await checkAuth(req.cookies, res); + if (!auth) { + return; + } + const allowKeys = ["resources", "cluster", "nodes"]; + if (allowKeys.includes(params.key)) { + const config = global.db.getUser(req.cookies.username); + res.status(200).send(config[params.key]); + } + else { + res.status(401).send({ auth: false, error: `User is not authorized to access /user/config/${params.key}.` }); + } +}); + +/** + * GET - get user accessible iso files + * response: + * - 200: Array. + * - 401: {auth: false} + */ +router.get("/iso", async (req, res) => { + // check auth + const auth = await checkAuth(req.cookies, res); + if (!auth) { + return; + } + // get user iso config + const userIsoConfig = config.useriso; + // get all isos + const isos = (await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=iso`, "GET", { token: true })).data.data; + const userIsos = []; + isos.forEach((iso) => { + iso.name = iso.volid.replace(`${userIsoConfig.storage}:iso/`, ""); + userIsos.push(iso); + }); + userIsos.sort(); + res.status(200).send(userIsos); +}); diff --git a/src/routes/global.js b/src/routes/global.js index efd32ac..4b3393d 100644 --- a/src/routes/global.js +++ b/src/routes/global.js @@ -1,7 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); -const db = global.db; const checkAuth = global.utils.checkAuth; /** @@ -20,7 +19,7 @@ router.get("/config/:key", async (req, res) => { } const allowKeys = ["resources"]; if (allowKeys.includes(params.key)) { - const config = db.getGlobal(); + const config = global.config; res.status(200).send(config[params.key]); } else { diff --git a/src/routes/proxmox.js b/src/routes/proxmox.js index db37e5e..54d1fdf 100644 --- a/src/routes/proxmox.js +++ b/src/routes/proxmox.js @@ -1,8 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); ; -const requestPVE = global.pve.requestPVE; - /** * GET - proxy proxmox api without privilege elevation * request and responses passed through to/from proxmox @@ -10,7 +8,7 @@ const requestPVE = global.pve.requestPVE; router.get("/*", async (req, res) => { // proxy endpoint for GET proxmox api with no token console.log(req.url); const path = req.url.replace("/api/proxmox", ""); - const result = await requestPVE(path, "GET", { cookies: req.cookies }); + const result = await global.pve.requestPVE(path, "GET", { cookies: req.cookies }); res.status(result.status).send(result.data); }); @@ -20,6 +18,6 @@ router.get("/*", async (req, res) => { // proxy endpoint for GET proxmox api wit */ router.post("/*", async (req, res) => { // proxy endpoint for POST proxmox api with no token const path = req.url.replace("/api/proxmox", ""); - const result = await requestPVE(path, "POST", { cookies: req.cookies }, JSON.stringify(req.body)); // need to stringify body because of other issues + const result = await global.pve.requestPVE(path, "POST", { cookies: req.cookies }, JSON.stringify(req.body)); // need to stringify body because of other issues res.status(result.status).send(result.data); }); diff --git a/src/routes/sync.js b/src/routes/sync.js index c3de968..d1f95fb 100644 --- a/src/routes/sync.js +++ b/src/routes/sync.js @@ -4,10 +4,7 @@ import * as cookie from "cookie"; import { Router } from "express"; export const router = Router({ mergeParams: true }); ; -const requestPVE = global.pve.requestPVE; const checkAuth = global.utils.checkAuth; -const db = global.db; -const pveAPIToken = global.db.pveAPIToken; const getObjectHash = global.utils.getObjectHash; const getTimeLeft = global.utils.getTimeLeft; @@ -24,8 +21,8 @@ let prevState = {}; // target ms value let targetMSTime = null; -const schemes = db.getGlobal().clientsync.schemes; -const resourceTypes = db.getGlobal().clientsync.resourcetypes; +const schemes = global.config.clientsync.schemes; +const resourceTypes = global.config.clientsync.resourcetypes; /** * GET - get list of supported synchronization schemes * responses: @@ -55,7 +52,7 @@ if (schemes.hash.enabled) { return; } // get current cluster resources - const status = (await requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data; + const status = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data; // filter out just state information of resources that are needed const state = extractClusterState(status, resourceTypes); res.status(200).send(getObjectHash(state)); @@ -158,13 +155,13 @@ if (schemes.interrupt.enabled) { // handle the wss upgrade request global.server.on("upgrade", async (req, socket, head) => { const cookies = cookie.parse(req.headers.cookie || ""); - const auth = (await requestPVE("/version", "GET", { cookies })).status === 200; + const auth = (await global.pve.requestPVE("/version", "GET", { cookies })).status === 200; if (!auth) { socket.destroy(); } else { wsServer.handleUpgrade(req, socket, head, (socket) => { - const pool = db.getUser(cookies.username).cluster.pool; + const pool = global.db.getUser(cookies.username).cluster.pool; wsServer.emit("connection", socket, cookies.username, pool); }); } @@ -185,7 +182,7 @@ if (schemes.interrupt.enabled) { return; } // get current cluster resources - const status = (await requestPVE("/cluster/resources", "GET", { token: pveAPIToken })).data.data; + const status = (await global.pve.requestPVE("/cluster/resources", "GET", { token: true })).data.data; // filter out just state information of resources that are needed, and hash each one const currState = extractClusterState(status, resourceTypes, true); // get a map of users to send sync notifications diff --git a/src/routes/user.js b/src/routes/user.js index 0535c44..5e672f1 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -1,11 +1,8 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); ; -const db = global.db; -const requestPVE = global.pve.requestPVE; const checkAuth = global.utils.checkAuth; const getUserResources = global.utils.getUserResources; -const pveAPIToken = global.db.pveAPIToken; /** * GET - get db user resource information including allocated, free, and maximum resource values along with resource metadata @@ -43,7 +40,7 @@ router.get("/config/:key", async (req, res) => { } const allowKeys = ["resources", "cluster", "nodes"]; if (allowKeys.includes(params.key)) { - const config = db.getUser(req.cookies.username); + const config = global.db.getUser(req.cookies.username); res.status(200).send(config[params.key]); } else { @@ -64,9 +61,9 @@ router.get("/iso", async (req, res) => { return; } // get user iso config - const userIsoConfig = db.getGlobal().useriso; + const userIsoConfig = global.config.useriso; // get all isos - const isos = (await requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=iso`, "GET", { token: pveAPIToken })).data.data; + const isos = (await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=iso`, "GET", { token: true })).data.data; const userIsos = []; isos.forEach((iso) => { iso.name = iso.volid.replace(`${userIsoConfig.storage}:iso/`, ""); diff --git a/src/utils.js b/src/utils.js index af63b7a..92750ba 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,8 +2,8 @@ import { createHash } from "crypto"; import path from "path"; import url from "url"; import * as fs from "fs"; - -import { requestPVE, getDiskInfo, getDeviceInfo } from "./pve.js"; +import { readFileSync } from "fs"; +import { exit } from "process"; /** * Check if a user is authorized to access a specified vm, or the cluster in general. @@ -13,10 +13,9 @@ import { requestPVE, getDiskInfo, getDeviceInfo } from "./pve.js"; * @returns {boolean} true if the user is authorized to access the specific vm or cluster in general, false otheriwse. */ export async function checkAuth (cookies, res, vmpath = null) { - const db = global.db; let auth = false; - if (db.getUser(cookies.username) === null) { + if (global.db.getUser(cookies.username) === null) { auth = false; res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in localdb.` }); res.end(); @@ -24,11 +23,11 @@ export async function checkAuth (cookies, res, vmpath = null) { } if (vmpath) { - const result = await requestPVE(`/${vmpath}/config`, "GET", { cookies }); + const result = await global.pve.requestPVE(`/${vmpath}/config`, "GET", { cookies }); auth = result.status === 200; } else { // if no path is specified, then do a simple authentication - const result = await requestPVE("/version", "GET", { cookies }); + const result = await global.pve.requestPVE("/version", "GET", { cookies }); auth = result.status === 200; } @@ -47,17 +46,17 @@ export async function checkAuth (cookies, res, vmpath = null) { * @returns */ async function getFullInstanceConfig (req, instance, diskprefixes) { - const config = (await requestPVE(`/nodes/${instance.node}/${instance.type}/${instance.vmid}/config`, "GET", { cookies: req.cookies })).data.data; + const config = (await global.pve.requestPVE(`/nodes/${instance.node}/${instance.type}/${instance.vmid}/config`, "GET", { cookies: req.cookies })).data.data; // fetch all instance disk and device data concurrently const promises = []; const mappings = []; for (const key in config) { if (diskprefixes.some(prefix => key.startsWith(prefix))) { - promises.push(getDiskInfo(instance.node, config, key)); + promises.push(global.pve.getDiskInfo(instance.node, config, key)); mappings.push(key); } else if (key.startsWith("hostpci")) { - promises.push(getDeviceInfo(instance.node, config[key].split(",")[0])); + promises.push(global.pve.getDeviceInfo(instance.node, config[key].split(",")[0])); mappings.push(key); } } @@ -78,7 +77,7 @@ async function getFullInstanceConfig (req, instance, diskprefixes) { */ async function getAllInstanceConfigs (req, diskprefixes) { // get the basic resources list - const resources = (await requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data; + const resources = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data; // filter resources by their type, we only want lxc and qemu const instances = []; @@ -109,9 +108,8 @@ async function getAllInstanceConfigs (req, diskprefixes) { * @returns {{used: Object, avail: Object, max: Object, resources: Object}} used, available, maximum, and resource metadata for the specified user. */ export async function getUserResources (req, username) { - const db = global.db; - const dbResources = db.getGlobal().resources; - const userResources = db.getUser(username).resources; + const dbResources = global.config.resources; + const userResources = global.db.getUser(username).resources; // setup disk prefixes object const diskprefixes = []; @@ -265,8 +263,7 @@ export async function getUserResources (req, username) { * @returns {boolean} true if the available resources can fullfill the requested resources, false otherwise. */ export async function approveResources (req, username, request, node) { - const db = global.db; - const dbResources = db.getGlobal().resources; + const dbResources = global.config.resources; const userResources = await getUserResources(req, username); let approved = true; Object.keys(request).every((key) => { @@ -334,15 +331,15 @@ export function getTimeLeft (timeout) { * @param {string} target folder to import modules. * @param {string} from source folder of calling module, optional for imports from the same base directory. */ -export function recursiveImport (router, baseroute, target, from = import.meta.url) { +export function recursiveImportRoutes (router, baseroute, target, from = import.meta.url) { const thisPath = path.dirname(url.fileURLToPath(import.meta.url)); const fromPath = path.relative(".", path.dirname(url.fileURLToPath(from))); const targetPath = path.relative(".", `${fromPath}/${target}`); - const importPath = path.relative(thisPath, targetPath); + const baseImportPath = path.relative(thisPath, targetPath); const files = fs.readdirSync(targetPath); files.forEach((file) => { if (file.endsWith(".js")) { - const path = `./${importPath}/${file}`; + const path = `./${baseImportPath}/${file}`; const route = `${baseroute}/${file.replace(".js", "")}`; import(path).then((module) => { router.use(route, module.router); @@ -351,3 +348,13 @@ export function recursiveImport (router, baseroute, target, from = import.meta.u } }); } + +export function readJSONFile (path) { + try { + return JSON.parse(readFileSync(path)); + } + catch (e) { + console.log(`Error: ${path} was not found.`); + exit(1); + } +};