25 Commits

Author SHA1 Message Date
alu f64c8899fe fix bug in disk routes using getUserObjFromUsername 2026-05-31 02:37:23 +00:00
alu ca1041ca16 cleanup api test files 2026-05-29 20:42:20 +00:00
alu 104640facd undo logical error in approveResources 2026-05-27 22:20:28 +00:00
alu af2194a8b3 various code cleanup and commenting 2026-05-26 22:35:56 +00:00
alu 46295fabde improve various error logging messages 2026-05-26 19:13:52 +00:00
alu 6c77443aee move and fix get avaliable pci devices to GET /node/type/instance/pci 2026-05-26 19:04:10 +00:00
alu 4bc71e2212 workaround for node pcie get devices 2026-05-26 18:44:03 +00:00
alu 24ed6907c7 initial updates to api v2.0.0:
-  switch access backend to access-manager-api
- change resource quota to pool based
-  simplify backend system
- various cleanup
2026-05-24 19:08:39 +00:00
alu cf47cf6c71 update configs 2026-04-29 21:33:56 +00:00
alu 7ea579df68 update eslint 2026-03-25 18:09:47 +00:00
alu e26849283e remove commented code 2025-11-17 03:43:57 +00:00
alu 07c48db808 improve return message when requests do not pass resource approval 2025-10-17 02:45:59 +00:00
alu f2f4f45097 fix bug with unconverted resource types 2025-10-07 21:36:36 +00:00
alu cc4caf9449 add mount point option in disk attach 2025-07-24 00:02:57 +00:00
alu 4ae30eb155 fix issue in qemu backup restore 2025-07-21 22:25:54 +00:00
alu c1a302a595 add backup default to user created disks 2025-07-14 20:57:08 +00:00
alu 56dc15f1d2 add backup update endpoint 2025-07-01 22:32:07 +00:00
alu b098a173fa add user backup endpoints 2025-06-25 19:41:17 +00:00
alu 3a41afb696 fix error messages in get pools/nodes 2025-06-24 20:02:30 +00:00
alu 56ef0b4074 fix swap value bug in container creation 2025-05-03 00:20:22 +00:00
alu 67fd748487 change net and hostpci arguments,
fix bug in ct-templates
2025-04-22 23:48:45 +00:00
alu f34c13827b add error handling in vm-isos and ct-templates when configured node is unreachable,
update disk attach logic for new fabric changes
2025-04-16 20:09:19 +00:00
alu 6300df344c update readme 2025-04-07 23:59:44 +00:00
alu 85c9c4cc6d add get instance config path 2025-04-05 00:18:43 +00:00
alu 35421714e6 update dependencies 2025-03-03 22:23:24 +00:00
29 changed files with 1394 additions and 1308 deletions
-42
View File
@@ -1,42 +0,0 @@
{
"env": {
"es2021": true,
"node": true
},
"extends": "standard",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"no-tabs": [
"error",
{
"allowIndentationTabs": true
}
],
"indent": [
"error",
"tab"
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
],
"brace-style": [
"error",
"stroustrup",
{
"allowSingleLine": false
}
]
}
}
+1
View File
@@ -3,3 +3,4 @@
**/localdb.json **/localdb.json
**/docs **/docs
**/config.json **/config.json
.vscode/settings.json
+18 -32
View File
@@ -11,63 +11,49 @@ 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 Proxmox VE, follow the following steps: In the Proxmox web GUI, perform 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:
- VM.* except VM.Audit, VM.Backup, VM.Clone, VM.Console, VM.Monitor, VM.PowerMgmt, VM.Snapshot, VM.Snapshot.Rollback - VM.* except VM.Clone, VM.Console, VM.Monitor, VM.PowerMgmt, VM.Snapshot, VM.Snapshot.Rollback
- Datastore.Allocate, Datastore.AllocateSpace, Datastore.Audit - Datastore.Allocate, Datastore.AllocateSpace, Datastore.AllocateTemplate, Datastore.Audit
- User.Modify - User.Modify
- Pool.Audit - Pool.Audit
- SDN.Use (if instances use SDN networks) - SDN.Use (if instances use SDN networks)
- Sys.Audit
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`
6. To prevent users from bypassing the API provided methods, create a new role with only the following permssions:
- Datastore.Audit
- VM.Audit
- VM.Console
- VM.Monitor
- VM.PowerMgmt
### Installation - API ### Installation - API
1. Clone this repo onto `Dashboard Host` 1. Clone this repo onto the `ProxmoxAAS-API` 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 `http://<proxmoxhost>:8006/api2/json` or `http://<proxmox URL>/api2/json` if Proxmox VE is behind a reverse proxy. - url: the URI to the Proxmox API, ie `https://pve.domain.net/api2/json`
- 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://<paasldap-host>:8082` - url: url to a PAAS-LDAP server API ie. `http://ldap.local`
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 `host.domain.tld` - hostname - the ProxmoxAAS-Dashboard URL, ie `paas.domain.net`
- domain - the base domain for the dashboard and proxmox, ie `domain.tld` - domain - the base domain for the dashboard and proxmox, ie `domain.net`
- 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
6. In `backups`:
- storage: name of storage for instance backups
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. 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.
+18
View File
@@ -0,0 +1,18 @@
### Get ticket
POST {{baseUrl}}/access/ticket
Content-Type: application/x-www-form-urlencoded
username={{username}}
&password={{password}}
### Get user
GET {{baseUrl}}/access/users/{{username}}
### Get group
GET {{baseUrl}}/access/groups/{{groupname}}
### Get all pools
GET {{baseUrl}}/access/pools/
### Get pool
GET {{baseUrl}}/access/pools/{{poolname}}
+36
View File
@@ -0,0 +1,36 @@
### Get ticket
POST {{baseUrl}}/access/ticket
Content-Type: application/x-www-form-urlencoded
username={{username}}
&password={{password}}
### Get instance resources
GET {{baseUrl}}/cluster/{{testvmpath}}
### Get instance backups
GET {{baseUrl}}/cluster/{{testvmpath}}/backup
### Test create instance
POST {{baseUrl}}/cluster/{{testvmpath}}/create
Content-Type: application/x-www-form-urlencoded
name=testvm
&pool={{poolname}}
&cores=8
&memory=8192
### Test fail create instance
POST {{baseUrl}}/cluster/{{testvmpath}}/create
Content-Type: application/x-www-form-urlencoded
name=testvm
&pool={{poolname}}
&cores=9999
&memory=8192
### Test delete instance
DELETE {{baseUrl}}/cluster/{{testvmpath}}/delete
+2
View File
@@ -0,0 +1,2 @@
### Get version
GET {{baseUrl}}/version
+13 -109
View File
@@ -1,4 +1,9 @@
{ {
"application": {
"hostname": "paas.mydomain.example",
"domain": "mydomain.example",
"listenPort": 8081
},
"backends": { "backends": {
"pve": { "pve": {
"import": "pve.js", "import": "pve.js",
@@ -17,49 +22,24 @@
} }
} }
}, },
"localdb": { "access_manager": {
"import": "localdb.js", "import": "access_manager.js",
"config": { "config": {
"dbfile": "localdb.json" "url": "http://localhost:8083"
}
},
"paasldap": {
"import": "paasldap.js",
"config": {
"url": "http://paasldap.mydomain.example",
"realm": "ldap"
} }
} }
}, },
"handlers": { "handlers": {
"instance": { "instance": "pve",
"pve": "pve" "users": ["access_manager"]
},
"users": {
"realm": {
"pve": [
"localdb"
],
"ldap": [
"localdb",
"paasldap"
]
},
"any": [
"localdb",
"paasldap"
]
}
},
"application": {
"hostname": "paas.mydomain.example",
"domain": "mydomain.example",
"listenPort": 8081
}, },
"useriso": { "useriso": {
"node": "examplenode1", "node": "examplenode1",
"storage": "cephfs" "storage": "cephfs"
}, },
"backups": {
"storage": "cephfs"
},
"resources": { "resources": {
"cpu": { "cpu": {
"type": "list", "type": "list",
@@ -157,81 +137,5 @@
"enabled": true "enabled": true
} }
} }
},
"defaultuser": {
"resources": {
"cpu": {
"global": [],
"nodes": {}
},
"cores": {
"global": {
"max": 0
},
"nodes": {}
},
"memory": {
"global": {
"max": 0
},
"nodes": {}
},
"swap": {
"global": {
"max": 0
},
"nodes": {}
},
"local": {
"global": {
"max": 0
},
"nodes": {}
},
"cephpl": {
"global": {
"max": 0
},
"nodes": {}
},
"network": {
"global": {
"max": 0
},
"nodes": {}
},
"pci": {
"global": [],
"nodes": {}
}
},
"nodes": [],
"cluster": {
"vmid": {
"min": -1,
"max": -1
},
"pool": ""
},
"templates": {
"instances": {
"lxc": {},
"qemu": {}
}
},
"network": {
"lxc": {
"type": "veth",
"bridge": "vmbr0",
"vlan": 10,
"ip": "dhcp",
"ip6": "dhcp"
},
"qemu": {
"type": "virtio",
"bridge": "vmbr0",
"vlan": 10
}
}
} }
} }
+31
View File
@@ -0,0 +1,31 @@
import { defineConfig } from "eslint/config";
import globals from "globals";
import js from "@eslint/js";
export default defineConfig([{
plugins: {
js,
},
extends: ["js/recommended"],
languageOptions: {
globals: {
...globals.node
},
ecmaVersion: "latest",
sourceType: "module"
},
rules: {
"no-tabs": ["error", { allowIndentationTabs: true }],
indent: ["error", "tab"],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
"brace-style": ["error", "stroustrup", { allowSingleLine: false }],
"no-unused-vars": ["warn", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}],
"prefer-const": ["error"]
}
}]);
+7 -8
View File
@@ -1,13 +1,13 @@
{ {
"name": "proxmoxaas-api", "name": "proxmoxaas-api",
"version": "1.0.0", "version": "2.0.0",
"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": "^0.5.0", "cookie": "^1.0.2",
"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",
@@ -17,13 +17,12 @@
"ws": "^8.13.0" "ws": "^8.13.0"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^8.43.0", "@eslint/eslintrc": "^3.3.5",
"eslint-config-standard": "^17.1.0", "@eslint/js": "^9.39.4",
"eslint-plugin-import": "^2.27.5", "eslint": "^10.1.0",
"eslint-plugin-n": "^16.0.1", "globals": "^17.4.0"
"eslint-plugin-promise": "^6.1.1"
}, },
"scripts": { "scripts": {
"lint": "DEBUG=eslint:cli-engine eslint --fix ." "lint": "DEBUG=eslint:cli-engine eslint --config dev_config/eslint.config.mjs --fix ."
} }
} }
+163
View File
@@ -0,0 +1,163 @@
import axios from "axios";
import { ACCESS_BACKEND } from "./backends.js";
import * as setCookie from "set-cookie-parser";
export default class ACCESS_MANAGER_API extends ACCESS_BACKEND {
#apiURL = null;
constructor (config) {
super();
this.#apiURL = config.url;
}
/**
* Send HTTP request to paas-LDAP API.
* @param {*} path HTTP path, prepended with the paas-LDAP API base url
* @param {*} method HTTP method
* @param {*} auth HTTP auth cookies
* @param {*} body body parameters and data to be sent. Optional.
* @returns {Object} HTTP response object
*/
async #requestAPI (path, method, auth = null, body = null) {
const url = `${this.#apiURL}${path}`;
const content = {
method,
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: body
};
if (auth) {
content.headers.Cookie = `PAASAccessManagerTicket=${auth.PAASAccessManagerTicket};`;
}
try {
const result = await axios.request(url, content);
return {
ok: result.status === 200,
status: result.status,
data: result.data,
headers: result.headers
};
}
catch (error) {
console.log(`access: error ocuured in access.requestAPI: ${method} ${path} resulted in ${error}`);
const result = error.response;
result.ok = result.status === 200;
return result;
}
}
async openSession (user, password) {
const credentials = { username: `${user.id}@${user.realm}`, password };
const response = await this.#requestAPI("/ticket", "POST", null, credentials);
if (response.ok) {
const cookies = setCookie.parse(response.headers["set-cookie"]);
cookies.forEach((e) => {
e.expiresMSFromNow = e.expires - Date.now();
});
return {
ok: true,
status: response.status,
message: "",
cookies
};
}
else {
return {
ok: false,
status: response.status,
message: response.data.error,
cookies: []
};
}
}
async closeSession(tokens) {
const response = await this.#requestAPI("/ticket", "DELETE", tokens);
return response;
}
async addUser (user, attributes, params) {}
async getUser (user, params) {
const response = await this.#requestAPI(`/users/${user.id}@${user.realm}`, "GET", params);
if (response.ok) { // if ok, return user data
return {
ok: true,
status: response.status,
user: response.data.user
};
}
else { // else return null
return {
ok: false,
status: response.status,
message: response.data.error
};
}
}
async setUser (user, attributes, params) {}
async delUser (user, params) {}
async addGroup (group, attributes, params) {}
async getGroup (group, params) {
const response = await this.#requestAPI(`/groups/${group.id}-${group.realm}`, "GET", params);
if (response.ok) { // if ok, return user data
return {
ok: true,
status: response.status,
group: response.data.group
};
}
else { // else return null
return {
ok: false,
status: response.status,
message: response.data.error
};
}
}
async setGroup (group, attributes, params) {}
async delGroup (group, attributes, params) {}
async addUserToGroup (user, group, params) {}
async delUserFromGroup (user, group, params) {}
async addPool (pool, attributes, params) {}
async getPool (pool, params) {
const response = await this.#requestAPI(`/pools/${pool}`, "GET", params);
if (response.ok) { // if ok, return user data
return {
ok: true,
status: response.status,
pool: response.data.pool
};
}
else { // else return null
return {
ok: false,
status: response.status,
message: response.data.error
};
}
}
async setPool (pool, attributes, params) {}
async delPool (pool, params) {}
async addGroupToPool (group, pool, params) {}
async delGroupFromPool (group, pool, params) {}
}
+116 -184
View File
@@ -17,43 +17,10 @@ export default async () => {
global.backends[name] = new Backend(config); global.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]; global.pve = global.backends[global.config.handlers.instance];
global.userManager = new USER_BACKEND_MANAGER(global.config.handlers.users); global.access = global.backends[global.config.handlers.users];
}; };
/**
* Interface for all backend types. Contains only two methods for opening and closing a session with the backend.
* Users will recieve tokens from all backends when first authenticating and will delete tokens when logging out.
*/
class BACKEND {
/**
* Opens a session with the backend and creates session tokens if needed
* @param {{id: string, realm: string}} user object containing id and realm
* @param {string} password
* @returns {{ok: boolean, status: number, message: string, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value
*/
openSession (user, password) {
return {
ok: true,
status: 200,
message: "",
cookies: []
};
}
/**
* Closes an opened session with the backend if needed
* @param {{name: string, value: string}[]} token list of session token objects with token name and value, may include irrelevant tokens for a specific backend
* @returns {boolean} true if session was closed successfully, false otherwise
*/
closeSession (tokens) {
return {
ok: true,
status: 200
};
}
}
export class AtomicChange { export class AtomicChange {
constructor (valid, delta, callback, status = { ok: true, status: 200, message: "" }) { constructor (valid, delta, callback, status = { ok: true, status: 200, message: "" }) {
this.valid = valid; this.valid = valid;
@@ -76,10 +43,43 @@ export function doNothingCallback (delta) {
} }
/** /**
* Interface for backend types that store/interact with user & group data. * Interface for all backend types. Contains only two methods for opening and closing a session with the backend.
* Users will recieve tokens from all backends when first authenticating and will delete tokens when logging out.
*/
export class BACKEND {
/**
* Opens a session with the backend and creates session tokens if needed
* @param {{id: string, realm: string}} user object containing id and realm
* @param {string} password
* @returns {{ok: boolean, status: number, message: string, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value
*/
async openSession (user, password) {
return {
ok: true,
status: 200,
message: "",
cookies: []
};
}
/**
* Closes an opened session with the backend if needed
* @param {{name: string, value: string}[]} token list of session token objects with token name and value, may include irrelevant tokens for a specific backend
* @returns {boolean} true if session was closed successfully, false otherwise
*/
async closeSession (tokens) {
return {
ok: true,
status: 200
};
}
}
/**
* Interface for backend types that store/interact with user, group, and pool data.
* Not all backends need to implement all interface methods. * Not all backends need to implement all interface methods.
*/ */
class USER_BACKEND extends BACKEND { export class ACCESS_BACKEND extends BACKEND {
/** /**
* Validate an add user operation with the following parameters. * Validate an add user operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation. * Returns whether the change is valid and a delta object to be used in the operation.
@@ -88,7 +88,7 @@ class USER_BACKEND extends BACKEND {
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
addUser (user, attributes, params) {} async addUser (user, attributes, params) {}
/** /**
* Get user from backend * Get user from backend
@@ -96,14 +96,7 @@ class USER_BACKEND extends BACKEND {
* @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 * @returns {Object} containing user data from this backend, null if user does not exist
*/ */
getUser (user, params) {} async getUser (user, params) {}
/**
* Get all users from backend
* @param {Object} params authentication params, usually req.cookies
* @returns {Array} containing each user data from this backend
*/
getAllUsers (params) {}
/** /**
* Validate a set user operation with the following parameters. * Validate a set user operation with the following parameters.
@@ -113,7 +106,7 @@ class USER_BACKEND extends BACKEND {
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
setUser (user, attributes, params) {} async setUser (user, attributes, params) {}
/** /**
* Validate a delete user operation with the following parameters. * Validate a delete user operation with the following parameters.
@@ -122,7 +115,7 @@ class USER_BACKEND extends BACKEND {
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
delUser (user, params) {} async delUser (user, params) {}
/** /**
* Validate an add group operation with the following parameters. * Validate an add group operation with the following parameters.
@@ -132,22 +125,15 @@ class USER_BACKEND extends BACKEND {
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
addGroup (group, attributes, params) {} async addGroup (group, attributes, params) {}
/** /**
* Get group from backend * Get group from backend
* @param {{id: string}} group * @param {{id: string, realm: 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 * @returns {Object} containing group data from this backend, null if user does not exist
*/ */
getGroup (group, params) {} async getGroup (group, params) {}
/**
* Get all users from backend
* @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. * Validate a set group operation with the following parameters.
@@ -157,35 +143,93 @@ class USER_BACKEND extends BACKEND {
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
setGroup (group, attributes, params) {} async setGroup (group, attributes, params) {}
/** /**
* Validate a del group operation with the following parameters. * 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. * Returns whether the change is valid and a delta object to be used in the operation.
* @param {{id: string, realm: string}} group * @param {{id: string, realm: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
delGroup (group, attributes, params) {} async delGroup (group, attributes, params) {}
/** /**
* Validate an add user to group operation with the following parameters. * 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. * 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, realm: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
addUserToGroup (user, group, params) {} async addUserToGroup (user, group, params) {}
/** /**
* Validate a remove user from group operation with the following parameters. * Validate a remove user from group operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation. * 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, realm: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
delUserFromGroup (user, group, params) {} async delUserFromGroup (user, group, params) {}
/**
* Validate an add pool 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}} pool
* @param {Object} attributes pool attributes
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
async addPool (pool, attributes, params) {}
/**
* Get pool from backend
* @param {string} pool
* @param {Object} params authentication params, usually req.cookies
* @returns {Object} containing pool data from this backend, null if poll does not exist
*/
async getPool (pool, params) {}
/**
* Validate a set pool operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation.
* @param {string} pool
* @param {Object} attributes pool attributes
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
async setPool (pool, attributes, params) {}
/**
* Validate a del pool operation with the following parameters.
* Returns whether the change is valid and a delta object to be used in the operation.
* @param {string} pool
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
async delPool (pool, params) {}
/**
* Validate an add group to pool 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 {string} pool
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
async addGroupToPool (group, pool, params) {}
/**
* Validate a remove group from pool 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 {string} pool
* @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object
*/
async delGroupFromPool (group, pool, params) {}
} }
/** /**
@@ -198,13 +242,13 @@ export class PVE_BACKEND extends BACKEND {
* @param {string} node node id * @param {string} node node id
* @returns {} * @returns {}
*/ */
getNode (node) {} async getNode (node) {}
/** /**
* Send a signal to synchronize a node after some change has been made. * Send a signal to synchronize a node after some change has been made.
* * @param {string} node node id * * @param {string} node node id
*/ */
syncNode (node) {} async syncNode (node) {}
/** /**
* Get and return instance data. * Get and return instance data.
@@ -213,14 +257,14 @@ export class PVE_BACKEND extends BACKEND {
* @param {string} type instance type * @param {string} type instance type
* @param {string} vmid instance id * @param {string} vmid instance id
*/ */
getInstance (node, type, instance) {} async getInstance (node, type, instance) {}
/** /**
* Send a signal to synchronize an instance after some change has been made. * Send a signal to synchronize an instance after some change has been made.
* @param {string} node node id * @param {string} node node id
* @param {string} instance instance id * @param {string} instance instance id
*/ */
syncInstance (node, instance) {} async syncInstance (node, instance) {}
/** /**
* Get meta data for a specific disk. Adds info that is not normally available in a instance's config. * Get meta data for a specific disk. Adds info that is not normally available in a instance's config.
@@ -250,122 +294,10 @@ export class PVE_BACKEND extends BACKEND {
async getDevice (node, instance, deviceid) {} async getDevice (node, instance, deviceid) {}
/** /**
* Get user resource data including used, available, and maximum resources. * Get pool resource data including used, available, and maximum resources.
* @param {{id: string, realm: string}} user object of user to get resource data. * @param {string} pool
* @param {Object} cookies object containing k-v store of cookies * @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. * @returns {{used: Object, avail: Object, max: Object, resources: Object}} used, available, maximum, and resource metadata for the specified user.
*/ */
getUserResources (user, cookies) {} async getPoolResources (user, cookies) {}
}
/**
* Interface for user database backends.
*/
export class DB_BACKEND extends USER_BACKEND {}
/**
* Interface for user auth backends.
*/
export class AUTH_BACKEND extends USER_BACKEND {}
/**
* Interface combining all user backends into a single interface
* Calling methods will also call sub handler methods
*/
class USER_BACKEND_MANAGER extends USER_BACKEND {
#config = null;
constructor (config) {
super();
this.#config = config;
}
getBackendsByUser (user) {
if (user != null) {
return this.#config.realm[user.realm];
}
else {
return null;
}
}
addUser (user, attributes, params) {}
async getUser (user, params) {
let userData = {};
for (const backend of this.#config.realm[user.realm]) {
const backendData = await global.backends[backend].getUser(user, params);
if (backendData) {
userData = { ...backendData, ...userData };
}
}
return userData;
}
async getAllUsers (params) {
const userData = {};
for (const backend of this.#config.any) {
const backendData = await global.backends[backend].getAllUsers(params);
if (backendData) {
for (const user of Object.keys(backendData)) {
userData[user] = { ...backendData[user], ...userData[user] };
}
}
}
return userData;
}
async setUser (user, attributes, params) {
const atomicChanges = [];
for (const backend of this.#config.realm[user.realm]) {
const atomicChange = await global.backends[backend].setUser(user, attributes, params);
if (atomicChange.valid === false) { // if any fails, preemptively exit
return atomicChange.status;
}
atomicChanges.push(atomicChange); // queue callback into array
}
const response = {
ok: true,
status: 200,
message: "",
allResponses: []
};
for (const atomicChange of atomicChanges) {
const atomicResponse = await atomicChange.commit();
if (atomicResponse.ok === false) {
response.ok = false;
response.status = atomicResponse.status;
response.message = atomicResponse.message;
}
response.allResponses.push(); // execute callback
}
return response;
}
delUser (user, params) {}
addGroup (group, attributes, params) {}
getGroup (group, params) {}
async getAllGroups (params) {
const groupData = {};
for (const backend of this.#config.any) {
const backendData = await global.backends[backend].getAllGroups(params);
if (backendData) {
for (const group of Object.keys(backendData)) {
groupData[group] = { ...backendData[group], ...groupData[group] };
}
}
}
return groupData;
}
setGroup (group, attributes, params) {}
delGroup (group, params) {}
addUserToGroup (user, group, params) {}
delUserFromGroup (user, group, params) {}
} }
-116
View File
@@ -1,116 +0,0 @@
import { readFileSync, writeFileSync } from "fs";
import { exit } from "process";
import { AtomicChange, DB_BACKEND, doNothingCallback } from "./backends.js";
export default class LocalDB extends DB_BACKEND {
#path = null;
#data = null;
#defaultuser = null;
constructor (config) {
super();
const path = config.dbfile;
try {
this.#path = path;
this.#load();
this.#defaultuser = global.config.defaultuser;
}
catch {
console.log(`error: ${path} was not found. Please follow the directions in the README to initialize localdb.json.`);
exit(1);
}
}
/**
* Load db from local file system. Reads from file path store in path.
*/
#load () {
this.#data = JSON.parse(readFileSync(this.#path));
}
/**
* Save db to local file system. Saves to file path stored in path.
*/
#save () {
writeFileSync(this.#path, JSON.stringify(this.#data));
}
addUser (user, attributes, params) {}
getUser (user, params) {
const requestedUser = `${user.id}@${user.realm}`;
const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token
// user can access a user's db data if they are an admin OR are requesting own data
const authorized = this.#data.users[requestingUser].cluster.admin || requestingUser === requestedUser;
if (authorized && this.#data.users[requestedUser]) {
return this.#data.users[requestedUser];
}
else {
return null;
}
}
async getAllUsers (params) {
const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token
if (this.#data.users[requestingUser].cluster.admin === true) {
return this.#data.users;
}
else {
return null;
}
}
setUser (user, attributes, params) {
if (attributes.resources && attributes.cluster && attributes.templates) {
const username = `${user.id}@${user.realm}`;
if (this.#data.users[username]) {
if (this.#data.users[params.username] && this.#data.users[params.username].cluster.admin) {
return new AtomicChange(false,
{
username,
attributes: {
resources: attributes.resources,
cluster: attributes.cluster,
templates: attributes.templates
}
},
(delta) => {
this.#data.users[delta.username] = delta.attributes;
this.#save();
return { ok: true, status: 200, message: "" };
},
{ ok: true, status: 200, message: "" }
);
}
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;
}
setGroup (group, attributes, params) {}
delGroup (group, params) {}
// assume that adding to group also adds to group's pool
addUserToGroup (user, group, params) {}
// assume that adding to group also adds to group's pool
delUserFromGroup (user, group, params) {}
}
-184
View File
@@ -1,184 +0,0 @@
import axios from "axios";
import { AtomicChange, AUTH_BACKEND, doNothingCallback } from "./backends.js";
import * as setCookie from "set-cookie-parser";
export default class PAASLDAP extends AUTH_BACKEND {
#url = null;
#realm = null;
constructor (config) {
super();
this.#url = config.url;
this.#realm = config.realm;
}
/**
* Send HTTP request to paas-LDAP API.
* @param {*} path HTTP path, prepended with the paas-LDAP API base url
* @param {*} method HTTP method
* @param {*} body body parameters and data to be sent. Optional.
* @returns {Object} HTTP response object
*/
async #request (path, method, auth = null, body = null) {
const url = `${this.#url}${path}`;
const content = {
method,
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
data: body
};
if (auth) {
content.headers.Cookie = `PAASLDAPAuthTicket=${auth.PAASLDAPAuthTicket};`;
}
try {
const result = await axios.request(url, content);
result.ok = result.status === 200;
return result;
}
catch (error) {
const result = error.response;
result.ok = result.status === 200;
return result;
}
}
#handleGenericReturn (res) {
return {
ok: res.ok,
status: res.status,
message: res.ok ? "" : res.data.error
};
}
async openSession (user, password) {
const username = user.id;
const content = { username, password };
const result = await this.#request("/ticket", "POST", null, content);
if (result.ok) {
const cookies = setCookie.parse(result.headers["set-cookie"]);
cookies.forEach((e) => {
e.expiresMSFromNow = e.expires - Date.now();
});
return {
ok: true,
status: result.status,
message: "",
cookies
};
}
else {
return {
ok: false,
status: result.status,
message: result.data.error,
cookies: []
};
}
}
async addUser (user, attributes, params) {}
async getUser (user, params) {
if (!params) { // params required, do nothing if params are missing
return null;
}
const res = await this.#request(`/users/${user.id}`, "GET", params);
if (res.ok) { // if ok, return user data
return res.data.user;
}
else { // else return null
return null;
}
}
async getAllUsers (params) {
if (!params) {
return null;
}
const res = await this.#request("/users", "GET", params);
if (res.ok) { // if ok, return user data
const users = res.data.users;
const usersFormatted = {};
// label each user object by user@realm
for (const user of users) {
usersFormatted[`${user.attributes.uid}@${this.#realm}`] = user;
}
return usersFormatted;
}
else { // else return null
return null;
}
}
async setUser (user, attributes, params) {
if (!attributes.userpassword && !attributes.cn && attributes.sn) {
return new AtomicChange(true, {}, doNothingCallback, null); // change has no ldap attributes
}
const ldapAttributes = {};
if (attributes.userpassword) {
ldapAttributes.userpassword = attributes.userpassword;
}
if (attributes.cn) {
ldapAttributes.cn = attributes.cn;
}
if (attributes.sn) {
ldapAttributes.sn = attributes.sn;
}
return new AtomicChange(
true,
{
user,
ldapAttributes,
params
},
async (delta) => {
const res = await this.#request(`/users/${delta.user.id}`, "POST", delta.params, delta.ldapAttributes);
return this.#handleGenericReturn(res);
},
{ ok: true, status: 200, message: "" }
);
}
async delUser (user, params) {}
async addGroup (group, attributes, params) {}
async getGroup (group, params) {
return await this.#request(`/groups/${group.id}`, "GET", params);
}
async getAllGroups (params) {
if (!params) {
return null;
}
const res = await this.#request("/groups", "GET", params);
if (res.ok) { // if ok, return user data
const groups = res.data.groups;
const groupsFormatted = {};
// label each user object by user@realm
for (const group of groups) {
groupsFormatted[`${group.attributes.cn}@${this.#realm}`] = group;
}
return groupsFormatted;
}
else { // else return null
return null;
}
}
async setGroup (group, attributes, params) {
// not implemented, LDAP groups do not have any attributes to change
return new AtomicChange(true, {}, doNothingCallback, null); ;
}
async delGroup (group, params) {}
async addUserToGroup (user, group, params) {}
async delUserFromGroup (user, group, params) {}
}
+82 -63
View File
@@ -26,8 +26,8 @@ export default class PVE extends PVE_BACKEND {
cookies: [] cookies: []
}; };
} }
const ticket = response.data.data.ticket; const ticket = response.data.ticket;
const csrftoken = response.data.data.CSRFPreventionToken; const csrftoken = response.data.CSRFPreventionToken;
return { return {
ok: true, ok: true,
status: response.status, status: response.status,
@@ -66,73 +66,39 @@ export default class PVE extends PVE_BACKEND {
data: body data: body
}; };
if (auth && auth.cookies) { if (auth && auth.cookies) { // user cookie credentials
content.headers.CSRFPreventionToken = auth.cookies.CSRFPreventionToken; content.headers.CSRFPreventionToken = auth.cookies.CSRFPreventionToken;
content.headers.Cookie = `PVEAuthCookie=${auth.cookies.PVEAuthCookie}; CSRFPreventionToken=${auth.cookies.CSRFPreventionToken}`; content.headers.Cookie = `PVEAuthCookie=${auth.cookies.PVEAuthCookie}; CSRFPreventionToken=${auth.cookies.CSRFPreventionToken}`;
} }
else if (auth && auth.token) { else if (auth && auth.token) { // upgraded request as api
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) { else if (auth && auth.root) { // upgraded request as root
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, this.#pveRoot); const rootauth = await this.requestPVE("/access/ticket", "POST", null, this.#pveRoot);
if (!(rootauth.status === 200)) { if (!(rootauth.status === 200)) {
return rootauth.response; return rootauth.response;
} }
const rootcookie = rootauth.data.data.ticket; const rootcookie = rootauth.data.ticket;
const rootcsrf = rootauth.data.data.CSRFPreventionToken; const rootcsrf = rootauth.data.CSRFPreventionToken;
content.headers.CSRFPreventionToken = rootcsrf; content.headers.CSRFPreventionToken = rootcsrf;
content.headers.Cookie = `PVEAuthCookie=${rootcookie}; CSRFPreventionToken=${rootcsrf}`; content.headers.Cookie = `PVEAuthCookie=${rootcookie}; CSRFPreventionToken=${rootcsrf}`;
} }
try { try {
return await axios.request(url, content); const result = await axios.request(url, content);
return {
ok: result.ok,
status: result.status,
data: result.data.data, // pve returns {data: {data: {...}}}, unwrap here to conform to standard {data: {...}} format
headers: result.headers
};
} }
catch (error) { catch (error) {
console.log(`backends: error ocuured in pve.requestPVE: ${error}`); console.log(`pve: error ocuured in pve.requestPVE: ${method} ${path} resulted in ${error}`);
return error.response; const result = error.response;
} result.ok = result.status === 200;
} return result;
/**
* Handle various proxmox API responses. Handles sync and async responses.
* In sync responses, responses are completed when the response arrives. Method returns the response directly.
* In async responses, proxmox sends responses with a UPID to track process completion. Method returns the status of the proxmox process once it completes.
* @param {string} node response originates from.
* @param {Object} result response from proxmox.
* @param {Object} res response object of ProxmoxAAS API call.
*/
async handleResponse (node, result, res) {
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
if (result.status !== 200) {
res.status(result.status).send({ error: result.statusText });
res.end();
}
else if (result.data.data && typeof (result.data.data) === "string" && result.data.data.startsWith("UPID:")) {
const upid = result.data.data;
let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
while (taskStatus.data.data.status !== "stopped") {
await waitFor(100);
taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
}
if (taskStatus.data.data.exitstatus === "OK") {
const result = taskStatus.data.data;
const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true });
result.log = taskLog.data.data;
res.status(200).send(result);
res.end();
}
else {
const result = taskStatus.data.data;
const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true });
result.log = taskLog.data.data;
res.status(500).send(result);
res.end();
}
}
else {
res.status(result.status).send(result.data);
res.end();
} }
} }
@@ -160,10 +126,53 @@ export default class PVE extends PVE_BACKEND {
return await axios.request(url, content); return await axios.request(url, content);
} }
catch (error) { catch (error) {
console.log(`pve: error ocuured in pve.requestFabric: ${method} ${path} resulted in ${error}`);
return error; return error;
} }
} }
/**
* Handle various proxmox API responses. Handles sync and async responses.
* In sync responses, responses are completed when the response arrives. Method returns the response directly.
* In async responses, proxmox sends responses with a UPID to track process completion. Method returns the status of the proxmox process once it completes.
* @param {string} node response originates from.
* @param {Object} result response from proxmox.
* @param {Object} res response object of ProxmoxAAS API call.
*/
async handleResponse (node, result, res) {
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
if (result.status !== 200) {
res.status(result.status).send({ error: result.statusText });
res.end();
}
else if (result.data && typeof (result.data) === "string" && result.data.startsWith("UPID:")) {
const upid = result.data;
let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
while (taskStatus.data.status !== "stopped") {
await waitFor(100);
taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
}
if (taskStatus.data.exitstatus === "OK") {
const result = taskStatus.data;
const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true });
result.log = taskLog.data;
res.status(200).send(result);
res.end();
}
else {
const result = taskStatus.data;
const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true });
result.log = taskLog.data;
res.status(500).send(result);
res.end();
}
}
else {
res.status(result.status).send(result.data);
res.end();
}
}
async getNode (node) { async getNode (node) {
const res = await this.requestFabric(`/nodes/${node}`, "GET"); const res = await this.requestFabric(`/nodes/${node}`, "GET");
if (res.status !== 200) { if (res.status !== 200) {
@@ -222,23 +231,33 @@ export default class PVE extends PVE_BACKEND {
} }
} }
async getUserResources (user, cookies) { async getPoolResources (cookies, pool) {
// get user resources with vm filter // get pool resources
const res = await this.requestPVE("/cluster/resources?type=vm", "GET", { cookies }); const res = await this.requestPVE(`/pools/?poolid=${pool}`, "GET", { cookies });
if (res.status !== 200) { if (res.status !== 200) {
return null; return null;
} }
const data = res.data;
if (data.length != 1) {
return null;
}
const poolPVE = data[0];
if (poolPVE.poolid != pool) {
return null;
}
const userPVEResources = res.data.data; const poolPVEResources = poolPVE.members;
const resources = {}; const resources = {};
// for each resource, add to the object // for each resource, add to the object
for (const resource of userPVEResources) { for (const resource of poolPVEResources) {
const instance = await this.getInstance(resource.node, resource.vmid); // only add type if it is vm or ct (ie has vmid)
if (instance) { if (resource.vmid) {
instance.node = resource.node; const instance = await this.getInstance(resource.node, resource.vmid);
resources[resource.vmid] = instance; if (instance) {
instance.node = resource.node;
resources[resource.vmid] = instance;
}
} }
} }
-9
View File
@@ -41,12 +41,3 @@ global.utils.recursiveImportRoutes(app, "/api", "routes");
app.get("/api/version", (req, res) => { app.get("/api/version", (req, res) => {
res.status(200).send({ version: global.package.version }); res.status(200).send({ version: global.package.version });
}); });
/**
* GET - echo request
* responses:
* - 200: {body: request.body, cookies: request.cookies}
*/
app.get("/api/echo", (req, res) => {
res.status(200).send({ body: req.body, cookies: req.cookies });
});
+18 -9
View File
@@ -62,14 +62,11 @@ router.post("/ticket", async (req, res) => {
password: req.body.password password: req.body.password
}; };
const domain = global.config.application.domain; // get user and user backends from config
const userObj = global.utils.getUserObjFromUsername(params.username); const userObj = global.utils.getUserObjFromUsername(params.username);
let backends = global.userManager.getBackendsByUser(userObj); const backends = [global.config.handlers.users, global.config.handlers.instance];
if (backends == null) {
res.status(401).send({ auth: false, error: `${params.username} not found in any ProxmoxAAS backends` }); // fetch cookies using cookie fetcher
return;
}
backends = backends.concat(["pve"]);
const cm = new CookieFetcher(); const cm = new CookieFetcher();
const error = await cm.fetchBackends(backends, userObj, params.password); const error = await cm.fetchBackends(backends, userObj, params.password);
if (error) { if (error) {
@@ -77,6 +74,11 @@ router.post("/ticket", async (req, res) => {
return; return;
} }
const cookies = cm.exportCookies(); const cookies = cm.exportCookies();
// get global config domain name
const domain = global.config.application.domain;
// for each cookie, add the cookie to response and also compute the minimum across all cookies
let minimumExpires = Infinity; 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);
@@ -85,6 +87,8 @@ router.post("/ticket", async (req, res) => {
minimumExpires = cookie.expiresMSFromNow; minimumExpires = cookie.expiresMSFromNow;
} }
} }
// set username and auth cookie with the minimum cookie length
const expiresDate = new Date(Date.now() + minimumExpires); const expiresDate = new Date(Date.now() + minimumExpires);
res.cookie("username", params.username, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" }); 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.cookie("auth", 1, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" });
@@ -97,17 +101,22 @@ router.post("/ticket", async (req, res) => {
* - 200: {auth: false} * - 200: {auth: false}
*/ */
router.delete("/ticket", async (req, res) => { router.delete("/ticket", async (req, res) => {
// must have cookies to delete, otherwise just return ok
if (Object.keys(req.cookies).length === 0) { if (Object.keys(req.cookies).length === 0) {
res.status(200).send({ auth: false }); res.status(200).send({ auth: false });
return; return;
} }
// for each cookie, set the expire date to 0
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, secure: true, sameSite: "none" });
} }
// call close session on each backend, even if was not used
await global.pve.closeSession(req.cookies); await global.pve.closeSession(req.cookies);
await global.userManager.closeSession(req.cookies); await global.access.closeSession(req.cookies);
res.status(200).send({ auth: false }); res.status(200).send({ auth: false });
}); });
@@ -134,6 +143,6 @@ router.post("/password", async (req, res) => {
const newAttributes = { const newAttributes = {
userpassword: params.password userpassword: params.password
}; };
const response = await global.userManager.setUser(userObj, newAttributes, req.cookies); const response = await global.access.setUser(userObj, newAttributes, req.cookies);
res.status(response.status).send(response); res.status(response.status).send(response);
}); });
+17 -21
View File
@@ -1,24 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); 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 * GET - get specific group
* request: * request:
@@ -32,10 +14,24 @@ router.get("/:groupname", async (req, res) => {
groupname: req.params.groupname groupname: req.params.groupname
}; };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
const group = await global.userManager.getGroup(params.groupname, req.cookies);
res.status(200).send({ group }); // attempt to parse group from groupname
const groupObj = global.utils.getGroupObjFromGroupname(params.groupname);
if (groupObj == null) {
res.status(400).send({ auth: true, error:`Groupname ${params.groupname} does not match format gid-realm or gid.` });
}
// get group
const g = await global.access.getGroup(groupObj, req.cookies);
if (g.ok !== true) {
res.status(g.status).send({ auth:true, error:g });
return;
}
const group = g.group;
res.status(200).send({ auth:true, group });
}); });
+78
View File
@@ -0,0 +1,78 @@
import { Router } from "express";
export const router = Router({ mergeParams: true });
/**
* GET - get all available cluster pools
* returns only pool IDs
* responses:
* - 200: List of pools
* - PVE error
*/
router.get("/", async (req, res) => {
// check auth
const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) {
return;
}
// get user object
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
// get all pool names using api token
const poolnames = await global.pve.requestPVE("/pools", "GET", { token: true });
// setup pools (return value)
const pools = {};
// for each poolname
for (const poolpartial of poolnames.data) {
const poolname = poolpartial.poolid;
// get the pool
const p = await global.access.getPool(poolname, req.cookies);
if (p.ok !== true) {
continue;
}
const pool = p.pool;
// if user is in the pool, add it to pools (return value)
if (global.utils.checkUserInPool(pool, userObj)) {
const resources = await global.utils.getPoolResources(req, poolname);
pool.resources = resources;
pools[poolname] = pool;
}
}
res.status(200).send({ pools });
res.end();
});
/**
* GET - get specific pool
* request:
* - poolname: name of pool to get
* responses:
* - 200: {auth: true, pool: Object}
* - 401: {auth: false}
*/
router.get("/:poolname", async (req, res) => {
const params = {
poolname: req.params.poolname
};
// check auth
const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) {
return;
}
// get pool
const p = await global.access.getPool(params.poolname, req.cookies);
if (p.ok !== true) {
res.status(p.status).send({ auth:true, error: p });
return;
}
const pool = p.pool;
// get resources
const resources = await global.utils.getPoolResources(req, params.poolname);
// append resources to pool
pool.resources = resources;
res.status(200).send({ auth: true, pool });
});
+15 -20
View File
@@ -1,24 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); 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 * GET - get specific user
* request: * request:
@@ -32,11 +14,24 @@ router.get("/:username", async (req, res) => {
username: req.params.username username: req.params.username
}; };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
// attempt to parse user from username
const userObj = global.utils.getUserObjFromUsername(params.username); const userObj = global.utils.getUserObjFromUsername(params.username);
const user = await global.userManager.getUser(userObj, req.cookies); if (userObj == null) {
res.status(400).send({ auth:true, error:`username ${params.username} does not match format uid@realm.` });
}
// get user
const u = await global.access.getUser(userObj, req.cookies);
if (u.ok !== true) {
res.status(u.status).send({ auth: true, error: u });
return;
}
const user = u.user;
res.status(200).send({ user }); res.status(200).send({ user });
}); });
+63 -105
View File
@@ -1,10 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources;
const getUserResources = global.utils.getUserResources;
const nodeRegexP = "[\\w-]+"; const nodeRegexP = "[\\w-]+";
const typeRegexP = "qemu|lxc"; const typeRegexP = "qemu|lxc";
const vmidRegexP = "\\d+"; const vmidRegexP = "\\d+";
@@ -13,33 +9,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 * GET - get all available cluster nodes
* uses existing user permissions without elevation * uses existing user permissions without elevation
@@ -50,76 +19,51 @@ router.get("/pools", async (req, res) => {
*/ */
router.get("/nodes", async (req, res) => { router.get("/nodes", async (req, res) => {
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
// get all nodes
const allNodes = await global.pve.requestPVE("/nodes", "GET", { cookies: req.cookies }); const allNodes = await global.pve.requestPVE("/nodes", "GET", { cookies: req.cookies });
if (allNodes.status === 200) { if (allNodes.status === 200) {
const allNodesIDs = Array.from(allNodes.data.data, (x) => x.node); const allNodesIDs = Array.from(allNodes.data, (x) => x.node);
res.status(allNodes.status).send({ nodes: allNodesIDs }); res.status(allNodes.status).send({ nodes: allNodesIDs });
res.end(); res.end();
} }
else { else {
res.status(allNodes.status).send({ error: allNodes.statusMessage }); res.status(allNodes.status).send({ error: allNodes.statusText });
res.end(); res.end();
} }
}); });
/** /**
* GET - get available pcie devices given node and user * GET - get basic resources for vm using the fabric format
* request: * request:
* - node: string - vm host node id * - node: string - vm host node id
* responses: * - type: string - vm type (lxc, qemu)
* - 200: PVE PCI Device Object * - vmid: number - vm id number
* response:
* - 200: Fabric instance config
* - 401: {auth: false} * - 401: {auth: false}
* - 401: {auth: false, path: string}
* - 500: {error: string}
*/ */
router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => { router.get(`${basePath}`, async (req, res) => {
const params = { const params = {
node: req.params.node node: req.params.node,
type: req.params.type,
vmid: req.params.vmid
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
// check auth const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
const auth = await checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
const userNodes = (await global.userManager.getUser(userObj, req.cookies)).cluster.nodes;
if (userNodes[params.node] !== true) {
res.status(401).send({ auth: false, path: params.node });
res.end();
return;
}
// get remaining user resources
const userAvailPci = (await getUserResources(req, userObj)).pci.nodes[params.node];
if (userAvailPci === undefined) { // user has no avaliable devices on this node, so send an empty list
res.status(200).send([]);
res.end();
}
else {
// get node avail devices
const node = await global.pve.getNode(params.node);
let availableDevices = [];
// get each device and filter out only thise which are not reserved
for (const device of Object.values(node.devices)) {
if (device.reserved === false) {
availableDevices.push(device);
}
}
// further filter out only devices which the user has access to
availableDevices = availableDevices.filter(nodeAvail => userAvailPci.some((userAvail) => {
return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0;
}));
res.status(200).send(availableDevices); // get current config
res.end(); const instance = await global.pve.getInstance(params.node, params.vmid);
} res.status(200).send(instance);
}); });
/** /**
@@ -134,8 +78,8 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
* - swap: number, optional - new amount of swap for instance * - swap: number, optional - new amount of swap for instance
* responses: * responses:
* - 200: PVE Task Object * - 200: PVE Task Object
* - 400: {request; Object, error: string, reason: Object}
* - 401: {auth: false, path: string} * - 401: {auth: false, path: string}
* - 500: {request: Object, error: string}
* - 500: PVE Task Object * - 500: PVE Task Object
*/ */
router.post(`${basePath}/resources`, async (req, res) => { router.post(`${basePath}/resources`, async (req, res) => {
@@ -150,11 +94,9 @@ router.post(`${basePath}/resources`, async (req, res) => {
boot: req.body.boot boot: req.body.boot
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
@@ -170,12 +112,16 @@ router.post(`${basePath}/resources`, async (req, res) => {
else if (params.type === "qemu") { else if (params.type === "qemu") {
request.cpu = params.proctype; request.cpu = params.proctype;
} }
// check resource approval // check resource approval
if (!await approveResources(req, userObj, request, params.node)) { const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
res.status(500).send({ request, error: "Could not fulfil request." }); const { approved, reason } = await global.utils.approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(400).send({ request, error: "Not enough resources to satisfy request.", reason });
res.end(); res.end();
return; return;
} }
// setup action // setup action
const action = { cores: params.cores, memory: params.memory }; const action = { cores: params.cores, memory: params.memory };
if (params.type === "lxc") { if (params.type === "lxc") {
@@ -186,6 +132,7 @@ router.post(`${basePath}/resources`, async (req, res) => {
action.boot = `order=${params.boot.toString().replaceAll(",", ";")};`; action.boot = `order=${params.boot.toString().replaceAll(",", ";")};`;
} }
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);
@@ -209,9 +156,9 @@ router.post(`${basePath}/resources`, async (req, res) => {
* - rootfssize: number, optional, - size of lxc instance rootfs * - rootfssize: number, optional, - size of lxc instance rootfs
* responses: * responses:
* - 200: PVE Task Object * - 200: PVE Task Object
* - 400: {request: Object, error: string, reason: Object}
* - 401: {auth: false, path: string} * - 401: {auth: false, path: string}
* - 500: {error: string} * - 500: {error: string}
* - 500: {request: Object, error: string}
* - 500: PVE Task Object * - 500: PVE Task Object
*/ */
router.post(`${basePath}/create`, async (req, res) => { router.post(`${basePath}/create`, async (req, res) => {
@@ -231,48 +178,52 @@ router.post(`${basePath}/create`, async (req, res) => {
rootfssize: req.body.rootfssize rootfssize: req.body.rootfssize
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
// get user db config
const user = await global.userManager.getUser(userObj, req.cookies); // get pool config
const pool = (await global.access.getPool(params.pool, req.cookies)).pool;
const vmid = Number.parseInt(params.vmid); const vmid = Number.parseInt(params.vmid);
const vmidMin = user.cluster.vmid.min; const vmidMin = pool["vmid-allowed"].min;
const vmidMax = user.cluster.vmid.max; const vmidMax = pool["vmid-allowed"].max;
// check vmid is within allowed range // check vmid is within allowed range
if (vmid < vmidMin || vmid > vmidMax) { if (vmid < vmidMin || vmid > vmidMax) {
res.status(500).send({ error: `Requested vmid ${vmid} is out of allowed range [${vmidMin},${vmidMax}].` }); res.status(500).send({ error: `Requested vmid ${vmid} is out of allowed range [${vmidMin},${vmidMax}].` });
res.end(); res.end();
return; return;
} }
// check node is within allowed list // check node is within allowed list
if (user.cluster.nodes[params.node] !== true) { if (pool["nodes-allowed"][params.node] !== true) {
res.status(500).send({ error: `Requested node ${params.node} is not in allowed nodes [${user.cluster.nodes}].` }); res.status(500).send({ error: `Requested node ${params.node} is not in allowed nodes [${pool["nodes-allowed"]}].` });
res.end(); res.end();
return; return;
} }
// check if pool is in user allowed pools
if (user.cluster.pools[params.pool] !== true) { // check if user is in pool
res.status(500).send({ error: `Requested pool ${params.pool} not in allowed pools [${user.pools}]` }); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if(global.utils.checkUserInPool(pool, userObj) !== true) {
res.status(500).send({ error: `Requested pool ${params.pool} does not contain user ${req.cookies.username}]` });
res.end(); res.end();
return; return;
} }
// setup request // setup request
const request = { const request = {
cores: Number(params.cores), cores: Number(params.cores),
memory: Number(params.memory) memory: Number(params.memory) * 1024 ** 2
}; };
if (params.type === "lxc") { if (params.type === "lxc") {
request.swap = params.swap; request.swap = Number(params.swap) * 1024 ** 2;
request[params.rootfslocation] = params.rootfssize; request[params.rootfslocation] = params.rootfssize * 1024 ** 3;
} }
for (const key of Object.keys(user.templates.instances[params.type])) { for (const key of Object.keys(pool.templates.instances[params.type])) {
const item = user.templates.instances[params.type][key]; const item = pool.templates.instances[params.type][key];
if (item.resource) { if (item.resource.enabled) {
if (request[item.resource.name]) { if (request[item.resource.name]) {
request[item.resource.name] += item.resource.amount; request[item.resource.name] += item.resource.amount;
} }
@@ -281,12 +232,15 @@ router.post(`${basePath}/create`, async (req, res) => {
} }
} }
} }
// check resource approval // check resource approval
if (!await approveResources(req, userObj, request, params.node)) { // check resource approval const { approved, reason } = await await global.utils.approveResources(req, userObj, params.node, params.pool, request);
res.status(500).send({ request, error: "Not enough resources to satisfy request." }); if (!approved) {
res.status(400).send({ request, error: "Not enough resources to satisfy request.", reason });
res.end(); res.end();
return; return;
} }
// setup action by adding non resource values // setup action by adding non resource values
const action = { const action = {
vmid: params.vmid, vmid: params.vmid,
@@ -294,10 +248,11 @@ router.post(`${basePath}/create`, async (req, res) => {
memory: Number(params.memory), memory: Number(params.memory),
pool: params.pool pool: params.pool
}; };
for (const key of Object.keys(user.templates.instances[params.type])) { for (const key of Object.keys(pool.templates.instances[params.type])) {
action[key] = user.templates.instances[params.type][key].value; action[key] = pool.templates.instances[params.type][key].value;
} }
if (params.type === "lxc") { if (params.type === "lxc") {
action.swap = params.swap;
action.hostname = params.name; action.hostname = params.name;
action.unprivileged = 1; action.unprivileged = 1;
action.features = "nesting=1"; action.features = "nesting=1";
@@ -308,6 +263,7 @@ router.post(`${basePath}/create`, async (req, res) => {
else { else {
action.name = params.name; action.name = params.name;
} }
// 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);
@@ -331,12 +287,14 @@ router.delete(`${basePath}/delete`, async (req, res) => {
type: req.params.type, type: req.params.type,
vmid: req.params.vmid vmid: req.params.vmid
}; };
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// 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);
+320
View File
@@ -0,0 +1,320 @@
import { Router } from "express";
export const router = Router({ mergeParams: true }); ;
/**
* GET - get backups for an instance
* request:
* - node: string - vm host node id
* - type: string - vm type (lxc, qemu)
* - vmid: number - vm id number
* responses:
* - 200: List of backups
* - 401: {auth: false, path: string}
* - 500: {error: string}
* - 500: PVE Task Object
*/
router.get("/", 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) {
return;
}
// get vm backups
const storage = global.config.backups.storage;
const backups = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content?content=backup&vmid=${params.vmid}`, "GET", { token: true });
if (backups.status === 200) {
res.status(backups.status).send(backups.data);
}
else {
res.status(backups.status).send({ auth: true, error: backups.statusText });
}
});
/**
* POST - create a new backup of instance using snapshot mode
* !!! Due to the time that backups can take, the API will not wait for the proxmox task to finish !!!
* request:
* - node: string - vm host node id
* - type: string - vm type (lxc, qemu)
* - vmid: number - vm id number
* - notes: notes template string or null if the default one should be used
* responses:
* - 200: PVE Task Object
* - 401: {auth: false, path: string}
* - 500: {error: string}
* - 500: PVE Task Object
*/
router.post("/", async (req, res) => {
const params = {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
notes: req.body.notes ? req.body.notes : "[PAAS] {{node}}.{{vmid}} ({{guestname}}) has been backed up"
};
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) {
return;
}
// get number of currently backups used
const storage = global.config.backups.storage;
const backups = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content?content=backup&vmid=${params.vmid}`, "GET", { token: true });
if (backups.status !== 200) {
res.status(backups.status).send({ error: backups.statusText });
return;
}
const numBackups = backups.data.length;
// get instance
const instance = await global.pve.getInstance(params.node, params.vmid);
if (instance === null) {
res.status(400).send({ error: `failed to get instance ${params.node}/${params.vmid}` });
return;
}
// get pool and pool allowed nodes
const pool = await global.access.getPool(instance.pool, req.cookies);
if (!pool.ok) {
res.status(pool.status).send({ error: `failed to get pool ${pool}` });
return;
}
const maxAllowed = pool.pool["backups-allowed"].max;
// check if used backups is more than maximum allowed, if so exit
if (numBackups >= maxAllowed) {
res.status(backups.status).send({ error: `${params.vmid} already has ${numBackups} >= ${maxAllowed} max backups allowed` });
return;
}
// create backup using vzdump path
const body = {
storage,
vmid: params.vmid,
mode: "snapshot",
remove: 0,
compress: "zstd",
"notes-template": params.notes
};
const result = await global.pve.requestPVE(`/nodes/${params.node}/vzdump`, "POST", { token: true }, body);
res.status(result.status).send(result.data);
});
/**
* POST - edit the notes for an existing backup
* request:
* - node: string - vm host node id
* - type: string - vm type (lxc, qemu)
* - vmid: number - vm id number
* - volid: volid of the backup to be deleted
* - notes: notes template string
* responses:
* - 200: PVE Task Object
* - 401: {auth: false, path: string}
* - 500: {error: string}
* - 500: PVE Task Object
*/
router.post("/notes", async (req, res) => {
const params = {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
volid: req.body.volid,
notes: req.body.notes
};
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) {
return;
}
// check if the specified volid is a backup for the instance
// for whatever reason, calling /nodes/node/storage/content/volid does not return the vmid number whereas /nodes/storage/content?... does
const storage = global.config.backups.storage;
const backups = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content?content=backup&vmid=${params.vmid}`, "GET", { token: true });
if (backups.status !== 200) {
res.status(backups.status).send({ error: backups.statusText });
return;
}
let found = false;
for (const volume of backups.data) {
if (volume.subtype === params.type && String(volume.vmid) === params.vmid && volume.content === "backup" && volume.volid === params.volid) {
found = true;
}
}
if (!found) {
res.status(500).send({ error: `Did not find backup volume ${params.volid} for ${params.node}.${params.vmid}` });
return;
}
// modify backup notes
const body = {
notes: params.notes
};
const result = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content/${params.volid}`, "PUT", { token: true }, body);
if (result.status === 200) {
res.status(result.status).send();
}
else {
res.status(result.status).send({ error: result.statusText });
}
});
/**
* DELETE - delete existing backup of instance
* request:
* - node: string - vm host node id
* - type: string - vm type (lxc, qemu)
* - vmid: number - vm id number
* - volid: volid of the backup to be deleted
* responses:
* - 200: PVE Task Object
* - 401: {auth: false, path: string}
* - 500: {error: string}
* - 500: PVE Task Object
*/
router.delete("/", async (req, res) => {
const params = {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
volid: req.body.volid
};
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) {
return;
}
// check if the specified volid is a backup for the instance
// for whatever reason, calling /nodes/node/storage/content/volid does not return the vmid number whereas /nodes/storage/content?... does
const storage = global.config.backups.storage;
const backups = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content?content=backup&vmid=${params.vmid}`, "GET", { token: true });
if (backups.status !== 200) {
res.status(backups.status).send({ error: backups.statusText });
return;
}
let found = false;
for (const volume of backups.data) {
if (volume.subtype === params.type && String(volume.vmid) === params.vmid && volume.content === "backup" && volume.volid === params.volid) {
found = true;
}
}
if (!found) {
res.status(500).send({ error: `Did not find backup volume ${params.volid} for ${params.node}.${params.vmid}` });
return;
}
// found a valid backup with matching vmid and volid
const result = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content/${params.volid}?delay=5`, "DELETE", { token: true });
res.status(result.status).send(result.data);
});
/**
* POST - restore instance using backup file. Ideally, PBS should be used instead so that individual disk level restore can be done.
* request:
* - node: string - vm host node id
* - type: string - vm type (lxc, qemu)
* - vmid: number - vm id number
* - volid: volid of the backup to be deleted
* responses:
* - 200: PVE Task Object
* - 401: {auth: false, path: string}
* - 500: {error: string}
* - 500: PVE Task Object
*/
router.post("/restore", async (req, res) => {
const params = {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
volid: req.body.volid
};
// check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) {
return;
}
// check if the specified volid is a backup for the instance
// for whatever reason, calling /nodes/node/storage/content/volid does not return the vmid number whereas /nodes/storage/content?... does
const storage = global.config.backups.storage;
const backups = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content?content=backup&vmid=${params.vmid}`, "GET", { token: true });
if (backups.status !== 200) {
res.status(backups.status).send({ error: backups.statusText });
return;
}
let found = false;
for (const volume of backups.data) {
if (volume.subtype === params.type && String(volume.vmid) === params.vmid && volume.content === "backup" && volume.volid === params.volid) {
found = true;
}
}
if (!found) {
res.status(500).send({ error: `Did not find backup volume ${params.volid} for ${params.node}.${params.vmid}` });
return;
}
// container restore
// need to use "advanced" mode to specify the storage used for each disk, so we also need to read the container's config
// for whatever reason, this will wipe disks that are not included in the backup !!!
if (params.type === "lxc") {
const body = {
vmid: params.vmid,
force: 1,
ostemplate: params.volid,
restore: 1
};
const instance = await global.pve.getInstance(params.node, params.vmid);
for (const v in instance.volumes) {
const volume = instance.volumes[v];
if (volume.type === "mp") {
body[v] = `${volume.storage}:${volume.size / 1024 ** 3},mp=${volume.mp},backup=1`;
}
else if (volume.type === "rootfs") {
body[v] = `${volume.storage}:${volume.size / 1024 ** 3}`;
}
}
const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/`, "POST", { token: true }, body);
if (result.status === 200) {
res.status(result.status).send();
}
else {
res.status(result.status).send({ error: result.statusText });
}
}
// VM restore, unlike the container restore, this should not affect disks which are not in the backup
else if (params.type === "qemu") { // vm restore
const body = {
vmid: params.vmid,
force: 1,
archive: params.volid
};
const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/`, "POST", { token: true }, body);
if (result.status === 200) {
res.status(result.status).send();
}
else {
res.status(result.status).send({ error: result.statusText });
}
}
});
+60 -16
View File
@@ -1,9 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources;
/** /**
* POST - detach mounted disk from instance * POST - detach mounted disk from instance
* request: * request:
@@ -25,12 +22,14 @@ router.post("/:disk/detach", async (req, res) => {
vmid: req.params.vmid, vmid: req.params.vmid,
disk: req.params.disk disk: req.params.disk
}; };
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// disk must exist // disk must exist
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
if (!disk) { if (!disk) {
@@ -38,14 +37,19 @@ router.post("/:disk/detach", async (req, res) => {
res.end(); res.end();
return; return;
} }
// disk cannot be unused // disk cannot be unused
if (params.disk.includes("unused")) { if (params.disk.includes("unused")) {
res.status(500).send({ error: `Requested disk ${params.disk} cannot be unused. Use /disk/delete to permanently delete unused disks.` }); res.status(500).send({ error: `Requested disk ${params.disk} cannot be unused. Use /disk/delete to permanently delete unused disks.` });
res.end(); res.end();
return; return;
} }
// setup detach action
const action = { delete: params.disk }; const action = { delete: params.disk };
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// 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); await global.pve.syncInstance(params.node, params.vmid);
@@ -72,11 +76,13 @@ router.post("/:disk/attach", async (req, res) => {
type: req.params.type, type: req.params.type,
vmid: req.params.vmid, vmid: req.params.vmid,
disk: req.params.disk, disk: req.params.disk,
source: req.body.source source: req.body.source,
mp: req.body.mp
}; };
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
@@ -88,6 +94,7 @@ router.post("/:disk/attach", async (req, res) => {
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 resourceConfig = global.config.resources; const resourceConfig = global.config.resources;
if (!resourceConfig[disk.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) { if (!resourceConfig[disk.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) {
@@ -95,9 +102,10 @@ router.post("/:disk/attach", async (req, res) => {
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.volid; action[params.disk] = params.type === "qemu" ? `${disk.file}` : `${disk.file},mp=${params.mp},backup=1`;
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
@@ -131,14 +139,22 @@ router.post("/:disk/resize", async (req, res) => {
size: req.body.size size: req.body.size
}; };
// attempt to parse user from username
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (userObj == null) {
res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` });
}
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// check disk existence // check disk existence
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
if (!disk) { // exit if disk does not exist if (!disk) { // exit if disk does not exist
@@ -146,16 +162,20 @@ router.post("/:disk/resize", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup request // setup request
const storage = disk.storage; // get the storage const storage = disk.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
if (!await approveResources(req, userObj, request, params.node)) { const { approved } = await global.utils.approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(500).send({ request, error: `Storage ${storage} could not fulfill request of size ${params.size}G.` }); res.status(500).send({ request, error: `Storage ${storage} could not fulfill request of size ${params.size}G.` });
res.end(); res.end();
return; return;
} }
// action approved, commit to action // action approved, commit to action
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);
@@ -190,14 +210,20 @@ router.post("/:disk/move", async (req, res) => {
delete: req.body.delete delete: req.body.delete
}; };
// attempt to parse user from username
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (userObj == null) {
res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` });
}
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// check disk existence // check disk existence
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
if (!disk) { // exit if disk does not exist if (!disk) { // exit if disk does not exist
@@ -213,7 +239,8 @@ router.post("/:disk/move", async (req, res) => {
request[dstStorage] = Number(size); // always decrease destination storage by size request[dstStorage] = Number(size); // always decrease destination storage by size
} }
// check request approval // check request approval
if (!await approveResources(req, userObj, request, params.node)) { const { approved } = await global.utils.approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` }); res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` });
res.end(); res.end();
return; return;
@@ -256,7 +283,7 @@ router.delete("/:disk/delete", async (req, res) => {
}; };
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
@@ -309,13 +336,26 @@ router.post("/:disk/create", async (req, res) => {
size: req.body.size, size: req.body.size,
iso: req.body.iso iso: req.body.iso
}; };
console.log(req.cookies)
// attempt to parse user from username
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (userObj == null) {
res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` });
return;
}
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// disk must not exist // disk must not exist
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
if (disk) { if (disk) {
@@ -323,13 +363,15 @@ router.post("/:disk/create", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup request // setup request
const request = {}; const request = {};
if (!params.disk.includes("ide")) { if (!params.disk.includes("ide")) { // ignore resource request if the type is ide (iso file)
// setup request // setup request
request[params.storage] = Number(params.size * 1024 ** 3); request[params.storage] = Number(params.size * 1024 ** 3);
// check request approval // check request approval
if (!await approveResources(req, userObj, request, params.node)) { const { approved } = await global.utils.approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` }); res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` });
res.end(); res.end();
return; return;
@@ -342,18 +384,20 @@ router.post("/:disk/create", async (req, res) => {
return; return;
} }
} }
// setup action // setup action
const action = {}; const action = {};
if (params.disk.includes("ide") && params.iso) { if (params.disk.includes("ide") && params.iso) {
action[params.disk] = `${params.iso},media=cdrom`; action[params.disk] = `${params.iso},media=cdrom`;
} }
else if (params.type === "qemu") { // type is qemu, use sata else if (params.type === "qemu") { // type is qemu, use sata
action[params.disk] = `${params.storage}:${params.size}`; action[params.disk] = `${params.storage}:${params.size},backup=1`;
} }
else { // type is lxc, use mp and add mp and backup values else { // type is lxc, use mp and add mp and backup values
action[params.disk] = `${params.storage}:${params.size},mp=/${params.disk}/,backup=1`; action[params.disk] = `${params.storage}:${params.size},mp=/${params.disk}/,backup=1`;
} }
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);
+40 -35
View File
@@ -1,9 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); ; export const router = Router({ mergeParams: true }); ;
const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources;
/** /**
* POST - create new virtual network interface * POST - create new virtual network interface
* request: * request:
@@ -26,27 +23,25 @@ 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: Number(req.params.netid.replace("net", "")), netid: req.params.netid,
rate: req.body.rate, rate: req.body.rate,
name: req.body.name name: req.body.name
}; };
// check netid is a valid number
if (isNaN(params.netid)) {
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
res.end();
return;
}
// check auth for specific instance // 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// net interface must not exist // net interface must not exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid); const net = await global.pve.getNet(params.node, params.vmid, params.netid);
if (net) { 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;
} }
@@ -55,26 +50,32 @@ router.post("/:netid/create", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup request
const request = { const request = {
network: Number(params.rate) network: Number(params.rate)
}; };
// check resource approval // check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (!await approveResources(req, userObj, request, params.node)) { const { approved } = await global.utils.approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
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 = (await global.access.getUser(userObj, req.cookies)).templates.network[params.type];
const action = {}; const action = {};
if (params.type === "lxc") { if (params.type === "lxc") {
action[`net${params.netid}`] = `name=${params.name},bridge=${nc.bridge},ip=${nc.ip},ip6=${nc.ip6},tag=${nc.vlan},type=${nc.type},rate=${params.rate}`; action[`${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[`net${params.netid}`] = `${nc.type},bridge=${nc.bridge},tag=${nc.vlan},rate=${params.rate}`; action[`${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);
@@ -102,21 +103,20 @@ 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: Number(req.params.netid.replace("net", "")), netid: req.params.netid,
rate: req.body.rate rate: req.body.rate
}; };
// check netid is a valid number
if (isNaN(params.netid)) {
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
res.end();
return;
}
// check auth for specific instance // 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// net interface must already exist // net interface must already exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid); const net = await global.pve.getNet(params.node, params.vmid, params.netid);
if (!net) { if (!net) {
@@ -124,20 +124,26 @@ router.post("/:netid/modify", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup request
const request = { const request = {
network: Number(params.rate) - Number(net.rate) network: Number(params.rate) - Number(net.rate)
}; };
// check resource approval // check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (!await approveResources(req, userObj, request, params.node)) { const { approved } = await global.utils.approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
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 action = {}; const action = {};
action[`net${params.netid}`] = net.value.replace(`rate=${net.rate}`, `rate=${params.rate}`); action[`${params.netid}`] = net.value.replace(`rate=${net.rate}`, `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);
@@ -163,20 +169,16 @@ 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: Number(req.params.netid.replace("net", "")) netid: req.params.netid
}; };
// check netid is a valid number
if (isNaN(params.netid)) {
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
res.end();
return;
}
// check auth for specific instance // 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// net interface must already exist // net interface must already exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid); const net = await global.pve.getNet(params.node, params.vmid, params.netid);
if (!net) { if (!net) {
@@ -184,10 +186,13 @@ router.delete("/:netid/delete", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup action // setup action
const action = { delete: `${params.netid}` };
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: `net${params.netid}` }); 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); await global.pve.syncInstance(params.node, params.vmid);
}); });
+119 -47
View File
@@ -1,8 +1,74 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); ; export const router = Router({ mergeParams: true }); ;
const checkAuth = global.utils.checkAuth; /**
const approveResources = global.utils.approveResources; * GET - get available pcie devices for the given node and user
* request:
* - node: string - vm host node id
* responses:
* - 200: PVE PCI Device Object
* - 401: {auth: false}
* - 401: {auth: false, path: string}
* - 500: {error: string}
*/
router.get("/", async (req, res) => {
const params = {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
};
// check auth
const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) {
return;
}
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// ensure that requested instance type is vmid
if (instance.type !== "VM") {
res.status(400).send({ auth: true, error: `actual instance type is ${instance.type} but must be VM` });
}
else if (params.type !== "qemu") {
res.status(400).send({ auth: true, error: `requested instance type is ${params.type} but must be qemu` });
}
// get pool and pool allowed nodes
const pool = await global.access.getPool(instance.pool, req.cookies);
const poolNodes = pool.pool["nodes-allowed"];
if (poolNodes[params.node] !== true) { // user does not have access to the node
res.status(401).send({ auth: false, path: params.node });
res.end();
return;
}
// get remaining user resources
const poolAvailPci = (await global.utils.getPoolResources(req, instance.pool)).pci.nodes[params.node]; // we assume that the node list is used. TODO support global lists
if (poolAvailPci === undefined) { // user has no available devices on this node, so send an empty list
res.status(200).send([]);
res.end();
}
else {
// get node avail devices
const node = await global.pve.getNode(params.node);
let availableDevices = [];
// get each device and filter out only thise which are not reserved
for (const device of Object.values(node.devices)) {
if (device.reserved === false) {
availableDevices.push(device);
}
}
//further filter out only devices which the user has access to
availableDevices = availableDevices.filter(nodeAvail => poolAvailPci.some((userAvail) => {
return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0;
}));
res.status(200).send(availableDevices);
res.end();
}
});
/** /**
* GET - get instance pcie device data * GET - get instance pcie device data
@@ -22,24 +88,20 @@ 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: Number(req.params.hostpci.replace("hostpci", "")) hostpci: req.params.hostpci
}; };
// check hostpci is a valid number
if (isNaN(params.hostpci)) {
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
res.end();
return;
}
// check auth for specific instance // 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get device // get device
const device = await global.pve.getDevice(params.node, params.vmid, params.hostpci); const device = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (!device) { if (!device) {
res.status(500).send({ error: `Could not find hostpci${params.hostpci}=${device} in ${params.node}.` }); res.status(500).send({ error: `Could not find ${params.hostpci}=${device} in ${params.node}.` });
res.end(); res.end();
return; return;
} }
@@ -68,40 +130,42 @@ 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: Number(req.params.hostpci.replace("hostpci", "")), hostpci: req.params.hostpci,
device: req.body.device, device: req.body.device,
pcie: req.body.pcie pcie: req.body.pcie
}; };
// check hostpci is a valid number
if (isNaN(params.hostpci)) {
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
res.end();
return;
}
// check if type is qemu // 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)." });
res.end(); res.end();
return; return;
} }
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// force all functions // force all functions
params.device = params.device.split(".")[0]; params.device = params.device.split(".")[0];
// device must exist to be modified // device must exist to be modified
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci); const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (!existingDevice) { if (!existingDevice) {
res.status(500).send({ error: `No device in hostpci${params.hostpci}.` }); res.status(500).send({ error: `No device in ${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, we do the split in case of existing partial-function hostpci
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (existingDevice.device_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 node = await global.pve.getNode(params.node);
const requestedDevice = node.devices[`${params.device}`]; const requestedDevice = node.devices[`${params.device}`];
@@ -112,21 +176,24 @@ router.post("/:hostpci/modify", async (req, res) => {
return; return;
} }
// check resource approval // check resource approval
if (!await approveResources(req, userObj, request, params.node)) { const { approved } = await global.utils.approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
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 ${requestedDevice.device_name}.` });
res.end(); res.end();
return; return;
} }
// check node availability // check node availability
if (!Object.values(node.devices).some(element => element.device_id.split(".")[0] === params.device && element.reserved === false)) { if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) {
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[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`; action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
@@ -153,58 +220,64 @@ router.post("/:hostpci/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,
hostpci: Number(req.params.hostpci.replace("hostpci", "")), hostpci: req.params.hostpci,
device: req.body.device, device: req.body.device,
pcie: req.body.pcie pcie: req.body.pcie
}; };
// check hostpci is a valid number
if (isNaN(params.hostpci)) {
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
res.end();
return;
}
// check if type is qemu // 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)." });
res.end(); res.end();
return; return;
} }
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// 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 // device must not exist to be added
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci); const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (existingDevice) { if (existingDevice) {
res.status(500).send({ error: `Existing device in hostpci${params.hostpci}.` }); res.status(500).send({ error: `Existing device in ${params.hostpci}.` });
res.end(); res.end();
return; return;
} }
// setup request // setup request
const node = await global.pve.getNode(params.node); const node = await global.pve.getNode(params.node);
const requestedDevice = node.devices[`${params.device}`]; const requestedDevice = node.devices[`${params.device}`];
const request = { pci: requestedDevice.device_name }; const request = { pci: requestedDevice.device_name };
// check resource approval // check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (!await approveResources(req, userObj, request, params.node)) { const { approved } = await global.utils.approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
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 ${requestedDevice.device_name}.` });
res.end(); res.end();
return; return;
} }
// check node availability // check node availability
// const node = await global.pve.getNode(params.node); if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) {
if (!Object.values(node.devices).some(element => element.device_id.split(".")[0] === params.device && element.reserved === false)) {
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` }); res.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[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`; action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
@@ -230,35 +303,34 @@ 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: Number(req.params.hostpci.replace("hostpci", "")) hostpci: req.params.hostpci
}; };
// check hostpci is a valid number
if (isNaN(params.hostpci)) {
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
res.end();
return;
}
// check if type is qemu // 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)." });
res.end(); res.end();
return; return;
} }
// 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 global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
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 device = global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (!device) { if (!device) {
res.status(500).send({ error: `Could not find hostpci${params.hostpci} in ${params.vmid}.` }); res.status(500).send({ error: `Could not find ${params.hostpci} in ${params.vmid}.` });
res.end(); res.end();
return; return;
} }
// setup action // setup action
const action = { delete: `hostpci${params.hostpci}` }; const action = { delete: `${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 result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
+4
View File
@@ -12,11 +12,15 @@ router.get("/config/:key", async (req, res) => {
const params = { const params = {
key: req.params.key key: req.params.key
}; };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
// check if users are allowed to get the config value
// return the value if so, otherwise send unauthorized
const allowKeys = ["resources"]; const allowKeys = ["resources"];
if (allowKeys.includes(params.key)) { if (allowKeys.includes(params.key)) {
const config = global.config; const config = global.config;
+10 -15
View File
@@ -2,11 +2,7 @@ import { WebSocketServer } from "ws";
import * as cookie from "cookie"; import * as cookie from "cookie";
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); ; export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
const getObjectHash = global.utils.getObjectHash;
const getTimeLeft = global.utils.getTimeLeft;
// maps usernames to socket object(s) // maps usernames to socket object(s)
const userSocketMap = {}; const userSocketMap = {};
@@ -47,15 +43,15 @@ if (schemes.hash.enabled) {
*/ */
router.get("/hash", async (req, res) => { router.get("/hash", async (req, res) => {
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await global.utils.checkAuth(req.cookies, res);
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 - do not use fabric here because fabric is not always updated to changes like up/down state changes
const status = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data; const status = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).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);
res.status(200).send(getObjectHash(state)); res.status(200).send(global.utils.getObjectHash(state));
}); });
console.log("clientsync: enabled hash sync"); console.log("clientsync: enabled hash sync");
} }
@@ -135,7 +131,7 @@ if (schemes.interrupt.enabled) {
// AND if the next event trigger is more than the new rate in the future, // AND if the next event trigger is more than the new rate in the future,
// restart the timer with the new rate // restart the timer with the new rate
// avoids a large requested rate preventing a faster rate from being fulfilled // avoids a large requested rate preventing a faster rate from being fulfilled
else if (rate < Math.min.apply(null, Object.values(requestedRates)) && getTimeLeft(timer) > rate) { else if (rate < Math.min.apply(null, Object.values(requestedRates)) && global.utils.getTimeLeft(timer) > rate) {
clearTimeout(timer); clearTimeout(timer);
timer = setTimeout(handleInterruptSync, rate); timer = setTimeout(handleInterruptSync, rate);
const time = global.process.uptime(); const time = global.process.uptime();
@@ -166,11 +162,10 @@ if (schemes.interrupt.enabled) {
} }
else { else {
wsServer.handleUpgrade(req, socket, head, async (socket) => { wsServer.handleUpgrade(req, socket, head, async (socket) => {
// get the user pools // use user cookies to determine which pools they can see, lazily assume that if a user can audit a pool they are also can audit pool member state
const userObj = global.utils.getUserObjFromUsername(cookies.username); const pools = await global.pve.requestPVE("/pools", "GET", { cookies });
const pools = Object.keys((await global.userManager.getUser(userObj, cookies)).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, Object.keys(pools.data));
}); });
} }
}); });
@@ -190,7 +185,7 @@ if (schemes.interrupt.enabled) {
return; return;
} }
// get current cluster resources // get current cluster resources
const status = (await global.pve.requestPVE("/cluster/resources", "GET", { token: true })).data.data; const status = (await global.pve.requestPVE("/cluster/resources", "GET", { token: true })).data;
// filter out just state information of resources that are needed, and hash each one // filter out just state information of resources that are needed, and hash each one
const currState = extractClusterState(status, resourceTypes, true); const currState = extractClusterState(status, resourceTypes, true);
// get a map of users to send sync notifications // get a map of users to send sync notifications
@@ -260,7 +255,7 @@ function extractClusterState (status, resourceTypes, hashIndividual = false) {
pool: resource.pool || null pool: resource.pool || null
}; };
if (hashIndividual) { if (hashIndividual) {
const hash = getObjectHash(state[resource.id]); const hash = global.utils.getObjectHash(state[resource.id]);
state[resource.id].hash = hash; state[resource.id].hash = hash;
} }
} }
+12 -56
View File
@@ -4,60 +4,6 @@ export const router = Router({ mergeParams: true }); ;
const config = global.config; const config = global.config;
const checkAuth = global.utils.checkAuth; const checkAuth = global.utils.checkAuth;
/**
* GET - get db user resource information including allocated, free, and maximum resource values along with resource metadata
* responses:
* - 200: {avail: Object, max: Object, used: Object, resources: Object}
* - 401: {auth: false}
*/
router.get("/dynamic/resources", async (req, res) => {
const params = {
username: req.cookies.username
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const userObj = global.utils.getUserObjFromUsername(params.username);
const resources = await global.utils.getUserResources(req, userObj);
res.status(200).send(resources);
});
/**
* GET - get db user configuration by key
* request:
* - key: string - user config key
* responses:
* - 200: Object
* - 401: {auth: false}
* - 401: {auth: false, error: string}
*/
router.get("/config/:key", async (req, res) => {
const params = {
key: req.params.key
};
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const allowKeys = ["resources", "cluster"];
if (allowKeys.includes(params.key)) {
const config = await global.userManager.getUser(userObj, req.cookies);
res.status(200).send(config[params.key]);
}
else {
res.status(401).send({ auth: false, error: `User is not authorized to access /user/config/${params.key}.` });
}
});
/** /**
* GET - get user accessible iso files * GET - get user accessible iso files
* response: * response:
@@ -73,7 +19,12 @@ 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 isos = (await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=iso`, "GET", { token: true })).data.data; const content = await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=iso`, "GET", { token: true });
if (content.status !== 200) {
res.status(content.status).send({ error: content.statusText });
return;
}
const isos = content.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/`, "");
@@ -98,7 +49,12 @@ 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 isos = (await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=vztmpl`, "GET", { token: true })).data.data; const content = await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=vztmpl`, "GET", { token: true });
if (content.status !== 200) {
res.status(content.status).send({ error: content.statusText });
return;
}
const isos = content.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/`, "");
+149 -95
View File
@@ -36,7 +36,7 @@ export async function checkAuth (cookies, res, vmpath = null) {
return false; return false;
} }
if ((await global.userManager.getUser(userObj, cookies)) === null) { // check if user exists in database if ((await global.access.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.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in database.` });
res.end(); res.end();
return false; return false;
@@ -60,43 +60,44 @@ export async function checkAuth (cookies, res, vmpath = null) {
} }
/** /**
* Get user resource data including used, available, and maximum resources. * Get pool resource data including used, available, and maximum resources.
* @param {Object} req ProxmoxAAS API request object. * @param {Object} req ProxmoxAAS API request object.
* @param {{id: string, realm: string}} user object of user to get resource data. * @param {{id: string, realm: string}} user object of user to get resource data.
* @returns {{used: Object, avail: Object, max: Object, resources: Object}} used, available, maximum, and resource metadata for the specified user. * @returns {{used: Object, avail: Object, max: Object, resources: Object}} used, available, maximum, and resource metadata for the specified user.
*/ */
export async function getUserResources (req, user) { export async function getPoolResources (req, pool) {
const dbResources = global.config.resources; const configResources = global.config.resources;
const userResources = (await global.userManager.getUser(user, req.cookies)).resources; const poolConfig = await global.access.getPool(pool, req.cookies);
const poolResources = poolConfig.pool.resources;
// setup the user resource object with used and avail for each resource and each resource pool // setup the pool 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)
for (const resourceName of Object.keys(userResources)) { for (const resourceName of Object.keys(poolResources)) {
if (dbResources[resourceName].type === "list") { if (configResources[resourceName].type === "list") {
userResources[resourceName].total = []; poolResources[resourceName].total = [];
userResources[resourceName].global.forEach((e) => { poolResources[resourceName].global.forEach((e) => {
e.used = 0; e.used = 0;
e.avail = e.max; e.avail = e.max;
const index = userResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match); const index = poolResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match);
if (index === -1) { if (index === -1) {
userResources[resourceName].total.push(structuredClone(e)); poolResources[resourceName].total.push(structuredClone(e));
} }
else { else {
userResources[resourceName].total[index].max += e.max; poolResources[resourceName].total[index].max += e.max;
userResources[resourceName].total[index].avail += e.avail; poolResources[resourceName].total[index].avail += e.avail;
} }
}); });
for (const nodeName of Object.keys(userResources[resourceName].nodes)) { for (const nodeName of Object.keys(poolResources[resourceName].nodes)) {
userResources[resourceName].nodes[nodeName].forEach((e) => { poolResources[resourceName].nodes[nodeName].forEach((e) => {
e.used = 0; e.used = 0;
e.avail = e.max; e.avail = e.max;
const index = userResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match); const index = poolResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match);
if (index === -1) { if (index === -1) {
userResources[resourceName].total.push(structuredClone(e)); poolResources[resourceName].total.push(structuredClone(e));
} }
else { else {
userResources[resourceName].total[index].max += e.max; poolResources[resourceName].total[index].max += e.max;
userResources[resourceName].total[index].avail += e.avail; poolResources[resourceName].total[index].avail += e.avail;
} }
}); });
} }
@@ -107,21 +108,21 @@ export async function getUserResources (req, user) {
used: 0, used: 0,
avail: 0 avail: 0
}; };
userResources[resourceName].global.used = 0; poolResources[resourceName].global.used = 0;
userResources[resourceName].global.avail = userResources[resourceName].global.max; poolResources[resourceName].global.avail = poolResources[resourceName].global.max;
total.max += userResources[resourceName].global.max; total.max += poolResources[resourceName].global.max;
total.avail += userResources[resourceName].global.avail; total.avail += poolResources[resourceName].global.avail;
for (const nodeName of Object.keys(userResources[resourceName].nodes)) { for (const nodeName of Object.keys(poolResources[resourceName].nodes)) {
userResources[resourceName].nodes[nodeName].used = 0; poolResources[resourceName].nodes[nodeName].used = 0;
userResources[resourceName].nodes[nodeName].avail = userResources[resourceName].nodes[nodeName].max; poolResources[resourceName].nodes[nodeName].avail = poolResources[resourceName].nodes[nodeName].max;
total.max += userResources[resourceName].nodes[nodeName].max; total.max += poolResources[resourceName].nodes[nodeName].max;
total.avail += userResources[resourceName].nodes[nodeName].avail; total.avail += poolResources[resourceName].nodes[nodeName].avail;
} }
userResources[resourceName].total = total; poolResources[resourceName].total = total;
} }
} }
const configs = await global.pve.getUserResources(user, req.cookies); const configs = await global.pve.getPoolResources(req.cookies, pool);
for (const vmid in configs) { for (const vmid in configs) {
const config = configs[vmid]; const config = configs[vmid];
@@ -129,20 +130,20 @@ export async function getUserResources (req, user) {
// count basic numeric resources // 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 configResources && configResources[resourceName].type === "numeric") {
const val = Number(config[resourceName]); const val = Number(config[resourceName]);
// 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[resourceName].nodes) { if (nodeName in poolResources[resourceName].nodes) {
userResources[resourceName].nodes[nodeName].used += val; poolResources[resourceName].nodes[nodeName].used += val;
userResources[resourceName].nodes[nodeName].avail -= val; poolResources[resourceName].nodes[nodeName].avail -= val;
} }
// otherwise add the resource to the global pool // otherwise add the resource to the global pool
else { else {
userResources[resourceName].global.used += val; poolResources[resourceName].global.used += val;
userResources[resourceName].global.avail -= val; poolResources[resourceName].global.avail -= val;
} }
userResources[resourceName].total.used += val; poolResources[resourceName].total.used += val;
userResources[resourceName].total.avail -= val; poolResources[resourceName].total.avail -= val;
} }
} }
// count disk resources in volumes // count disk resources in volumes
@@ -151,38 +152,38 @@ export async function getUserResources (req, user) {
const storage = disk.storage; const storage = disk.storage;
const size = disk.size; const size = disk.size;
// only process disk if its storage is in the user resources to be counted // only process disk if its storage is in the user resources to be counted
if (storage in userResources) { if (storage in poolResources) {
// 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 poolResources[storage].nodes) {
userResources[storage].nodes[nodeName].used += size; poolResources[storage].nodes[nodeName].used += size;
userResources[storage].nodes[nodeName].avail -= size; poolResources[storage].nodes[nodeName].avail -= size;
} }
// otherwise add the resource to the global pool // otherwise add the resource to the global pool
else { else {
userResources[storage].global.used += size; poolResources[storage].global.used += size;
userResources[storage].global.avail -= size; poolResources[storage].global.avail -= size;
} }
userResources[storage].total.used += size; poolResources[storage].total.used += size;
userResources[storage].total.avail -= size; poolResources[storage].total.avail -= size;
} }
} }
// count net resources in nets // count net resources in nets
for (const netid in config.nets) { for (const netid in config.nets) {
const net = config.nets[netid]; const net = config.nets[netid];
const rate = net.rate; const rate = net.rate;
if (userResources.network) { if (poolResources.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 poolResources.network.nodes) {
userResources.network.nodes[nodeName].used += rate; poolResources.network.nodes[nodeName].used += rate;
userResources.network.nodes[nodeName].avail -= rate; poolResources.network.nodes[nodeName].avail -= rate;
} }
// otherwise add the resource to the global pool // otherwise add the resource to the global pool
else { else {
userResources.network.global.used += rate; poolResources.network.global.used += rate;
userResources.network.global.avail -= rate; poolResources.network.global.avail -= rate;
} }
userResources.network.total.used += rate; poolResources.network.total.used += rate;
userResources.network.total.avail -= rate; poolResources.network.total.avail -= rate;
} }
} }
// count pci device resources in devices // count pci device resources in devices
@@ -190,78 +191,88 @@ export async function getUserResources (req, user) {
const device = config.devices[deviceid]; const device = config.devices[deviceid];
const name = device.device_name; const name = device.device_name;
// if the node has a node specific rule, add it there // if the node has a node specific rule, add it there
if (nodeName in userResources.pci.nodes) { if (nodeName in poolResources.pci.nodes) {
const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match)); const index = poolResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match));
if (index >= 0) { if (index >= 0) {
userResources.pci.nodes[nodeName][index].used++; poolResources.pci.nodes[nodeName][index].used++;
userResources.pci.nodes[nodeName][index].avail--; poolResources.pci.nodes[nodeName][index].avail--;
} }
} }
// otherwise try to 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 = poolResources.pci.global.findIndex((availEelement) => name.includes(availEelement.match));
if (index >= 0) { // device resource is in the user's global list then increment it by 1 if (index >= 0) { // device resource is in the user's global list then increment it by 1
userResources.pci.global[index].used++; poolResources.pci.global[index].used++;
userResources.pci.global[index].avail--; poolResources.pci.global[index].avail--;
} }
} }
// finally, add the device to the total map // finally, add the device to the total map
const index = userResources.pci.total.findIndex((availEelement) => name.includes(availEelement.match)); const index = poolResources.pci.total.findIndex((availEelement) => name.includes(availEelement.match));
if (index >= 0) { if (index >= 0) {
userResources.pci.total[index].used++; poolResources.pci.total[index].used++;
userResources.pci.total[index].avail--; poolResources.pci.total[index].avail--;
} }
} }
} }
return userResources; return poolResources;
} }
/** /**
* Check approval for user requesting additional resources. Generally, subtracts the request from available resources and ensures request can be fulfilled by the available resources. * Check approval for user requesting additional resources. Generally, subtracts the request from available resources and ensures request can be fulfilled by the available resources.
* @param {Object} req ProxmoxAAS API request object. * @param {Object} req ProxmoxAAS API request object.
* @param {{id: string, realm: string}} user object of user requesting additional resources. * @param {{id: string, realm: string}} user object of user requesting additional resources.
* @param {string} node name of node hosting requested resource(s)
* @param {string} pool name of pool hosting requested resource(s)
* @param {Object} request k-v pairs of resources and requested amounts * @param {Object} request k-v pairs of resources and requested amounts
* @returns {boolean} true if the available resources can fullfill the requested resources, false otherwise. * @returns {boolean, Object} true if the available resources can fullfill the requested resources, false otherwise.
*/ */
export async function approveResources (req, user, request, node) { export async function approveResources (req, user, node, pool, request) {
const dbResources = global.config.resources; const configResources = global.config.resources;
const userResources = await getUserResources(req, user); const poolResources = await getPoolResources(req, pool);
let approved = true; const reason = {};
Object.keys(request).every((key) => {
for (const key in request) {
// if requested resource is not specified in user resources, assume it's not allowed // if requested resource is not specified in user resources, assume it's not allowed
if (!(key in userResources)) { if (!(key in poolResources)) {
approved = false; reason[key] = { approved: false, reason: `${key} not allowed` };
return false; continue;
} }
const inNode = node in userResources[key].nodes; // use node specific quota if there is one available, otherwise use the global resource quota
const resourceData = inNode ? userResources[key].nodes[node] : userResources[key].global; const inNode = node in poolResources[key].nodes;
const resourceData = inNode ? poolResources[key].nodes[node] : poolResources[key].global;
// if the resource type is list, check if the requested resource exists in the list // if the resource type is list, check if the requested resource exists in the list
if (dbResources[key].type === "list") { if (configResources[key].type === "list") {
const index = resourceData.findIndex((availElement) => request[key].includes(availElement.match)); const index = resourceData.findIndex((availElement) => request[key].includes(availElement.match));
// if no matching resource when index == -1, then remaining is -1 otherwise use the remaining value // if no matching resource when index == -1, then remaining is -1 otherwise use the remaining value
const avail = index === -1 ? false : resourceData[index].avail > 0; const avail = index === -1 ? false : resourceData[index].avail > 0;
if (avail !== dbResources[key].whitelist) { if (avail !== configResources[key].whitelist) {
approved = false; reason[key] = { approved: false, reason: `${key} ${configResources[key].whitelist ? "not in whitelist" : "in blacklist"}` };
return false; continue;
} }
} }
// if either the requested or avail resource is NaN, block
else if (isNaN(resourceData.avail) || isNaN(request[key])) { // if either the requested or avail resource is not strictly a number, block
approved = false; else if (typeof (resourceData.avail) !== "number" || typeof (request[key]) !== "number") {
return false; reason[key] = { approved: false, reason: `expected ${key} to be a number but got ${request[key]}` };
} continue;
// if the avail resources is less than the requested resources, block
else if (resourceData.avail - request[key] < 0) {
approved = false;
return false;
} }
return true; // if the avail resources is less than the requested resources, block
else if (resourceData.avail - request[key] < 0) {
reason[key] = { approved: false, reason: `${key} requested ${request[key]} which is more than ${resourceData.avail} available` };
continue;
}
reason[key] = { approved: true, reason: "ok" };
}
const approved = Object.values(reason).every((element) => {
return element.approved === true;
}); });
return approved; // if all requested resources pass, allow return { approved, reason }; // if all requested resources pass, allow
} }
/** /**
@@ -316,15 +327,15 @@ export function readJSONFile (path) {
return JSON.parse(readFileSync(path)); return JSON.parse(readFileSync(path));
} }
catch (e) { catch (e) {
console.log(`error: ${path} was not found.`); console.log(`json: error opening ${path}: ${e}`);
exit(1); exit(1);
} }
}; };
/** /**
* * Parse username into user object using the uid@realm format.
* @param {*} username * @param {*} username
* @returns {Object | null} user object containing username and realm or null if user does not exist * @returns {Object | null} user object containing userid and realm or null if username format was invalid
*/ */
export function getUserObjFromUsername (username) { export function getUserObjFromUsername (username) {
if (username) { if (username) {
@@ -337,3 +348,46 @@ export function getUserObjFromUsername (username) {
return null; return null;
} }
} }
/**
* Parse groupname into group object using the gid-realm format.
* @param {*} groupname
* @returns {Object | null} user object containing groupid and realm or null if groupname format was invalid
*/
export function getGroupObjFromGroupname (groupname) {
if (groupname) {
if (groupname.includes("-")) {
const groupRealm = groupname.split("-").at(-1);
const groupID = groupname.replace(`-${groupRealm}`, "");
const groupObj = { id: groupID, realm: groupRealm };
return groupObj;
}
else {
const groupRealm = "pve";
const groupID = groupname;
const groupObj = { id: groupID, realm: groupRealm };
return groupObj;
}
}
else {
return null;
}
}
/**
* Inspect pool object and return true if pool contains any groups which contain the user object.
* @param {Object} poolObj pool data object
* @param {Object} userObj user object containing id and realm
* @returns {boolean} true if userObj in poolObj
*/
export function checkUserInPool(poolObj, userObj) {
for (const group of poolObj.groups) {
// assumption: pool listed groups are all relevant memberships (ie are paas client role)
for (const user of group.users) {
if (user.username.uid === userObj.id && user.username.realm === userObj.realm) {
return true;
}
}
}
return false;
}
-140
View File
@@ -1,140 +0,0 @@
{
"users": {
"exampleuser@auth": {
"resources": {
"cpu": {
"global": [
{
"match": "kvm64",
"name": "kvm64",
"max": 1
},
{
"match": "host",
"name": "host",
"max": 1
}
],
"nodes": {}
},
"cores": {
"global": {
"max": 128
},
"nodes": {}
},
"memory": {
"global": {
"max": 137438953472
},
"nodes": {}
},
"swap": {
"global": {
"max": 137438953472
},
"nodes": {}
},
"local": {
"global": {
"max": 1099511627776
},
"nodes": {}
},
"cephpl": {
"global": {
"max": 1099511627776
},
"nodes": {}
},
"network": {
"global": {
"max": 100000
},
"nodes": {}
},
"pci": {
"global": [],
"nodes": {
"example-node-0": [
{
"match": "[device 1]",
"name": "Device 1",
"max": 1
},
{
"match": "[device 2]",
"name": "Device 2",
"max": 1
}
]
}
}
},
"cluster": {
"admin": false,
"nodes": {
"example-node-0": true,
"example-node-1": true,
"example-node-2": true
},
"vmid": {
"min": 100,
"max": 199
},
"pools": {
"example-pool-1": true,
"example-pool-2": true
}
},
"templates": {
"instances": {
"lxc": {
"net0": {
"value": "name=eth0,bridge=vmbr0,ip=dhcp,ip6=dhcp,tag=10,type=veth,rate=1000",
"resource": {
"name": "network",
"amount": 1000
}
}
},
"qemu": {
"cpu": {
"value": "host",
"resource": null
},
"machine": {
"value": "q35",
"resource": null
},
"net0": {
"value": "virtio,bridge=vmbr0,tag=10,rate=1000",
"resource": {
"name": "network",
"amount": 1000
}
},
"scsihw": {
"value": "virtio-scsi-single",
"resource": null
}
}
},
"network": {
"lxc": {
"type": "veth",
"bridge": "vmbr0",
"vlan": 10,
"ip": "dhcp",
"ip6": "dhcp"
},
"qemu": {
"type": "virtio",
"bridge": "vmbr0",
"vlan": 10
}
}
}
}
}
}