Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f64c8899fe | |||
| ca1041ca16 | |||
| 104640facd | |||
| af2194a8b3 | |||
| 46295fabde | |||
| 6c77443aee | |||
| 4bc71e2212 | |||
| 24ed6907c7 | |||
| cf47cf6c71 | |||
| 7ea579df68 | |||
| e26849283e | |||
| 07c48db808 | |||
| f2f4f45097 | |||
| cc4caf9449 | |||
| 4ae30eb155 | |||
| c1a302a595 | |||
| 56dc15f1d2 | |||
| b098a173fa | |||
| 3a41afb696 | |||
| 56ef0b4074 | |||
| 67fd748487 | |||
| f34c13827b | |||
| 6300df344c | |||
| 85c9c4cc6d | |||
| 35421714e6 | |||
| 8b6613aa93 | |||
| 4bd4e136dd | |||
| 7d2db031a9 | |||
| 10d979e545 | |||
| b0b4c68f15 | |||
| 87ebb6b679 | |||
| aa40d1f577 | |||
| cfcf08b373 | |||
| d080e71601 | |||
| 3001febbc2 | |||
| 7626dcf387 | |||
| 4984877ab7 | |||
| 072b5ef2d4 | |||
| 42dea83463 | |||
| ee3e768ada | |||
| c059b528fa | |||
| 783bc37c94 | |||
| 9f6b03db32 | |||
| 3b81bd20ea | |||
| 79ec20ad74 | |||
| 8f7ea51787 | |||
| 800033c6f8 | |||
| 7f48f49445 | |||
| c8404c366f | |||
| c63690c181 | |||
| 34f2669ab9 | |||
| afecfcafd0 | |||
| ab0188a8bc | |||
| 85b8ae8560 | |||
| 01f55aa0cb | |||
| b12f38e608 | |||
| bb7404a82d |
@@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+2
-1
@@ -2,4 +2,5 @@
|
|||||||
**/node_modules
|
**/node_modules
|
||||||
**/localdb.json
|
**/localdb.json
|
||||||
**/docs
|
**/docs
|
||||||
**/config.json
|
**/config.json
|
||||||
|
.vscode/settings.json
|
||||||
@@ -11,60 +11,117 @@ 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)
|
||||||
|
- 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
|
# Backends
|
||||||
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
|
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.
|
||||||
After these steps, the ProxmoxAAS Dashboard should be available and fully functional at `paas.<FQDN>` or `paas.<FQDN>/dashboard/`.
|
|
||||||
|
|
||||||
# Backends
|
## Interface
|
||||||
|
|
||||||
|
Each backend must implement the following methods:
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>openSession</td>
|
||||||
|
<td>opens a session to the backend by creating a session token</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>closeSession</td>
|
||||||
|
<td>closes a session to the backend</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
Additionally, backends dealing with user data may also need to implement:
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td>addUser</td>
|
||||||
|
<td>create a user</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>getUser</td>
|
||||||
|
<td>retrieve user data including membership</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>setUser</td>
|
||||||
|
<td>modify a user</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>delUser</td>
|
||||||
|
<td>delete a user</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>addGroup</td>
|
||||||
|
<td>create a group</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>getGroup</td>
|
||||||
|
<td>retrieve group data including members</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>setGroup</td>
|
||||||
|
<td>modify group data except membership</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>delGroup</td>
|
||||||
|
<td>delete group</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>addUserToGroup</td>
|
||||||
|
<td>add user to group as member</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>delUserFromGroup</td>
|
||||||
|
<td>remove user from group</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
Not all user backends will necessarily implement all the methods fully. For example, backends which do not store group data may not need to implement the group related methods.
|
||||||
|
|
||||||
|
Specific documentation can be found in `src/backends/backends.js`.
|
||||||
|
|
||||||
|
## Multiple Interfaces
|
||||||
|
|
||||||
|
Multiple backends can be specified using the config. During a backend operation involving users, each backend method will be called in the order specified in the config. If the operation is to retrieve user data, the responses will be merged favoring the last backend called.
|
||||||
@@ -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}}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
### Get version
|
||||||
|
GET {{baseUrl}}/version
|
||||||
+16
-97
@@ -1,9 +1,15 @@
|
|||||||
{
|
{
|
||||||
|
"application": {
|
||||||
|
"hostname": "paas.mydomain.example",
|
||||||
|
"domain": "mydomain.example",
|
||||||
|
"listenPort": 8081
|
||||||
|
},
|
||||||
"backends": {
|
"backends": {
|
||||||
"pve": {
|
"pve": {
|
||||||
"import": "pve.js",
|
"import": "pve.js",
|
||||||
"config": {
|
"config": {
|
||||||
"url": "https://pve.mydomain.example/api2/json",
|
"url": "https://pve.mydomain.example/api2/json",
|
||||||
|
"fabric": "http://localhost:8082",
|
||||||
"token": {
|
"token": {
|
||||||
"user": "proxmoxaas-api",
|
"user": "proxmoxaas-api",
|
||||||
"realm": "pam",
|
"realm": "pam",
|
||||||
@@ -16,35 +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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers": {
|
"handlers": {
|
||||||
"pve": "pve",
|
"instance": "pve",
|
||||||
"db": "localdb",
|
"users": ["access_manager"]
|
||||||
"auth": {
|
|
||||||
"pve": "pve"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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",
|
||||||
@@ -63,7 +58,7 @@
|
|||||||
"memory": {
|
"memory": {
|
||||||
"name": "RAM",
|
"name": "RAM",
|
||||||
"type": "numeric",
|
"type": "numeric",
|
||||||
"multiplier": 1048576,
|
"multiplier": 1,
|
||||||
"base": 1024,
|
"base": 1024,
|
||||||
"compact": true,
|
"compact": true,
|
||||||
"unit": "B",
|
"unit": "B",
|
||||||
@@ -72,7 +67,7 @@
|
|||||||
"swap": {
|
"swap": {
|
||||||
"name": "SWAP",
|
"name": "SWAP",
|
||||||
"type": "numeric",
|
"type": "numeric",
|
||||||
"multiplier": 1048576,
|
"multiplier": 1,
|
||||||
"base": 1024,
|
"base": 1024,
|
||||||
"compact": true,
|
"compact": true,
|
||||||
"unit": "B",
|
"unit": "B",
|
||||||
@@ -142,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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "proxmoxaas-api",
|
"name": "proxmoxaas-api",
|
||||||
"version": "0.0.1",
|
"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 ."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
}
|
||||||
+221
-67
@@ -2,7 +2,7 @@ import path from "path";
|
|||||||
import url from "url";
|
import url from "url";
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
const backends = {};
|
global.backends = {};
|
||||||
for (const name in global.config.backends) {
|
for (const name in global.config.backends) {
|
||||||
// get files and config
|
// get files and config
|
||||||
const target = global.config.backends[name].import;
|
const target = global.config.backends[name].import;
|
||||||
@@ -14,33 +14,50 @@ export default async () => {
|
|||||||
const importPath = `./${path.relative(thisPath, targetPath)}`;
|
const importPath = `./${path.relative(thisPath, targetPath)}`;
|
||||||
// import and add to list of imported handlers
|
// import and add to list of imported handlers
|
||||||
const Backend = (await import(importPath)).default;
|
const Backend = (await import(importPath)).default;
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
// assign backends to handlers by type
|
global.pve = global.backends[global.config.handlers.instance];
|
||||||
const handlers = global.config.handlers;
|
global.access = global.backends[global.config.handlers.users];
|
||||||
global.pve = backends[handlers.pve];
|
|
||||||
global.db = backends[handlers.db];
|
|
||||||
global.auth = handlers.auth;
|
|
||||||
Object.keys(global.auth).forEach((e) => {
|
|
||||||
global.auth[e] = backends[global.auth[e]];
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export class AtomicChange {
|
||||||
|
constructor (valid, delta, callback, status = { ok: true, status: 200, message: "" }) {
|
||||||
|
this.valid = valid;
|
||||||
|
this.delta = delta;
|
||||||
|
this.callback = callback;
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the change using the saved delta using the callback function
|
||||||
|
*/
|
||||||
|
async commit () {
|
||||||
|
const res = await this.callback(this.delta);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doNothingCallback (delta) {
|
||||||
|
return { ok: true, status: 200, message: "" };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for all backend types. Contains only two methods for opening and closing a session with the backend.
|
* 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.
|
* Users will recieve tokens from all backends when first authenticating and will delete tokens when logging out.
|
||||||
*/
|
*/
|
||||||
class BACKEND {
|
export class BACKEND {
|
||||||
/**
|
/**
|
||||||
* Opens a session with the backend and creates session tokens if needed
|
* Opens a session with the backend and creates session tokens if needed
|
||||||
* @param {{username: string, password: string}} credentials object containing username and password fields
|
* @param {{id: string, realm: string}} user object containing id and realm
|
||||||
* @returns {{ok: boolean, status: number, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value
|
* @param {string} password
|
||||||
|
* @returns {{ok: boolean, status: number, message: string, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value
|
||||||
*/
|
*/
|
||||||
openSession (credentials) {
|
async openSession (user, password) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200,
|
status: 200,
|
||||||
|
message: "",
|
||||||
cookies: []
|
cookies: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -50,7 +67,7 @@ class BACKEND {
|
|||||||
* @param {{name: string, value: string}[]} token list of session token objects with token name and value, may include irrelevant tokens for a specific backend
|
* @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
|
* @returns {boolean} true if session was closed successfully, false otherwise
|
||||||
*/
|
*/
|
||||||
closeSession (tokens) {
|
async closeSession (tokens) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
status: 200
|
status: 200
|
||||||
@@ -59,91 +76,228 @@ class BACKEND {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for backend types that store/interact with user & group data.
|
* 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 {
|
||||||
/**
|
/**
|
||||||
* Add user to backend
|
* Validate an add user operation with the following parameters.
|
||||||
|
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||||
* @param {{id: string, realm: string}} user
|
* @param {{id: string, realm: string}} user
|
||||||
* @param {Object} attributes user attributes
|
* @param {Object} attributes user attributes
|
||||||
* @param {Object} params authentication params, usually req.cookies
|
* @param {Object} params authentication params, usually req.cookies
|
||||||
|
* @returns {AtomicChange} atomic change object
|
||||||
*/
|
*/
|
||||||
addUser (user, attributes, params = null) {}
|
async addUser (user, attributes, params) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user from backend
|
* Get user from backend
|
||||||
* @param {{id: string, realm: string}} user
|
* @param {{id: string, realm: string}} user
|
||||||
* @param {Object} params authentication params, usually req.cookies
|
* @param {Object} params authentication params, usually req.cookies
|
||||||
|
* @returns {Object} containing user data from this backend, null if user does not exist
|
||||||
*/
|
*/
|
||||||
getUser (user, params = null) {}
|
async getUser (user, params) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modify user in backend
|
* Validate a set user operation with the following parameters.
|
||||||
|
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||||
* @param {{id: string, realm: string}} user
|
* @param {{id: string, realm: string}} user
|
||||||
* @param {Object} attributes new user attributes to modify
|
* @param {Object} attributes new user attributes to modify
|
||||||
* @param {Object} params authentication params, usually req.cookies
|
* @param {Object} params authentication params, usually req.cookies
|
||||||
|
* @returns {AtomicChange} atomic change object
|
||||||
*/
|
*/
|
||||||
setUser (user, attributes, params = null) {}
|
async setUser (user, attributes, params) {}
|
||||||
/**
|
|
||||||
* Delete user from backend
|
|
||||||
* @param {{id: string, realm: string}} user
|
|
||||||
* @param {Object} params authentication params, usually req.cookies
|
|
||||||
*/
|
|
||||||
deluser (user, params = null) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add group to backend
|
* Validate a delete user operation with the following parameters.
|
||||||
* @param {{id: string}} group
|
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||||
|
* @param {{id: string, realm: string}} user
|
||||||
|
* @param {Object} params authentication params, usually req.cookies
|
||||||
|
* @returns {AtomicChange} atomic change object
|
||||||
|
*/
|
||||||
|
async delUser (user, params) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate an add group operation with the following parameters.
|
||||||
|
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||||
|
* @param {{id: string, realm: string}} group
|
||||||
* @param {Object} attributes group attributes
|
* @param {Object} attributes group attributes
|
||||||
* @param {Object} params authentication params, usually req.cookies
|
* @param {Object} params authentication params, usually req.cookies
|
||||||
|
* @returns {AtomicChange} atomic change object
|
||||||
*/
|
*/
|
||||||
addGroup (group, attributes, params = null) {}
|
async addGroup (group, attributes, params) {}
|
||||||
/**
|
|
||||||
* Get group from backend
|
|
||||||
* @param {{id: string}} group
|
|
||||||
* @param {Object} params authentication params, usually req.cookies
|
|
||||||
*/
|
|
||||||
getGroup (group, params = null) {}
|
|
||||||
/**
|
|
||||||
* Modify group in backend
|
|
||||||
* @param {{id: string}} group
|
|
||||||
* @param {Object} attributes new group attributes to modify
|
|
||||||
* @param {Object} params authentication params, usually req.cookies
|
|
||||||
*/
|
|
||||||
setGroup (group, attributes, params = null) {}
|
|
||||||
/**
|
|
||||||
* Delete group from backend
|
|
||||||
* @param {{id: string}} group
|
|
||||||
* @param {Object} params authentication params, usually req.cookies
|
|
||||||
*/
|
|
||||||
delGroup (group, params = null) {}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add user to group
|
* Get group from backend
|
||||||
* @param {{id: string, realm: string}} user
|
* @param {{id: string, realm: string}} group
|
||||||
* @param {{id: string}} group
|
|
||||||
* @param {Object} params authentication params, usually req.cookies
|
* @param {Object} params authentication params, usually req.cookies
|
||||||
|
* @returns {Object} containing group data from this backend, null if user does not exist
|
||||||
*/
|
*/
|
||||||
addUserToGroup (user, group, params = null) {}
|
async getGroup (group, params) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove user from group
|
* Validate a set group operation with the following parameters.
|
||||||
* @param {{id: string, realm: string}} user
|
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||||
* @param {{id: string}} group
|
* @param {{id: string, realm: string}} group
|
||||||
|
* @param {Object} attributes group attributes
|
||||||
* @param {Object} params authentication params, usually req.cookies
|
* @param {Object} params authentication params, usually req.cookies
|
||||||
|
* @returns {AtomicChange} atomic change object
|
||||||
*/
|
*/
|
||||||
delUserFromGroup (user, group, params = null) {}
|
async setGroup (group, attributes, params) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a del group operation with the following parameters.
|
||||||
|
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||||
|
* @param {{id: string, realm: string}} group
|
||||||
|
* @param {Object} params authentication params, usually req.cookies
|
||||||
|
* @returns {AtomicChange} atomic change object
|
||||||
|
*/
|
||||||
|
async delGroup (group, attributes, params) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate an add user to group operation with the following parameters.
|
||||||
|
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||||
|
* @param {{id: string, realm: string}} user
|
||||||
|
* @param {{id: string, realm: string}} group
|
||||||
|
* @param {Object} params authentication params, usually req.cookies
|
||||||
|
* @returns {AtomicChange} atomic change object
|
||||||
|
*/
|
||||||
|
async addUserToGroup (user, group, params) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a remove user from group operation with the following parameters.
|
||||||
|
* Returns whether the change is valid and a delta object to be used in the operation.
|
||||||
|
* @param {{id: string, realm: string}} user
|
||||||
|
* @param {{id: string, realm: string}} group
|
||||||
|
* @param {Object} params authentication params, usually req.cookies
|
||||||
|
* @returns {AtomicChange} atomic change object
|
||||||
|
*/
|
||||||
|
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) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for proxmox api backends.
|
* Interface for proxmox api backends.
|
||||||
*/
|
*/
|
||||||
export class PVE_BACKEND extends BACKEND {}
|
export class PVE_BACKEND extends BACKEND {
|
||||||
|
/**
|
||||||
|
* Get and return node data.
|
||||||
|
* Returns the node data or null if the node does not exist.
|
||||||
|
* @param {string} node node id
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
|
async getNode (node) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for user database backends.
|
* Send a signal to synchronize a node after some change has been made.
|
||||||
*/
|
* * @param {string} node node id
|
||||||
export class DB_BACKEND extends USER_BACKEND {}
|
*/
|
||||||
|
async syncNode (node) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for user auth backends.
|
* Get and return instance data.
|
||||||
*/
|
* Returns the instance data or null if the instance does not exist.
|
||||||
export class AUTH_BACKEND extends USER_BACKEND {}
|
* @param {string} node node id
|
||||||
|
* @param {string} type instance type
|
||||||
|
* @param {string} vmid instance id
|
||||||
|
*/
|
||||||
|
async getInstance (node, type, instance) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a signal to synchronize an instance after some change has been made.
|
||||||
|
* @param {string} node node id
|
||||||
|
* @param {string} instance instance id
|
||||||
|
*/
|
||||||
|
async syncInstance (node, instance) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get meta data for a specific disk. Adds info that is not normally available in a instance's config.
|
||||||
|
* @param {string} node containing the query disk.
|
||||||
|
* @param {string} instance with query disk.
|
||||||
|
* @param {string} disk name of the query disk, ie. sata0.
|
||||||
|
* @returns {Objetc} k-v pairs of specific disk data, including storage and size of unused disks.
|
||||||
|
*/
|
||||||
|
async getDisk (node, instance, disk) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get meta data for a specific net. Adds info that is not normally available in a instance's config.
|
||||||
|
* @param {string} node containing the query net.
|
||||||
|
* @param {string} instance with query net.
|
||||||
|
* @param {string} netid id number of the query net, ie. 0 -> net0.
|
||||||
|
* @returns {Objetc} k-v pairs of specific net data, including rate and vlan.
|
||||||
|
*/
|
||||||
|
async getNet (node, instance, netid) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get meta data for a specific device. Adds info that is not normally available in a instance's config.
|
||||||
|
* @param {string} node containing the query device.
|
||||||
|
* @param {string} instance with query device.
|
||||||
|
* @param {string} deviceid id number of the query device, ie. 0 -> pci0.
|
||||||
|
* @returns {Objetc} k-v pairs of specific device data, including name and manfacturer.
|
||||||
|
*/
|
||||||
|
async getDevice (node, instance, deviceid) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get pool resource data including used, available, and maximum resources.
|
||||||
|
* @param {string} pool
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
async getPoolResources (user, cookies) {}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
import { readFileSync, writeFileSync } from "fs";
|
|
||||||
import { exit } from "process";
|
|
||||||
import { DB_BACKEND } 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 = null) {
|
|
||||||
const username = `${user.id}@${user.realm}`;
|
|
||||||
attributes = attributes || this.#defaultuser;
|
|
||||||
this.#data.users[username] = attributes;
|
|
||||||
this.#save();
|
|
||||||
}
|
|
||||||
|
|
||||||
getUser (user, params = null) {
|
|
||||||
const username = `${user.id}@${user.realm}`;
|
|
||||||
if (this.#data.users[username]) {
|
|
||||||
return this.#data.users[username];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setUser (user, attributes, params = null) {
|
|
||||||
const username = `${user.id}@${user.realm}`;
|
|
||||||
if (this.#data.users[username]) {
|
|
||||||
this.#data.users[username] = attributes;
|
|
||||||
this.#save();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delUser (user, params = null) {
|
|
||||||
const username = `${user.id}@${user.realm}`;
|
|
||||||
if (this.#data.users[username]) {
|
|
||||||
delete this.#data.users[username];
|
|
||||||
this.#save();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// group methods not implemented because db backend does not store groups
|
|
||||||
addGroup (group, atrributes, params = null) {}
|
|
||||||
getGroup (group, params = null) {}
|
|
||||||
setGroup (group, attributes, params = null) {}
|
|
||||||
delGroup (group, params = null) {}
|
|
||||||
|
|
||||||
// assume that adding to group also adds to group's pool
|
|
||||||
addUserToGroup (user, group, params = null) {
|
|
||||||
const username = `${user.id}@${user.realm}`;
|
|
||||||
if (this.#data.users[username]) {
|
|
||||||
this.#data.users[username].cluster.pools[group.id] = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// assume that adding to group also adds to group's pool
|
|
||||||
delUserFromGroup (user, group, params = null) {
|
|
||||||
const username = `${user.id}@${user.realm}`;
|
|
||||||
if (this.#data.users[username] && this.#data.users[username].cluster.pools[group.id]) {
|
|
||||||
delete this.#data.users[username].cluster.pools[group.id];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import { AUTH_BACKEND } from "./backends.js";
|
|
||||||
import * as setCookie from "set-cookie-parser";
|
|
||||||
|
|
||||||
export default class PAASLDAP extends AUTH_BACKEND {
|
|
||||||
#url = null;
|
|
||||||
|
|
||||||
constructor (config) {
|
|
||||||
super();
|
|
||||||
this.#url = 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 {*} body body parameters and data to be sent. Optional.
|
|
||||||
* @returns {Object} HTTP response object or HTTP error 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) {
|
|
||||||
error.ok = false;
|
|
||||||
error.status = 500;
|
|
||||||
error.data = {
|
|
||||||
error: error.code
|
|
||||||
};
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async openSession (credentials) {
|
|
||||||
const userRealm = credentials.username.split("@").at(-1);
|
|
||||||
const uid = credentials.username.replace(`@${userRealm}`, "");
|
|
||||||
const content = { uid, password: credentials.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,
|
|
||||||
cookies
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async addUser (user, attributes, params = null) {
|
|
||||||
return await this.#request(`/users/${user.id}`, "POST", params, attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUser (user, params = null) {
|
|
||||||
return await this.#request(`/users/${user.id}`, "GET", params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setUser (user, attributes, params = null) {
|
|
||||||
return await this.#request(`/users/${user.id}`, "POST", params, attributes);
|
|
||||||
}
|
|
||||||
|
|
||||||
async delUser (user, params = null) {
|
|
||||||
return await this.#request(`/users/${user.id}`, "DELETE", params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async addGroup (group, attributes, params = null) {
|
|
||||||
return await this.#request(`/groups/${group.id}`, "POST", params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGroup (group, params = null) {
|
|
||||||
return await this.#request(`/groups/${group.id}`, "GET", params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async setGroup (group, attributes, params = null) {
|
|
||||||
// not implemented, LDAP groups do not have any attributes to change
|
|
||||||
}
|
|
||||||
|
|
||||||
async delGroup (group, params = null) {
|
|
||||||
return await this.#request(`/groups/${group.id}`, "DELETE", params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async addUserToGroup (user, group, params = null) {
|
|
||||||
return await this.#request(`/groups/${group.id}/members/${user.id}`, "POST", params);
|
|
||||||
}
|
|
||||||
|
|
||||||
async delUserFromGroup (user, group, params = null) {
|
|
||||||
return await this.#request(`/groups/${group.id}/members/${user.id}`, "DELETE", params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+150
-79
@@ -5,21 +5,29 @@ export default class PVE extends PVE_BACKEND {
|
|||||||
#pveAPIURL = null;
|
#pveAPIURL = null;
|
||||||
#pveAPIToken = null;
|
#pveAPIToken = null;
|
||||||
#pveRoot = null;
|
#pveRoot = null;
|
||||||
|
#paasFabric = null;
|
||||||
|
|
||||||
constructor (config) {
|
constructor (config) {
|
||||||
super();
|
super();
|
||||||
this.#pveAPIURL = config.url;
|
this.#pveAPIURL = config.url;
|
||||||
this.#pveAPIToken = config.token;
|
this.#pveAPIToken = config.token;
|
||||||
this.#pveRoot = config.root;
|
this.#pveRoot = config.root;
|
||||||
|
this.#paasFabric = config.fabric;
|
||||||
}
|
}
|
||||||
|
|
||||||
async openSession (credentials) {
|
async openSession (user, password) {
|
||||||
|
const credentials = { username: `${user.id}@${user.realm}`, password };
|
||||||
const response = await global.pve.requestPVE("/access/ticket", "POST", null, credentials);
|
const response = await global.pve.requestPVE("/access/ticket", "POST", null, credentials);
|
||||||
if (!(response.status === 200)) {
|
if (!(response.status === 200)) {
|
||||||
return response;
|
return {
|
||||||
|
ok: false,
|
||||||
|
status: response.status,
|
||||||
|
message: "Authorization failed",
|
||||||
|
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,
|
||||||
@@ -58,20 +66,68 @@ 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) { // upgraded request as root
|
||||||
|
const rootauth = await this.requestPVE("/access/ticket", "POST", null, this.#pveRoot);
|
||||||
|
if (!(rootauth.status === 200)) {
|
||||||
|
return rootauth.response;
|
||||||
|
}
|
||||||
|
const rootcookie = rootauth.data.ticket;
|
||||||
|
const rootcsrf = rootauth.data.CSRFPreventionToken;
|
||||||
|
content.headers.CSRFPreventionToken = rootcsrf;
|
||||||
|
content.headers.Cookie = `PVEAuthCookie=${rootcookie}; CSRFPreventionToken=${rootcsrf}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
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) {
|
||||||
|
console.log(`pve: error ocuured in pve.requestPVE: ${method} ${path} resulted in ${error}`);
|
||||||
|
const result = error.response;
|
||||||
|
result.ok = result.status === 200;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send HTTP request to PAAS Fabric
|
||||||
|
* @param {string} path HTTP path, prepended with the proxmox API base url.
|
||||||
|
* @param {string} method HTTP method.
|
||||||
|
* @param {Object} auth authentication method. Set auth.cookies with user cookies or auth.token with PVE API Token. Optional.
|
||||||
|
* @param {string} body body parameters and data to be sent. Optional.
|
||||||
|
* @returns {Object} HTTP response object or HTTP error object.
|
||||||
|
*/
|
||||||
|
async requestFabric (path, method, body = null) {
|
||||||
|
const url = `${this.#paasFabric}${path}`;
|
||||||
|
const content = {
|
||||||
|
method,
|
||||||
|
mode: "cors",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
data: body
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await axios.request(url, content);
|
return await axios.request(url, content);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
return error.response;
|
console.log(`pve: error ocuured in pve.requestFabric: ${method} ${path} resulted in ${error}`);
|
||||||
|
return error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,24 +141,28 @@ export default class PVE extends PVE_BACKEND {
|
|||||||
*/
|
*/
|
||||||
async handleResponse (node, result, res) {
|
async handleResponse (node, result, res) {
|
||||||
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
|
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
|
||||||
if (result.data.data && typeof (result.data.data) === "string" && result.data.data.startsWith("UPID:")) {
|
if (result.status !== 200) {
|
||||||
const upid = result.data.data;
|
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 });
|
let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
|
||||||
while (taskStatus.data.data.status !== "stopped") {
|
while (taskStatus.data.status !== "stopped") {
|
||||||
await waitFor(1000);
|
await waitFor(100);
|
||||||
taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
|
taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
|
||||||
}
|
}
|
||||||
if (taskStatus.data.data.exitstatus === "OK") {
|
if (taskStatus.data.exitstatus === "OK") {
|
||||||
const result = taskStatus.data.data;
|
const result = taskStatus.data;
|
||||||
const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true });
|
const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true });
|
||||||
result.log = taskLog.data.data;
|
result.log = taskLog.data;
|
||||||
res.status(200).send(result);
|
res.status(200).send(result);
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
const result = taskStatus.data.data;
|
const result = taskStatus.data;
|
||||||
const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true });
|
const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true });
|
||||||
result.log = taskLog.data.data;
|
result.log = taskLog.data;
|
||||||
res.status(500).send(result);
|
res.status(500).send(result);
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
@@ -113,83 +173,94 @@ export default class PVE extends PVE_BACKEND {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async getNode (node) {
|
||||||
* Get meta data for a specific disk. Adds info that is not normally available in a instance's config.
|
const res = await this.requestFabric(`/nodes/${node}`, "GET");
|
||||||
* @param {string} node containing the query disk.
|
if (res.status !== 200) {
|
||||||
* @param {string} config of instance with query disk.
|
console.error(res);
|
||||||
* @param {string} disk name of the query disk, ie. sata0.
|
return null;
|
||||||
* @returns {Objetc} k-v pairs of specific disk data, including storage and size of unused disks.
|
|
||||||
*/
|
|
||||||
async getDiskInfo (node, config, disk) {
|
|
||||||
try {
|
|
||||||
const storageID = config[disk].split(":")[0];
|
|
||||||
const volID = config[disk].split(",")[0];
|
|
||||||
const volInfo = await this.requestPVE(`/nodes/${node}/storage/${storageID}/content/${volID}`, "GET", { token: true });
|
|
||||||
volInfo.data.data.storage = storageID;
|
|
||||||
return volInfo.data.data;
|
|
||||||
}
|
}
|
||||||
catch {
|
|
||||||
|
return res.data.node;
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncNode (node) {
|
||||||
|
this.requestFabric(`/nodes/${node}/sync`, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getInstance (node, instance) {
|
||||||
|
const res = await this.requestFabric(`/nodes/${node}/instances/${instance}`, "GET");
|
||||||
|
if (res.status !== 200) {
|
||||||
|
console.error(res);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncInstance (node, vmid) {
|
||||||
|
this.requestFabric(`/nodes/${node}/instances/${vmid}/sync`, "POST");
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDisk (node, instance, disk) {
|
||||||
|
const config = await this.getInstance(node, instance);
|
||||||
|
if (config != null && config.volumes[disk] != null) {
|
||||||
|
return config.volumes[disk];
|
||||||
|
}
|
||||||
|
else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async getNet (node, instance, netid) {
|
||||||
* Get meta data for a specific pci device. Adds info that is not normally available in a instance's config.
|
const config = await this.getInstance(node, instance);
|
||||||
* @param {string} node containing the query device.
|
if (config != null && config.nets[netid] != null) {
|
||||||
* @param {string} qid pci bus id number of the query device, ie. 89ab:cd:ef.0.
|
return config.nets[netid];
|
||||||
* @returns {Object} k-v pairs of specific device data, including device name and manufacturer.
|
|
||||||
*/
|
|
||||||
async getDeviceInfo (node, qid) {
|
|
||||||
try {
|
|
||||||
const result = (await this.requestPVE(`/nodes/${node}/hardware/pci`, "GET", { token: true })).data.data;
|
|
||||||
const deviceData = [];
|
|
||||||
result.forEach((element) => {
|
|
||||||
if (element.id.startsWith(qid)) {
|
|
||||||
deviceData.push(element);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
deviceData.sort((a, b) => {
|
|
||||||
return a.id < b.id;
|
|
||||||
});
|
|
||||||
const device = deviceData[0];
|
|
||||||
device.subfn = structuredClone(deviceData.slice(1));
|
|
||||||
return device;
|
|
||||||
}
|
}
|
||||||
catch {
|
else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async getDevice (node, instance, deviceid) {
|
||||||
* Get available devices on specific node.
|
const config = await this.getInstance(node, instance);
|
||||||
* @param {string} node to get devices from.
|
if (config != null && config.devices[deviceid] != null) {
|
||||||
* @returns {Array.<Object>} array of k-v pairs of specific device data, including device name and manufacturer, which are available on the specified node.
|
return config.devices[deviceid];
|
||||||
*/
|
|
||||||
async getNodeAvailDevices (node) {
|
|
||||||
// get node pci devices
|
|
||||||
let nodeAvailPci = this.requestPVE(`/nodes/${node}/hardware/pci`, "GET", { token: true });
|
|
||||||
// for each node container, get its config and remove devices which are already used
|
|
||||||
const vms = (await this.requestPVE(`/nodes/${node}/qemu`, "GET", { token: true })).data.data;
|
|
||||||
|
|
||||||
const promises = [];
|
|
||||||
for (const vm of vms) {
|
|
||||||
promises.push(this.requestPVE(`/nodes/${node}/qemu/${vm.vmid}/config`, "GET", { token: true }));
|
|
||||||
}
|
}
|
||||||
const configs = await Promise.all(promises);
|
else {
|
||||||
configs.forEach((e, i) => {
|
return null;
|
||||||
configs[i] = e.data.data;
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
nodeAvailPci = (await nodeAvailPci).data.data;
|
async getPoolResources (cookies, pool) {
|
||||||
|
// get pool resources
|
||||||
|
const res = await this.requestPVE(`/pools/?poolid=${pool}`, "GET", { cookies });
|
||||||
|
if (res.status !== 200) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = res.data;
|
||||||
|
if (data.length != 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const poolPVE = data[0];
|
||||||
|
if (poolPVE.poolid != pool) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
for (const config of configs) {
|
const poolPVEResources = poolPVE.members;
|
||||||
Object.keys(config).forEach((key) => {
|
const resources = {};
|
||||||
if (key.startsWith("hostpci")) {
|
|
||||||
const deviceID = config[key].split(",")[0];
|
// for each resource, add to the object
|
||||||
nodeAvailPci = nodeAvailPci.filter(element => !element.id.includes(deviceID));
|
for (const resource of poolPVEResources) {
|
||||||
|
// only add type if it is vm or ct (ie has vmid)
|
||||||
|
if (resource.vmid) {
|
||||||
|
const instance = await this.getInstance(resource.node, resource.vmid);
|
||||||
|
if (instance) {
|
||||||
|
instance.node = resource.node;
|
||||||
|
resources[resource.vmid] = instance;
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
return nodeAvailPci;
|
|
||||||
|
return resources;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ export const router = Router({ mergeParams: true }); ;
|
|||||||
|
|
||||||
const checkAuth = global.utils.checkAuth;
|
const checkAuth = global.utils.checkAuth;
|
||||||
|
|
||||||
|
global.utils.recursiveImportRoutes(router, "", "access", import.meta.url);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET - check authentication
|
* GET - check authentication
|
||||||
* responses:
|
* responses:
|
||||||
@@ -23,12 +25,12 @@ router.get("/", async (req, res) => {
|
|||||||
class CookieFetcher {
|
class CookieFetcher {
|
||||||
#fetchedBackends = [];
|
#fetchedBackends = [];
|
||||||
#cookies = [];
|
#cookies = [];
|
||||||
async fetchBackends (backends, credentials) {
|
async fetchBackends (backends, user, password) {
|
||||||
for (const backend of backends) {
|
for (const backend of backends) {
|
||||||
if (this.#fetchedBackends.indexOf(backend) === -1) {
|
if (this.#fetchedBackends.indexOf(backend) === -1) {
|
||||||
const response = await backend.openSession(credentials);
|
const response = await global.backends[backend].openSession(user, password);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return false;
|
return response.message;
|
||||||
}
|
}
|
||||||
this.#cookies = this.#cookies.concat(response.cookies);
|
this.#cookies = this.#cookies.concat(response.cookies);
|
||||||
this.#fetchedBackends.push(backend);
|
this.#fetchedBackends.push(backend);
|
||||||
@@ -37,7 +39,7 @@ class CookieFetcher {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportCookies () {
|
exportCookies () {
|
||||||
@@ -59,25 +61,37 @@ router.post("/ticket", async (req, res) => {
|
|||||||
username: req.body.username,
|
username: req.body.username,
|
||||||
password: req.body.password
|
password: req.body.password
|
||||||
};
|
};
|
||||||
const domain = global.config.application.domain;
|
|
||||||
const userRealm = params.username.split("@").at(-1);
|
// get user and user backends from config
|
||||||
const backends = [global.pve, global.db];
|
const userObj = global.utils.getUserObjFromUsername(params.username);
|
||||||
if (userRealm in global.auth) {
|
const backends = [global.config.handlers.users, global.config.handlers.instance];
|
||||||
backends.push(global.auth[userRealm]);
|
|
||||||
}
|
// fetch cookies using cookie fetcher
|
||||||
const cm = new CookieFetcher();
|
const cm = new CookieFetcher();
|
||||||
const success = await cm.fetchBackends(backends, params);
|
const error = await cm.fetchBackends(backends, userObj, params.password);
|
||||||
if (!success) {
|
if (error) {
|
||||||
res.status(401).send({ auth: false });
|
res.status(401).send({ auth: false, error });
|
||||||
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;
|
||||||
for (const cookie of cookies) {
|
for (const cookie of cookies) {
|
||||||
const expiresDate = new Date(Date.now() + cookie.expiresMSFromNow);
|
const expiresDate = new Date(Date.now() + cookie.expiresMSFromNow);
|
||||||
res.cookie(cookie.name, cookie.value, { domain, path: "/", httpOnly: true, secure: true, expires: expiresDate });
|
res.cookie(cookie.name, cookie.value, { domain, path: "/", httpOnly: true, secure: true, expires: expiresDate, sameSite: "none" });
|
||||||
|
if (cookie.expiresMSFromNow < minimumExpires) {
|
||||||
|
minimumExpires = cookie.expiresMSFromNow;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
res.cookie("username", params.username, { domain, path: "/", secure: true });
|
|
||||||
res.cookie("auth", 1, { domain, path: "/", secure: true });
|
// set username and auth cookie with the minimum cookie length
|
||||||
|
const expiresDate = new Date(Date.now() + minimumExpires);
|
||||||
|
res.cookie("username", params.username, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" });
|
||||||
|
res.cookie("auth", 1, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" });
|
||||||
res.status(200).send({ auth: true });
|
res.status(200).send({ auth: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -87,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 });
|
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.db.closeSession(req.cookies);
|
await global.access.closeSession(req.cookies);
|
||||||
res.status(200).send({ auth: false });
|
res.status(200).send({ auth: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,24 +133,16 @@ router.post("/password", async (req, res) => {
|
|||||||
password: req.body.password
|
password: req.body.password
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRealm = params.username.split("@").at(-1);
|
// check auth
|
||||||
const authHandlers = global.config.handlers.auth;
|
const auth = await checkAuth(req.cookies, res);
|
||||||
const userID = params.username.replace(`@${userRealm}`, "");
|
if (!auth) {
|
||||||
const userObj = { id: userID, realm: userRealm };
|
return;
|
||||||
if (userRealm in authHandlers) {
|
|
||||||
const handler = authHandlers[userRealm];
|
|
||||||
const newAttributes = {
|
|
||||||
userpassword: params.password
|
|
||||||
};
|
|
||||||
const response = await handler.setUser(userObj, newAttributes, req.cookies);
|
|
||||||
if (response.ok) {
|
|
||||||
res.status(response.status).send(response.data);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(response.status).send({ error: response.data.error });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
res.status(501).send({ error: `Auth type ${userRealm} not implemented yet.` });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userObj = global.utils.getUserObjFromUsername(params.username);
|
||||||
|
const newAttributes = {
|
||||||
|
userpassword: params.password
|
||||||
|
};
|
||||||
|
const response = await global.access.setUser(userObj, newAttributes, req.cookies);
|
||||||
|
res.status(response.status).send(response);
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
export const router = Router({ mergeParams: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET - get specific group
|
||||||
|
* request:
|
||||||
|
* - groupname: name of group to get
|
||||||
|
* responses:
|
||||||
|
* - 200: {auth: true, group: Object}
|
||||||
|
* - 401: {auth: false}
|
||||||
|
*/
|
||||||
|
router.get("/:groupname", async (req, res) => {
|
||||||
|
const params = {
|
||||||
|
groupname: req.params.groupname
|
||||||
|
};
|
||||||
|
// check auth
|
||||||
|
const auth = await global.utils.checkAuth(req.cookies, res);
|
||||||
|
if (!auth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 });
|
||||||
|
});
|
||||||
@@ -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 });
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { Router } from "express";
|
||||||
|
export const router = Router({ mergeParams: true });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET - get specific user
|
||||||
|
* request:
|
||||||
|
* - username: username (id@realm) of user to get
|
||||||
|
* responses:
|
||||||
|
* - 200: {auth: true, user: Object}
|
||||||
|
* - 401: {auth: false}
|
||||||
|
*/
|
||||||
|
router.get("/:username", async (req, res) => {
|
||||||
|
const params = {
|
||||||
|
username: req.params.username
|
||||||
|
};
|
||||||
|
// check auth
|
||||||
|
const auth = await global.utils.checkAuth(req.cookies, res);
|
||||||
|
if (!auth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to parse user from username
|
||||||
|
const userObj = global.utils.getUserObjFromUsername(params.username);
|
||||||
|
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 });
|
||||||
|
});
|
||||||
+96
-72
@@ -1,11 +1,6 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
export const router = Router({ mergeParams: true });
|
export const router = Router({ mergeParams: true });
|
||||||
|
|
||||||
const db = global.db;
|
|
||||||
const checkAuth = global.utils.checkAuth;
|
|
||||||
const 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+";
|
||||||
@@ -15,44 +10,60 @@ 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 available pcie devices given node and user
|
* GET - get all available cluster nodes
|
||||||
* request:
|
* uses existing user permissions without elevation
|
||||||
* - node: string - vm host node id
|
* returns only node IDs
|
||||||
* responses:
|
* responses:
|
||||||
* - 200: PVE PCI Device Object
|
* - 200: List of nodes
|
||||||
* - 401: {auth: false}
|
* - PVE error
|
||||||
* - 401: {auth: false, path: string}
|
|
||||||
* - 500: {error: string}
|
|
||||||
*/
|
*/
|
||||||
router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
|
router.get("/nodes", async (req, res) => {
|
||||||
const params = {
|
|
||||||
node: req.params.node
|
|
||||||
};
|
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
|
||||||
|
|
||||||
// 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 userNodes = db.getUser(userObj).cluster.nodes;
|
|
||||||
if (userNodes[params.node] !== true) {
|
// get all nodes
|
||||||
res.status(401).send({ auth: false, path: params.node });
|
const allNodes = await global.pve.requestPVE("/nodes", "GET", { cookies: req.cookies });
|
||||||
|
if (allNodes.status === 200) {
|
||||||
|
const allNodesIDs = Array.from(allNodes.data, (x) => x.node);
|
||||||
|
res.status(allNodes.status).send({ nodes: allNodesIDs });
|
||||||
res.end();
|
res.end();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
res.status(allNodes.status).send({ error: allNodes.statusText });
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET - get basic resources for vm using the fabric format
|
||||||
|
* request:
|
||||||
|
* - node: string - vm host node id
|
||||||
|
* - type: string - vm type (lxc, qemu)
|
||||||
|
* - vmid: number - vm id number
|
||||||
|
* response:
|
||||||
|
* - 200: Fabric instance config
|
||||||
|
* - 401: {auth: false}
|
||||||
|
*/
|
||||||
|
router.get(`${basePath}`, async (req, res) => {
|
||||||
|
const params = {
|
||||||
|
node: req.params.node,
|
||||||
|
type: req.params.type,
|
||||||
|
vmid: req.params.vmid
|
||||||
|
};
|
||||||
|
|
||||||
|
// check auth for specific instance
|
||||||
|
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||||
|
const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
|
||||||
|
if (!auth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// get remaining user resources
|
|
||||||
const userAvailPci = (await getUserResources(req, userObj)).pci.nodes[params.node];
|
// get current config
|
||||||
// get node avail devices
|
const instance = await global.pve.getInstance(params.node, params.vmid);
|
||||||
let nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
|
res.status(200).send(instance);
|
||||||
nodeAvailPci = nodeAvailPci.filter(nodeAvail => userAvailPci.some((userAvail) => {
|
|
||||||
return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0;
|
|
||||||
}));
|
|
||||||
res.status(200).send(nodeAvailPci);
|
|
||||||
res.end();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,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) => {
|
||||||
@@ -83,34 +94,34 @@ router.post(`${basePath}/resources`, async (req, res) => {
|
|||||||
boot: req.body.boot
|
boot: req.body.boot
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
|
||||||
|
|
||||||
// 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 current config
|
// get current config
|
||||||
const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true });
|
const instance = await global.pve.getInstance(params.node, params.vmid);
|
||||||
const request = {
|
const request = {
|
||||||
cores: Number(params.cores) - Number(currentConfig.data.data.cores),
|
cores: Number(params.cores) - Number(instance.cores),
|
||||||
memory: Number(params.memory) - Number(currentConfig.data.data.memory)
|
memory: Number(params.memory) - Number(instance.memory)
|
||||||
};
|
};
|
||||||
if (params.type === "lxc") {
|
if (params.type === "lxc") {
|
||||||
request.swap = Number(params.swap) - Number(currentConfig.data.data.swap);
|
request.swap = Number(params.swap) - Number(instance.swap);
|
||||||
}
|
}
|
||||||
else if (params.type === "qemu") {
|
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") {
|
||||||
@@ -121,9 +132,11 @@ 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);
|
||||||
|
await global.pve.syncInstance(params.node, params.vmid);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,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) => {
|
||||||
@@ -165,50 +178,52 @@ router.post(`${basePath}/create`, async (req, res) => {
|
|||||||
rootfssize: req.body.rootfssize
|
rootfssize: req.body.rootfssize
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
|
||||||
|
|
||||||
// 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 db.getUser(userObj);
|
// 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;
|
||||||
}
|
}
|
||||||
@@ -217,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,
|
||||||
@@ -230,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";
|
||||||
@@ -244,9 +263,11 @@ 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);
|
||||||
|
await global.pve.syncNode(params.node);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -266,13 +287,16 @@ 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);
|
||||||
|
await global.pve.syncNode(params.node);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
+88
-53
@@ -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,30 +22,37 @@ 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;
|
||||||
}
|
}
|
||||||
// get current config
|
|
||||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
|
||||||
// disk must exist
|
// disk must exist
|
||||||
if (!config[params.disk]) {
|
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
|
||||||
|
if (!disk) {
|
||||||
res.status(500).send({ error: `Disk ${params.disk} does not exist.` });
|
res.status(500).send({ error: `Disk ${params.disk} does not exist.` });
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,37 +76,42 @@ 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;
|
||||||
}
|
}
|
||||||
// get current config
|
|
||||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
|
||||||
// disk must exist
|
// disk must exist
|
||||||
if (!config[`unused${params.source}`]) {
|
const disk = await global.pve.getDisk(params.node, params.vmid, `unused${params.source}`);
|
||||||
res.status(403).send({ error: `Requested disk unused${params.source} does not exist.` });
|
if (!disk) {
|
||||||
|
res.status(500).send({ error: `Requested disk unused${params.source} does not exist.` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// target disk must be allowed according to source disk's storage options
|
// target disk must be allowed according to source disk's storage options
|
||||||
const diskConfig = await global.pve.getDiskInfo(params.node, config, `unused${params.source}`); // get target disk
|
|
||||||
const resourceConfig = global.config.resources;
|
const resourceConfig = global.config.resources;
|
||||||
if (!resourceConfig[diskConfig.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) {
|
if (!resourceConfig[disk.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) {
|
||||||
res.status(500).send({ error: `Requested target ${params.disk} is not in allowed list [${resourceConfig[diskConfig.storage].disks}].` });
|
res.status(500).send({ error: `Requested target ${params.disk} is not in allowed list [${resourceConfig[disk.storage].disks}].` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup action using source disk info from vm config
|
// setup action using source disk info from vm config
|
||||||
const action = {};
|
const action = {};
|
||||||
action[params.disk] = config[`unused${params.source}`];
|
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
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,39 +139,48 @@ router.post("/:disk/resize", async (req, res) => {
|
|||||||
size: req.body.size
|
size: req.body.size
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
// attempt to parse user from username
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||||
const userObj = { id: userID, realm: userRealm };
|
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 current config
|
|
||||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
// get instance config for pool membership
|
||||||
|
const instance = await global.pve.getInstance(params.node, params.vmid);
|
||||||
|
|
||||||
// check disk existence
|
// check disk existence
|
||||||
const diskConfig = await global.pve.getDiskInfo(params.node, config, params.disk); // get target disk
|
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
|
||||||
if (!diskConfig) { // exit if disk does not exist
|
if (!disk) { // exit if disk does not exist
|
||||||
res.status(500).send({ error: `requested disk ${params.disk} does not exist.` });
|
res.status(500).send({ error: `requested disk ${params.disk} does not exist.` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup request
|
// setup request
|
||||||
const storage = diskConfig.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);
|
||||||
await global.pve.handleResponse(params.node, result, res);
|
await global.pve.handleResponse(params.node, result, res);
|
||||||
|
await global.pve.syncInstance(params.node, params.vmid);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -192,34 +210,37 @@ router.post("/:disk/move", async (req, res) => {
|
|||||||
delete: req.body.delete
|
delete: req.body.delete
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
// attempt to parse user from username
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||||
const userObj = { id: userID, realm: userRealm };
|
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 current config
|
// get instance config for pool membership
|
||||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
const instance = await global.pve.getInstance(params.node, params.vmid);
|
||||||
// check disk existence
|
// check disk existence
|
||||||
const diskConfig = await global.pve.getDiskInfo(params.node, config, params.disk); // get target disk
|
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
|
||||||
if (!diskConfig) { // exit if disk does not exist
|
if (!disk) { // exit if disk does not exist
|
||||||
res.status(500).send({ error: `requested disk ${params.disk} does not exist.` });
|
res.status(500).send({ error: `requested disk ${params.disk} does not exist.` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// setup request
|
// setup request
|
||||||
const size = parseInt(diskConfig.size); // get source disk size
|
const size = parseInt(disk.size); // get source disk size
|
||||||
const dstStorage = params.storage; // get destination storage
|
const dstStorage = params.storage; // get destination storage
|
||||||
const request = {};
|
const request = {};
|
||||||
if (!params.delete) { // if not delete, then request storage, otherwise it is net 0
|
if (!params.delete) { // if not delete, then request storage, otherwise it is net 0
|
||||||
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;
|
||||||
@@ -236,6 +257,7 @@ router.post("/:disk/move", async (req, res) => {
|
|||||||
// commit action
|
// commit action
|
||||||
const result = await global.pve.requestPVE(`${vmpath}/${route}`, "POST", { token: true }, action);
|
const result = await global.pve.requestPVE(`${vmpath}/${route}`, "POST", { token: true }, action);
|
||||||
await global.pve.handleResponse(params.node, result, res);
|
await global.pve.handleResponse(params.node, result, res);
|
||||||
|
await global.pve.syncInstance(params.node, params.vmid);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -261,15 +283,14 @@ 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;
|
||||||
}
|
}
|
||||||
// get current config
|
|
||||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
|
||||||
// disk must exist
|
// disk must exist
|
||||||
if (!config[params.disk]) {
|
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
|
||||||
res.status(403).send({ error: `Requested disk unused${params.source} does not exist.` });
|
if (!disk) {
|
||||||
|
res.status(500).send({ error: `Disk ${params.disk} does not exist.` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -285,6 +306,7 @@ router.delete("/:disk/delete", async (req, res) => {
|
|||||||
// commit action
|
// commit action
|
||||||
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
|
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
|
||||||
await global.pve.handleResponse(params.node, result, res);
|
await global.pve.handleResponse(params.node, result, res);
|
||||||
|
await global.pve.syncInstance(params.node, params.vmid);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -315,31 +337,41 @@ router.post("/:disk/create", async (req, res) => {
|
|||||||
iso: req.body.iso
|
iso: req.body.iso
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
console.log(req.cookies)
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
// attempt to parse user from 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 current config
|
|
||||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
// get instance config for pool membership
|
||||||
|
const instance = await global.pve.getInstance(params.node, params.vmid);
|
||||||
|
|
||||||
// disk must not exist
|
// disk must not exist
|
||||||
if (config[params.disk]) {
|
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
|
||||||
res.status(403).send({ error: `Requested disk ${params.disk} already exists.` });
|
if (disk) {
|
||||||
|
res.status(500).send({ error: `Disk ${params.disk} does already exists.` });
|
||||||
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;
|
||||||
@@ -352,19 +384,22 @@ 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);
|
||||||
|
await global.pve.syncInstance(params.node, params.vmid);
|
||||||
});
|
});
|
||||||
|
|||||||
+51
-38
@@ -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 db = global.db;
|
|
||||||
const checkAuth = global.utils.checkAuth;
|
|
||||||
const approveResources = global.utils.approveResources;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* POST - create new virtual network interface
|
* POST - create new virtual network interface
|
||||||
* request:
|
* request:
|
||||||
@@ -27,26 +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: 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
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
|
||||||
|
|
||||||
// check auth for specific instance
|
// check auth for specific instance
|
||||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// get current config
|
|
||||||
const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true });
|
// 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
|
||||||
if (currentConfig.data.data[`net${params.netid}`]) {
|
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
|
||||||
res.status(500).send({ error: `Network interface net${params.netid} already exists.` });
|
if (net) {
|
||||||
|
res.status(500).send({ error: `Network interface ${params.netid} already exists.` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -55,28 +50,36 @@ 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
|
||||||
if (!await approveResources(req, userObj, request, params.node)) {
|
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||||
|
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 = db.getUser(userObj).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);
|
||||||
|
await global.pve.syncInstance(params.node, params.vmid);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -100,46 +103,51 @@ router.post("/:netid/modify", async (req, res) => {
|
|||||||
node: req.params.node,
|
node: req.params.node,
|
||||||
type: req.params.type,
|
type: req.params.type,
|
||||||
vmid: req.params.vmid,
|
vmid: req.params.vmid,
|
||||||
netid: req.params.netid.replace("net", ""),
|
netid: req.params.netid,
|
||||||
rate: req.body.rate
|
rate: req.body.rate
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
|
||||||
|
|
||||||
// check auth for specific instance
|
// check auth for specific instance
|
||||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
|
||||||
if (!auth) {
|
if (!auth) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// get current config
|
|
||||||
const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true });
|
// 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
|
||||||
if (!currentConfig.data.data[`net${params.netid}`]) {
|
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
|
||||||
|
if (!net) {
|
||||||
res.status(500).send({ error: `Network interface net${params.netid} does not exist.` });
|
res.status(500).send({ error: `Network interface net${params.netid} does not exist.` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const currentNetworkConfig = currentConfig.data.data[`net${params.netid}`];
|
|
||||||
const currentNetworkRate = currentNetworkConfig.split("rate=")[1].split(",")[0];
|
// setup request
|
||||||
const request = {
|
const request = {
|
||||||
network: Number(params.rate) - Number(currentNetworkRate)
|
network: Number(params.rate) - Number(net.rate)
|
||||||
};
|
};
|
||||||
|
|
||||||
// check resource approval
|
// check resource approval
|
||||||
if (!await approveResources(req, userObj, request, params.node)) {
|
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||||
|
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}`] = currentNetworkConfig.replace(`rate=${currentNetworkRate}`, `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);
|
||||||
|
await global.pve.syncInstance(params.node, params.vmid);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,25 +169,30 @@ router.delete("/:netid/delete", async (req, res) => {
|
|||||||
node: req.params.node,
|
node: req.params.node,
|
||||||
type: req.params.type,
|
type: req.params.type,
|
||||||
vmid: req.params.vmid,
|
vmid: req.params.vmid,
|
||||||
netid: req.params.netid.replace("net", "")
|
netid: req.params.netid
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 current config
|
|
||||||
const currentConfig = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true });
|
|
||||||
// net interface must already exist
|
// net interface must already exist
|
||||||
if (!currentConfig.data.data[`net${params.netid}`]) {
|
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
|
||||||
|
if (!net) {
|
||||||
res.status(500).send({ error: `Network interface net${params.netid} does not exist.` });
|
res.status(500).send({ error: `Network interface net${params.netid} does not exist.` });
|
||||||
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);
|
||||||
});
|
});
|
||||||
|
|||||||
+157
-99
@@ -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,30 +88,24 @@ router.get("/:hostpci", async (req, res) => {
|
|||||||
node: req.params.node,
|
node: req.params.node,
|
||||||
type: req.params.type,
|
type: req.params.type,
|
||||||
vmid: req.params.vmid,
|
vmid: req.params.vmid,
|
||||||
hostpci: req.params.hostpci.replace("hostpci", "")
|
hostpci: req.params.hostpci
|
||||||
};
|
};
|
||||||
|
|
||||||
// check auth for specific instance
|
// check auth for specific instance
|
||||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||||
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
|
|
||||||
const config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
// get device
|
||||||
if (!config[`hostpci${params.hostpci}`]) {
|
const device = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||||
res.status(500).send({ error: `Could not find hostpci${params.hostpci} in ${params.vmid}.` });
|
if (!device) {
|
||||||
|
res.status(500).send({ error: `Could not find ${params.hostpci}=${device} in ${params.node}.` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const device = config[`hostpci${params.hostpci}`].split(",")[0];
|
res.status(200).send(device);
|
||||||
// get node's pci devices
|
|
||||||
const deviceData = await global.pve.getDeviceInfo(params.node, device);
|
|
||||||
if (!deviceData) {
|
|
||||||
res.status(500).send({ error: `Could not find hostpci${params.hostpci}=${device} in ${params.node}.` });
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.status(200).send(deviceData);
|
|
||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,72 +130,74 @@ router.post("/:hostpci/modify", async (req, res) => {
|
|||||||
node: req.params.node,
|
node: req.params.node,
|
||||||
type: req.params.type,
|
type: req.params.type,
|
||||||
vmid: req.params.vmid,
|
vmid: req.params.vmid,
|
||||||
hostpci: req.params.hostpci.replace("hostpci", ""),
|
hostpci: req.params.hostpci,
|
||||||
device: req.body.device,
|
device: req.body.device,
|
||||||
pcie: req.body.pcie
|
pcie: req.body.pcie
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
|
||||||
|
|
||||||
// check if type is qemu
|
// check if type is qemu
|
||||||
if (params.type !== "qemu") {
|
if (params.type !== "qemu") {
|
||||||
res.status(500).send({ error: "Type must be qemu (vm)." });
|
res.status(500).send({ error: "Type must be qemu (vm)." });
|
||||||
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];
|
||||||
// get instance config to check if device has not changed
|
|
||||||
const config = (await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { token: true })).data.data;
|
// device must exist to be modified
|
||||||
const currentDeviceData = await global.pve.getDeviceInfo(params.node, config[`hostpci${params.hostpci}`].split(",")[0]);
|
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||||
if (!currentDeviceData) {
|
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
|
|
||||||
if (currentDeviceData.id.split(".")[0] !== params.device) {
|
// only check user and node availability if base id is different, we do the split in case of existing partial-function hostpci
|
||||||
|
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||||
|
if (existingDevice.device_bus.split(".")[0] !== params.device) {
|
||||||
// setup request
|
// setup request
|
||||||
const deviceData = await global.pve.getDeviceInfo(params.node, params.device);
|
const node = await global.pve.getNode(params.node);
|
||||||
const request = { pci: deviceData.device_name };
|
const requestedDevice = node.devices[`${params.device}`];
|
||||||
|
const request = { pci: requestedDevice.device_name };
|
||||||
|
if (!requestedDevice) {
|
||||||
|
res.status(500).send({ request, error: `Could not fulfil request for ${params.device}.` });
|
||||||
|
res.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
// check resource approval
|
// check resource approval
|
||||||
if (!await approveResources(req, userObj, request, params.node)) {
|
const { approved } = await global.utils.approveResources(req, userObj, params.node, instance.pool, request);
|
||||||
res.status(500).send({ request, error: `Could not fulfil request for ${deviceData.device_name}.` });
|
if (!approved) {
|
||||||
|
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 nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
|
if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) {
|
||||||
if (!nodeAvailPci.some(element => element.id.split(".")[0] === params.device)) {
|
|
||||||
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
|
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup action
|
// setup action
|
||||||
const action = {};
|
const action = {};
|
||||||
action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
||||||
|
|
||||||
// commit action
|
// commit action
|
||||||
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, global.config.backends.pve.config.root);
|
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
|
||||||
if (!(rootauth.status === 200)) {
|
|
||||||
res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." });
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rootcookies = {
|
|
||||||
PVEAuthCookie: rootauth.data.data.ticket,
|
|
||||||
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
|
|
||||||
};
|
|
||||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action);
|
|
||||||
await global.pve.handleResponse(params.node, result, res);
|
await global.pve.handleResponse(params.node, result, res);
|
||||||
|
await global.pve.syncNode(params.node);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -152,74 +214,74 @@ router.post("/:hostpci/modify", async (req, res) => {
|
|||||||
* - 500: {request: Object, error: string}
|
* - 500: {request: Object, error: string}
|
||||||
* - 500: PVE Task Object
|
* - 500: PVE Task Object
|
||||||
*/
|
*/
|
||||||
router.post("/create", async (req, res) => {
|
router.post("/:hostpci/create", async (req, res) => {
|
||||||
req.params = Object.assign({}, req.routeparams, req.params);
|
req.params = Object.assign({}, req.routeparams, req.params);
|
||||||
const params = {
|
const params = {
|
||||||
node: req.params.node,
|
node: req.params.node,
|
||||||
type: req.params.type,
|
type: req.params.type,
|
||||||
vmid: req.params.vmid,
|
vmid: req.params.vmid,
|
||||||
|
hostpci: req.params.hostpci,
|
||||||
device: req.body.device,
|
device: req.body.device,
|
||||||
pcie: req.body.pcie
|
pcie: req.body.pcie
|
||||||
};
|
};
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
|
||||||
|
|
||||||
// check if type is qemu
|
// check if type is qemu
|
||||||
if (params.type !== "qemu") {
|
if (params.type !== "qemu") {
|
||||||
res.status(500).send({ error: "Type must be qemu (vm)." });
|
res.status(500).send({ error: "Type must be qemu (vm)." });
|
||||||
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];
|
||||||
// get instance config to find next available hostpci slot
|
|
||||||
const config = global.pve.requestPVE(`/nodes/${params.node}/${params.type}/${params.vmid}/config`, "GET", { cookies: params.cookies });
|
// device must not exist to be added
|
||||||
let hostpci = 0;
|
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||||
while (config[`hostpci${hostpci}`]) {
|
if (existingDevice) {
|
||||||
hostpci++;
|
res.status(500).send({ error: `Existing device in ${params.hostpci}.` });
|
||||||
}
|
|
||||||
// setup request
|
|
||||||
const deviceData = await global.pve.getDeviceInfo(params.node, params.device);
|
|
||||||
const request = {
|
|
||||||
pci: deviceData.device_name
|
|
||||||
};
|
|
||||||
// check resource approval
|
|
||||||
if (!await approveResources(req, userObj, request, params.node)) {
|
|
||||||
res.status(500).send({ request, error: `Could not fulfil request for ${deviceData.device_name}.` });
|
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setup request
|
||||||
|
const node = await global.pve.getNode(params.node);
|
||||||
|
const requestedDevice = node.devices[`${params.device}`];
|
||||||
|
const request = { pci: requestedDevice.device_name };
|
||||||
|
|
||||||
|
// check resource approval
|
||||||
|
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||||
|
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.end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// check node availability
|
// check node availability
|
||||||
const nodeAvailPci = await global.pve.getNodeAvailDevices(params.node, req.cookies);
|
if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) {
|
||||||
if (!nodeAvailPci.some(element => element.id.split(".")[0] === params.device)) {
|
|
||||||
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
|
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
|
||||||
res.end();
|
res.end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup action
|
// setup action
|
||||||
const action = {};
|
const action = {};
|
||||||
action[`hostpci${hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
||||||
|
|
||||||
// commit action
|
// commit action
|
||||||
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, global.config.backends.pve.config.root);
|
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
|
||||||
if (!(rootauth.status === 200)) {
|
|
||||||
res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." });
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rootcookies = {
|
|
||||||
PVEAuthCookie: rootauth.data.data.ticket,
|
|
||||||
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
|
|
||||||
};
|
|
||||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action);
|
|
||||||
await global.pve.handleResponse(params.node, result, res);
|
await global.pve.handleResponse(params.node, result, res);
|
||||||
|
await global.pve.syncNode(params.node);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -241,40 +303,36 @@ router.delete("/:hostpci/delete", async (req, res) => {
|
|||||||
node: req.params.node,
|
node: req.params.node,
|
||||||
type: req.params.type,
|
type: req.params.type,
|
||||||
vmid: req.params.vmid,
|
vmid: req.params.vmid,
|
||||||
hostpci: req.params.hostpci.replace("hostpci", "")
|
hostpci: req.params.hostpci
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 config = (await global.pve.requestPVE(`${vmpath}/config`, "GET", { cookies: req.cookies })).data.data;
|
const device = global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||||
if (!config[`hostpci${params.hostpci}`]) {
|
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 rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, global.config.backends.pve.config.root);
|
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
|
||||||
if (!(rootauth.status === 200)) {
|
|
||||||
res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." });
|
|
||||||
res.end();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const rootcookies = {
|
|
||||||
PVEAuthCookie: rootauth.data.data.ticket,
|
|
||||||
CSRFPreventionToken: rootauth.data.data.CSRFPreventionToken
|
|
||||||
};
|
|
||||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { cookies: rootcookies }, action);
|
|
||||||
await global.pve.handleResponse(params.node, result, res);
|
await global.pve.handleResponse(params.node, result, res);
|
||||||
|
await global.pve.syncNode(params.node);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,15 +12,21 @@ 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;
|
||||||
res.status(200).send(config[params.key]);
|
const result = {};
|
||||||
|
result[params.key] = config[params.key];
|
||||||
|
res.status(200).send(result);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
res.status(401).send({ auth: false, error: `User is not authorized to access /global/config/${params.key}.` });
|
res.status(401).send({ auth: false, error: `User is not authorized to access /global/config/${params.key}.` });
|
||||||
|
|||||||
+12
-19
@@ -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
|
// 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();
|
||||||
@@ -165,14 +161,11 @@ if (schemes.interrupt.enabled) {
|
|||||||
socket.destroy();
|
socket.destroy();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
wsServer.handleUpgrade(req, socket, head, (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 userRealm = cookies.username.split("@").at(-1);
|
const pools = await global.pve.requestPVE("/pools", "GET", { cookies });
|
||||||
const userID = cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
|
||||||
const pools = Object.keys(global.db.getUser(userObj).cluster.pools);
|
|
||||||
// emit the connection to initialize socket
|
// emit the connection to initialize socket
|
||||||
wsServer.emit("connection", socket, cookies.username, pools);
|
wsServer.emit("connection", socket, cookies.username, Object.keys(pools.data));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -192,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
|
||||||
@@ -262,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
-57
@@ -3,61 +3,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;
|
||||||
const getUserResources = global.utils.getUserResources;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) => {
|
|
||||||
// check auth
|
|
||||||
const auth = await checkAuth(req.cookies, res);
|
|
||||||
if (!auth) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const userRealm = req.cookies.username.split("@").at(-1);
|
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
|
||||||
|
|
||||||
const resources = await 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 userRealm = req.cookies.username.split("@").at(-1);
|
|
||||||
const userID = req.cookies.username.replace(`@${userRealm}`, "");
|
|
||||||
const userObj = { id: userID, realm: userRealm };
|
|
||||||
|
|
||||||
// check auth
|
|
||||||
const auth = await checkAuth(req.cookies, res);
|
|
||||||
if (!auth) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const allowKeys = ["resources", "cluster"];
|
|
||||||
if (allowKeys.includes(params.key)) {
|
|
||||||
const config = global.db.getUser(userObj);
|
|
||||||
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
|
||||||
@@ -74,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/`, "");
|
||||||
@@ -99,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/`, "");
|
||||||
|
|||||||
+225
-196
@@ -15,18 +15,34 @@ import { exit } from "process";
|
|||||||
export async function checkAuth (cookies, res, vmpath = null) {
|
export async function checkAuth (cookies, res, vmpath = null) {
|
||||||
let auth = false;
|
let auth = false;
|
||||||
|
|
||||||
const userRealm = cookies.username.split("@").at(-1);
|
const userObj = getUserObjFromUsername(cookies.username); // check if username exists and is valid
|
||||||
const userID = cookies.username.replace(`@${userRealm}`, "");
|
if (!userObj) {
|
||||||
const userObj = { id: userID, realm: userRealm };
|
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Username was missing or invalid." });
|
||||||
|
|
||||||
if (global.db.getUser(userObj) === null) {
|
|
||||||
auth = false;
|
|
||||||
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in localdb.` });
|
|
||||||
res.end();
|
res.end();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vmpath) {
|
if (!cookies.PVEAuthCookie) { // check if PVE token exists
|
||||||
|
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Token was missing or invalid." });
|
||||||
|
res.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pveTicket = cookies.PVEAuthCookie;
|
||||||
|
const result = await global.pve.requestPVE("/access/ticket", "POST", null, { username: cookies.username, password: pveTicket });
|
||||||
|
if (result.status !== 200) { // check if PVE token is valid by using /access/ticket to validate ticket with Proxmox
|
||||||
|
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Username did not match token." });
|
||||||
|
res.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((await global.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.end();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vmpath) { // if a path is specified, check the permissions on the path
|
||||||
const result = await global.pve.requestPVE(`/${vmpath}/config`, "GET", { cookies });
|
const result = await global.pve.requestPVE(`/${vmpath}/config`, "GET", { cookies });
|
||||||
auth = result.status === 200;
|
auth = result.status === 200;
|
||||||
}
|
}
|
||||||
@@ -39,120 +55,49 @@ export async function checkAuth (cookies, res, vmpath = null) {
|
|||||||
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "User token did not pass authentication check." });
|
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "User token did not pass authentication check." });
|
||||||
res.end();
|
res.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
return auth;
|
return auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the full config of an instance, including searching disk information.
|
* Get pool resource data including used, available, and maximum resources.
|
||||||
* @param {Object} req ProxmoxAAS API request object.
|
|
||||||
* @param {Object} instance to get config as object containing node, type, and id.
|
|
||||||
* @param {Array} diskprefixes Array containing prefixes for disks.
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async function getFullInstanceConfig (req, instance, diskprefixes) {
|
|
||||||
const config = (await global.pve.requestPVE(`/nodes/${instance.node}/${instance.type}/${instance.vmid}/config`, "GET", { cookies: req.cookies })).data.data;
|
|
||||||
// fetch all instance disk and device data concurrently
|
|
||||||
const promises = [];
|
|
||||||
const mappings = [];
|
|
||||||
for (const key in config) {
|
|
||||||
if (diskprefixes.some(prefix => key.startsWith(prefix))) {
|
|
||||||
promises.push(global.pve.getDiskInfo(instance.node, config, key));
|
|
||||||
mappings.push(key);
|
|
||||||
}
|
|
||||||
else if (key.startsWith("hostpci")) {
|
|
||||||
promises.push(global.pve.getDeviceInfo(instance.node, config[key].split(",")[0]));
|
|
||||||
mappings.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const results = await Promise.all(promises);
|
|
||||||
results.forEach((e, i) => {
|
|
||||||
const key = mappings[i];
|
|
||||||
config[key] = e;
|
|
||||||
});
|
|
||||||
config.node = instance.node;
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all configs for every instance owned by the user. Uses the expanded config data from getFullInstanceConfig.
|
|
||||||
* @param {Object} req ProxmoxAAS API request object.
|
|
||||||
* @param {Object} dbResources data about application resources, to indicate which resources are tracked.
|
|
||||||
* @returns {Object} k-v pairs of resource name and used amounts
|
|
||||||
*/
|
|
||||||
async function getAllInstanceConfigs (req, diskprefixes) {
|
|
||||||
// get the basic resources list
|
|
||||||
const resources = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data;
|
|
||||||
|
|
||||||
// filter resources by their type, we only want lxc and qemu
|
|
||||||
const instances = [];
|
|
||||||
for (const resource of resources) {
|
|
||||||
if (resource.type === "lxc" || resource.type === "qemu") {
|
|
||||||
instances.push(resource);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all instance configs, also include detailed disk and device info
|
|
||||||
const promises = [];
|
|
||||||
const mappings = [];
|
|
||||||
for (let i = 0; i < instances.length; i++) {
|
|
||||||
const instance = instances[i];
|
|
||||||
const config = getFullInstanceConfig(req, instance, diskprefixes);
|
|
||||||
promises.push(config);
|
|
||||||
mappings.push(i);
|
|
||||||
}
|
|
||||||
const configs = await Promise.all(promises);
|
|
||||||
|
|
||||||
return configs;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user resource data including used, available, and maximum resources.
|
|
||||||
* @param {Object} req ProxmoxAAS API request object.
|
* @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 = global.db.getUser(user).resources;
|
const poolConfig = await global.access.getPool(pool, req.cookies);
|
||||||
|
const poolResources = poolConfig.pool.resources;
|
||||||
|
|
||||||
// setup disk prefixes object
|
// setup the pool resource object with used and avail for each resource and each resource pool
|
||||||
const diskprefixes = [];
|
|
||||||
for (const resourceName of Object.keys(dbResources)) {
|
|
||||||
if (dbResources[resourceName].type === "storage") {
|
|
||||||
for (const diskPrefix of dbResources[resourceName].disks) {
|
|
||||||
diskprefixes.push(diskPrefix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// setup the user resource object with used and avail for each resource and each resource pool
|
|
||||||
// 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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -163,147 +108,171 @@ 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 getAllInstanceConfigs(req, diskprefixes);
|
const configs = await global.pve.getPoolResources(req.cookies, pool);
|
||||||
|
|
||||||
for (const config of configs) {
|
for (const vmid in configs) {
|
||||||
|
const config = configs[vmid];
|
||||||
const nodeName = config.node;
|
const nodeName = config.node;
|
||||||
|
// count basic numeric resources
|
||||||
for (const resourceName of Object.keys(config)) {
|
for (const resourceName of Object.keys(config)) {
|
||||||
// numeric resource type
|
// numeric resource type
|
||||||
if (resourceName in dbResources && dbResources[resourceName].type === "numeric") {
|
if (resourceName in 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;
|
||||||
}
|
}
|
||||||
else if (diskprefixes.some(prefix => resourceName.startsWith(prefix))) {
|
}
|
||||||
const diskInfo = config[resourceName];
|
// count disk resources in volumes
|
||||||
if (diskInfo) { // only count if disk exists
|
for (const diskid in config.volumes) {
|
||||||
const val = Number(diskInfo.size);
|
const disk = config.volumes[diskid];
|
||||||
const storage = diskInfo.storage;
|
const storage = disk.storage;
|
||||||
// if the instance's node is restricted by this resource, add it to the instance's used value
|
const size = disk.size;
|
||||||
if (nodeName in userResources[storage].nodes) {
|
// only process disk if its storage is in the user resources to be counted
|
||||||
userResources[storage].nodes[nodeName].used += val;
|
if (storage in poolResources) {
|
||||||
userResources[storage].nodes[nodeName].avail -= val;
|
|
||||||
}
|
|
||||||
// otherwise add the resource to the global pool
|
|
||||||
else {
|
|
||||||
userResources[storage].global.used += val;
|
|
||||||
userResources[storage].global.avail -= val;
|
|
||||||
}
|
|
||||||
userResources[storage].total.used += val;
|
|
||||||
userResources[storage].total.avail -= val;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (resourceName.startsWith("net") && config[resourceName].includes("rate=")) { // only count net instances with a rate limit
|
|
||||||
const val = Number(config[resourceName].split("rate=")[1].split(",")[0]);
|
|
||||||
// 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[storage].nodes) {
|
||||||
userResources.network.nodes[nodeName].used += val;
|
poolResources[storage].nodes[nodeName].used += size;
|
||||||
userResources.network.nodes[nodeName].avail -= val;
|
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.network.global.used += val;
|
poolResources[storage].global.used += size;
|
||||||
userResources.network.global.avail -= val;
|
poolResources[storage].global.avail -= size;
|
||||||
}
|
}
|
||||||
userResources.network.total.used += val;
|
poolResources[storage].total.used += size;
|
||||||
userResources.network.total.avail -= val;
|
poolResources[storage].total.avail -= size;
|
||||||
}
|
}
|
||||||
else if (resourceName.startsWith("hostpci")) {
|
}
|
||||||
const deviceInfo = config[resourceName];
|
// count net resources in nets
|
||||||
if (deviceInfo) { // only count if device exists
|
for (const netid in config.nets) {
|
||||||
const deviceName = deviceInfo.device_name;
|
const net = config.nets[netid];
|
||||||
// if the instance's node is restricted by this resource, add it to the instance's used value
|
const rate = net.rate;
|
||||||
if (nodeName in userResources.pci.nodes) {
|
if (poolResources.network) {
|
||||||
const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => deviceName.includes(availEelement.match));
|
// if the instance's node is restricted by this resource, add it to the instance's used value
|
||||||
userResources.pci.nodes[nodeName][index].used++;
|
if (nodeName in poolResources.network.nodes) {
|
||||||
userResources.pci.nodes[nodeName][index].avail--;
|
poolResources.network.nodes[nodeName].used += rate;
|
||||||
}
|
poolResources.network.nodes[nodeName].avail -= rate;
|
||||||
// otherwise add the resource to the global pool
|
|
||||||
else {
|
|
||||||
const index = userResources.pci.global.findIndex((availEelement) => deviceName.includes(availEelement.match));
|
|
||||||
userResources.pci.global[index].used++;
|
|
||||||
userResources.pci.global[index].avail--;
|
|
||||||
}
|
|
||||||
const index = userResources.pci.total.findIndex((availEelement) => deviceName.includes(availEelement.match));
|
|
||||||
userResources.pci.total[index].used++;
|
|
||||||
userResources.pci.total[index].avail--;
|
|
||||||
}
|
}
|
||||||
|
// otherwise add the resource to the global pool
|
||||||
|
else {
|
||||||
|
poolResources.network.global.used += rate;
|
||||||
|
poolResources.network.global.avail -= rate;
|
||||||
|
}
|
||||||
|
poolResources.network.total.used += rate;
|
||||||
|
poolResources.network.total.avail -= rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// count pci device resources in devices
|
||||||
|
for (const deviceid in config.devices) {
|
||||||
|
const device = config.devices[deviceid];
|
||||||
|
const name = device.device_name;
|
||||||
|
// if the node has a node specific rule, add it there
|
||||||
|
if (nodeName in poolResources.pci.nodes) {
|
||||||
|
const index = poolResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match));
|
||||||
|
if (index >= 0) {
|
||||||
|
poolResources.pci.nodes[nodeName][index].used++;
|
||||||
|
poolResources.pci.nodes[nodeName][index].avail--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// otherwise try to add the resource to the global pool
|
||||||
|
else {
|
||||||
|
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
|
||||||
|
poolResources.pci.global[index].used++;
|
||||||
|
poolResources.pci.global[index].avail--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// finally, add the device to the total map
|
||||||
|
const index = poolResources.pci.total.findIndex((availEelement) => name.includes(availEelement.match));
|
||||||
|
if (index >= 0) {
|
||||||
|
poolResources.pci.total[index].used++;
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -331,7 +300,7 @@ export function getTimeLeft (timeout) {
|
|||||||
/**
|
/**
|
||||||
* Recursively import routes from target folder.
|
* Recursively import routes from target folder.
|
||||||
* @param {Object} router or app object.
|
* @param {Object} router or app object.
|
||||||
* @param {string} baseroute API route for each imported module.
|
* @param {string} baseroute base route of imported modules starting from the current path.
|
||||||
* @param {string} target folder to import modules.
|
* @param {string} target folder to import modules.
|
||||||
* @param {string} from source folder of calling module, optional for imports from the same base directory.
|
* @param {string} from source folder of calling module, optional for imports from the same base directory.
|
||||||
*/
|
*/
|
||||||
@@ -358,7 +327,67 @@ 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
|
||||||
|
* @returns {Object | null} user object containing userid and realm or null if username format was invalid
|
||||||
|
*/
|
||||||
|
export function getUserObjFromUsername (username) {
|
||||||
|
if (username) {
|
||||||
|
const userRealm = username.split("@").at(-1);
|
||||||
|
const userID = username.replace(`@${userRealm}`, "");
|
||||||
|
const userObj = { id: userID, realm: userRealm };
|
||||||
|
return userObj;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,131 +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": 131072
|
|
||||||
},
|
|
||||||
"nodes": {}
|
|
||||||
},
|
|
||||||
"swap": {
|
|
||||||
"global": {
|
|
||||||
"max": 131072
|
|
||||||
},
|
|
||||||
"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": {
|
|
||||||
"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
|
|
||||||
},
|
|
||||||
"net0": {
|
|
||||||
"value": "virtio,bridge=vmbr0,tag=10,rate=1000",
|
|
||||||
"resource": {
|
|
||||||
"name": "network",
|
|
||||||
"amount": 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"network": {
|
|
||||||
"lxc": {
|
|
||||||
"type": "veth",
|
|
||||||
"bridge": "vmbr0",
|
|
||||||
"vlan": 10,
|
|
||||||
"ip": "dhcp",
|
|
||||||
"ip6": "dhcp"
|
|
||||||
},
|
|
||||||
"qemu": {
|
|
||||||
"type": "virtio",
|
|
||||||
"bridge": "vmbr0",
|
|
||||||
"vlan": 10
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user