Compare commits
32 Commits
update-loc
...
v1.0.0
Author | SHA1 | Date | |
---|---|---|---|
8b6613aa93 | |||
4bd4e136dd | |||
7d2db031a9 | |||
10d979e545 | |||
b0b4c68f15 | |||
87ebb6b679 | |||
aa40d1f577 | |||
cfcf08b373 | |||
d080e71601 | |||
3001febbc2 | |||
7626dcf387 | |||
4984877ab7 | |||
072b5ef2d4 | |||
42dea83463 | |||
ee3e768ada | |||
c059b528fa | |||
783bc37c94 | |||
9f6b03db32 | |||
3b81bd20ea | |||
79ec20ad74 | |||
8f7ea51787 | |||
800033c6f8 | |||
7f48f49445 | |||
c8404c366f | |||
c63690c181 | |||
34f2669ab9 | |||
afecfcafd0 | |||
ab0188a8bc | |||
85b8ae8560 | |||
01f55aa0cb | |||
b12f38e608 | |||
bb7404a82d |
71
README.md
71
README.md
@@ -19,6 +19,7 @@ In Proxmox VE, follow the following steps:
|
||||
- Datastore.Allocate, Datastore.AllocateSpace, Datastore.Audit
|
||||
- User.Modify
|
||||
- Pool.Audit
|
||||
- SDN.Use (if instances use SDN networks)
|
||||
4. Add a new API Token Permission with path: `/`, select the API token created previously, and role: `proxmoxaas-api`
|
||||
5. Add a new User Permission with path: `/`, select the `proxmoxaas-api` user, and role: `proxmoxaas-api`
|
||||
|
||||
@@ -68,3 +69,73 @@ server {
|
||||
After these steps, the ProxmoxAAS Dashboard should be available and fully functional at `paas.<FQDN>` or `paas.<FQDN>/dashboard/`.
|
||||
|
||||
# Backends
|
||||
|
||||
Backend handlers are used to interface with any number and type of backend data source used to store ProxmoxAAS data. Most data involves users, groups, and membership relationships. The default backends are sufficient to run a small cluster, but additional backend handlers can be created.
|
||||
|
||||
## Interface
|
||||
|
||||
Each backend must implement the following methods:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>openSession</td>
|
||||
<td>opens a session to the backend by creating a session token</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>closeSession</td>
|
||||
<td>closes a session to the backend</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Additionally, backends dealing with user data may also need to implement:
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td>addUser</td>
|
||||
<td>create a user</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>getUser</td>
|
||||
<td>retrieve user data including membership</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>setUser</td>
|
||||
<td>modify a user</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>delUser</td>
|
||||
<td>delete a user</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>addGroup</td>
|
||||
<td>create a group</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>getGroup</td>
|
||||
<td>retrieve group data including members</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>setGroup</td>
|
||||
<td>modify group data except membership</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>delGroup</td>
|
||||
<td>delete group</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>addUserToGroup</td>
|
||||
<td>add user to group as member</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>delUserFromGroup</td>
|
||||
<td>remove user from group</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
Not all user backends will necessarily implement all the methods fully. For example, backends which do not store group data may not need to implement the group related methods.
|
||||
|
||||
Specific documentation can be found in `src/backends/backends.js`.
|
||||
|
||||
## Multiple Interfaces
|
||||
|
||||
Multiple backends can be specified using the config. During a backend operation involving users, each backend method will be called in the order specified in the config. If the operation is to retrieve user data, the responses will be merged favoring the last backend called.
|
@@ -4,6 +4,7 @@
|
||||
"import": "pve.js",
|
||||
"config": {
|
||||
"url": "https://pve.mydomain.example/api2/json",
|
||||
"fabric": "http://localhost:8082",
|
||||
"token": {
|
||||
"user": "proxmoxaas-api",
|
||||
"realm": "pam",
|
||||
@@ -25,15 +26,29 @@
|
||||
"paasldap": {
|
||||
"import": "paasldap.js",
|
||||
"config": {
|
||||
"url": "http://paasldap.mydomain.example"
|
||||
"url": "http://paasldap.mydomain.example",
|
||||
"realm": "ldap"
|
||||
}
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"pve": "pve",
|
||||
"db": "localdb",
|
||||
"auth": {
|
||||
"instance": {
|
||||
"pve": "pve"
|
||||
},
|
||||
"users": {
|
||||
"realm": {
|
||||
"pve": [
|
||||
"localdb"
|
||||
],
|
||||
"ldap": [
|
||||
"localdb",
|
||||
"paasldap"
|
||||
]
|
||||
},
|
||||
"any": [
|
||||
"localdb",
|
||||
"paasldap"
|
||||
]
|
||||
}
|
||||
},
|
||||
"application": {
|
||||
@@ -63,7 +78,7 @@
|
||||
"memory": {
|
||||
"name": "RAM",
|
||||
"type": "numeric",
|
||||
"multiplier": 1048576,
|
||||
"multiplier": 1,
|
||||
"base": 1024,
|
||||
"compact": true,
|
||||
"unit": "B",
|
||||
@@ -72,7 +87,7 @@
|
||||
"swap": {
|
||||
"name": "SWAP",
|
||||
"type": "numeric",
|
||||
"multiplier": 1048576,
|
||||
"multiplier": 1,
|
||||
"base": 1024,
|
||||
"compact": true,
|
||||
"unit": "B",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "proxmoxaas-api",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.0",
|
||||
"description": "REST API for ProxmoxAAS",
|
||||
"main": "src/main.js",
|
||||
"type": "module",
|
||||
|
@@ -2,7 +2,7 @@ import path from "path";
|
||||
import url from "url";
|
||||
|
||||
export default async () => {
|
||||
const backends = {};
|
||||
global.backends = {};
|
||||
for (const name in global.config.backends) {
|
||||
// get files and config
|
||||
const target = global.config.backends[name].import;
|
||||
@@ -14,17 +14,11 @@ export default async () => {
|
||||
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);
|
||||
global.backends[name] = new Backend(config);
|
||||
console.log(`backends: initialized backend ${name} from ${importPath}`);
|
||||
}
|
||||
// assign backends to handlers by type
|
||||
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]];
|
||||
});
|
||||
global.pve = global.backends[global.config.handlers.instance.pve];
|
||||
global.userManager = new USER_BACKEND_MANAGER(global.config.handlers.users);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -34,13 +28,15 @@ export default async () => {
|
||||
class BACKEND {
|
||||
/**
|
||||
* Opens a session with the backend and creates session tokens if needed
|
||||
* @param {{username: string, password: string}} credentials object containing username and password fields
|
||||
* @returns {{ok: boolean, status: number, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value
|
||||
* @param {{id: string, realm: string}} user object containing id and realm
|
||||
* @param {string} password
|
||||
* @returns {{ok: boolean, status: number, message: string, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value
|
||||
*/
|
||||
openSession (credentials) {
|
||||
openSession (user, password) {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
message: "",
|
||||
cookies: []
|
||||
};
|
||||
}
|
||||
@@ -58,85 +54,209 @@ class BACKEND {
|
||||
}
|
||||
}
|
||||
|
||||
export class AtomicChange {
|
||||
constructor (valid, delta, callback, status = { ok: true, status: 200, message: "" }) {
|
||||
this.valid = valid;
|
||||
this.delta = delta;
|
||||
this.callback = callback;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the change using the saved delta using the callback function
|
||||
*/
|
||||
async commit () {
|
||||
const res = await this.callback(this.delta);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
export function doNothingCallback (delta) {
|
||||
return { ok: true, status: 200, message: "" };
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for backend types that store/interact with user & group data.
|
||||
* Not all backends need to implement all interface methods.
|
||||
*/
|
||||
class USER_BACKEND extends BACKEND {
|
||||
/**
|
||||
* Add user to backend
|
||||
* Validate an add user operation with the following parameters.
|
||||
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||
* @param {{id: string, realm: string}} user
|
||||
* @param {Object} attributes user attributes
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {AtomicChange} atomic change object
|
||||
*/
|
||||
addUser (user, attributes, params = null) {}
|
||||
addUser (user, attributes, params) {}
|
||||
|
||||
/**
|
||||
* Get user from backend
|
||||
* @param {{id: string, realm: string}} user
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {Object} containing user data from this backend, null if user does not exist
|
||||
*/
|
||||
getUser (user, params = null) {}
|
||||
getUser (user, params) {}
|
||||
|
||||
/**
|
||||
* Modify user in backend
|
||||
* Get all users from backend
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {Array} containing each user data from this backend
|
||||
*/
|
||||
getAllUsers (params) {}
|
||||
|
||||
/**
|
||||
* Validate a set user operation with the following parameters.
|
||||
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||
* @param {{id: string, realm: string}} user
|
||||
* @param {Object} attributes new user attributes to modify
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {AtomicChange} atomic change object
|
||||
*/
|
||||
setUser (user, attributes, params = null) {}
|
||||
/**
|
||||
* Delete user from backend
|
||||
* @param {{id: string, realm: string}} user
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
*/
|
||||
deluser (user, params = null) {}
|
||||
setUser (user, attributes, params) {}
|
||||
|
||||
/**
|
||||
* Add group to backend
|
||||
* @param {{id: string}} group
|
||||
* Validate a delete user operation with the following parameters.
|
||||
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||
* @param {{id: string, realm: string}} user
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {AtomicChange} atomic change object
|
||||
*/
|
||||
delUser (user, params) {}
|
||||
|
||||
/**
|
||||
* Validate an add group operation with the following parameters.
|
||||
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||
* @param {{id: string, realm: string}} group
|
||||
* @param {Object} attributes group attributes
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {AtomicChange} atomic change object
|
||||
*/
|
||||
addGroup (group, attributes, params = null) {}
|
||||
addGroup (group, attributes, params) {}
|
||||
|
||||
/**
|
||||
* Get group from backend
|
||||
* @param {{id: string}} group
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {Object} containing group data from this backend, null if user does not exist
|
||||
*/
|
||||
getGroup (group, params = null) {}
|
||||
/**
|
||||
* Modify group in backend
|
||||
* @param {{id: string}} group
|
||||
* @param {Object} attributes new group attributes to modify
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
*/
|
||||
setGroup (group, attributes, params = null) {}
|
||||
/**
|
||||
* Delete group from backend
|
||||
* @param {{id: string}} group
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
*/
|
||||
delGroup (group, params = null) {}
|
||||
getGroup (group, params) {}
|
||||
|
||||
/**
|
||||
* Add user to group
|
||||
* @param {{id: string, realm: string}} user
|
||||
* @param {{id: string}} group
|
||||
* Get all users from backend
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {Array} containing each group data from this backend
|
||||
*/
|
||||
addUserToGroup (user, group, params = null) {}
|
||||
getAllGroups (params) {}
|
||||
|
||||
/**
|
||||
* Remove user from group
|
||||
* Validate a set group operation with the following parameters.
|
||||
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||
* @param {{id: string, realm: string}} group
|
||||
* @param {Object} attributes group attributes
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {AtomicChange} atomic change object
|
||||
*/
|
||||
setGroup (group, attributes, params) {}
|
||||
/**
|
||||
* Validate a del group operation with the following parameters.
|
||||
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||
* @param {{id: string, realm: string}} group
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {AtomicChange} atomic change object
|
||||
*/
|
||||
delGroup (group, attributes, params) {}
|
||||
|
||||
/**
|
||||
* Validate an add user to group operation with the following parameters.
|
||||
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||
* @param {{id: string, realm: string}} user
|
||||
* @param {{id: string}} group
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {AtomicChange} atomic change object
|
||||
*/
|
||||
delUserFromGroup (user, group, params = null) {}
|
||||
addUserToGroup (user, group, params) {}
|
||||
|
||||
/**
|
||||
* Validate a remove user from group operation with the following parameters.
|
||||
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||
* @param {{id: string, realm: string}} user
|
||||
* @param {{id: string}} group
|
||||
* @param {Object} params authentication params, usually req.cookies
|
||||
* @returns {AtomicChange} atomic change object
|
||||
*/
|
||||
delUserFromGroup (user, group, params) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for proxmox api backends.
|
||||
*/
|
||||
export class PVE_BACKEND extends BACKEND {}
|
||||
export class PVE_BACKEND extends BACKEND {
|
||||
/**
|
||||
* Get and return node data.
|
||||
* Returns the node data or null if the node does not exist.
|
||||
* @param {string} node node id
|
||||
* @returns {}
|
||||
*/
|
||||
getNode (node) {}
|
||||
|
||||
/**
|
||||
* Send a signal to synchronize a node after some change has been made.
|
||||
* * @param {string} node node id
|
||||
*/
|
||||
syncNode (node) {}
|
||||
|
||||
/**
|
||||
* Get and return instance data.
|
||||
* Returns the instance data or null if the instance does not exist.
|
||||
* @param {string} node node id
|
||||
* @param {string} type instance type
|
||||
* @param {string} vmid instance id
|
||||
*/
|
||||
getInstance (node, type, instance) {}
|
||||
|
||||
/**
|
||||
* Send a signal to synchronize an instance after some change has been made.
|
||||
* @param {string} node node id
|
||||
* @param {string} instance instance id
|
||||
*/
|
||||
syncInstance (node, instance) {}
|
||||
|
||||
/**
|
||||
* 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} 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 getDisk (node, instance, disk) {}
|
||||
|
||||
/**
|
||||
* Get meta data for a specific net. Adds info that is not normally available in a instance's config.
|
||||
* @param {string} node containing the query net.
|
||||
* @param {string} instance with query net.
|
||||
* @param {string} netid id number of the query net, ie. 0 -> net0.
|
||||
* @returns {Objetc} k-v pairs of specific net data, including rate and vlan.
|
||||
*/
|
||||
async getNet (node, instance, netid) {}
|
||||
|
||||
/**
|
||||
* Get meta data for a specific device. Adds info that is not normally available in a instance's config.
|
||||
* @param {string} node containing the query device.
|
||||
* @param {string} instance with query device.
|
||||
* @param {string} deviceid id number of the query device, ie. 0 -> pci0.
|
||||
* @returns {Objetc} k-v pairs of specific device data, including name and manfacturer.
|
||||
*/
|
||||
async getDevice (node, instance, deviceid) {}
|
||||
|
||||
/**
|
||||
* Get user resource data including used, available, and maximum resources.
|
||||
* @param {{id: string, realm: string}} user object of user to get resource data.
|
||||
* @param {Object} cookies object containing k-v store of cookies
|
||||
* @returns {{used: Object, avail: Object, max: Object, resources: Object}} used, available, maximum, and resource metadata for the specified user.
|
||||
*/
|
||||
getUserResources (user, cookies) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for user database backends.
|
||||
@@ -147,3 +267,105 @@ export class DB_BACKEND extends USER_BACKEND {}
|
||||
* Interface for user auth backends.
|
||||
*/
|
||||
export class AUTH_BACKEND extends USER_BACKEND {}
|
||||
|
||||
/**
|
||||
* Interface combining all user backends into a single interface
|
||||
* Calling methods will also call sub handler methods
|
||||
*/
|
||||
class USER_BACKEND_MANAGER extends USER_BACKEND {
|
||||
#config = null;
|
||||
|
||||
constructor (config) {
|
||||
super();
|
||||
this.#config = config;
|
||||
}
|
||||
|
||||
getBackendsByUser (user) {
|
||||
if (user != null) {
|
||||
return this.#config.realm[user.realm];
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
addUser (user, attributes, params) {}
|
||||
|
||||
async getUser (user, params) {
|
||||
let userData = {};
|
||||
for (const backend of this.#config.realm[user.realm]) {
|
||||
const backendData = await global.backends[backend].getUser(user, params);
|
||||
if (backendData) {
|
||||
userData = { ...backendData, ...userData };
|
||||
}
|
||||
}
|
||||
return userData;
|
||||
}
|
||||
|
||||
async getAllUsers (params) {
|
||||
const userData = {};
|
||||
for (const backend of this.#config.any) {
|
||||
const backendData = await global.backends[backend].getAllUsers(params);
|
||||
if (backendData) {
|
||||
for (const user of Object.keys(backendData)) {
|
||||
userData[user] = { ...backendData[user], ...userData[user] };
|
||||
}
|
||||
}
|
||||
}
|
||||
return userData;
|
||||
}
|
||||
|
||||
async setUser (user, attributes, params) {
|
||||
const atomicChanges = [];
|
||||
for (const backend of this.#config.realm[user.realm]) {
|
||||
const atomicChange = await global.backends[backend].setUser(user, attributes, params);
|
||||
if (atomicChange.valid === false) { // if any fails, preemptively exit
|
||||
return atomicChange.status;
|
||||
}
|
||||
atomicChanges.push(atomicChange); // queue callback into array
|
||||
}
|
||||
const response = {
|
||||
ok: true,
|
||||
status: 200,
|
||||
message: "",
|
||||
allResponses: []
|
||||
};
|
||||
for (const atomicChange of atomicChanges) {
|
||||
const atomicResponse = await atomicChange.commit();
|
||||
if (atomicResponse.ok === false) {
|
||||
response.ok = false;
|
||||
response.status = atomicResponse.status;
|
||||
response.message = atomicResponse.message;
|
||||
}
|
||||
response.allResponses.push(); // execute callback
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
delUser (user, params) {}
|
||||
|
||||
addGroup (group, attributes, params) {}
|
||||
|
||||
getGroup (group, params) {}
|
||||
|
||||
async getAllGroups (params) {
|
||||
const groupData = {};
|
||||
for (const backend of this.#config.any) {
|
||||
const backendData = await global.backends[backend].getAllGroups(params);
|
||||
if (backendData) {
|
||||
for (const group of Object.keys(backendData)) {
|
||||
groupData[group] = { ...backendData[group], ...groupData[group] };
|
||||
}
|
||||
}
|
||||
}
|
||||
return groupData;
|
||||
}
|
||||
|
||||
setGroup (group, attributes, params) {}
|
||||
|
||||
delGroup (group, params) {}
|
||||
|
||||
addUserToGroup (user, group, params) {}
|
||||
|
||||
delUserFromGroup (user, group, params) {}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { exit } from "process";
|
||||
import { DB_BACKEND } from "./backends.js";
|
||||
import { AtomicChange, DB_BACKEND, doNothingCallback } from "./backends.js";
|
||||
|
||||
export default class LocalDB extends DB_BACKEND {
|
||||
#path = null;
|
||||
@@ -35,74 +35,82 @@ export default class LocalDB extends DB_BACKEND {
|
||||
writeFileSync(this.#path, JSON.stringify(this.#data));
|
||||
}
|
||||
|
||||
addUser (user, attributes, params = null) {
|
||||
const username = `${user.id}@${user.realm}`;
|
||||
attributes = attributes || this.#defaultuser;
|
||||
this.#data.users[username] = attributes;
|
||||
this.#save();
|
||||
}
|
||||
addUser (user, attributes, params) {}
|
||||
|
||||
getUser (user, params = null) {
|
||||
const username = `${user.id}@${user.realm}`;
|
||||
if (this.#data.users[username]) {
|
||||
return this.#data.users[username];
|
||||
getUser (user, params) {
|
||||
const requestedUser = `${user.id}@${user.realm}`;
|
||||
const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token
|
||||
// user can access a user's db data if they are an admin OR are requesting own data
|
||||
const authorized = this.#data.users[requestingUser].cluster.admin || requestingUser === requestedUser;
|
||||
if (authorized && this.#data.users[requestedUser]) {
|
||||
return this.#data.users[requestedUser];
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
setUser (user, attributes, params = null) {
|
||||
const username = `${user.id}@${user.realm}`;
|
||||
if (this.#data.users[username]) {
|
||||
this.#data.users[username] = attributes;
|
||||
this.#save();
|
||||
return true;
|
||||
async getAllUsers (params) {
|
||||
const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token
|
||||
if (this.#data.users[requestingUser].cluster.admin === true) {
|
||||
return this.#data.users;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
delUser (user, params = null) {
|
||||
setUser (user, attributes, params) {
|
||||
if (attributes.resources && attributes.cluster && attributes.templates) {
|
||||
const username = `${user.id}@${user.realm}`;
|
||||
if (this.#data.users[username]) {
|
||||
delete this.#data.users[username];
|
||||
if (this.#data.users[params.username] && this.#data.users[params.username].cluster.admin) {
|
||||
return new AtomicChange(false,
|
||||
{
|
||||
username,
|
||||
attributes: {
|
||||
resources: attributes.resources,
|
||||
cluster: attributes.cluster,
|
||||
templates: attributes.templates
|
||||
}
|
||||
},
|
||||
(delta) => {
|
||||
this.#data.users[delta.username] = delta.attributes;
|
||||
this.#save();
|
||||
return true;
|
||||
return { ok: true, status: 200, message: "" };
|
||||
},
|
||||
{ ok: true, status: 200, message: "" }
|
||||
);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
return new AtomicChange(false, {}, doNothingCallback, { ok: false, status: 401, message: `${params.username} is not an admin user in localdb` });
|
||||
}
|
||||
}
|
||||
else {
|
||||
// return false;
|
||||
return new AtomicChange(false, {}, doNothingCallback, { ok: false, status: 400, message: `${username} was not found in localdb` });
|
||||
}
|
||||
}
|
||||
else {
|
||||
return new AtomicChange(true, {}, doNothingCallback, null);
|
||||
}
|
||||
}
|
||||
|
||||
delUser (user, params) {}
|
||||
|
||||
// group methods not implemented because db backend does not store groups
|
||||
addGroup (group, atrributes, params = null) {}
|
||||
getGroup (group, params = null) {}
|
||||
setGroup (group, attributes, params = null) {}
|
||||
delGroup (group, params = null) {}
|
||||
|
||||
// assume that adding to group also adds to group's pool
|
||||
addUserToGroup (user, group, params = null) {
|
||||
const username = `${user.id}@${user.realm}`;
|
||||
if (this.#data.users[username]) {
|
||||
this.#data.users[username].cluster.pools[group.id] = true;
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
addGroup (group, atrributes, params) {}
|
||||
getGroup (group, params) {}
|
||||
getAllGroups (params) {
|
||||
return null;
|
||||
}
|
||||
|
||||
setGroup (group, attributes, params) {}
|
||||
delGroup (group, params) {}
|
||||
|
||||
// assume that adding to group also adds to group's pool
|
||||
delUserFromGroup (user, group, params = null) {
|
||||
const username = `${user.id}@${user.realm}`;
|
||||
if (this.#data.users[username] && this.#data.users[username].cluster.pools[group.id]) {
|
||||
delete this.#data.users[username].cluster.pools[group.id];
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
addUserToGroup (user, group, params) {}
|
||||
|
||||
// assume that adding to group also adds to group's pool
|
||||
delUserFromGroup (user, group, params) {}
|
||||
}
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import axios from "axios";
|
||||
import { AUTH_BACKEND } from "./backends.js";
|
||||
import { AtomicChange, AUTH_BACKEND, doNothingCallback } from "./backends.js";
|
||||
import * as setCookie from "set-cookie-parser";
|
||||
|
||||
export default class PAASLDAP extends AUTH_BACKEND {
|
||||
#url = null;
|
||||
#realm = null;
|
||||
|
||||
constructor (config) {
|
||||
super();
|
||||
this.#url = config.url;
|
||||
this.#realm = config.realm;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -15,7 +17,7 @@ export default class PAASLDAP extends AUTH_BACKEND {
|
||||
* @param {*} path HTTP path, prepended with the paas-LDAP API base url
|
||||
* @param {*} method HTTP method
|
||||
* @param {*} body body parameters and data to be sent. Optional.
|
||||
* @returns {Object} HTTP response object or HTTP error object.
|
||||
* @returns {Object} HTTP response object
|
||||
*/
|
||||
async #request (path, method, auth = null, body = null) {
|
||||
const url = `${this.#url}${path}`;
|
||||
@@ -39,19 +41,23 @@ export default class PAASLDAP extends AUTH_BACKEND {
|
||||
return result;
|
||||
}
|
||||
catch (error) {
|
||||
error.ok = false;
|
||||
error.status = 500;
|
||||
error.data = {
|
||||
error: error.code
|
||||
};
|
||||
return error;
|
||||
const result = error.response;
|
||||
result.ok = result.status === 200;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
async openSession (credentials) {
|
||||
const userRealm = credentials.username.split("@").at(-1);
|
||||
const uid = credentials.username.replace(`@${userRealm}`, "");
|
||||
const content = { uid, password: credentials.password };
|
||||
#handleGenericReturn (res) {
|
||||
return {
|
||||
ok: res.ok,
|
||||
status: res.status,
|
||||
message: res.ok ? "" : res.data.error
|
||||
};
|
||||
}
|
||||
|
||||
async openSession (user, password) {
|
||||
const username = user.id;
|
||||
const content = { username, password };
|
||||
const result = await this.#request("/ticket", "POST", null, content);
|
||||
if (result.ok) {
|
||||
const cookies = setCookie.parse(result.headers["set-cookie"]);
|
||||
@@ -61,51 +67,118 @@ export default class PAASLDAP extends AUTH_BACKEND {
|
||||
return {
|
||||
ok: true,
|
||||
status: result.status,
|
||||
message: "",
|
||||
cookies
|
||||
};
|
||||
}
|
||||
else {
|
||||
return result;
|
||||
return {
|
||||
ok: false,
|
||||
status: result.status,
|
||||
message: result.data.error,
|
||||
cookies: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async addUser (user, attributes, params = null) {
|
||||
return await this.#request(`/users/${user.id}`, "POST", params, attributes);
|
||||
async addUser (user, attributes, params) {}
|
||||
|
||||
async getUser (user, params) {
|
||||
if (!params) { // params required, do nothing if params are missing
|
||||
return null;
|
||||
}
|
||||
const res = await this.#request(`/users/${user.id}`, "GET", params);
|
||||
if (res.ok) { // if ok, return user data
|
||||
return res.data.user;
|
||||
}
|
||||
else { // else return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async getUser (user, params = null) {
|
||||
return await this.#request(`/users/${user.id}`, "GET", params);
|
||||
async getAllUsers (params) {
|
||||
if (!params) {
|
||||
return null;
|
||||
}
|
||||
const res = await this.#request("/users", "GET", params);
|
||||
if (res.ok) { // if ok, return user data
|
||||
const users = res.data.users;
|
||||
const usersFormatted = {};
|
||||
// label each user object by user@realm
|
||||
for (const user of users) {
|
||||
usersFormatted[`${user.attributes.uid}@${this.#realm}`] = user;
|
||||
}
|
||||
return usersFormatted;
|
||||
}
|
||||
else { // else return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setUser (user, attributes, params = null) {
|
||||
return await this.#request(`/users/${user.id}`, "POST", params, attributes);
|
||||
async setUser (user, attributes, params) {
|
||||
if (!attributes.userpassword && !attributes.cn && attributes.sn) {
|
||||
return new AtomicChange(true, {}, doNothingCallback, null); // change has no ldap attributes
|
||||
}
|
||||
const ldapAttributes = {};
|
||||
if (attributes.userpassword) {
|
||||
ldapAttributes.userpassword = attributes.userpassword;
|
||||
}
|
||||
if (attributes.cn) {
|
||||
ldapAttributes.cn = attributes.cn;
|
||||
}
|
||||
if (attributes.sn) {
|
||||
ldapAttributes.sn = attributes.sn;
|
||||
}
|
||||
return new AtomicChange(
|
||||
true,
|
||||
{
|
||||
user,
|
||||
ldapAttributes,
|
||||
params
|
||||
},
|
||||
async (delta) => {
|
||||
const res = await this.#request(`/users/${delta.user.id}`, "POST", delta.params, delta.ldapAttributes);
|
||||
return this.#handleGenericReturn(res);
|
||||
},
|
||||
{ ok: true, status: 200, message: "" }
|
||||
);
|
||||
}
|
||||
|
||||
async delUser (user, params = null) {
|
||||
return await this.#request(`/users/${user.id}`, "DELETE", params);
|
||||
}
|
||||
async delUser (user, params) {}
|
||||
|
||||
async addGroup (group, attributes, params = null) {
|
||||
return await this.#request(`/groups/${group.id}`, "POST", params);
|
||||
}
|
||||
async addGroup (group, attributes, params) {}
|
||||
|
||||
async getGroup (group, params = null) {
|
||||
async getGroup (group, params) {
|
||||
return await this.#request(`/groups/${group.id}`, "GET", params);
|
||||
}
|
||||
|
||||
async setGroup (group, attributes, params = null) {
|
||||
async getAllGroups (params) {
|
||||
if (!params) {
|
||||
return null;
|
||||
}
|
||||
const res = await this.#request("/groups", "GET", params);
|
||||
if (res.ok) { // if ok, return user data
|
||||
const groups = res.data.groups;
|
||||
const groupsFormatted = {};
|
||||
// label each user object by user@realm
|
||||
for (const group of groups) {
|
||||
groupsFormatted[`${group.attributes.cn}@${this.#realm}`] = group;
|
||||
}
|
||||
return groupsFormatted;
|
||||
}
|
||||
else { // else return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setGroup (group, attributes, params) {
|
||||
// not implemented, LDAP groups do not have any attributes to change
|
||||
return new AtomicChange(true, {}, doNothingCallback, null); ;
|
||||
}
|
||||
|
||||
async delGroup (group, params = null) {
|
||||
return await this.#request(`/groups/${group.id}`, "DELETE", params);
|
||||
}
|
||||
async delGroup (group, params) {}
|
||||
|
||||
async addUserToGroup (user, group, params = null) {
|
||||
return await this.#request(`/groups/${group.id}/members/${user.id}`, "POST", params);
|
||||
}
|
||||
async addUserToGroup (user, group, params) {}
|
||||
|
||||
async delUserFromGroup (user, group, params = null) {
|
||||
return await this.#request(`/groups/${group.id}/members/${user.id}`, "DELETE", params);
|
||||
}
|
||||
async delUserFromGroup (user, group, params) {}
|
||||
}
|
||||
|
@@ -5,18 +5,26 @@ export default class PVE extends PVE_BACKEND {
|
||||
#pveAPIURL = null;
|
||||
#pveAPIToken = null;
|
||||
#pveRoot = null;
|
||||
#paasFabric = null;
|
||||
|
||||
constructor (config) {
|
||||
super();
|
||||
this.#pveAPIURL = config.url;
|
||||
this.#pveAPIToken = config.token;
|
||||
this.#pveRoot = config.root;
|
||||
this.#paasFabric = config.fabric;
|
||||
}
|
||||
|
||||
async openSession (credentials) {
|
||||
async openSession (user, password) {
|
||||
const credentials = { username: `${user.id}@${user.realm}`, password };
|
||||
const response = await global.pve.requestPVE("/access/ticket", "POST", null, credentials);
|
||||
if (!(response.status === 200)) {
|
||||
return response;
|
||||
return {
|
||||
ok: false,
|
||||
status: response.status,
|
||||
message: "Authorization failed",
|
||||
cookies: []
|
||||
};
|
||||
}
|
||||
const ticket = response.data.data.ticket;
|
||||
const csrftoken = response.data.data.CSRFPreventionToken;
|
||||
@@ -66,11 +74,22 @@ export default class PVE extends PVE_BACKEND {
|
||||
const token = this.#pveAPIToken;
|
||||
content.headers.Authorization = `PVEAPIToken=${token.user}@${token.realm}!${token.id}=${token.uuid}`;
|
||||
}
|
||||
else if (auth && auth.root) {
|
||||
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, this.#pveRoot);
|
||||
if (!(rootauth.status === 200)) {
|
||||
return rootauth.response;
|
||||
}
|
||||
const rootcookie = rootauth.data.data.ticket;
|
||||
const rootcsrf = rootauth.data.data.CSRFPreventionToken;
|
||||
content.headers.CSRFPreventionToken = rootcsrf;
|
||||
content.headers.Cookie = `PVEAuthCookie=${rootcookie}; CSRFPreventionToken=${rootcsrf}`;
|
||||
}
|
||||
|
||||
try {
|
||||
return await axios.request(url, content);
|
||||
}
|
||||
catch (error) {
|
||||
console.log(`backends: error ocuured in pve.requestPVE: ${error}`);
|
||||
return error.response;
|
||||
}
|
||||
}
|
||||
@@ -85,11 +104,15 @@ export default class PVE extends PVE_BACKEND {
|
||||
*/
|
||||
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:")) {
|
||||
if (result.status !== 200) {
|
||||
res.status(result.status).send({ error: result.statusText });
|
||||
res.end();
|
||||
}
|
||||
else 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);
|
||||
await waitFor(100);
|
||||
taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
|
||||
}
|
||||
if (taskStatus.data.data.exitstatus === "OK") {
|
||||
@@ -114,82 +137,111 @@ export default class PVE extends PVE_BACKEND {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Send HTTP request to PAAS Fabric
|
||||
* @param {string} path HTTP path, prepended with the proxmox API base url.
|
||||
* @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 getDiskInfo (node, config, disk) {
|
||||
async requestFabric (path, method, body = null) {
|
||||
const url = `${this.#paasFabric}${path}`;
|
||||
const content = {
|
||||
method,
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
data: body
|
||||
};
|
||||
|
||||
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;
|
||||
return await axios.request(url, content);
|
||||
}
|
||||
catch {
|
||||
catch (error) {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
async getNode (node) {
|
||||
const res = await this.requestFabric(`/nodes/${node}`, "GET");
|
||||
if (res.status !== 200) {
|
||||
console.error(res);
|
||||
return null;
|
||||
}
|
||||
|
||||
return res.data.node;
|
||||
}
|
||||
|
||||
async syncNode (node) {
|
||||
this.requestFabric(`/nodes/${node}/sync`, "POST");
|
||||
}
|
||||
|
||||
async getInstance (node, instance) {
|
||||
const res = await this.requestFabric(`/nodes/${node}/instances/${instance}`, "GET");
|
||||
if (res.status !== 200) {
|
||||
console.error(res);
|
||||
return null;
|
||||
}
|
||||
|
||||
return res.data.instance;
|
||||
}
|
||||
|
||||
async syncInstance (node, vmid) {
|
||||
this.requestFabric(`/nodes/${node}/instances/${vmid}/sync`, "POST");
|
||||
}
|
||||
|
||||
async getDisk (node, instance, disk) {
|
||||
const config = await this.getInstance(node, instance);
|
||||
if (config != null && config.volumes[disk] != null) {
|
||||
return config.volumes[disk];
|
||||
}
|
||||
else {
|
||||
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);
|
||||
async getNet (node, instance, netid) {
|
||||
const config = await this.getInstance(node, instance);
|
||||
if (config != null && config.nets[netid] != null) {
|
||||
return config.nets[netid];
|
||||
}
|
||||
});
|
||||
deviceData.sort((a, b) => {
|
||||
return a.id < b.id;
|
||||
});
|
||||
const device = deviceData[0];
|
||||
device.subfn = structuredClone(deviceData.slice(1));
|
||||
return device;
|
||||
}
|
||||
catch {
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available devices on specific node.
|
||||
* @param {string} node to get devices from.
|
||||
* @returns {Array.<Object>} 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 }));
|
||||
async getDevice (node, instance, deviceid) {
|
||||
const config = await this.getInstance(node, instance);
|
||||
if (config != null && config.devices[deviceid] != null) {
|
||||
return config.devices[deviceid];
|
||||
}
|
||||
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));
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
return nodeAvailPci;
|
||||
|
||||
async getUserResources (user, cookies) {
|
||||
// get user resources with vm filter
|
||||
const res = await this.requestPVE("/cluster/resources?type=vm", "GET", { cookies });
|
||||
if (res.status !== 200) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userPVEResources = res.data.data;
|
||||
|
||||
const resources = {};
|
||||
|
||||
// for each resource, add to the object
|
||||
for (const resource of userPVEResources) {
|
||||
const instance = await this.getInstance(resource.node, resource.vmid);
|
||||
if (instance) {
|
||||
instance.node = resource.node;
|
||||
resources[resource.vmid] = instance;
|
||||
}
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,8 @@ export const router = Router({ mergeParams: true }); ;
|
||||
|
||||
const checkAuth = global.utils.checkAuth;
|
||||
|
||||
global.utils.recursiveImportRoutes(router, "", "access", import.meta.url);
|
||||
|
||||
/**
|
||||
* GET - check authentication
|
||||
* responses:
|
||||
@@ -23,12 +25,12 @@ router.get("/", async (req, res) => {
|
||||
class CookieFetcher {
|
||||
#fetchedBackends = [];
|
||||
#cookies = [];
|
||||
async fetchBackends (backends, credentials) {
|
||||
async fetchBackends (backends, user, password) {
|
||||
for (const backend of backends) {
|
||||
if (this.#fetchedBackends.indexOf(backend) === -1) {
|
||||
const response = await backend.openSession(credentials);
|
||||
const response = await global.backends[backend].openSession(user, password);
|
||||
if (!response.ok) {
|
||||
return false;
|
||||
return response.message;
|
||||
}
|
||||
this.#cookies = this.#cookies.concat(response.cookies);
|
||||
this.#fetchedBackends.push(backend);
|
||||
@@ -37,7 +39,7 @@ class CookieFetcher {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
exportCookies () {
|
||||
@@ -59,25 +61,33 @@ router.post("/ticket", async (req, res) => {
|
||||
username: req.body.username,
|
||||
password: req.body.password
|
||||
};
|
||||
|
||||
const domain = global.config.application.domain;
|
||||
const userRealm = params.username.split("@").at(-1);
|
||||
const backends = [global.pve, global.db];
|
||||
if (userRealm in global.auth) {
|
||||
backends.push(global.auth[userRealm]);
|
||||
const userObj = global.utils.getUserObjFromUsername(params.username);
|
||||
let backends = global.userManager.getBackendsByUser(userObj);
|
||||
if (backends == null) {
|
||||
res.status(401).send({ auth: false, error: `${params.username} not found in any ProxmoxAAS backends` });
|
||||
return;
|
||||
}
|
||||
backends = backends.concat(["pve"]);
|
||||
const cm = new CookieFetcher();
|
||||
const success = await cm.fetchBackends(backends, params);
|
||||
if (!success) {
|
||||
res.status(401).send({ auth: false });
|
||||
const error = await cm.fetchBackends(backends, userObj, params.password);
|
||||
if (error) {
|
||||
res.status(401).send({ auth: false, error });
|
||||
return;
|
||||
}
|
||||
const cookies = cm.exportCookies();
|
||||
let minimumExpires = Infinity;
|
||||
for (const cookie of cookies) {
|
||||
const expiresDate = new Date(Date.now() + cookie.expiresMSFromNow);
|
||||
res.cookie(cookie.name, cookie.value, { domain, path: "/", httpOnly: true, secure: true, expires: expiresDate });
|
||||
res.cookie(cookie.name, cookie.value, { domain, path: "/", httpOnly: true, secure: true, expires: expiresDate, sameSite: "none" });
|
||||
if (cookie.expiresMSFromNow < minimumExpires) {
|
||||
minimumExpires = cookie.expiresMSFromNow;
|
||||
}
|
||||
res.cookie("username", params.username, { domain, path: "/", secure: true });
|
||||
res.cookie("auth", 1, { domain, path: "/", secure: true });
|
||||
}
|
||||
const expiresDate = new Date(Date.now() + minimumExpires);
|
||||
res.cookie("username", params.username, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" });
|
||||
res.cookie("auth", 1, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" });
|
||||
res.status(200).send({ auth: true });
|
||||
});
|
||||
|
||||
@@ -94,10 +104,10 @@ router.delete("/ticket", async (req, res) => {
|
||||
const domain = global.config.application.domain;
|
||||
const expire = new Date(0);
|
||||
for (const cookie in req.cookies) {
|
||||
res.cookie(cookie, "", { domain, path: "/", expires: expire });
|
||||
res.cookie(cookie, "", { domain, path: "/", expires: expire, secure: true, sameSite: "none" });
|
||||
}
|
||||
await global.pve.closeSession(req.cookies);
|
||||
await global.db.closeSession(req.cookies);
|
||||
await global.userManager.closeSession(req.cookies);
|
||||
res.status(200).send({ auth: false });
|
||||
});
|
||||
|
||||
@@ -114,24 +124,16 @@ router.post("/password", async (req, res) => {
|
||||
password: req.body.password
|
||||
};
|
||||
|
||||
const userRealm = params.username.split("@").at(-1);
|
||||
const authHandlers = global.config.handlers.auth;
|
||||
const userID = params.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
if (userRealm in authHandlers) {
|
||||
const handler = authHandlers[userRealm];
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userObj = global.utils.getUserObjFromUsername(params.username);
|
||||
const newAttributes = {
|
||||
userpassword: params.password
|
||||
};
|
||||
const response = await handler.setUser(userObj, newAttributes, req.cookies);
|
||||
if (response.ok) {
|
||||
res.status(response.status).send(response.data);
|
||||
}
|
||||
else {
|
||||
res.status(response.status).send({ error: response.data.error });
|
||||
}
|
||||
}
|
||||
else {
|
||||
res.status(501).send({ error: `Auth type ${userRealm} not implemented yet.` });
|
||||
}
|
||||
const response = await global.userManager.setUser(userObj, newAttributes, req.cookies);
|
||||
res.status(response.status).send(response);
|
||||
});
|
41
src/routes/access/groups.js
Normal file
41
src/routes/access/groups.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Router } from "express";
|
||||
export const router = Router({ mergeParams: true });
|
||||
|
||||
const checkAuth = global.utils.checkAuth;
|
||||
|
||||
/**
|
||||
* GET - get all groups
|
||||
* responses:
|
||||
* - 200: {auth: true, groups: Array}
|
||||
* - 401: {auth: false}
|
||||
*/
|
||||
router.get("/", async (req, res) => {
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
const groups = await global.userManager.getAllGroups(req.cookies);
|
||||
res.status(200).send({ groups });
|
||||
});
|
||||
|
||||
/**
|
||||
* GET - get specific group
|
||||
* request:
|
||||
* - groupname: name of group to get
|
||||
* responses:
|
||||
* - 200: {auth: true, group: Object}
|
||||
* - 401: {auth: false}
|
||||
*/
|
||||
router.get("/:groupname", async (req, res) => {
|
||||
const params = {
|
||||
groupname: req.params.groupname
|
||||
};
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
const group = await global.userManager.getGroup(params.groupname, req.cookies);
|
||||
res.status(200).send({ group });
|
||||
});
|
42
src/routes/access/users.js
Normal file
42
src/routes/access/users.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Router } from "express";
|
||||
export const router = Router({ mergeParams: true });
|
||||
|
||||
const checkAuth = global.utils.checkAuth;
|
||||
|
||||
/**
|
||||
* GET - get all users
|
||||
* responses:
|
||||
* - 200: {auth:true, users: Array}
|
||||
* - 401: {auth: false}
|
||||
*/
|
||||
router.get("/", async (req, res) => {
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
const users = await global.userManager.getAllUsers(req.cookies);
|
||||
res.status(200).send({ users });
|
||||
});
|
||||
|
||||
/**
|
||||
* GET - get specific user
|
||||
* request:
|
||||
* - username: username (id@realm) of user to get
|
||||
* responses:
|
||||
* - 200: {auth: true, user: Object}
|
||||
* - 401: {auth: false}
|
||||
*/
|
||||
router.get("/:username", async (req, res) => {
|
||||
const params = {
|
||||
username: req.params.username
|
||||
};
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
const userObj = global.utils.getUserObjFromUsername(params.username);
|
||||
const user = await global.userManager.getUser(userObj, req.cookies);
|
||||
res.status(200).send({ user });
|
||||
});
|
@@ -1,7 +1,6 @@
|
||||
import { Router } from "express";
|
||||
export const router = Router({ mergeParams: true });
|
||||
|
||||
const db = global.db;
|
||||
const checkAuth = global.utils.checkAuth;
|
||||
const approveResources = global.utils.approveResources;
|
||||
const getUserResources = global.utils.getUserResources;
|
||||
@@ -14,6 +13,61 @@ const basePath = `/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP}
|
||||
|
||||
global.utils.recursiveImportRoutes(router, basePath, "cluster", import.meta.url);
|
||||
|
||||
/**
|
||||
* GET - get all available cluster pools
|
||||
* returns only pool IDs
|
||||
* responses:
|
||||
* - 200: List of pools
|
||||
* - PVE error
|
||||
*/
|
||||
router.get("/pools", async (req, res) => {
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allPools = await global.pve.requestPVE("/pools", "GET", { token: true });
|
||||
|
||||
if (allPools.status === 200) {
|
||||
const allPoolsIDs = Array.from(allPools.data.data, (x) => x.poolid);
|
||||
res.status(allPools.status).send({ pools: allPoolsIDs });
|
||||
res.end();
|
||||
}
|
||||
else {
|
||||
res.status(allPools.status).send({ error: allPools.statusMessage });
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET - get all available cluster nodes
|
||||
* uses existing user permissions without elevation
|
||||
* returns only node IDs
|
||||
* responses:
|
||||
* - 200: List of nodes
|
||||
* - PVE error
|
||||
*/
|
||||
router.get("/nodes", async (req, res) => {
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allNodes = await global.pve.requestPVE("/nodes", "GET", { cookies: req.cookies });
|
||||
|
||||
if (allNodes.status === 200) {
|
||||
const allNodesIDs = Array.from(allNodes.data.data, (x) => x.node);
|
||||
res.status(allNodes.status).send({ nodes: allNodesIDs });
|
||||
res.end();
|
||||
}
|
||||
else {
|
||||
res.status(allNodes.status).send({ error: allNodes.statusMessage });
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET - get available pcie devices given node and user
|
||||
* request:
|
||||
@@ -29,16 +83,14 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
|
||||
node: req.params.node
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
const userNodes = db.getUser(userObj).cluster.nodes;
|
||||
const userNodes = (await global.userManager.getUser(userObj, req.cookies)).cluster.nodes;
|
||||
if (userNodes[params.node] !== true) {
|
||||
res.status(401).send({ auth: false, path: params.node });
|
||||
res.end();
|
||||
@@ -46,13 +98,28 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
|
||||
}
|
||||
// get remaining user resources
|
||||
const userAvailPci = (await getUserResources(req, userObj)).pci.nodes[params.node];
|
||||
if (userAvailPci === undefined) { // user has no avaliable devices on this node, so send an empty list
|
||||
res.status(200).send([]);
|
||||
res.end();
|
||||
}
|
||||
else {
|
||||
// get node avail devices
|
||||
let nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
|
||||
nodeAvailPci = nodeAvailPci.filter(nodeAvail => userAvailPci.some((userAvail) => {
|
||||
const node = await global.pve.getNode(params.node);
|
||||
let availableDevices = [];
|
||||
// get each device and filter out only thise which are not reserved
|
||||
for (const device of Object.values(node.devices)) {
|
||||
if (device.reserved === false) {
|
||||
availableDevices.push(device);
|
||||
}
|
||||
}
|
||||
// further filter out only devices which the user has access to
|
||||
availableDevices = availableDevices.filter(nodeAvail => userAvailPci.some((userAvail) => {
|
||||
return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0;
|
||||
}));
|
||||
res.status(200).send(nodeAvailPci);
|
||||
|
||||
res.status(200).send(availableDevices);
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -83,9 +150,7 @@ router.post(`${basePath}/resources`, async (req, res) => {
|
||||
boot: req.body.boot
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
@@ -94,13 +159,13 @@ router.post(`${basePath}/resources`, async (req, res) => {
|
||||
return;
|
||||
}
|
||||
// get current config
|
||||
const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true });
|
||||
const instance = await global.pve.getInstance(params.node, params.vmid);
|
||||
const request = {
|
||||
cores: Number(params.cores) - Number(currentConfig.data.data.cores),
|
||||
memory: Number(params.memory) - Number(currentConfig.data.data.memory)
|
||||
cores: Number(params.cores) - Number(instance.cores),
|
||||
memory: Number(params.memory) - Number(instance.memory)
|
||||
};
|
||||
if (params.type === "lxc") {
|
||||
request.swap = Number(params.swap) - Number(currentConfig.data.data.swap);
|
||||
request.swap = Number(params.swap) - Number(instance.swap);
|
||||
}
|
||||
else if (params.type === "qemu") {
|
||||
request.cpu = params.proctype;
|
||||
@@ -124,6 +189,7 @@ router.post(`${basePath}/resources`, async (req, res) => {
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -165,9 +231,7 @@ router.post(`${basePath}/create`, async (req, res) => {
|
||||
rootfssize: req.body.rootfssize
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
@@ -175,7 +239,7 @@ router.post(`${basePath}/create`, async (req, res) => {
|
||||
return;
|
||||
}
|
||||
// get user db config
|
||||
const user = await db.getUser(userObj);
|
||||
const user = await global.userManager.getUser(userObj, req.cookies);
|
||||
const vmid = Number.parseInt(params.vmid);
|
||||
const vmidMin = user.cluster.vmid.min;
|
||||
const vmidMax = user.cluster.vmid.max;
|
||||
@@ -247,6 +311,7 @@ router.post(`${basePath}/create`, async (req, res) => {
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}`, "POST", { token: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncNode(params.node);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -275,4 +340,5 @@ router.delete(`${basePath}/delete`, async (req, res) => {
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(vmpath, "DELETE", { token: true });
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncNode(params.node);
|
||||
});
|
||||
|
@@ -31,10 +31,9 @@ router.post("/:disk/detach", async (req, res) => {
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// get current config
|
||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
||||
// disk must exist
|
||||
if (!config[params.disk]) {
|
||||
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
|
||||
if (!disk) {
|
||||
res.status(500).send({ error: `Disk ${params.disk} does not exist.` });
|
||||
res.end();
|
||||
return;
|
||||
@@ -49,6 +48,7 @@ router.post("/:disk/detach", async (req, res) => {
|
||||
const method = params.type === "qemu" ? "POST" : "PUT";
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -80,29 +80,30 @@ router.post("/:disk/attach", async (req, res) => {
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// get current config
|
||||
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.` });
|
||||
const disk = await global.pve.getDisk(params.node, params.vmid, `unused${params.source}`);
|
||||
if (!disk) {
|
||||
res.status(500).send({ error: `Requested disk unused${params.source} does not exist.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// target disk must be allowed according to source disk's storage options
|
||||
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}].` });
|
||||
if (!resourceConfig[disk.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) {
|
||||
res.status(500).send({ error: `Requested target ${params.disk} is not in allowed list [${resourceConfig[disk.storage].disks}].` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// setup action using source disk info from vm config
|
||||
const action = {};
|
||||
action[params.disk] = config[`unused${params.source}`];
|
||||
action[params.disk] = disk.volid;
|
||||
const method = params.type === "qemu" ? "POST" : "PUT";
|
||||
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -130,9 +131,7 @@ router.post("/:disk/resize", async (req, res) => {
|
||||
size: req.body.size
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
@@ -140,17 +139,15 @@ router.post("/:disk/resize", async (req, res) => {
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// get current config
|
||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
||||
// check disk existence
|
||||
const diskConfig = await global.pve.getDiskInfo(params.node, config, params.disk); // get target disk
|
||||
if (!diskConfig) { // exit if disk does not exist
|
||||
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
|
||||
if (!disk) { // exit if disk does not exist
|
||||
res.status(500).send({ error: `requested disk ${params.disk} does not exist.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// setup request
|
||||
const storage = diskConfig.storage; // get the storage
|
||||
const storage = disk.storage; // get the storage
|
||||
const request = {};
|
||||
request[storage] = Number(params.size * 1024 ** 3); // setup request object
|
||||
// check request approval
|
||||
@@ -163,6 +160,7 @@ router.post("/:disk/resize", async (req, res) => {
|
||||
const action = { disk: params.disk, size: `+${params.size}G` };
|
||||
const result = await global.pve.requestPVE(`${vmpath}/resize`, "PUT", { token: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -192,9 +190,7 @@ router.post("/:disk/move", async (req, res) => {
|
||||
delete: req.body.delete
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
@@ -202,17 +198,15 @@ router.post("/:disk/move", async (req, res) => {
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// get current config
|
||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
||||
// check disk existence
|
||||
const diskConfig = await global.pve.getDiskInfo(params.node, config, params.disk); // get target disk
|
||||
if (!diskConfig) { // exit if disk does not exist
|
||||
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
|
||||
if (!disk) { // exit if disk does not exist
|
||||
res.status(500).send({ error: `requested disk ${params.disk} does not exist.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// setup request
|
||||
const size = parseInt(diskConfig.size); // get source disk size
|
||||
const size = parseInt(disk.size); // get source disk size
|
||||
const dstStorage = params.storage; // get destination storage
|
||||
const request = {};
|
||||
if (!params.delete) { // if not delete, then request storage, otherwise it is net 0
|
||||
@@ -236,6 +230,7 @@ router.post("/:disk/move", async (req, res) => {
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/${route}`, "POST", { token: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -265,11 +260,10 @@ router.delete("/:disk/delete", async (req, res) => {
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// get current config
|
||||
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.` });
|
||||
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
|
||||
if (!disk) {
|
||||
res.status(500).send({ error: `Disk ${params.disk} does not exist.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
@@ -285,6 +279,7 @@ router.delete("/:disk/delete", async (req, res) => {
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -314,22 +309,17 @@ router.post("/:disk/create", async (req, res) => {
|
||||
size: req.body.size,
|
||||
iso: req.body.iso
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// get current config
|
||||
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.` });
|
||||
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
|
||||
if (disk) {
|
||||
res.status(500).send({ error: `Disk ${params.disk} does already exists.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
@@ -367,4 +357,5 @@ router.post("/:disk/create", async (req, res) => {
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { Router } from "express";
|
||||
export const router = Router({ mergeParams: true }); ;
|
||||
|
||||
const db = global.db;
|
||||
const checkAuth = global.utils.checkAuth;
|
||||
const approveResources = global.utils.approveResources;
|
||||
|
||||
@@ -27,14 +26,16 @@ router.post("/:netid/create", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
netid: req.params.netid.replace("net", ""),
|
||||
netid: Number(req.params.netid.replace("net", "")),
|
||||
rate: req.body.rate,
|
||||
name: req.body.name
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
// check netid is a valid number
|
||||
if (isNaN(params.netid)) {
|
||||
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
@@ -42,10 +43,9 @@ router.post("/:netid/create", async (req, res) => {
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// get current config
|
||||
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}`]) {
|
||||
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
|
||||
if (net) {
|
||||
res.status(500).send({ error: `Network interface net${params.netid} already exists.` });
|
||||
res.end();
|
||||
return;
|
||||
@@ -59,13 +59,14 @@ router.post("/:netid/create", async (req, res) => {
|
||||
network: Number(params.rate)
|
||||
};
|
||||
// check resource approval
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// setup action
|
||||
const nc = db.getUser(userObj).templates.network[params.type];
|
||||
const nc = (await global.userManager.getUser(userObj, req.cookies)).templates.network[params.type];
|
||||
const action = {};
|
||||
if (params.type === "lxc") {
|
||||
action[`net${params.netid}`] = `name=${params.name},bridge=${nc.bridge},ip=${nc.ip},ip6=${nc.ip6},tag=${nc.vlan},type=${nc.type},rate=${params.rate}`;
|
||||
@@ -77,6 +78,7 @@ router.post("/:netid/create", async (req, res) => {
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -100,34 +102,33 @@ router.post("/:netid/modify", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
netid: req.params.netid.replace("net", ""),
|
||||
netid: Number(req.params.netid.replace("net", "")),
|
||||
rate: req.body.rate
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
|
||||
// check netid is a valid number
|
||||
if (isNaN(params.netid)) {
|
||||
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// get current config
|
||||
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}`]) {
|
||||
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
|
||||
if (!net) {
|
||||
res.status(500).send({ error: `Network interface net${params.netid} does not exist.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
const currentNetworkConfig = currentConfig.data.data[`net${params.netid}`];
|
||||
const currentNetworkRate = currentNetworkConfig.split("rate=")[1].split(",")[0];
|
||||
const request = {
|
||||
network: Number(params.rate) - Number(currentNetworkRate)
|
||||
network: Number(params.rate) - Number(net.rate)
|
||||
};
|
||||
// check resource approval
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
|
||||
res.end();
|
||||
@@ -135,11 +136,12 @@ router.post("/:netid/modify", async (req, res) => {
|
||||
}
|
||||
// setup action
|
||||
const action = {};
|
||||
action[`net${params.netid}`] = currentNetworkConfig.replace(`rate=${currentNetworkRate}`, `rate=${params.rate}`);
|
||||
action[`net${params.netid}`] = net.value.replace(`rate=${net.rate}`, `rate=${params.rate}`);
|
||||
const method = params.type === "qemu" ? "POST" : "PUT";
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -161,18 +163,23 @@ router.delete("/:netid/delete", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
netid: req.params.netid.replace("net", "")
|
||||
netid: Number(req.params.netid.replace("net", ""))
|
||||
};
|
||||
// check netid is a valid number
|
||||
if (isNaN(params.netid)) {
|
||||
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// get current config
|
||||
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}`]) {
|
||||
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
|
||||
if (!net) {
|
||||
res.status(500).send({ error: `Network interface net${params.netid} does not exist.` });
|
||||
res.end();
|
||||
return;
|
||||
@@ -182,4 +189,5 @@ router.delete("/:netid/delete", async (req, res) => {
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, { delete: `net${params.netid}` });
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
@@ -22,30 +22,28 @@ router.get("/:hostpci", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
hostpci: req.params.hostpci.replace("hostpci", "")
|
||||
hostpci: Number(req.params.hostpci.replace("hostpci", ""))
|
||||
};
|
||||
// check hostpci is a valid number
|
||||
if (isNaN(params.hostpci)) {
|
||||
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// check device is in instance config
|
||||
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();
|
||||
return;
|
||||
}
|
||||
const device = config[`hostpci${params.hostpci}`].split(",")[0];
|
||||
// get node's pci devices
|
||||
const deviceData = await global.pve.getDeviceInfo(params.node, device);
|
||||
if (!deviceData) {
|
||||
// get device
|
||||
const device = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||
if (!device) {
|
||||
res.status(500).send({ error: `Could not find hostpci${params.hostpci}=${device} in ${params.node}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
res.status(200).send(deviceData);
|
||||
res.status(200).send(device);
|
||||
res.end();
|
||||
});
|
||||
|
||||
@@ -70,15 +68,16 @@ router.post("/:hostpci/modify", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
hostpci: req.params.hostpci.replace("hostpci", ""),
|
||||
hostpci: Number(req.params.hostpci.replace("hostpci", "")),
|
||||
device: req.body.device,
|
||||
pcie: req.body.pcie
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
|
||||
// check hostpci is a valid number
|
||||
if (isNaN(params.hostpci)) {
|
||||
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check if type is qemu
|
||||
if (params.type !== "qemu") {
|
||||
res.status(500).send({ error: "Type must be qemu (vm)." });
|
||||
@@ -93,28 +92,33 @@ 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 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) {
|
||||
// device must exist to be modified
|
||||
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||
if (!existingDevice) {
|
||||
res.status(500).send({ error: `No device in hostpci${params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// only check user and node availability if base id is different
|
||||
if (currentDeviceData.id.split(".")[0] !== params.device) {
|
||||
// only check user and node availability if base id is different, we do the split in case of existing partial-function hostpci
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
if (existingDevice.device_id.split(".")[0] !== params.device) {
|
||||
// setup request
|
||||
const deviceData = await global.pve.getDeviceInfo(params.node, params.device);
|
||||
const request = { pci: deviceData.device_name };
|
||||
const node = await global.pve.getNode(params.node);
|
||||
const requestedDevice = node.devices[`${params.device}`];
|
||||
const request = { pci: requestedDevice.device_name };
|
||||
if (!requestedDevice) {
|
||||
res.status(500).send({ request, error: `Could not fulfil request for ${params.device}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check resource approval
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
res.status(500).send({ request, error: `Could not fulfil request for ${deviceData.device_name}.` });
|
||||
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check node availability
|
||||
const nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
|
||||
if (!nodeAvailPci.some(element => element.id.split(".")[0] === params.device)) {
|
||||
if (!Object.values(node.devices).some(element => element.device_id.split(".")[0] === params.device && element.reserved === false)) {
|
||||
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
|
||||
res.end();
|
||||
return;
|
||||
@@ -124,18 +128,9 @@ router.post("/:hostpci/modify", async (req, res) => {
|
||||
const action = {};
|
||||
action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
||||
// commit action
|
||||
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, 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();
|
||||
return;
|
||||
}
|
||||
const rootcookies = {
|
||||
PVEAuthCookie: rootauth.data.data.ticket,
|
||||
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
|
||||
};
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action);
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncNode(params.node);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -152,20 +147,22 @@ router.post("/:hostpci/modify", async (req, res) => {
|
||||
* - 500: {request: Object, error: string}
|
||||
* - 500: PVE Task Object
|
||||
*/
|
||||
router.post("/create", async (req, res) => {
|
||||
router.post("/:hostpci/create", async (req, res) => {
|
||||
req.params = Object.assign({}, req.routeparams, req.params);
|
||||
const params = {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
hostpci: Number(req.params.hostpci.replace("hostpci", "")),
|
||||
device: req.body.device,
|
||||
pcie: req.body.pcie
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
|
||||
// check hostpci is a valid number
|
||||
if (isNaN(params.hostpci)) {
|
||||
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check if type is qemu
|
||||
if (params.type !== "qemu") {
|
||||
res.status(500).send({ error: "Type must be qemu (vm)." });
|
||||
@@ -180,46 +177,38 @@ 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 = global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { cookies: params.cookies });
|
||||
let hostpci = 0;
|
||||
while (config[`hostpci${hostpci}`]) {
|
||||
hostpci++;
|
||||
// device must not exist to be added
|
||||
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||
if (existingDevice) {
|
||||
res.status(500).send({ error: `Existing device in hostpci${params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// setup request
|
||||
const deviceData = await global.pve.getDeviceInfo(params.node, params.device);
|
||||
const request = {
|
||||
pci: deviceData.device_name
|
||||
};
|
||||
const node = await global.pve.getNode(params.node);
|
||||
const requestedDevice = node.devices[`${params.device}`];
|
||||
const request = { pci: requestedDevice.device_name };
|
||||
// check resource approval
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
res.status(500).send({ request, error: `Could not fulfil request for ${deviceData.device_name}.` });
|
||||
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check node availability
|
||||
const nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
|
||||
if (!nodeAvailPci.some(element => element.id.split(".")[0] === params.device)) {
|
||||
// const node = await global.pve.getNode(params.node);
|
||||
if (!Object.values(node.devices).some(element => element.device_id.split(".")[0] === params.device && element.reserved === false)) {
|
||||
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// setup action
|
||||
const action = {};
|
||||
action[`hostpci${hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
||||
action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
||||
// commit action
|
||||
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, 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();
|
||||
return;
|
||||
}
|
||||
const rootcookies = {
|
||||
PVEAuthCookie: rootauth.data.data.ticket,
|
||||
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
|
||||
};
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action);
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncNode(params.node);
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -241,8 +230,14 @@ router.delete("/:hostpci/delete", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
hostpci: req.params.hostpci.replace("hostpci", "")
|
||||
hostpci: Number(req.params.hostpci.replace("hostpci", ""))
|
||||
};
|
||||
// check hostpci is a valid number
|
||||
if (isNaN(params.hostpci)) {
|
||||
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check if type is qemu
|
||||
if (params.type !== "qemu") {
|
||||
res.status(500).send({ error: "Type must be qemu (vm)." });
|
||||
@@ -256,8 +251,8 @@ router.delete("/:hostpci/delete", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
// check device is in instance config
|
||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
||||
if (!config[`hostpci${params.hostpci}`]) {
|
||||
const device = global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||
if (!device) {
|
||||
res.status(500).send({ error: `Could not find hostpci${params.hostpci} in ${params.vmid}.` });
|
||||
res.end();
|
||||
return;
|
||||
@@ -265,16 +260,7 @@ router.delete("/:hostpci/delete", async (req, res) => {
|
||||
// setup action
|
||||
const action = { 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 global.pve.requestPVE("/access/ticket", "POST", null, 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();
|
||||
return;
|
||||
}
|
||||
const rootcookies = {
|
||||
PVEAuthCookie: rootauth.data.data.ticket,
|
||||
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
|
||||
};
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action);
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncNode(params.node);
|
||||
});
|
||||
|
@@ -20,7 +20,9 @@ router.get("/config/:key", async (req, res) => {
|
||||
const allowKeys = ["resources"];
|
||||
if (allowKeys.includes(params.key)) {
|
||||
const config = global.config;
|
||||
res.status(200).send(config[params.key]);
|
||||
const result = {};
|
||||
result[params.key] = config[params.key];
|
||||
res.status(200).send(result);
|
||||
}
|
||||
else {
|
||||
res.status(401).send({ auth: false, error: `User is not authorized to access /global/config/${params.key}.` });
|
||||
|
@@ -51,7 +51,7 @@ if (schemes.hash.enabled) {
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
// get current cluster resources
|
||||
// get current cluster resources - do not use fabric here because fabric is not always updated to changes like up/down state changes
|
||||
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);
|
||||
@@ -165,12 +165,10 @@ if (schemes.interrupt.enabled) {
|
||||
socket.destroy();
|
||||
}
|
||||
else {
|
||||
wsServer.handleUpgrade(req, socket, head, (socket) => {
|
||||
wsServer.handleUpgrade(req, socket, head, async (socket) => {
|
||||
// get the user pools
|
||||
const userRealm = cookies.username.split("@").at(-1);
|
||||
const userID = cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
const pools = Object.keys(global.db.getUser(userObj).cluster.pools);
|
||||
const userObj = global.utils.getUserObjFromUsername(cookies.username);
|
||||
const pools = Object.keys((await global.userManager.getUser(userObj, cookies)).cluster.pools);
|
||||
// emit the connection to initialize socket
|
||||
wsServer.emit("connection", socket, cookies.username, pools);
|
||||
});
|
||||
|
@@ -3,7 +3,6 @@ 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
|
||||
@@ -12,17 +11,19 @@ const getUserResources = global.utils.getUserResources;
|
||||
* - 401: {auth: false}
|
||||
*/
|
||||
router.get("/dynamic/resources", async (req, res) => {
|
||||
const params = {
|
||||
username: req.cookies.username
|
||||
};
|
||||
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
const userObj = global.utils.getUserObjFromUsername(params.username);
|
||||
|
||||
const resources = await getUserResources(req, userObj);
|
||||
const resources = await global.utils.getUserResources(req, userObj);
|
||||
res.status(200).send(resources);
|
||||
});
|
||||
|
||||
@@ -40,9 +41,7 @@ router.get("/config/:key", async (req, res) => {
|
||||
key: req.params.key
|
||||
};
|
||||
|
||||
const userRealm = req.cookies.username.split("@").at(-1);
|
||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
|
||||
// check auth
|
||||
const auth = await checkAuth(req.cookies, res);
|
||||
@@ -51,7 +50,7 @@ router.get("/config/:key", async (req, res) => {
|
||||
}
|
||||
const allowKeys = ["resources", "cluster"];
|
||||
if (allowKeys.includes(params.key)) {
|
||||
const config = global.db.getUser(userObj);
|
||||
const config = await global.userManager.getUser(userObj, req.cookies);
|
||||
res.status(200).send(config[params.key]);
|
||||
}
|
||||
else {
|
||||
|
203
src/utils.js
203
src/utils.js
@@ -15,18 +15,34 @@ import { exit } from "process";
|
||||
export async function checkAuth (cookies, res, vmpath = null) {
|
||||
let auth = false;
|
||||
|
||||
const userRealm = cookies.username.split("@").at(-1);
|
||||
const userID = cookies.username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
|
||||
if (global.db.getUser(userObj) === null) {
|
||||
auth = false;
|
||||
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in localdb.` });
|
||||
const userObj = getUserObjFromUsername(cookies.username); // check if username exists and is valid
|
||||
if (!userObj) {
|
||||
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Username was missing or invalid." });
|
||||
res.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vmpath) {
|
||||
if (!cookies.PVEAuthCookie) { // check if PVE token exists
|
||||
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Token was missing or invalid." });
|
||||
res.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
const pveTicket = cookies.PVEAuthCookie;
|
||||
const result = await global.pve.requestPVE("/access/ticket", "POST", null, { username: cookies.username, password: pveTicket });
|
||||
if (result.status !== 200) { // check if PVE token is valid by using /access/ticket to validate ticket with Proxmox
|
||||
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Username did not match token." });
|
||||
res.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((await global.userManager.getUser(userObj, cookies)) === null) { // check if user exists in database
|
||||
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in database.` });
|
||||
res.end();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (vmpath) { // if a path is specified, check the permissions on the path
|
||||
const result = await global.pve.requestPVE(`/${vmpath}/config`, "GET", { cookies });
|
||||
auth = result.status === 200;
|
||||
}
|
||||
@@ -39,72 +55,10 @@ export async function checkAuth (cookies, res, vmpath = null) {
|
||||
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "User token did not pass authentication check." });
|
||||
res.end();
|
||||
}
|
||||
|
||||
return auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full config of an instance, including searching disk information.
|
||||
* @param {Object} req ProxmoxAAS API request object.
|
||||
* @param {Object} instance to get config as object containing node, type, and id.
|
||||
* @param {Array} diskprefixes Array containing prefixes for disks.
|
||||
* @returns
|
||||
*/
|
||||
async function getFullInstanceConfig (req, instance, diskprefixes) {
|
||||
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(global.pve.getDiskInfo(instance.node, config, key));
|
||||
mappings.push(key);
|
||||
}
|
||||
else if (key.startsWith("hostpci")) {
|
||||
promises.push(global.pve.getDeviceInfo(instance.node, config[key].split(",")[0]));
|
||||
mappings.push(key);
|
||||
}
|
||||
}
|
||||
const results = await Promise.all(promises);
|
||||
results.forEach((e, i) => {
|
||||
const key = mappings[i];
|
||||
config[key] = e;
|
||||
});
|
||||
config.node = instance.node;
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all configs for every instance owned by the user. Uses the expanded config data from getFullInstanceConfig.
|
||||
* @param {Object} req ProxmoxAAS API request object.
|
||||
* @param {Object} dbResources data about application resources, to indicate which resources are tracked.
|
||||
* @returns {Object} k-v pairs of resource name and used amounts
|
||||
*/
|
||||
async function getAllInstanceConfigs (req, diskprefixes) {
|
||||
// get the basic resources list
|
||||
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 = [];
|
||||
for (const resource of resources) {
|
||||
if (resource.type === "lxc" || resource.type === "qemu") {
|
||||
instances.push(resource);
|
||||
}
|
||||
}
|
||||
|
||||
// get all instance configs, also include detailed disk and device info
|
||||
const promises = [];
|
||||
const mappings = [];
|
||||
for (let i = 0; i < instances.length; i++) {
|
||||
const instance = instances[i];
|
||||
const config = getFullInstanceConfig(req, instance, diskprefixes);
|
||||
promises.push(config);
|
||||
mappings.push(i);
|
||||
}
|
||||
const configs = await Promise.all(promises);
|
||||
|
||||
return configs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user resource data including used, available, and maximum resources.
|
||||
* @param {Object} req ProxmoxAAS API request object.
|
||||
@@ -113,17 +67,7 @@ async function getAllInstanceConfigs (req, diskprefixes) {
|
||||
*/
|
||||
export async function getUserResources (req, user) {
|
||||
const dbResources = global.config.resources;
|
||||
const userResources = global.db.getUser(user).resources;
|
||||
|
||||
// setup disk prefixes object
|
||||
const diskprefixes = [];
|
||||
for (const resourceName of Object.keys(dbResources)) {
|
||||
if (dbResources[resourceName].type === "storage") {
|
||||
for (const diskPrefix of dbResources[resourceName].disks) {
|
||||
diskprefixes.push(diskPrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
const userResources = (await global.userManager.getUser(user, req.cookies)).resources;
|
||||
|
||||
// setup the user resource object with used and avail for each resource and each resource pool
|
||||
// also add a total counter for each resource (only used for display, not used to check requests)
|
||||
@@ -177,10 +121,12 @@ export async function getUserResources (req, user) {
|
||||
}
|
||||
}
|
||||
|
||||
const configs = await getAllInstanceConfigs(req, diskprefixes);
|
||||
const configs = await global.pve.getUserResources(user, req.cookies);
|
||||
|
||||
for (const config of configs) {
|
||||
for (const vmid in configs) {
|
||||
const config = configs[vmid];
|
||||
const nodeName = config.node;
|
||||
// count basic numeric resources
|
||||
for (const resourceName of Object.keys(config)) {
|
||||
// numeric resource type
|
||||
if (resourceName in dbResources && dbResources[resourceName].type === "numeric") {
|
||||
@@ -198,63 +144,75 @@ export async function getUserResources (req, user) {
|
||||
userResources[resourceName].total.used += val;
|
||||
userResources[resourceName].total.avail -= val;
|
||||
}
|
||||
else if (diskprefixes.some(prefix => resourceName.startsWith(prefix))) {
|
||||
const diskInfo = config[resourceName];
|
||||
if (diskInfo) { // only count if disk exists
|
||||
const val = Number(diskInfo.size);
|
||||
const storage = diskInfo.storage;
|
||||
}
|
||||
// count disk resources in volumes
|
||||
for (const diskid in config.volumes) {
|
||||
const disk = config.volumes[diskid];
|
||||
const storage = disk.storage;
|
||||
const size = disk.size;
|
||||
// only process disk if its storage is in the user resources to be counted
|
||||
if (storage in userResources) {
|
||||
// if the instance's node is restricted by this resource, add it to the instance's used value
|
||||
if (nodeName in userResources[storage].nodes) {
|
||||
userResources[storage].nodes[nodeName].used += val;
|
||||
userResources[storage].nodes[nodeName].avail -= val;
|
||||
userResources[storage].nodes[nodeName].used += size;
|
||||
userResources[storage].nodes[nodeName].avail -= size;
|
||||
}
|
||||
// otherwise add the resource to the global pool
|
||||
else {
|
||||
userResources[storage].global.used += val;
|
||||
userResources[storage].global.avail -= val;
|
||||
userResources[storage].global.used += size;
|
||||
userResources[storage].global.avail -= size;
|
||||
}
|
||||
userResources[storage].total.used += val;
|
||||
userResources[storage].total.avail -= val;
|
||||
userResources[storage].total.used += size;
|
||||
userResources[storage].total.avail -= size;
|
||||
}
|
||||
}
|
||||
else if (resourceName.startsWith("net") && config[resourceName].includes("rate=")) { // only count net instances with a rate limit
|
||||
const val = Number(config[resourceName].split("rate=")[1].split(",")[0]);
|
||||
// count net resources in nets
|
||||
for (const netid in config.nets) {
|
||||
const net = config.nets[netid];
|
||||
const rate = net.rate;
|
||||
if (userResources.network) {
|
||||
// if the instance's node is restricted by this resource, add it to the instance's used value
|
||||
if (nodeName in userResources.network.nodes) {
|
||||
userResources.network.nodes[nodeName].used += val;
|
||||
userResources.network.nodes[nodeName].avail -= val;
|
||||
userResources.network.nodes[nodeName].used += rate;
|
||||
userResources.network.nodes[nodeName].avail -= rate;
|
||||
}
|
||||
// otherwise add the resource to the global pool
|
||||
else {
|
||||
userResources.network.global.used += val;
|
||||
userResources.network.global.avail -= val;
|
||||
userResources.network.global.used += rate;
|
||||
userResources.network.global.avail -= rate;
|
||||
}
|
||||
userResources.network.total.used += val;
|
||||
userResources.network.total.avail -= val;
|
||||
userResources.network.total.used += rate;
|
||||
userResources.network.total.avail -= rate;
|
||||
}
|
||||
else if (resourceName.startsWith("hostpci")) {
|
||||
const deviceInfo = config[resourceName];
|
||||
if (deviceInfo) { // only count if device exists
|
||||
const deviceName = deviceInfo.device_name;
|
||||
// if the instance's node is restricted by this resource, add it to the instance's used value
|
||||
}
|
||||
// count pci device resources in devices
|
||||
for (const deviceid in config.devices) {
|
||||
const device = config.devices[deviceid];
|
||||
const name = device.device_name;
|
||||
// if the node has a node specific rule, add it there
|
||||
if (nodeName in userResources.pci.nodes) {
|
||||
const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => deviceName.includes(availEelement.match));
|
||||
const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match));
|
||||
if (index >= 0) {
|
||||
userResources.pci.nodes[nodeName][index].used++;
|
||||
userResources.pci.nodes[nodeName][index].avail--;
|
||||
}
|
||||
// otherwise add the resource to the global pool
|
||||
}
|
||||
// otherwise try to add the resource to the global pool
|
||||
else {
|
||||
const index = userResources.pci.global.findIndex((availEelement) => deviceName.includes(availEelement.match));
|
||||
const index = userResources.pci.global.findIndex((availEelement) => name.includes(availEelement.match));
|
||||
if (index >= 0) { // device resource is in the user's global list then increment it by 1
|
||||
userResources.pci.global[index].used++;
|
||||
userResources.pci.global[index].avail--;
|
||||
}
|
||||
const index = userResources.pci.total.findIndex((availEelement) => deviceName.includes(availEelement.match));
|
||||
}
|
||||
// finally, add the device to the total map
|
||||
const index = userResources.pci.total.findIndex((availEelement) => name.includes(availEelement.match));
|
||||
if (index >= 0) {
|
||||
userResources.pci.total[index].used++;
|
||||
userResources.pci.total[index].avail--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return userResources;
|
||||
}
|
||||
@@ -331,7 +289,7 @@ export function getTimeLeft (timeout) {
|
||||
/**
|
||||
* Recursively import routes from target folder.
|
||||
* @param {Object} router or app object.
|
||||
* @param {string} baseroute API route for each imported module.
|
||||
* @param {string} baseroute base route of imported modules starting from the current path.
|
||||
* @param {string} target folder to import modules.
|
||||
* @param {string} from source folder of calling module, optional for imports from the same base directory.
|
||||
*/
|
||||
@@ -362,3 +320,20 @@ export function readJSONFile (path) {
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} username
|
||||
* @returns {Object | null} user object containing username and realm or null if user does not exist
|
||||
*/
|
||||
export function getUserObjFromUsername (username) {
|
||||
if (username) {
|
||||
const userRealm = username.split("@").at(-1);
|
||||
const userID = username.replace(`@${userRealm}`, "");
|
||||
const userObj = { id: userID, realm: userRealm };
|
||||
return userObj;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@@ -25,13 +25,13 @@
|
||||
},
|
||||
"memory": {
|
||||
"global": {
|
||||
"max": 131072
|
||||
"max": 137438953472
|
||||
},
|
||||
"nodes": {}
|
||||
},
|
||||
"swap": {
|
||||
"global": {
|
||||
"max": 131072
|
||||
"max": 137438953472
|
||||
},
|
||||
"nodes": {}
|
||||
},
|
||||
@@ -72,6 +72,7 @@
|
||||
}
|
||||
},
|
||||
"cluster": {
|
||||
"admin": false,
|
||||
"nodes": {
|
||||
"example-node-0": true,
|
||||
"example-node-1": true,
|
||||
@@ -102,12 +103,20 @@
|
||||
"value": "host",
|
||||
"resource": null
|
||||
},
|
||||
"machine": {
|
||||
"value": "q35",
|
||||
"resource": null
|
||||
},
|
||||
"net0": {
|
||||
"value": "virtio,bridge=vmbr0,tag=10,rate=1000",
|
||||
"resource": {
|
||||
"name": "network",
|
||||
"amount": 1000
|
||||
}
|
||||
},
|
||||
"scsihw": {
|
||||
"value": "virtio-scsi-single",
|
||||
"resource": null
|
||||
}
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user