Compare commits

..

No commits in common. "main" and "update-localdb" have entirely different histories.

19 changed files with 601 additions and 1135 deletions

108
README.md

@ -11,7 +11,7 @@ ProxmoxAAS API provides functionality for the Dashboard by providing a proxy API
- Server with NodeJS (v18.0+) and NPM installed - Server with NodeJS (v18.0+) and NPM installed
### Configuring API Token and Permissions ### Configuring API Token and Permissions
In the Proxmox web GUI, perform the following steps: In Proxmox VE, follow the following steps:
1. Add a new user `proxmoxaas-api` to Proxmox VE 1. Add a new user `proxmoxaas-api` to Proxmox VE
2. Create a new API token for the user `proxmoxaas-api` and copy the secret key to a safe location 2. Create a new API token for the user `proxmoxaas-api` and copy the secret key to a safe location
3. Create a new role `proxmoxaas-api` with at least the following permissions: 3. Create a new role `proxmoxaas-api` with at least the following permissions:
@ -19,100 +19,52 @@ In the Proxmox web GUI, perform the following steps:
- Datastore.Allocate, Datastore.AllocateSpace, Datastore.Audit - Datastore.Allocate, Datastore.AllocateSpace, Datastore.Audit
- User.Modify - User.Modify
- Pool.Audit - 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` 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` 5. Add a new User Permission with path: `/`, select the `proxmoxaas-api` user, and role: `proxmoxaas-api`
### Installation - API ### Installation - API
1. Clone this repo onto the `ProxmoxAAS-API` host 1. Clone this repo onto `Dashboard Host`
2. Run `npm install` to initiaze the package requirements 2. Run `npm install` to initiaze the package requirements
3. Copy `template.config.json` as `config.json` and modify the following values: 3. Copy `template.config.json` as `config.json` and modify the following values:
1. In `backends/pve/config`: 1. In `backends/pve/config`:
- url: the URI to the Proxmox API, ie `https://pve.domain.net/api2/json` - url: the URI to the Proxmox API, ie `http://<proxmoxhost>:8006/api2/json` or `http://<proxmox URL>/api2/json` if Proxmox VE is behind a reverse proxy.
- fabric: the URL to the ProxmoxAAS-Fabric, ie `https://fabric.local`
- token: the user(name), authentication realm (pam), token id, and token secrey key (uuid) - token: the user(name), authentication realm (pam), token id, and token secrey key (uuid)
- root (**Optional**): In order to allow users to customize instance pcie devices, the API must use the root credentials for privilege elevation. Provide the root username, ie. `root@pam`, and root user password - root (**Optional**): In order to allow users to customize instance pcie devices, the API must use the root credentials for privilege elevation. Provide the root username, ie. `root@pam`, and root user password
2. In `backends/paasldap/config` (**Optional**): 2. In `backends/paasldap/config` (**Optional**):
- url: url to a PAAS-LDAP server API ie. `http://ldap.local` - url: url to a PAAS-LDAP server API ie. `http://<paasldap-host>:8082`
3. In `handlers/auth`: 3. In `handlers/auth`:
- Add any authentication handlers to be used by the API. Add the realm name (ie. `pve`) as the key and the handler name as provided in `backends`. For example, a PAAS-LDAP handler could be added as `"paas-ldap": "paasldap"` and users in the realm `user@paas-ldap` will use this handler to perform auth actions. Refer to [backends](#Backends) - Add any authentication handlers to be used by the API. Add the realm name (ie. `pve`) as the key and the handler name as provided in `backends`. For example, a PAAS-LDAP handler could be added as `"paas-ldap": "paasldap"` and users in the realm `user@paas-ldap` will use this handler to perform auth actions. Refer to [backends](#Backends)
4. In `application`: 4. In `application`:
- hostname - the ProxmoxAAS-Dashboard URL, ie `paas.domain.net` - hostname - the ProxmoxAAS-Dashboard URL, ie `host.domain.tld`
- domain - the base domain for the dashboard and proxmox, ie `domain.net` - domain - the base domain for the dashboard and proxmox, ie `domain.tld`
- listenPort - the port you want the API to listen on, ie `8081` - listenPort - the port you want the API to listen on, ie `8081`
5. In `useriso`: 5. In `useriso`:
- node: host of storage with user accessible iso files - node: host of storage with user accessible iso files
- storage: name of storage with user accessible iso files - storage: name of storage with user accessible iso files
4. Start the service using `node .`, or call the provided shell script, or use the provided systemctl service script 4. Start the service using `node .`, or call the provided shell script, or use the provided systemctl service script
### Installation - Reverse Proxy
1. Configure nginx or preferred reverse proxy to reverse proxy the dashboard. The configuration should include at least the following:
```
server {
listen 443 ssl;
server_name paas.<FQDN>;
location / {
return 301 "/dashboard/";
}
location /dashboard/ {
proxy_pass http://proxmoxaas.dmz:8080/;
proxy_redirect default;
}
location /api/ {
proxy_pass http://proxmoxaas.dmz:80/api/;
proxy_redirect default;
}
}
```
2. Start nginx with the new configurations
### Result
After these steps, the ProxmoxAAS Dashboard should be available and fully functional at `paas.<FQDN>` or `paas.<FQDN>/dashboard/`.
# Backends # 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,7 +4,6 @@
"import": "pve.js", "import": "pve.js",
"config": { "config": {
"url": "https://pve.mydomain.example/api2/json", "url": "https://pve.mydomain.example/api2/json",
"fabric": "http://localhost:8082",
"token": { "token": {
"user": "proxmoxaas-api", "user": "proxmoxaas-api",
"realm": "pam", "realm": "pam",
@ -26,29 +25,15 @@
"paasldap": { "paasldap": {
"import": "paasldap.js", "import": "paasldap.js",
"config": { "config": {
"url": "http://paasldap.mydomain.example", "url": "http://paasldap.mydomain.example"
"realm": "ldap"
} }
} }
}, },
"handlers": { "handlers": {
"instance": { "pve": "pve",
"db": "localdb",
"auth": {
"pve": "pve" "pve": "pve"
},
"users": {
"realm": {
"pve": [
"localdb"
],
"ldap": [
"localdb",
"paasldap"
]
},
"any": [
"localdb",
"paasldap"
]
} }
}, },
"application": { "application": {
@ -78,7 +63,7 @@
"memory": { "memory": {
"name": "RAM", "name": "RAM",
"type": "numeric", "type": "numeric",
"multiplier": 1, "multiplier": 1048576,
"base": 1024, "base": 1024,
"compact": true, "compact": true,
"unit": "B", "unit": "B",
@ -87,7 +72,7 @@
"swap": { "swap": {
"name": "SWAP", "name": "SWAP",
"type": "numeric", "type": "numeric",
"multiplier": 1, "multiplier": 1048576,
"base": 1024, "base": 1024,
"compact": true, "compact": true,
"unit": "B", "unit": "B",

@ -1,13 +1,13 @@
{ {
"name": "proxmoxaas-api", "name": "proxmoxaas-api",
"version": "1.0.0", "version": "0.0.1",
"description": "REST API for ProxmoxAAS", "description": "REST API for ProxmoxAAS",
"main": "src/main.js", "main": "src/main.js",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"axios": "^1.3.2", "axios": "^1.3.2",
"body-parser": "^1.20.1", "body-parser": "^1.20.1",
"cookie": "^1.0.2", "cookie": "^0.5.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.18.2", "express": "^4.18.2",

@ -2,7 +2,7 @@ import path from "path";
import url from "url"; import url from "url";
export default async () => { export default async () => {
global.backends = {}; const backends = {};
for (const name in global.config.backends) { for (const name in global.config.backends) {
// get files and config // get files and config
const target = global.config.backends[name].import; const target = global.config.backends[name].import;
@ -14,11 +14,17 @@ export default async () => {
const importPath = `./${path.relative(thisPath, targetPath)}`; const importPath = `./${path.relative(thisPath, targetPath)}`;
// import and add to list of imported handlers // import and add to list of imported handlers
const Backend = (await import(importPath)).default; const Backend = (await import(importPath)).default;
global.backends[name] = new Backend(config); backends[name] = new Backend(config);
console.log(`backends: initialized backend ${name} from ${importPath}`); console.log(`backends: initialized backend ${name} from ${importPath}`);
} }
global.pve = global.backends[global.config.handlers.instance.pve]; // assign backends to handlers by type
global.userManager = new USER_BACKEND_MANAGER(global.config.handlers.users); 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]];
});
}; };
/** /**
@ -28,15 +34,13 @@ export default async () => {
class BACKEND { class BACKEND {
/** /**
* Opens a session with the backend and creates session tokens if needed * Opens a session with the backend and creates session tokens if needed
* @param {{id: string, realm: string}} user object containing id and realm * @param {{username: string, password: string}} credentials object containing username and password fields
* @param {string} password * @returns {{ok: boolean, status: number, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value
* @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 (user, password) { openSession (credentials) {
return { return {
ok: true, ok: true,
status: 200, status: 200,
message: "",
cookies: [] cookies: []
}; };
} }
@ -54,209 +58,85 @@ 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. * Interface for backend types that store/interact with user & group data.
* Not all backends need to implement all interface methods. * Not all backends need to implement all interface methods.
*/ */
class USER_BACKEND extends BACKEND { class USER_BACKEND extends BACKEND {
/** /**
* Validate an add user operation with the following parameters. * Add user to backend
* 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, realm: string}} user
* @param {Object} attributes user attributes * @param {Object} attributes user attributes
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/ */
addUser (user, attributes, params) {} addUser (user, attributes, params = null) {}
/** /**
* Get user from backend * Get user from backend
* @param {{id: string, realm: string}} user * @param {{id: string, realm: string}} user
* @param {Object} params authentication params, usually req.cookies * @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) {} getUser (user, params = null) {}
/** /**
* Get all users from backend * Modify user in 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 {{id: string, realm: string}} user
* @param {Object} attributes new user attributes to modify * @param {Object} attributes new user attributes to modify
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/ */
setUser (user, attributes, params) {} setUser (user, attributes, params = null) {}
/** /**
* Validate a delete user operation with the following parameters. * Delete user from backend
* 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, realm: string}} user
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/ */
delUser (user, params) {} deluser (user, params = null) {}
/** /**
* Validate an add group operation with the following parameters. * Add group to backend
* Returns whether the change is valid and a delta object to be used in the operation. * @param {{id: string}} group
* @param {{id: string, realm: string}} group
* @param {Object} attributes group attributes * @param {Object} attributes group attributes
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/ */
addGroup (group, attributes, params) {} addGroup (group, attributes, params = null) {}
/** /**
* Get group from backend * Get group from backend
* @param {{id: string}} group * @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies * @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) {} 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) {}
/** /**
* Get all users from backend * Add user to group
* @param {Object} params authentication params, usually req.cookies
* @returns {Array} containing each group data from this backend
*/
getAllGroups (params) {}
/**
* 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, realm: string}} user
* @param {{id: string}} group * @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/ */
addUserToGroup (user, group, params) {} addUserToGroup (user, group, params = null) {}
/** /**
* Validate a remove user from group operation with the following parameters. * Remove user from group
* 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, realm: string}} user
* @param {{id: string}} group * @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/ */
delUserFromGroup (user, group, params) {} delUserFromGroup (user, group, params = null) {}
} }
/** /**
* Interface for proxmox api backends. * 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. * Interface for user database backends.
@ -267,105 +147,3 @@ export class DB_BACKEND extends USER_BACKEND {}
* Interface for user auth backends. * Interface for user auth backends.
*/ */
export class AUTH_BACKEND extends USER_BACKEND {} 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 { readFileSync, writeFileSync } from "fs";
import { exit } from "process"; import { exit } from "process";
import { AtomicChange, DB_BACKEND, doNothingCallback } from "./backends.js"; import { DB_BACKEND } from "./backends.js";
export default class LocalDB extends DB_BACKEND { export default class LocalDB extends DB_BACKEND {
#path = null; #path = null;
@ -35,82 +35,74 @@ export default class LocalDB extends DB_BACKEND {
writeFileSync(this.#path, JSON.stringify(this.#data)); writeFileSync(this.#path, JSON.stringify(this.#data));
} }
addUser (user, attributes, params) {} addUser (user, attributes, params = null) {
const username = `${user.id}@${user.realm}`;
getUser (user, params) { attributes = attributes || this.#defaultuser;
const requestedUser = `${user.id}@${user.realm}`; this.#data.users[username] = attributes;
const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token this.#save();
// 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;
}
} }
async getAllUsers (params) { getUser (user, params = null) {
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 null;
}
}
setUser (user, attributes, params) {
if (attributes.resources && attributes.cluster && attributes.templates) {
const username = `${user.id}@${user.realm}`; const username = `${user.id}@${user.realm}`;
if (this.#data.users[username]) { if (this.#data.users[username]) {
if (this.#data.users[params.username] && this.#data.users[params.username].cluster.admin) { return this.#data.users[username];
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 { ok: true, status: 200, message: "" };
},
{ ok: true, status: 200, message: "" }
);
} }
else { else {
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) {}
getGroup (group, params) {}
getAllGroups (params) {
return null; return null;
} }
}
setGroup (group, attributes, params) {}
delGroup (group, params) {} setUser (user, attributes, params = null) {
const username = `${user.id}@${user.realm}`;
// assume that adding to group also adds to group's pool if (this.#data.users[username]) {
addUserToGroup (user, group, params) {} this.#data.users[username] = attributes;
this.#save();
// assume that adding to group also adds to group's pool return true;
delUserFromGroup (user, group, params) {} }
else {
return false;
}
}
delUser (user, params = null) {
const username = `${user.id}@${user.realm}`;
if (this.#data.users[username]) {
delete this.#data.users[username];
this.#save();
return true;
}
else {
return false;
}
}
// 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;
}
}
// 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;
}
}
} }

@ -1,15 +1,13 @@
import axios from "axios"; import axios from "axios";
import { AtomicChange, AUTH_BACKEND, doNothingCallback } from "./backends.js"; import { AUTH_BACKEND } from "./backends.js";
import * as setCookie from "set-cookie-parser"; import * as setCookie from "set-cookie-parser";
export default class PAASLDAP extends AUTH_BACKEND { export default class PAASLDAP extends AUTH_BACKEND {
#url = null; #url = null;
#realm = null;
constructor (config) { constructor (config) {
super(); super();
this.#url = config.url; this.#url = config.url;
this.#realm = config.realm;
} }
/** /**
@ -17,7 +15,7 @@ export default class PAASLDAP extends AUTH_BACKEND {
* @param {*} path HTTP path, prepended with the paas-LDAP API base url * @param {*} path HTTP path, prepended with the paas-LDAP API base url
* @param {*} method HTTP method * @param {*} method HTTP method
* @param {*} body body parameters and data to be sent. Optional. * @param {*} body body parameters and data to be sent. Optional.
* @returns {Object} HTTP response object * @returns {Object} HTTP response object or HTTP error object.
*/ */
async #request (path, method, auth = null, body = null) { async #request (path, method, auth = null, body = null) {
const url = `${this.#url}${path}`; const url = `${this.#url}${path}`;
@ -41,23 +39,19 @@ export default class PAASLDAP extends AUTH_BACKEND {
return result; return result;
} }
catch (error) { catch (error) {
const result = error.response; error.ok = false;
result.ok = result.status === 200; error.status = 500;
return result; error.data = {
} error: error.code
}
#handleGenericReturn (res) {
return {
ok: res.ok,
status: res.status,
message: res.ok ? "" : res.data.error
}; };
return error;
}
} }
async openSession (user, password) { async openSession (credentials) {
const username = user.id; const userRealm = credentials.username.split("@").at(-1);
const content = { username, password }; const uid = credentials.username.replace(`@${userRealm}`, "");
const content = { uid, password: credentials.password };
const result = await this.#request("/ticket", "POST", null, content); const result = await this.#request("/ticket", "POST", null, content);
if (result.ok) { if (result.ok) {
const cookies = setCookie.parse(result.headers["set-cookie"]); const cookies = setCookie.parse(result.headers["set-cookie"]);
@ -67,118 +61,51 @@ export default class PAASLDAP extends AUTH_BACKEND {
return { return {
ok: true, ok: true,
status: result.status, status: result.status,
message: "",
cookies cookies
}; };
} }
else { else {
return { return result;
ok: false,
status: result.status,
message: result.data.error,
cookies: []
};
} }
} }
async addUser (user, attributes, params) {} async addUser (user, attributes, params = null) {
return await this.#request(`/users/${user.id}`, "POST", params, attributes);
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 getAllUsers (params) { async getUser (user, params = null) {
if (!params) { return await this.#request(`/users/${user.id}`, "GET", 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) { async setUser (user, attributes, params = null) {
if (!attributes.userpassword && !attributes.cn && attributes.sn) { return await this.#request(`/users/${user.id}`, "POST", params, attributes);
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) {} async delUser (user, params = null) {
return await this.#request(`/users/${user.id}`, "DELETE", params);
}
async addGroup (group, attributes, params) {} async addGroup (group, attributes, params = null) {
return await this.#request(`/groups/${group.id}`, "POST", params);
}
async getGroup (group, params) { async getGroup (group, params = null) {
return await this.#request(`/groups/${group.id}`, "GET", params); return await this.#request(`/groups/${group.id}`, "GET", params);
} }
async getAllGroups (params) { async setGroup (group, attributes, params = null) {
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 // not implemented, LDAP groups do not have any attributes to change
return new AtomicChange(true, {}, doNothingCallback, null); ;
} }
async delGroup (group, params) {} async delGroup (group, params = null) {
return await this.#request(`/groups/${group.id}`, "DELETE", params);
async addUserToGroup (user, group, params) {} }
async delUserFromGroup (user, group, params) {} async addUserToGroup (user, group, params = null) {
return await this.#request(`/groups/${group.id}/members/${user.id}`, "POST", params);
}
async delUserFromGroup (user, group, params = null) {
return await this.#request(`/groups/${group.id}/members/${user.id}`, "DELETE", params);
}
} }

@ -5,26 +5,18 @@ export default class PVE extends PVE_BACKEND {
#pveAPIURL = null; #pveAPIURL = null;
#pveAPIToken = null; #pveAPIToken = null;
#pveRoot = null; #pveRoot = null;
#paasFabric = null;
constructor (config) { constructor (config) {
super(); super();
this.#pveAPIURL = config.url; this.#pveAPIURL = config.url;
this.#pveAPIToken = config.token; this.#pveAPIToken = config.token;
this.#pveRoot = config.root; this.#pveRoot = config.root;
this.#paasFabric = config.fabric;
} }
async openSession (user, password) { async openSession (credentials) {
const credentials = { username: `${user.id}@${user.realm}`, password };
const response = await global.pve.requestPVE("/access/ticket", "POST", null, credentials); const response = await global.pve.requestPVE("/access/ticket", "POST", null, credentials);
if (!(response.status === 200)) { if (!(response.status === 200)) {
return { return response;
ok: false,
status: response.status,
message: "Authorization failed",
cookies: []
};
} }
const ticket = response.data.data.ticket; const ticket = response.data.data.ticket;
const csrftoken = response.data.data.CSRFPreventionToken; const csrftoken = response.data.data.CSRFPreventionToken;
@ -74,22 +66,11 @@ export default class PVE extends PVE_BACKEND {
const token = this.#pveAPIToken; const token = this.#pveAPIToken;
content.headers.Authorization = `PVEAPIToken=${token.user}@${token.realm}!${token.id}=${token.uuid}`; 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 { try {
return await axios.request(url, content); return await axios.request(url, content);
} }
catch (error) { catch (error) {
console.log(`backends: error ocuured in pve.requestPVE: ${error}`);
return error.response; return error.response;
} }
} }
@ -104,15 +85,11 @@ export default class PVE extends PVE_BACKEND {
*/ */
async handleResponse (node, result, res) { async handleResponse (node, result, res) {
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
if (result.status !== 200) { if (result.data.data && typeof (result.data.data) === "string" && result.data.data.startsWith("UPID:")) {
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; const upid = result.data.data;
let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true }); let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
while (taskStatus.data.data.status !== "stopped") { while (taskStatus.data.data.status !== "stopped") {
await waitFor(100); await waitFor(1000);
taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true }); taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
} }
if (taskStatus.data.data.exitstatus === "OK") { if (taskStatus.data.data.exitstatus === "OK") {
@ -137,111 +114,82 @@ export default class PVE extends PVE_BACKEND {
} }
/** /**
* Send HTTP request to PAAS Fabric * Get meta data for a specific disk. Adds info that is not normally available in a instance's config.
* @param {string} path HTTP path, prepended with the proxmox API base url. * @param {string} node containing the query disk.
* @param {string} method HTTP method. * @param {string} config of instance with query disk.
* @param {Object} auth authentication method. Set auth.cookies with user cookies or auth.token with PVE API Token. Optional. * @param {string} disk name of the query disk, ie. sata0.
* @param {string} body body parameters and data to be sent. Optional. * @returns {Objetc} k-v pairs of specific disk data, including storage and size of unused disks.
* @returns {Object} HTTP response object or HTTP error object.
*/ */
async requestFabric (path, method, body = null) { async getDiskInfo (node, config, disk) {
const url = `${this.#paasFabric}${path}`;
const content = {
method,
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: body
};
try { try {
return await axios.request(url, content); const storageID = config[disk].split(":")[0];
const volID = config[disk].split(",")[0];
const volInfo = await this.requestPVE(`/nodes/${node}/storage/${storageID}/content/${volID}`, "GET", { token: true });
volInfo.data.data.storage = storageID;
return volInfo.data.data;
} }
catch (error) { catch {
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; return null;
} }
} }
async getNet (node, instance, netid) { /**
const config = await this.getInstance(node, instance); * Get meta data for a specific pci device. Adds info that is not normally available in a instance's config.
if (config != null && config.nets[netid] != null) { * @param {string} node containing the query device.
return config.nets[netid]; * @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);
} }
else { });
deviceData.sort((a, b) => {
return a.id < b.id;
});
const device = deviceData[0];
device.subfn = structuredClone(deviceData.slice(1));
return device;
}
catch {
return null; return null;
} }
} }
async getDevice (node, instance, deviceid) { /**
const config = await this.getInstance(node, instance); * Get available devices on specific node.
if (config != null && config.devices[deviceid] != null) { * @param {string} node to get devices from.
return config.devices[deviceid]; * @returns {Array.<Object>} array of k-v pairs of specific device data, including device name and manufacturer, which are available on the specified node.
} */
else { async getNodeAvailDevices (node) {
return null; // 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;
async getUserResources (user, cookies) { const promises = [];
// get user resources with vm filter for (const vm of vms) {
const res = await this.requestPVE("/cluster/resources?type=vm", "GET", { cookies }); promises.push(this.requestPVE(`/nodes/${node}/qemu/${vm.vmid}/config`, "GET", { token: true }));
if (res.status !== 200) {
return null;
} }
const configs = await Promise.all(promises);
configs.forEach((e, i) => {
configs[i] = e.data.data;
});
const userPVEResources = res.data.data; nodeAvailPci = (await nodeAvailPci).data.data;
const resources = {}; for (const config of configs) {
Object.keys(config).forEach((key) => {
// for each resource, add to the object if (key.startsWith("hostpci")) {
for (const resource of userPVEResources) { const deviceID = config[key].split(",")[0];
const instance = await this.getInstance(resource.node, resource.vmid); nodeAvailPci = nodeAvailPci.filter(element => !element.id.includes(deviceID));
if (instance) { }
instance.node = resource.node; });
resources[resource.vmid] = instance; }
} return nodeAvailPci;
}
return resources;
} }
} }

@ -1,41 +0,0 @@
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 });
});

@ -1,42 +0,0 @@
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 });
});

@ -3,8 +3,6 @@ export const router = Router({ mergeParams: true }); ;
const checkAuth = global.utils.checkAuth; const checkAuth = global.utils.checkAuth;
global.utils.recursiveImportRoutes(router, "", "access", import.meta.url);
/** /**
* GET - check authentication * GET - check authentication
* responses: * responses:
@ -25,12 +23,12 @@ router.get("/", async (req, res) => {
class CookieFetcher { class CookieFetcher {
#fetchedBackends = []; #fetchedBackends = [];
#cookies = []; #cookies = [];
async fetchBackends (backends, user, password) { async fetchBackends (backends, credentials) {
for (const backend of backends) { for (const backend of backends) {
if (this.#fetchedBackends.indexOf(backend) === -1) { if (this.#fetchedBackends.indexOf(backend) === -1) {
const response = await global.backends[backend].openSession(user, password); const response = await backend.openSession(credentials);
if (!response.ok) { if (!response.ok) {
return response.message; return false;
} }
this.#cookies = this.#cookies.concat(response.cookies); this.#cookies = this.#cookies.concat(response.cookies);
this.#fetchedBackends.push(backend); this.#fetchedBackends.push(backend);
@ -39,7 +37,7 @@ class CookieFetcher {
continue; continue;
} }
} }
return null; return true;
} }
exportCookies () { exportCookies () {
@ -61,33 +59,25 @@ router.post("/ticket", async (req, res) => {
username: req.body.username, username: req.body.username,
password: req.body.password password: req.body.password
}; };
const domain = global.config.application.domain; const domain = global.config.application.domain;
const userObj = global.utils.getUserObjFromUsername(params.username); const userRealm = params.username.split("@").at(-1);
let backends = global.userManager.getBackendsByUser(userObj); const backends = [global.pve, global.db];
if (backends == null) { if (userRealm in global.auth) {
res.status(401).send({ auth: false, error: `${params.username} not found in any ProxmoxAAS backends` }); backends.push(global.auth[userRealm]);
return;
} }
backends = backends.concat(["pve"]);
const cm = new CookieFetcher(); const cm = new CookieFetcher();
const error = await cm.fetchBackends(backends, userObj, params.password); const success = await cm.fetchBackends(backends, params);
if (error) { if (!success) {
res.status(401).send({ auth: false, error }); res.status(401).send({ auth: false });
return; return;
} }
const cookies = cm.exportCookies(); const cookies = cm.exportCookies();
let minimumExpires = Infinity;
for (const cookie of cookies) { for (const cookie of cookies) {
const expiresDate = new Date(Date.now() + cookie.expiresMSFromNow); const expiresDate = new Date(Date.now() + cookie.expiresMSFromNow);
res.cookie(cookie.name, cookie.value, { domain, path: "/", httpOnly: true, secure: true, expires: expiresDate, sameSite: "none" }); res.cookie(cookie.name, cookie.value, { domain, path: "/", httpOnly: true, secure: true, expires: expiresDate });
if (cookie.expiresMSFromNow < minimumExpires) {
minimumExpires = cookie.expiresMSFromNow;
} }
} res.cookie("username", params.username, { domain, path: "/", secure: true });
const expiresDate = new Date(Date.now() + minimumExpires); res.cookie("auth", 1, { domain, path: "/", secure: true });
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 }); res.status(200).send({ auth: true });
}); });
@ -104,10 +94,10 @@ router.delete("/ticket", async (req, res) => {
const domain = global.config.application.domain; const domain = global.config.application.domain;
const expire = new Date(0); const expire = new Date(0);
for (const cookie in req.cookies) { for (const cookie in req.cookies) {
res.cookie(cookie, "", { domain, path: "/", expires: expire, secure: true, sameSite: "none" }); res.cookie(cookie, "", { domain, path: "/", expires: expire });
} }
await global.pve.closeSession(req.cookies); await global.pve.closeSession(req.cookies);
await global.userManager.closeSession(req.cookies); await global.db.closeSession(req.cookies);
res.status(200).send({ auth: false }); res.status(200).send({ auth: false });
}); });
@ -124,16 +114,24 @@ router.post("/password", async (req, res) => {
password: req.body.password password: req.body.password
}; };
// check auth const userRealm = params.username.split("@").at(-1);
const auth = await checkAuth(req.cookies, res); const authHandlers = global.config.handlers.auth;
if (!auth) { const userID = params.username.replace(`@${userRealm}`, "");
return; const userObj = { id: userID, realm: userRealm };
} if (userRealm in authHandlers) {
const handler = authHandlers[userRealm];
const userObj = global.utils.getUserObjFromUsername(params.username);
const newAttributes = { const newAttributes = {
userpassword: params.password userpassword: params.password
}; };
const response = await global.userManager.setUser(userObj, newAttributes, req.cookies); const response = await handler.setUser(userObj, newAttributes, req.cookies);
res.status(response.status).send(response); 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.` });
}
}); });

@ -1,6 +1,7 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); export const router = Router({ mergeParams: true });
const db = global.db;
const checkAuth = global.utils.checkAuth; const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources; const approveResources = global.utils.approveResources;
const getUserResources = global.utils.getUserResources; const getUserResources = global.utils.getUserResources;
@ -13,61 +14,6 @@ const basePath = `/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP}
global.utils.recursiveImportRoutes(router, basePath, "cluster", import.meta.url); 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 * GET - get available pcie devices given node and user
* request: * request:
@ -83,14 +29,16 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
node: req.params.node node: req.params.node
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
const userNodes = (await global.userManager.getUser(userObj, req.cookies)).cluster.nodes; const userNodes = db.getUser(userObj).cluster.nodes;
if (userNodes[params.node] !== true) { if (userNodes[params.node] !== true) {
res.status(401).send({ auth: false, path: params.node }); res.status(401).send({ auth: false, path: params.node });
res.end(); res.end();
@ -98,58 +46,13 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
} }
// get remaining user resources // get remaining user resources
const userAvailPci = (await getUserResources(req, userObj)).pci.nodes[params.node]; 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 // get node avail devices
const node = await global.pve.getNode(params.node); let nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
let availableDevices = []; nodeAvailPci = nodeAvailPci.filter(nodeAvail => userAvailPci.some((userAvail) => {
// 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; 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(); res.end();
}
});
/**
* GET - get basic resources for vm using the fabric format
* request:
* - node: string - vm host node id
* - type: string - vm type (lxc, qemu)
* - vmid: number - vm id number
* response:
* - 200: Fabric instance config
* - 401: {auth: false}
*/
router.get(`${basePath}`, async (req, res) => {
const params = {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid
};
// 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 instance = await global.pve.getInstance(params.node, params.vmid);
res.status(200).send(instance);
}); });
/** /**
@ -180,7 +83,9 @@ router.post(`${basePath}/resources`, async (req, res) => {
boot: req.body.boot boot: req.body.boot
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@ -189,13 +94,13 @@ router.post(`${basePath}/resources`, async (req, res) => {
return; return;
} }
// get current config // get current config
const instance = await global.pve.getInstance(params.node, params.vmid); const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true });
const request = { const request = {
cores: Number(params.cores) - Number(instance.cores), cores: Number(params.cores) - Number(currentConfig.data.data.cores),
memory: Number(params.memory) - Number(instance.memory) memory: Number(params.memory) - Number(currentConfig.data.data.memory)
}; };
if (params.type === "lxc") { if (params.type === "lxc") {
request.swap = Number(params.swap) - Number(instance.swap); request.swap = Number(params.swap) - Number(currentConfig.data.data.swap);
} }
else if (params.type === "qemu") { else if (params.type === "qemu") {
request.cpu = params.proctype; request.cpu = params.proctype;
@ -219,7 +124,6 @@ router.post(`${basePath}/resources`, async (req, res) => {
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
}); });
/** /**
@ -261,7 +165,9 @@ router.post(`${basePath}/create`, async (req, res) => {
rootfssize: req.body.rootfssize rootfssize: req.body.rootfssize
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await checkAuth(req.cookies, res);
@ -269,7 +175,7 @@ router.post(`${basePath}/create`, async (req, res) => {
return; return;
} }
// get user db config // get user db config
const user = await global.userManager.getUser(userObj, req.cookies); const user = await db.getUser(userObj);
const vmid = Number.parseInt(params.vmid); const vmid = Number.parseInt(params.vmid);
const vmidMin = user.cluster.vmid.min; const vmidMin = user.cluster.vmid.min;
const vmidMax = user.cluster.vmid.max; const vmidMax = user.cluster.vmid.max;
@ -341,7 +247,6 @@ router.post(`${basePath}/create`, async (req, res) => {
// commit action // commit action
const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}`, "POST", { token: true }, 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.handleResponse(params.node, result, res);
await global.pve.syncNode(params.node);
}); });
/** /**
@ -370,5 +275,4 @@ router.delete(`${basePath}/delete`, async (req, res) => {
// commit action // commit action
const result = await global.pve.requestPVE(vmpath, "DELETE", { token: true }); const result = await global.pve.requestPVE(vmpath, "DELETE", { token: true });
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncNode(params.node);
}); });

@ -31,9 +31,10 @@ router.post("/:disk/detach", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// disk must exist // disk must exist
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); if (!config[params.disk]) {
if (!disk) {
res.status(500).send({ error: `Disk ${params.disk} does not exist.` }); res.status(500).send({ error: `Disk ${params.disk} does not exist.` });
res.end(); res.end();
return; return;
@ -48,7 +49,6 @@ router.post("/:disk/detach", async (req, res) => {
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
}); });
/** /**
@ -80,30 +80,29 @@ router.post("/:disk/attach", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// disk must exist // disk must exist
const disk = await global.pve.getDisk(params.node, params.vmid, `unused${params.source}`); if (!config[`unused${params.source}`]) {
if (!disk) { res.status(403).send({ error: `Requested disk unused${params.source} does not exist.` });
res.status(500).send({ error: `Requested disk unused${params.source} does not exist.` });
res.end(); res.end();
return; return;
} }
// target disk must be allowed according to source disk's storage options // 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; const resourceConfig = global.config.resources;
if (!resourceConfig[disk.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) { 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[disk.storage].disks}].` }); res.status(500).send({ error: `Requested target ${params.disk} is not in allowed list [${resourceConfig[diskConfig.storage].disks}].` });
res.end(); res.end();
return; return;
} }
// setup action using source disk info from vm config // setup action using source disk info from vm config
const action = {}; const action = {};
action[params.disk] = disk.file; action[params.disk] = config[`unused${params.source}`];
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
}); });
/** /**
@ -131,7 +130,9 @@ router.post("/:disk/resize", async (req, res) => {
size: req.body.size size: req.body.size
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@ -139,15 +140,17 @@ router.post("/:disk/resize", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// check disk existence // check disk existence
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk const diskConfig = await global.pve.getDiskInfo(params.node, config, params.disk); // get target disk
if (!disk) { // exit if disk does not exist if (!diskConfig) { // exit if disk does not exist
res.status(500).send({ error: `requested disk ${params.disk} does not exist.` }); res.status(500).send({ error: `requested disk ${params.disk} does not exist.` });
res.end(); res.end();
return; return;
} }
// setup request // setup request
const storage = disk.storage; // get the storage const storage = diskConfig.storage; // get the storage
const request = {}; const request = {};
request[storage] = Number(params.size * 1024 ** 3); // setup request object request[storage] = Number(params.size * 1024 ** 3); // setup request object
// check request approval // check request approval
@ -160,7 +163,6 @@ router.post("/:disk/resize", async (req, res) => {
const action = { disk: params.disk, size: `+${params.size}G` }; const action = { disk: params.disk, size: `+${params.size}G` };
const result = await global.pve.requestPVE(`${vmpath}/resize`, "PUT", { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/resize`, "PUT", { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
}); });
/** /**
@ -190,7 +192,9 @@ router.post("/:disk/move", async (req, res) => {
delete: req.body.delete delete: req.body.delete
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@ -198,15 +202,17 @@ router.post("/:disk/move", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// check disk existence // check disk existence
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk const diskConfig = await global.pve.getDiskInfo(params.node, config, params.disk); // get target disk
if (!disk) { // exit if disk does not exist if (!diskConfig) { // exit if disk does not exist
res.status(500).send({ error: `requested disk ${params.disk} does not exist.` }); res.status(500).send({ error: `requested disk ${params.disk} does not exist.` });
res.end(); res.end();
return; return;
} }
// setup request // setup request
const size = parseInt(disk.size); // get source disk size const size = parseInt(diskConfig.size); // get source disk size
const dstStorage = params.storage; // get destination storage const dstStorage = params.storage; // get destination storage
const request = {}; const request = {};
if (!params.delete) { // if not delete, then request storage, otherwise it is net 0 if (!params.delete) { // if not delete, then request storage, otherwise it is net 0
@ -230,7 +236,6 @@ router.post("/:disk/move", async (req, res) => {
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/${route}`, "POST", { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/${route}`, "POST", { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
}); });
/** /**
@ -260,10 +265,11 @@ router.delete("/:disk/delete", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// disk must exist // disk must exist
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); if (!config[params.disk]) {
if (!disk) { res.status(403).send({ error: `Requested disk unused${params.source} does not exist.` });
res.status(500).send({ error: `Disk ${params.disk} does not exist.` });
res.end(); res.end();
return; return;
} }
@ -279,7 +285,6 @@ router.delete("/:disk/delete", async (req, res) => {
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
}); });
/** /**
@ -309,17 +314,22 @@ router.post("/:disk/create", async (req, res) => {
size: req.body.size, size: req.body.size,
iso: req.body.iso iso: req.body.iso
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get current config
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
// disk must not exist // disk must not exist
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); if (config[params.disk]) {
if (disk) { res.status(403).send({ error: `Requested disk ${params.disk} already exists.` });
res.status(500).send({ error: `Disk ${params.disk} does already exists.` });
res.end(); res.end();
return; return;
} }
@ -357,5 +367,4 @@ router.post("/:disk/create", async (req, res) => {
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
}); });

@ -1,6 +1,7 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); ; export const router = Router({ mergeParams: true }); ;
const db = global.db;
const checkAuth = global.utils.checkAuth; const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources; const approveResources = global.utils.approveResources;
@ -26,20 +27,26 @@ router.post("/:netid/create", async (req, res) => {
node: req.params.node, node: req.params.node,
type: req.params.type, type: req.params.type,
vmid: req.params.vmid, vmid: req.params.vmid,
netid: req.params.netid, netid: req.params.netid.replace("net", ""),
rate: req.body.rate, rate: req.body.rate,
name: req.body.name 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 auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; 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 // net interface must not exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid); if (currentConfig.data.data[`net${params.netid}`]) {
if (net) { res.status(500).send({ error: `Network interface net${params.netid} already exists.` });
res.status(500).send({ error: `Network interface ${params.netid} already exists.` });
res.end(); res.end();
return; return;
} }
@ -52,26 +59,24 @@ router.post("/:netid/create", async (req, res) => {
network: Number(params.rate) network: Number(params.rate)
}; };
// check resource approval // check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (!await approveResources(req, userObj, request, params.node)) { 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.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
res.end(); res.end();
return; return;
} }
// setup action // setup action
const nc = (await global.userManager.getUser(userObj, req.cookies)).templates.network[params.type]; const nc = db.getUser(userObj).templates.network[params.type];
const action = {}; const action = {};
if (params.type === "lxc") { if (params.type === "lxc") {
action[`${params.netid}`] = `name=${params.name},bridge=${nc.bridge},ip=${nc.ip},ip6=${nc.ip6},tag=${nc.vlan},type=${nc.type},rate=${params.rate}`; 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}`;
} }
else { else {
action[`${params.netid}`] = `${nc.type},bridge=${nc.bridge},tag=${nc.vlan},rate=${params.rate}`; action[`net${params.netid}`] = `${nc.type},bridge=${nc.bridge},tag=${nc.vlan},rate=${params.rate}`;
} }
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
}); });
/** /**
@ -95,27 +100,34 @@ router.post("/:netid/modify", async (req, res) => {
node: req.params.node, node: req.params.node,
type: req.params.type, type: req.params.type,
vmid: req.params.vmid, vmid: req.params.vmid,
netid: req.params.netid, netid: req.params.netid.replace("net", ""),
rate: req.body.rate 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 auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; 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 // net interface must already exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid); if (!currentConfig.data.data[`net${params.netid}`]) {
if (!net) {
res.status(500).send({ error: `Network interface net${params.netid} does not exist.` }); res.status(500).send({ error: `Network interface net${params.netid} does not exist.` });
res.end(); res.end();
return; return;
} }
const currentNetworkConfig = currentConfig.data.data[`net${params.netid}`];
const currentNetworkRate = currentNetworkConfig.split("rate=")[1].split(",")[0];
const request = { const request = {
network: Number(params.rate) - Number(net.rate) network: Number(params.rate) - Number(currentNetworkRate)
}; };
// check resource approval // check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (!await approveResources(req, userObj, request, params.node)) { 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.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
res.end(); res.end();
@ -123,12 +135,11 @@ router.post("/:netid/modify", async (req, res) => {
} }
// setup action // setup action
const action = {}; const action = {};
action[`${params.netid}`] = net.value.replace(`rate=${net.rate}`, `rate=${params.rate}`); action[`net${params.netid}`] = currentNetworkConfig.replace(`rate=${currentNetworkRate}`, `rate=${params.rate}`);
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
}); });
/** /**
@ -150,7 +161,7 @@ router.delete("/:netid/delete", async (req, res) => {
node: req.params.node, node: req.params.node,
type: req.params.type, type: req.params.type,
vmid: req.params.vmid, vmid: req.params.vmid,
netid: req.params.netid netid: req.params.netid.replace("net", "")
}; };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@ -158,9 +169,10 @@ router.delete("/:netid/delete", async (req, res) => {
if (!auth) { if (!auth) {
return; 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 // net interface must already exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid); if (!currentConfig.data.data[`net${params.netid}`]) {
if (!net) {
res.status(500).send({ error: `Network interface net${params.netid} does not exist.` }); res.status(500).send({ error: `Network interface net${params.netid} does not exist.` });
res.end(); res.end();
return; return;
@ -168,7 +180,6 @@ router.delete("/:netid/delete", async (req, res) => {
// setup action // setup action
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, { delete: `${params.netid}` }); 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.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid);
}); });

@ -22,7 +22,7 @@ router.get("/:hostpci", async (req, res) => {
node: req.params.node, node: req.params.node,
type: req.params.type, type: req.params.type,
vmid: req.params.vmid, vmid: req.params.vmid,
hostpci: req.params.hostpci hostpci: req.params.hostpci.replace("hostpci", "")
}; };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@ -30,14 +30,22 @@ router.get("/:hostpci", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get device // check device is in instance config
const device = await global.pve.getDevice(params.node, params.vmid, params.hostpci); const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
if (!device) { if (!config[`hostpci${params.hostpci}`]) {
res.status(500).send({ error: `Could not find ${params.hostpci}=${device} in ${params.node}.` }); res.status(500).send({ error: `Could not find hostpci${params.hostpci} in ${params.vmid}.` });
res.end(); res.end();
return; return;
} }
res.status(200).send(device); const device = config[`hostpci${params.hostpci}`].split(",")[0];
// get node's pci devices
const deviceData = await global.pve.getDeviceInfo(params.node, device);
if (!deviceData) {
res.status(500).send({ error: `Could not find hostpci${params.hostpci}=${device} in ${params.node}.` });
res.end();
return;
}
res.status(200).send(deviceData);
res.end(); res.end();
}); });
@ -62,10 +70,15 @@ router.post("/:hostpci/modify", async (req, res) => {
node: req.params.node, node: req.params.node,
type: req.params.type, type: req.params.type,
vmid: req.params.vmid, vmid: req.params.vmid,
hostpci: req.params.hostpci, hostpci: req.params.hostpci.replace("hostpci", ""),
device: req.body.device, device: req.body.device,
pcie: req.body.pcie 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 if type is qemu // check if type is qemu
if (params.type !== "qemu") { if (params.type !== "qemu") {
res.status(500).send({ error: "Type must be qemu (vm)." }); res.status(500).send({ error: "Type must be qemu (vm)." });
@ -80,33 +93,28 @@ router.post("/:hostpci/modify", async (req, res) => {
} }
// force all functions // force all functions
params.device = params.device.split(".")[0]; params.device = params.device.split(".")[0];
// device must exist to be modified // get instance config to check if device has not changed
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci); const config = (await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true })).data.data;
if (!existingDevice) { const currentDeviceData = await global.pve.getDeviceInfo(params.node, config[`hostpci${params.hostpci}`].split(",")[0]);
res.status(500).send({ error: `No device in ${params.hostpci}.` }); if (!currentDeviceData) {
res.status(500).send({ error: `No device in hostpci${params.hostpci}.` });
res.end(); res.end();
return; return;
} }
// only check user and node availability if base id is different, we do the split in case of existing partial-function hostpci // only check user and node availability if base id is different
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); if (currentDeviceData.id.split(".")[0] !== params.device) {
if (existingDevice.device_bus.split(".")[0] !== params.device) {
// setup request // setup request
const node = await global.pve.getNode(params.node); const deviceData = await global.pve.getDeviceInfo(params.node, params.device);
const requestedDevice = node.devices[`${params.device}`]; const request = { pci: deviceData.device_name };
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 // check resource approval
if (!await approveResources(req, userObj, request, params.node)) { if (!await approveResources(req, userObj, request, params.node)) {
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` }); res.status(500).send({ request, error: `Could not fulfil request for ${deviceData.device_name}.` });
res.end(); res.end();
return; return;
} }
// check node availability // check node availability
if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) { const nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
if (!nodeAvailPci.some(element => element.id.split(".")[0] === params.device)) {
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` }); res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
res.end(); res.end();
return; return;
@ -114,11 +122,20 @@ router.post("/:hostpci/modify", async (req, res) => {
} }
// setup action // setup action
const action = {}; const action = {};
action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`; action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, 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);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncNode(params.node);
}); });
/** /**
@ -135,16 +152,20 @@ router.post("/:hostpci/modify", async (req, res) => {
* - 500: {request: Object, error: string} * - 500: {request: Object, error: string}
* - 500: PVE Task Object * - 500: PVE Task Object
*/ */
router.post("/:hostpci/create", async (req, res) => { router.post("/create", async (req, res) => {
req.params = Object.assign({}, req.routeparams, req.params); req.params = Object.assign({}, req.routeparams, req.params);
const params = { const params = {
node: req.params.node, node: req.params.node,
type: req.params.type, type: req.params.type,
vmid: req.params.vmid, vmid: req.params.vmid,
hostpci: req.params.hostpci,
device: req.body.device, device: req.body.device,
pcie: req.body.pcie 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 if type is qemu // check if type is qemu
if (params.type !== "qemu") { if (params.type !== "qemu") {
res.status(500).send({ error: "Type must be qemu (vm)." }); res.status(500).send({ error: "Type must be qemu (vm)." });
@ -159,38 +180,46 @@ router.post("/:hostpci/create", async (req, res) => {
} }
// force all functions // force all functions
params.device = params.device.split(".")[0]; params.device = params.device.split(".")[0];
// device must not exist to be added // get instance config to find next available hostpci slot
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci); const config = global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { cookies: params.cookies });
if (existingDevice) { let hostpci = 0;
res.status(500).send({ error: `Existing device in ${params.hostpci}.` }); while (config[`hostpci${hostpci}`]) {
res.end(); hostpci++;
return;
} }
// setup request // setup request
const node = await global.pve.getNode(params.node); const deviceData = await global.pve.getDeviceInfo(params.node, params.device);
const requestedDevice = node.devices[`${params.device}`]; const request = {
const request = { pci: requestedDevice.device_name }; pci: deviceData.device_name
};
// check resource approval // check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (!await approveResources(req, userObj, request, params.node)) { if (!await approveResources(req, userObj, request, params.node)) {
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` }); res.status(500).send({ request, error: `Could not fulfil request for ${deviceData.device_name}.` });
res.end(); res.end();
return; return;
} }
// check node availability // check node availability
// const node = await global.pve.getNode(params.node); const nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) { if (!nodeAvailPci.some(element => element.id.split(".")[0] === params.device)) {
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` }); res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
res.end(); res.end();
return; return;
} }
// setup action // setup action
const action = {}; const action = {};
action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`; action[`hostpci${hostpci}`] = `${params.device},pcie=${params.pcie}`;
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, 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);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncNode(params.node);
}); });
/** /**
@ -212,7 +241,7 @@ router.delete("/:hostpci/delete", async (req, res) => {
node: req.params.node, node: req.params.node,
type: req.params.type, type: req.params.type,
vmid: req.params.vmid, vmid: req.params.vmid,
hostpci: req.params.hostpci hostpci: req.params.hostpci.replace("hostpci", "")
}; };
// check if type is qemu // check if type is qemu
if (params.type !== "qemu") { if (params.type !== "qemu") {
@ -227,16 +256,25 @@ router.delete("/:hostpci/delete", async (req, res) => {
return; return;
} }
// check device is in instance config // check device is in instance config
const device = global.pve.getDevice(params.node, params.vmid, params.hostpci); const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
if (!device) { if (!config[`hostpci${params.hostpci}`]) {
res.status(500).send({ error: `Could not find ${params.hostpci} in ${params.vmid}.` }); res.status(500).send({ error: `Could not find hostpci${params.hostpci} in ${params.vmid}.` });
res.end(); res.end();
return; return;
} }
// setup action // setup action
const action = { delete: `${params.hostpci}` }; 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 // commit action, need to use root user here because proxmox api only allows root to modify hostpci for whatever reason
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, 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);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncNode(params.node);
}); });

@ -20,9 +20,7 @@ router.get("/config/:key", async (req, res) => {
const allowKeys = ["resources"]; const allowKeys = ["resources"];
if (allowKeys.includes(params.key)) { if (allowKeys.includes(params.key)) {
const config = global.config; const config = global.config;
const result = {}; res.status(200).send(config[params.key]);
result[params.key] = config[params.key];
res.status(200).send(result);
} }
else { else {
res.status(401).send({ auth: false, error: `User is not authorized to access /global/config/${params.key}.` }); 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) { if (!auth) {
return; return;
} }
// get current cluster resources - do not use fabric here because fabric is not always updated to changes like up/down state changes // get current cluster resources
const status = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data; const status = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data;
// filter out just state information of resources that are needed // filter out just state information of resources that are needed
const state = extractClusterState(status, resourceTypes); const state = extractClusterState(status, resourceTypes);
@ -165,10 +165,12 @@ if (schemes.interrupt.enabled) {
socket.destroy(); socket.destroy();
} }
else { else {
wsServer.handleUpgrade(req, socket, head, async (socket) => { wsServer.handleUpgrade(req, socket, head, (socket) => {
// get the user pools // get the user pools
const userObj = global.utils.getUserObjFromUsername(cookies.username); const userRealm = cookies.username.split("@").at(-1);
const pools = Object.keys((await global.userManager.getUser(userObj, cookies)).cluster.pools); const userID = cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
const pools = Object.keys(global.db.getUser(userObj).cluster.pools);
// emit the connection to initialize socket // emit the connection to initialize socket
wsServer.emit("connection", socket, cookies.username, pools); wsServer.emit("connection", socket, cookies.username, pools);
}); });

@ -3,6 +3,7 @@ export const router = Router({ mergeParams: true }); ;
const config = global.config; const config = global.config;
const checkAuth = global.utils.checkAuth; 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 * GET - get db user resource information including allocated, free, and maximum resource values along with resource metadata
@ -11,19 +12,17 @@ const checkAuth = global.utils.checkAuth;
* - 401: {auth: false} * - 401: {auth: false}
*/ */
router.get("/dynamic/resources", async (req, res) => { router.get("/dynamic/resources", async (req, res) => {
const params = {
username: req.cookies.username
};
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
const userObj = global.utils.getUserObjFromUsername(params.username); const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
const resources = await global.utils.getUserResources(req, userObj); const resources = await getUserResources(req, userObj);
res.status(200).send(resources); res.status(200).send(resources);
}); });
@ -41,7 +40,9 @@ router.get("/config/:key", async (req, res) => {
key: req.params.key key: req.params.key
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userRealm = req.cookies.username.split("@").at(-1);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await checkAuth(req.cookies, res);
@ -50,7 +51,7 @@ router.get("/config/:key", async (req, res) => {
} }
const allowKeys = ["resources", "cluster"]; const allowKeys = ["resources", "cluster"];
if (allowKeys.includes(params.key)) { if (allowKeys.includes(params.key)) {
const config = await global.userManager.getUser(userObj, req.cookies); const config = global.db.getUser(userObj);
res.status(200).send(config[params.key]); res.status(200).send(config[params.key]);
} }
else { else {
@ -73,12 +74,7 @@ router.get("/vm-isos", async (req, res) => {
// get user iso config // get user iso config
const userIsoConfig = config.useriso; const userIsoConfig = config.useriso;
// get all isos // get all isos
const content = await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=iso`, "GET", { token: true }); const isos = (await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=iso`, "GET", { token: true })).data.data;
if (content.status !== 200) {
res.status(content.status).send({ error: content.statusText });
return;
}
const isos = content.data.data;
const userIsos = []; const userIsos = [];
isos.forEach((iso) => { isos.forEach((iso) => {
iso.name = iso.volid.replace(`${userIsoConfig.storage}:iso/`, ""); iso.name = iso.volid.replace(`${userIsoConfig.storage}:iso/`, "");
@ -103,12 +99,7 @@ router.get("/ct-templates", async (req, res) => {
// get user iso config // get user iso config
const userIsoConfig = config.useriso; const userIsoConfig = config.useriso;
// get all isos // get all isos
const content = await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=vztmpl`, "GET", { token: true }); const isos = (await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=vztmpl`, "GET", { token: true })).data.data;
if (content.status !== 200) {
res.status(content.status).send({ error: content.statusText });
return;
}
const isos = content.data.data;
const userIsos = []; const userIsos = [];
isos.forEach((iso) => { isos.forEach((iso) => {
iso.name = iso.volid.replace(`${userIsoConfig.storage}:vztmpl/`, ""); iso.name = iso.volid.replace(`${userIsoConfig.storage}:vztmpl/`, "");

@ -15,34 +15,18 @@ import { exit } from "process";
export async function checkAuth (cookies, res, vmpath = null) { export async function checkAuth (cookies, res, vmpath = null) {
let auth = false; let auth = false;
const userObj = getUserObjFromUsername(cookies.username); // check if username exists and is valid const userRealm = cookies.username.split("@").at(-1);
if (!userObj) { const userID = cookies.username.replace(`@${userRealm}`, "");
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Username was missing or invalid." }); 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.` });
res.end(); res.end();
return false; return false;
} }
if (!cookies.PVEAuthCookie) { // check if PVE token exists if (vmpath) {
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 }); const result = await global.pve.requestPVE(`/${vmpath}/config`, "GET", { cookies });
auth = result.status === 200; auth = result.status === 200;
} }
@ -55,10 +39,72 @@ 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.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "User token did not pass authentication check." });
res.end(); res.end();
} }
return auth; 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. * Get user resource data including used, available, and maximum resources.
* @param {Object} req ProxmoxAAS API request object. * @param {Object} req ProxmoxAAS API request object.
@ -67,7 +113,17 @@ export async function checkAuth (cookies, res, vmpath = null) {
*/ */
export async function getUserResources (req, user) { export async function getUserResources (req, user) {
const dbResources = global.config.resources; const dbResources = global.config.resources;
const userResources = (await global.userManager.getUser(user, req.cookies)).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);
}
}
}
// setup the user resource object with used and avail for each resource and each resource pool // 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) // also add a total counter for each resource (only used for display, not used to check requests)
@ -121,12 +177,10 @@ export async function getUserResources (req, user) {
} }
} }
const configs = await global.pve.getUserResources(user, req.cookies); const configs = await getAllInstanceConfigs(req, diskprefixes);
for (const vmid in configs) { for (const config of configs) {
const config = configs[vmid];
const nodeName = config.node; const nodeName = config.node;
// count basic numeric resources
for (const resourceName of Object.keys(config)) { for (const resourceName of Object.keys(config)) {
// numeric resource type // numeric resource type
if (resourceName in dbResources && dbResources[resourceName].type === "numeric") { if (resourceName in dbResources && dbResources[resourceName].type === "numeric") {
@ -144,75 +198,63 @@ export async function getUserResources (req, user) {
userResources[resourceName].total.used += val; userResources[resourceName].total.used += val;
userResources[resourceName].total.avail -= val; userResources[resourceName].total.avail -= val;
} }
} else if (diskprefixes.some(prefix => resourceName.startsWith(prefix))) {
// count disk resources in volumes const diskInfo = config[resourceName];
for (const diskid in config.volumes) { if (diskInfo) { // only count if disk exists
const disk = config.volumes[diskid]; const val = Number(diskInfo.size);
const storage = disk.storage; const storage = diskInfo.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 the instance's node is restricted by this resource, add it to the instance's used value
if (nodeName in userResources[storage].nodes) { if (nodeName in userResources[storage].nodes) {
userResources[storage].nodes[nodeName].used += size; userResources[storage].nodes[nodeName].used += val;
userResources[storage].nodes[nodeName].avail -= size; userResources[storage].nodes[nodeName].avail -= val;
} }
// otherwise add the resource to the global pool // otherwise add the resource to the global pool
else { else {
userResources[storage].global.used += size; userResources[storage].global.used += val;
userResources[storage].global.avail -= size; userResources[storage].global.avail -= val;
} }
userResources[storage].total.used += size; userResources[storage].total.used += val;
userResources[storage].total.avail -= size; userResources[storage].total.avail -= val;
} }
} }
// count net resources in nets else if (resourceName.startsWith("net") && config[resourceName].includes("rate=")) { // only count net instances with a rate limit
for (const netid in config.nets) { const val = Number(config[resourceName].split("rate=")[1].split(",")[0]);
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 the instance's node is restricted by this resource, add it to the instance's used value
if (nodeName in userResources.network.nodes) { if (nodeName in userResources.network.nodes) {
userResources.network.nodes[nodeName].used += rate; userResources.network.nodes[nodeName].used += val;
userResources.network.nodes[nodeName].avail -= rate; userResources.network.nodes[nodeName].avail -= val;
} }
// otherwise add the resource to the global pool // otherwise add the resource to the global pool
else { else {
userResources.network.global.used += rate; userResources.network.global.used += val;
userResources.network.global.avail -= rate; userResources.network.global.avail -= val;
} }
userResources.network.total.used += rate; userResources.network.total.used += val;
userResources.network.total.avail -= rate; userResources.network.total.avail -= val;
} }
} else if (resourceName.startsWith("hostpci")) {
// count pci device resources in devices const deviceInfo = config[resourceName];
for (const deviceid in config.devices) { if (deviceInfo) { // only count if device exists
const device = config.devices[deviceid]; const deviceName = deviceInfo.device_name;
const name = device.device_name; // if the instance's node is restricted by this resource, add it to the instance's used value
// if the node has a node specific rule, add it there
if (nodeName in userResources.pci.nodes) { if (nodeName in userResources.pci.nodes) {
const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match)); const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => deviceName.includes(availEelement.match));
if (index >= 0) {
userResources.pci.nodes[nodeName][index].used++; userResources.pci.nodes[nodeName][index].used++;
userResources.pci.nodes[nodeName][index].avail--; 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 { else {
const index = userResources.pci.global.findIndex((availEelement) => name.includes(availEelement.match)); const index = userResources.pci.global.findIndex((availEelement) => deviceName.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].used++;
userResources.pci.global[index].avail--; 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].used++;
userResources.pci.total[index].avail--; userResources.pci.total[index].avail--;
} }
} }
} }
}
return userResources; return userResources;
} }
@ -289,7 +331,7 @@ export function getTimeLeft (timeout) {
/** /**
* Recursively import routes from target folder. * Recursively import routes from target folder.
* @param {Object} router or app object. * @param {Object} router or app object.
* @param {string} baseroute base route of imported modules starting from the current path. * @param {string} baseroute API route for each imported module.
* @param {string} target folder to import modules. * @param {string} target folder to import modules.
* @param {string} from source folder of calling module, optional for imports from the same base directory. * @param {string} from source folder of calling module, optional for imports from the same base directory.
*/ */
@ -320,20 +362,3 @@ export function readJSONFile (path) {
exit(1); 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": { "memory": {
"global": { "global": {
"max": 137438953472 "max": 131072
}, },
"nodes": {} "nodes": {}
}, },
"swap": { "swap": {
"global": { "global": {
"max": 137438953472 "max": 131072
}, },
"nodes": {} "nodes": {}
}, },
@ -72,7 +72,6 @@
} }
}, },
"cluster": { "cluster": {
"admin": false,
"nodes": { "nodes": {
"example-node-0": true, "example-node-0": true,
"example-node-1": true, "example-node-1": true,
@ -103,20 +102,12 @@
"value": "host", "value": "host",
"resource": null "resource": null
}, },
"machine": {
"value": "q35",
"resource": null
},
"net0": { "net0": {
"value": "virtio,bridge=vmbr0,tag=10,rate=1000", "value": "virtio,bridge=vmbr0,tag=10,rate=1000",
"resource": { "resource": {
"name": "network", "name": "network",
"amount": 1000 "amount": 1000
} }
},
"scsihw": {
"value": "virtio-scsi-single",
"resource": null
} }
} }
}, },