implement new db strategy

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
This commit is contained in:
Arthur Lu 2023-04-19 01:03:55 +00:00
parent c92fc95e92
commit dacbd965cf
3 changed files with 106 additions and 147 deletions

105
db.js
View File

@ -16,92 +16,25 @@ function init () {
} }
} }
/** function getResourceMeta () {
* user requests additional resources specified in k-v pairs return db.resources;
* @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 getUserMax (username) {
*/ return db.users[username].maximum;
function requestResources (user, resources) { }
let approved = true;
Object.keys(resources).forEach((element) => { function getResourceUnits () {
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 return db.units;
approved = false; }
}
else if (db[user].available[element] - resources[element] < 0) { function putUserResources (username, used) {
approved = false; let userEntry = db.users[username];
} userEntry.used = used;
userEntry.avail = {};
Object.keys(max).forEach((k) => {
userEntry.avail[k] = max[k] - used[k];
}); });
return approved;
} }
/** module.exports = {init, getResourceMeta, getUserMax, getResourceUnits, putUserResources};
* 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};

62
main.js
View File

@ -7,8 +7,8 @@ const morgan = require("morgan");
var api = require("./package.json"); var api = require("./package.json");
const {pveAPIToken, listenPort, domain} = require("./vars.js"); const {pveAPIToken, listenPort, domain} = require("./vars.js");
const {checkAuth, requestPVE, handleResponse, getUnusedDiskData, getDiskConfig} = require("./pveutils.js"); const {checkAuth, requestPVE, handleResponse, getUsedResources} = require("./pveutils.js");
const {init, requestResources, allocateResources, releaseResources, getResources} = require("./db.js"); const {init, getResourceMeta, getUserMax, getResourceUnits} = require("./db.js");
const app = express(); const app = express();
app.use(helmet()); app.use(helmet());
@ -19,82 +19,68 @@ app.use(morgan("combined"));
app.get("/api/version", (req, res) => { app.get("/api/version", (req, res) => {
res.send({version: api.version}); res.status(200).send({version: api.version});
}); });
app.get("/api/echo", (req, res) => { 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) => { app.get("/api/auth", async (req, res) => {
let result = await checkAuth(req.cookies); await checkAuth(req.cookies);
res.send({auth: result}); res.status(200).send({auth: true});
}); });
app.get("/api/proxmox/*", async (req, res) => { // proxy endpoint for GET proxmox api with no token app.get("/api/proxmox/*", async (req, res) => { // proxy endpoint for GET proxmox api with no token
path = req.url.replace("/api/proxmox", ""); path = req.url.replace("/api/proxmox", "");
let result = await requestPVE(path, "GET", req.cookies); 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 app.post("/api/proxmox/*", async (req, res) => { // proxy endpoint for POST proxmox api with no token
path = req.url.replace("/api/proxmox", ""); 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 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) => { app.get("/api/user/resources", async(req, res) => {
let auth = await checkAuth(req.cookies); await checkAuth(req.cookies, res);
if (!auth) { let rm = getResourceMeta();
res.status(401).send({auth: auth}); let used = await getUsedResources(req, rm);
return; let max = await getUserMax(req.cookies.username);
} avail = {};
Object.keys(max).forEach((k) => {
res.status(200).send({resources: getResources(req.cookies.username)}); avail[k] = max[k] - used[k];
});
let units = getResourceUnits();
res.status(200).send({used: used, maximum: max, available: avail, units: units});
return; return;
}); });
app.post("/api/disk/detach", async (req, res) => { app.post("/api/disk/detach", async (req, res) => {
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
await checkAuth(req.cookies, res);
// check auth
let auth = await checkAuth(req.cookies, vmpath);
if (!auth) {
res.status(401).send({auth: auth});
return;
}
if (req.body.disk.includes("unused")) { 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.`}}); 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; return;
} }
let action = JSON.stringify({delete: req.body.disk}); let action = JSON.stringify({delete: req.body.disk});
let method = req.body.type === "qemu" ? "POST" : "PUT"; let method = req.body.type === "qemu" ? "POST" : "PUT";
let result = await requestPVE(`${vmpath}/config`, method, req.cookies, action, pveAPIToken); let result = await requestPVE(`${vmpath}/config`, method, req.cookies, action, pveAPIToken);
result = await handleResponse(req.body.node, result); await handleResponse(req.body.node, result, res);
res.status(result.status).send({auth: auth, data: result.data});
}); });
app.post("/api/disk/attach", async (req, res) => { app.post("/api/disk/attach", async (req, res) => {
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
await checkAuth(req.cookies, res);
// check auth
let auth = await checkAuth(req.cookies, vmpath);
if (!auth) {
res.status(401).send({auth: auth});
return;
}
let action = {}; let action = {};
action[req.body.disk] = req.body.data; action[req.body.disk] = req.body.data;
action = JSON.stringify(action); action = JSON.stringify(action);
let method = req.body.type === "qemu" ? "POST" : "PUT"; let method = req.body.type === "qemu" ? "POST" : "PUT";
let result = await requestPVE(`${vmpath}/config`, method, req.cookies, action, pveAPIToken); let result = await requestPVE(`${vmpath}/config`, method, req.cookies, action, pveAPIToken);
result = await handleResponse(req.body.node, result); await handleResponse(req.body.node, result, res);
res.status(result.status).send({auth: auth, data: result.data});
}); });
/*
app.post("/api/disk/resize", async (req, res) => { app.post("/api/disk/resize", async (req, res) => {
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; 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}); res.status(result.status).send({auth: auth, data: result.data, deallocated: release});
}); });*/
app.listen(listenPort, () => { app.listen(listenPort, () => {
init(); init();

View File

@ -1,14 +1,20 @@
const axios = require('axios'); const axios = require('axios');
const {pveAPI, pveAPIToken} = require("./vars.js"); const {pveAPI, pveAPIToken} = require("./vars.js");
async function checkAuth (cookies, vmpath = null) { async function checkAuth (cookies, res, vmpath = null) {
let auth = false;
if (vmpath) { if (vmpath) {
let result = await requestPVE(`/${vmpath}/config`, "GET", cookies); 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 else { // if no path is specified, then do a simple authentication
let result = await requestPVE("/version", "GET", cookies); 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)); const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
if (response.data.data) { if (result.data.data) {
let upid = response.data.data; let upid = result.data.data;
while (true) { while (true) {
let taskStatus = await requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", null, null, pveAPIToken); let taskStatus = await requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", null, null, pveAPIToken);
if (taskStatus.data.data.status === "stopped" && taskStatus.data.data.exitstatus === "OK") { 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") { 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 { else {
await waitFor(1000); await waitFor(1000);
@ -62,28 +72,58 @@ async function handleResponse (node, response) {
} }
} }
else { else {
return response; res.status(result.status).send(result.data);
res.end();
return;
} }
} }
async function getUnusedDiskData (node, type, vmid, disk) { async function getUsedResources (req, resourceMeta) {
let diskDataConfig = await getDiskConfig(node, type, vmid, disk); let response = await requestPVE("/cluster/resources", "GET", req.cookies);
let storageID = diskDataConfig.split(":")[0]; 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 storageData = await requestPVE(`/nodes/${node}/storage/${storageID}/content`, "GET", null, null, pveAPIToken);
let diskDataStorage = null; let diskInfo = null;
storageData.data.data.forEach((element) => { storageData.data.data.forEach((element) => {
if (element.volid === diskDataConfig) { if (element.volid === volIDTarget) {
element.storage = storageID; element.storage = storageID;
diskDataStorage = element; diskInfo = element;
} }
}); });
return diskDataStorage; return diskInfo;
} }
async function getDiskConfig (node, type, vmid, disk) { module.exports = {checkAuth, requestPVE, handleResponse, getUsedResources};
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};