From 2ef4bfd3d2a563fa4778d0c69a17d451816e84fd Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 19 Apr 2023 01:03:55 +0000 Subject: [PATCH] implement new db strategy Signed-off-by: Arthur Lu --- db.js | 105 ++++++++++------------------------------------------ main.js | 62 ++++++++++++------------------- pveutils.js | 86 ++++++++++++++++++++++++++++++------------ 3 files changed, 106 insertions(+), 147 deletions(-) diff --git a/db.js b/db.js index 5935b11..190b2cf 100644 --- a/db.js +++ b/db.js @@ -16,92 +16,25 @@ function init () { } } -/** - * user requests additional resources specified in k-v pairs - * @param {string} user user's proxmox username in the form username@authrealm - * @param {Object} resources k-v pairs with resource name as keys and resource ammount as values - * @returns {boolean} whether the user is approved to allocate requested resources - */ -function requestResources (user, resources) { - let approved = true; - Object.keys(resources).forEach((element) => { - if(!(element in db[user].available)) { // if the resource does not exist in the user's entry, assume the user is not allowed to use it - approved = false; - } - else if (db[user].available[element] - resources[element] < 0) { - approved = false; - } +function getResourceMeta () { + return db.resources; +} + +function getUserMax (username) { + return db.users[username].maximum; +} + +function getResourceUnits () { + return db.units; +} + +function putUserResources (username, used) { + let userEntry = db.users[username]; + userEntry.used = used; + userEntry.avail = {}; + Object.keys(max).forEach((k) => { + userEntry.avail[k] = max[k] - used[k]; }); - return approved; } -/** - * user allocates additional resources specified in k-v pairs - * @param {string} user user's proxmox username in the form username@authrealm - * @param {Object} resources k-v pairs with resource name as keys and resource ammount as values - * @returns {boolean} true if resources were successfully allocated, false otherwise - */ -function allocateResources (user, resources) { - let newdb = {}; - Object.assign(newdb, db); - Object.keys(resources).forEach((element) => { - if(typeof(resources[element]) === "number" && isFinite(resources[element])) { - newdb[user].available[element] -= resources[element]; - } - else { - return false; - } - }); - try { - fs.writeFileSync(filename, JSON.stringify(newdb)); - Object.assign(db, newdb); - return true; - } - catch { - fs.writeFileSync(filename, JSON.stringify(db)) - return false; - } -} - -/** - * user releases allocated resources specified in k-v pairs - * @param {string} user user's proxmox username in the form username@authrealm - * @param {Object} resources k-v pairs with resource name as keys and resource ammount as values - * @returns {boolean} true if resources were successfully deallocated, false otherwise - */ -function releaseResources (user, resources) { - let newdb = {}; - Object.assign(newdb, db); - Object.keys(resources).forEach((element) => { - if(typeof(resources[element]) === "number" && isFinite(resources[element]) && resources[element]) { - newdb[user].available[element] += resources[element]; - } - else { - return false; - } - }); - try { - fs.writeFileSync(filename, JSON.stringify(newdb)); - Object.assign(db, newdb); - return true; - } - catch { - fs.writeFileSync(filename, JSON.stringify(db)) - return false; - } -} - -/** - * return a read only copy of the user resources - * @param {string} user user's proxmox username in the form username@authrealm - * @returns {Object} user's remaining resources as k-v pairs with resource name as keys and resource ammount as values - */ -function getResources (user) { - let returnVal = {}; - if(user in db) { - Object.assign(returnVal, db[user]); - } - return returnVal; -} - -module.exports = {init, requestResources, allocateResources, releaseResources, getResources}; \ No newline at end of file +module.exports = {init, getResourceMeta, getUserMax, getResourceUnits, putUserResources}; \ No newline at end of file diff --git a/main.js b/main.js index 3d100f0..4424127 100644 --- a/main.js +++ b/main.js @@ -7,8 +7,8 @@ const morgan = require("morgan"); var api = require("./package.json"); const {pveAPIToken, listenPort, domain} = require("./vars.js"); -const {checkAuth, requestPVE, handleResponse, getUnusedDiskData, getDiskConfig} = require("./pveutils.js"); -const {init, requestResources, allocateResources, releaseResources, getResources} = require("./db.js"); +const {checkAuth, requestPVE, handleResponse, getUsedResources} = require("./pveutils.js"); +const {init, getResourceMeta, getUserMax, getResourceUnits} = require("./db.js"); const app = express(); app.use(helmet()); @@ -19,82 +19,68 @@ app.use(morgan("combined")); app.get("/api/version", (req, res) => { - res.send({version: api.version}); + res.status(200).send({version: api.version}); }); app.get("/api/echo", (req, res) => { - res.send({body: req.body, cookies: req.cookies}); + res.status(200).send({body: req.body, cookies: req.cookies}); }); app.get("/api/auth", async (req, res) => { - let result = await checkAuth(req.cookies); - res.send({auth: result}); + await checkAuth(req.cookies); + res.status(200).send({auth: true}); }); app.get("/api/proxmox/*", async (req, res) => { // proxy endpoint for GET proxmox api with no token path = req.url.replace("/api/proxmox", ""); let result = await requestPVE(path, "GET", req.cookies); - res.send(result.data, result.status); + res.status(result.status).send(result.data); }); app.post("/api/proxmox/*", async (req, res) => { // proxy endpoint for POST proxmox api with no token path = req.url.replace("/api/proxmox", ""); let result = await requestPVE(path, "POST", req.cookies, JSON.stringify(req.body)); // need to stringify body because of other issues - res.send(result.data, result.status); + res.status(result.status).send(result.data); }); app.get("/api/user/resources", async(req, res) => { - let auth = await checkAuth(req.cookies); - if (!auth) { - res.status(401).send({auth: auth}); - return; - } - - res.status(200).send({resources: getResources(req.cookies.username)}); + await checkAuth(req.cookies, res); + let rm = getResourceMeta(); + let used = await getUsedResources(req, rm); + let max = await getUserMax(req.cookies.username); + avail = {}; + Object.keys(max).forEach((k) => { + avail[k] = max[k] - used[k]; + }); + let units = getResourceUnits(); + res.status(200).send({used: used, maximum: max, available: avail, units: units}); return; }); app.post("/api/disk/detach", async (req, res) => { let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; - - // check auth - let auth = await checkAuth(req.cookies, vmpath); - if (!auth) { - res.status(401).send({auth: auth}); - return; - } - + await checkAuth(req.cookies, res); if (req.body.disk.includes("unused")) { res.status(500).send({auth: auth, data:{error: `Requested disk ${req.body.disk} cannot be unused. Use /disk/delete to permanently delete unused disks.`}}); return; } - let action = JSON.stringify({delete: req.body.disk}); let method = req.body.type === "qemu" ? "POST" : "PUT"; let result = await requestPVE(`${vmpath}/config`, method, req.cookies, action, pveAPIToken); - result = await handleResponse(req.body.node, result); - res.status(result.status).send({auth: auth, data: result.data}); + await handleResponse(req.body.node, result, res); }); app.post("/api/disk/attach", async (req, res) => { let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; - - // check auth - let auth = await checkAuth(req.cookies, vmpath); - if (!auth) { - res.status(401).send({auth: auth}); - return; - } - + await checkAuth(req.cookies, res); let action = {}; action[req.body.disk] = req.body.data; action = JSON.stringify(action); let method = req.body.type === "qemu" ? "POST" : "PUT"; let result = await requestPVE(`${vmpath}/config`, method, req.cookies, action, pveAPIToken); - result = await handleResponse(req.body.node, result); - res.status(result.status).send({auth: auth, data: result.data}); + await handleResponse(req.body.node, result, res); }); - +/* app.post("/api/disk/resize", async (req, res) => { let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; @@ -401,7 +387,7 @@ app.delete("/api/instance", async (req, res) => { } res.status(result.status).send({auth: auth, data: result.data, deallocated: release}); -}); +});*/ app.listen(listenPort, () => { init(); diff --git a/pveutils.js b/pveutils.js index 8895e74..685bad2 100644 --- a/pveutils.js +++ b/pveutils.js @@ -1,14 +1,20 @@ const axios = require('axios'); const {pveAPI, pveAPIToken} = require("./vars.js"); -async function checkAuth (cookies, vmpath = null) { +async function checkAuth (cookies, res, vmpath = null) { + let auth = false; if (vmpath) { let result = await requestPVE(`/${vmpath}/config`, "GET", cookies); - return result.status === 200; + auth = result.status === 200; } else { // if no path is specified, then do a simple authentication let result = await requestPVE("/version", "GET", cookies); - return result.status === 200; + auth = result.status === 200; + } + if (!auth) { + res.status(401).send({auth: auth}); + res.end(); + return; } } @@ -44,17 +50,21 @@ async function requestPVE (path, method, cookies, body = null, token = null) { } } -async function handleResponse (node, response) { +async function handleResponse (node, result, res) { const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); - if (response.data.data) { - let upid = response.data.data; + if (result.data.data) { + let upid = result.data.data; while (true) { let taskStatus = await requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", null, null, pveAPIToken); if (taskStatus.data.data.status === "stopped" && taskStatus.data.data.exitstatus === "OK") { - return {status: 200, data: taskStatus.data.data}; + res.status(200).send(taskStatus.data.data); + res.end(); + return; } else if (taskStatus.data.data.status === "stopped") { - return {status: 500, data: taskStatus.data.data}; + res.status(500).send(taskStatus.data.data); + res.end(); + return; } else { await waitFor(1000); @@ -62,28 +72,58 @@ async function handleResponse (node, response) { } } else { - return response; + res.status(result.status).send(result.data); + res.end(); + return; } } -async function getUnusedDiskData (node, type, vmid, disk) { - let diskDataConfig = await getDiskConfig(node, type, vmid, disk); - let storageID = diskDataConfig.split(":")[0]; +async function getUsedResources (req, resourceMeta) { + let response = await requestPVE("/cluster/resources", "GET", req.cookies); + let used = {}; + let diskprefixes = []; + for (let resourceName of Object.keys(resourceMeta)) { + if (resourceMeta[resourceName].type === "numeric") { + used[resourceName] = 0; + } + else if (resourceMeta[resourceName].type === "disk") { + resourceMeta[resourceName].storages.forEach((element) => { + used[element] = 0; + }); + diskprefixes.push(resourceName); + } + } + for (instance of response.data.data) { + if (instance.type === "lxc" || instance.type === "qemu") { + let config = await requestPVE(`/nodes/${instance.node}/${instance.type}/${instance.vmid}/config`, "GET", req.cookies); + config = config.data.data; + for (key of Object.keys(config)) { + if (Object.keys(used).includes(key) && resourceMeta[key].type === "numeric") { + used[key] += config[key]; + } + else if (diskprefixes.some(prefix => key.startsWith(prefix))) { + let diskInfo = await getDiskInfo(instance.node, instance.type, instance.vmid, key); + used[diskInfo.storage] += diskInfo.size; + } + } + } + } + return used; +} + +async function getDiskInfo (node, type, vmid, disk) { + let config = await requestPVE(`/nodes/${node}/${type}/${vmid}/config`, "GET", null, null, pveAPIToken); + let storageID = config.data.data[disk].split(":")[0]; + let volIDTarget = config.data.data[disk].split(",")[0]; let storageData = await requestPVE(`/nodes/${node}/storage/${storageID}/content`, "GET", null, null, pveAPIToken); - let diskDataStorage = null; + let diskInfo = null; storageData.data.data.forEach((element) => { - if (element.volid === diskDataConfig) { + if (element.volid === volIDTarget) { element.storage = storageID; - diskDataStorage = element; + diskInfo = element; } }); - return diskDataStorage; + return diskInfo; } -async function getDiskConfig (node, type, vmid, disk) { - let config = await requestPVE(`/nodes/${node}/${type}/${vmid}/config`, "GET", null, null, pveAPIToken); - - return config.data.data[disk]; -} - -module.exports = {checkAuth, requestPVE, handleResponse, getUnusedDiskData, getDiskConfig}; \ No newline at end of file +module.exports = {checkAuth, requestPVE, handleResponse, getUsedResources}; \ No newline at end of file