implement new db strategy
Signed-off-by: Arthur Lu <learthurgo@gmail.com>
This commit is contained in:
parent
c92fc95e92
commit
dacbd965cf
105
db.js
105
db.js
@ -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
62
main.js
@ -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();
|
||||||
|
86
pveutils.js
86
pveutils.js
@ -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};
|
|
Loading…
Reference in New Issue
Block a user