Compare commits
17
Commits
v1.0.0
..
cf47cf6c71
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf47cf6c71 | ||
|
|
7ea579df68 | ||
|
|
e26849283e | ||
|
|
07c48db808 | ||
|
|
f2f4f45097 | ||
|
|
cc4caf9449 | ||
|
|
4ae30eb155 | ||
|
|
c1a302a595 | ||
|
|
56dc15f1d2 | ||
|
|
b098a173fa | ||
|
|
3a41afb696 | ||
|
|
56ef0b4074 | ||
|
|
67fd748487 | ||
|
|
f34c13827b | ||
|
|
6300df344c | ||
|
|
85c9c4cc6d | ||
|
|
35421714e6 |
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -11,63 +11,49 @@ ProxmoxAAS API provides functionality for the Dashboard by providing a proxy API
|
||||
- Server with NodeJS (v18.0+) and NPM installed
|
||||
|
||||
### 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
|
||||
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:
|
||||
- VM.* except VM.Audit, VM.Backup, VM.Clone, VM.Console, VM.Monitor, VM.PowerMgmt, VM.Snapshot, VM.Snapshot.Rollback
|
||||
- Datastore.Allocate, Datastore.AllocateSpace, Datastore.Audit
|
||||
- VM.* except VM.Clone, VM.Console, VM.Monitor, VM.PowerMgmt, VM.Snapshot, VM.Snapshot.Rollback
|
||||
- Datastore.Allocate, Datastore.AllocateSpace, Datastore.AllocateTemplate, Datastore.Audit
|
||||
- User.Modify
|
||||
- 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`
|
||||
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
|
||||
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
|
||||
3. Copy `template.config.json` as `config.json` and modify the following values:
|
||||
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)
|
||||
- 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**):
|
||||
- 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`:
|
||||
- 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`:
|
||||
- hostname - the ProxmoxAAS-Dashboard URL, ie `host.domain.tld`
|
||||
- domain - the base domain for the dashboard and proxmox, ie `domain.tld`
|
||||
- hostname - the ProxmoxAAS-Dashboard URL, ie `paas.domain.net`
|
||||
- domain - the base domain for the dashboard and proxmox, ie `domain.net`
|
||||
- listenPort - the port you want the API to listen on, ie `8081`
|
||||
5. In `useriso`:
|
||||
- node: host 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
|
||||
|
||||
### Installation - Reverse Proxy
|
||||
1. Configure nginx or preferred reverse proxy to reverse proxy the dashboard. The configuration should include at least the following:
|
||||
```
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name paas.<FQDN>;
|
||||
location / {
|
||||
return 301 "/dashboard/";
|
||||
}
|
||||
location /dashboard/ {
|
||||
proxy_pass http://proxmoxaas.dmz:8080/;
|
||||
proxy_redirect default;
|
||||
}
|
||||
location /api/ {
|
||||
proxy_pass http://proxmoxaas.dmz:80/api/;
|
||||
proxy_redirect default;
|
||||
}
|
||||
}
|
||||
```
|
||||
2. Start nginx with the new configurations
|
||||
|
||||
### Result
|
||||
After these steps, the ProxmoxAAS Dashboard should be available and fully functional at `paas.<FQDN>` or `paas.<FQDN>/dashboard/`.
|
||||
|
||||
# Backends
|
||||
|
||||
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.
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
"node": "examplenode1",
|
||||
"storage": "cephfs"
|
||||
},
|
||||
"backups": {
|
||||
"storage": "cephfs"
|
||||
},
|
||||
"resources": {
|
||||
"cpu": {
|
||||
"type": "list",
|
||||
|
||||
@@ -85,6 +85,9 @@
|
||||
"pools": {
|
||||
"example-pool-1": true,
|
||||
"example-pool-2": true
|
||||
},
|
||||
"backups": {
|
||||
"max": 5
|
||||
}
|
||||
},
|
||||
"templates": {
|
||||
@@ -0,0 +1,30 @@
|
||||
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": "^_"
|
||||
}]
|
||||
}
|
||||
}]);
|
||||
+6
-7
@@ -7,7 +7,7 @@
|
||||
"dependencies": {
|
||||
"axios": "^1.3.2",
|
||||
"body-parser": "^1.20.1",
|
||||
"cookie": "^0.5.0",
|
||||
"cookie": "^1.0.2",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
@@ -17,13 +17,12 @@
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-standard": "^17.1.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-n": "^16.0.1",
|
||||
"eslint-plugin-promise": "^6.1.1"
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "^9.39.4",
|
||||
"eslint": "^10.1.0",
|
||||
"globals": "^17.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "DEBUG=eslint:cli-engine eslint --fix ."
|
||||
"lint": "DEBUG=eslint:cli-engine eslint --config dev_config/eslint.config.mjs --fix ."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AtomicChange, DB_BACKEND, doNothingCallback } from "./backends.js";
|
||||
export default class LocalDB extends DB_BACKEND {
|
||||
#path = null;
|
||||
#data = null;
|
||||
#defaultuser = null;
|
||||
//#defaultuser = null;
|
||||
|
||||
constructor (config) {
|
||||
super();
|
||||
@@ -13,7 +13,7 @@ export default class LocalDB extends DB_BACKEND {
|
||||
try {
|
||||
this.#path = path;
|
||||
this.#load();
|
||||
this.#defaultuser = global.config.defaultuser;
|
||||
//this.#defaultuser = global.config.defaultuser;
|
||||
}
|
||||
catch {
|
||||
console.log(`error: ${path} was not found. Please follow the directions in the README to initialize localdb.json.`);
|
||||
@@ -87,7 +87,6 @@ export default class LocalDB extends DB_BACKEND {
|
||||
}
|
||||
}
|
||||
else {
|
||||
// return false;
|
||||
return new AtomicChange(false, {}, doNothingCallback, { ok: false, status: 400, message: `${username} was not found in localdb` });
|
||||
}
|
||||
}
|
||||
|
||||
+49
-16
@@ -35,7 +35,7 @@ router.get("/pools", async (req, res) => {
|
||||
res.end();
|
||||
}
|
||||
else {
|
||||
res.status(allPools.status).send({ error: allPools.statusMessage });
|
||||
res.status(allPools.status).send({ error: allPools.statusText });
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
@@ -63,13 +63,13 @@ router.get("/nodes", async (req, res) => {
|
||||
res.end();
|
||||
}
|
||||
else {
|
||||
res.status(allNodes.status).send({ error: allNodes.statusMessage });
|
||||
res.status(allNodes.status).send({ error: allNodes.statusText });
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET - get available pcie devices given node and user
|
||||
* GET - get available pcie devices for the given node and user
|
||||
* request:
|
||||
* - node: string - vm host node id
|
||||
* responses:
|
||||
@@ -82,7 +82,6 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
|
||||
const params = {
|
||||
node: req.params.node
|
||||
};
|
||||
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
|
||||
// check auth
|
||||
@@ -91,14 +90,15 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
|
||||
return;
|
||||
}
|
||||
const userNodes = (await global.userManager.getUser(userObj, req.cookies)).cluster.nodes;
|
||||
if (userNodes[params.node] !== true) {
|
||||
if (userNodes[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 userAvailPci = (await getUserResources(req, userObj)).pci.nodes[params.node];
|
||||
if (userAvailPci === undefined) { // user has no avaliable devices on this node, so send an empty list
|
||||
const userAvailPci = (await getUserResources(req, userObj)).pci.nodes[params.node]; // we assume that the node list is used. TODO support global lists
|
||||
if (userAvailPci === undefined) { // user has no available devices on this node, so send an empty list
|
||||
res.status(200).send([]);
|
||||
res.end();
|
||||
}
|
||||
@@ -122,6 +122,36 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET - get basic resources for vm using the fabric format
|
||||
* request:
|
||||
* - node: string - vm host node id
|
||||
* - type: string - vm type (lxc, qemu)
|
||||
* - vmid: number - vm id number
|
||||
* response:
|
||||
* - 200: Fabric instance config
|
||||
* - 401: {auth: false}
|
||||
*/
|
||||
router.get(`${basePath}`, async (req, res) => {
|
||||
const params = {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid
|
||||
};
|
||||
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get current config
|
||||
const instance = await global.pve.getInstance(params.node, params.vmid);
|
||||
|
||||
res.status(200).send(instance);
|
||||
});
|
||||
|
||||
/**
|
||||
* POST - set basic resources for vm
|
||||
* request:
|
||||
@@ -134,8 +164,8 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
|
||||
* - swap: number, optional - new amount of swap for instance
|
||||
* responses:
|
||||
* - 200: PVE Task Object
|
||||
* - 400: {request; Object, error: string, reason: Object}
|
||||
* - 401: {auth: false, path: string}
|
||||
* - 500: {request: Object, error: string}
|
||||
* - 500: PVE Task Object
|
||||
*/
|
||||
router.post(`${basePath}/resources`, async (req, res) => {
|
||||
@@ -171,8 +201,9 @@ router.post(`${basePath}/resources`, async (req, res) => {
|
||||
request.cpu = params.proctype;
|
||||
}
|
||||
// check resource approval
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
res.status(500).send({ request, error: "Could not fulfil request." });
|
||||
const { approved, reason } = await approveResources(req, userObj, request, params.node);
|
||||
if (!approved) {
|
||||
res.status(400).send({ request, error: "Not enough resources to satisfy request.", reason });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
@@ -209,9 +240,9 @@ router.post(`${basePath}/resources`, async (req, res) => {
|
||||
* - rootfssize: number, optional, - size of lxc instance rootfs
|
||||
* responses:
|
||||
* - 200: PVE Task Object
|
||||
* - 400: {request: Object, error: string, reason: Object}
|
||||
* - 401: {auth: false, path: string}
|
||||
* - 500: {error: string}
|
||||
* - 500: {request: Object, error: string}
|
||||
* - 500: PVE Task Object
|
||||
*/
|
||||
router.post(`${basePath}/create`, async (req, res) => {
|
||||
@@ -264,11 +295,11 @@ router.post(`${basePath}/create`, async (req, res) => {
|
||||
// setup request
|
||||
const request = {
|
||||
cores: Number(params.cores),
|
||||
memory: Number(params.memory)
|
||||
memory: Number(params.memory) * 1024 ** 2
|
||||
};
|
||||
if (params.type === "lxc") {
|
||||
request.swap = params.swap;
|
||||
request[params.rootfslocation] = params.rootfssize;
|
||||
request.swap = Number(params.swap) * 1024 ** 2;
|
||||
request[params.rootfslocation] = params.rootfssize * 1024 ** 3;
|
||||
}
|
||||
for (const key of Object.keys(user.templates.instances[params.type])) {
|
||||
const item = user.templates.instances[params.type][key];
|
||||
@@ -282,8 +313,9 @@ router.post(`${basePath}/create`, async (req, res) => {
|
||||
}
|
||||
}
|
||||
// check resource approval
|
||||
if (!await approveResources(req, userObj, request, params.node)) { // check resource approval
|
||||
res.status(500).send({ request, error: "Not enough resources to satisfy request." });
|
||||
const { approved, reason } = await approveResources(req, userObj, request, params.node);
|
||||
if (!approved) {
|
||||
res.status(400).send({ request, error: "Not enough resources to satisfy request.", reason });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
@@ -298,6 +330,7 @@ router.post(`${basePath}/create`, async (req, res) => {
|
||||
action[key] = user.templates.instances[params.type][key].value;
|
||||
}
|
||||
if (params.type === "lxc") {
|
||||
action.swap = params.swap;
|
||||
action.hostname = params.name;
|
||||
action.unprivileged = 1;
|
||||
action.features = "nesting=1";
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
import { Router } from "express";
|
||||
export const router = Router({ mergeParams: true }); ;
|
||||
|
||||
const checkAuth = global.utils.checkAuth;
|
||||
|
||||
/**
|
||||
* 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 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.data);
|
||||
}
|
||||
else {
|
||||
res.status(backups.status).send({ 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 checkAuth(req.cookies, res, vmpath);
|
||||
if (!auth) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if number of backups is less than the allowed number
|
||||
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 });
|
||||
const numBackups = backups.data.data.length;
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
const maxAllowed = (await global.userManager.getUser(userObj, req.cookies)).cluster.backups.max;
|
||||
if (backups.status !== 200) {
|
||||
res.status(backups.status).send({ error: backups.statusText });
|
||||
return;
|
||||
}
|
||||
else 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.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 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.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;
|
||||
}
|
||||
|
||||
// create backup using vzdump path
|
||||
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 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.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.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 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.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);
|
||||
console.log(result);
|
||||
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 });
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -72,7 +72,8 @@ router.post("/:disk/attach", async (req, res) => {
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
disk: req.params.disk,
|
||||
source: req.body.source
|
||||
source: req.body.source,
|
||||
mp: req.body.mp
|
||||
};
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
@@ -97,7 +98,12 @@ router.post("/:disk/attach", async (req, res) => {
|
||||
}
|
||||
// setup action using source disk info from vm config
|
||||
const action = {};
|
||||
action[params.disk] = disk.volid;
|
||||
if (params.type === "qemu") {
|
||||
action[params.disk] = `${disk.file}`;
|
||||
}
|
||||
else if (params.type === "lxc") {
|
||||
action[params.disk] = `${disk.file},mp=${params.mp},backup=1`;
|
||||
}
|
||||
const method = params.type === "qemu" ? "POST" : "PUT";
|
||||
|
||||
// commit action
|
||||
@@ -151,7 +157,8 @@ router.post("/:disk/resize", async (req, res) => {
|
||||
const request = {};
|
||||
request[storage] = Number(params.size * 1024 ** 3); // setup request object
|
||||
// check request approval
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
const { approved } = await approveResources(req, userObj, request, params.node);
|
||||
if (!approved) {
|
||||
res.status(500).send({ request, error: `Storage ${storage} could not fulfill request of size ${params.size}G.` });
|
||||
res.end();
|
||||
return;
|
||||
@@ -213,7 +220,8 @@ router.post("/:disk/move", async (req, res) => {
|
||||
request[dstStorage] = Number(size); // always decrease destination storage by size
|
||||
}
|
||||
// check request approval
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
const { approved } = await approveResources(req, userObj, request, params.node);
|
||||
if (!approved) {
|
||||
res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` });
|
||||
res.end();
|
||||
return;
|
||||
@@ -329,7 +337,8 @@ router.post("/:disk/create", async (req, res) => {
|
||||
// setup request
|
||||
request[params.storage] = Number(params.size * 1024 ** 3);
|
||||
// check request approval
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
const { approved } = await approveResources(req, userObj, request, params.node);
|
||||
if (!approved) {
|
||||
res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` });
|
||||
res.end();
|
||||
return;
|
||||
@@ -348,7 +357,7 @@ router.post("/:disk/create", async (req, res) => {
|
||||
action[params.disk] = `${params.iso},media=cdrom`;
|
||||
}
|
||||
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
|
||||
action[params.disk] = `${params.storage}:${params.size},mp=/${params.disk}/,backup=1`;
|
||||
|
||||
+12
-29
@@ -26,17 +26,10 @@ router.post("/:netid/create", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
netid: Number(req.params.netid.replace("net", "")),
|
||||
netid: req.params.netid,
|
||||
rate: req.body.rate,
|
||||
name: req.body.name
|
||||
};
|
||||
// check netid is a valid number
|
||||
if (isNaN(params.netid)) {
|
||||
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
||||
@@ -46,7 +39,7 @@ router.post("/:netid/create", async (req, res) => {
|
||||
// net interface must not exist
|
||||
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
|
||||
if (net) {
|
||||
res.status(500).send({ error: `Network interface net${params.netid} already exists.` });
|
||||
res.status(500).send({ error: `Network interface ${params.netid} already exists.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
@@ -60,7 +53,8 @@ router.post("/:netid/create", async (req, res) => {
|
||||
};
|
||||
// check resource approval
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
const { approved } = await approveResources(req, userObj, request, params.node);
|
||||
if (!approved) {
|
||||
res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
|
||||
res.end();
|
||||
return;
|
||||
@@ -69,10 +63,10 @@ router.post("/:netid/create", async (req, res) => {
|
||||
const nc = (await global.userManager.getUser(userObj, req.cookies)).templates.network[params.type];
|
||||
const action = {};
|
||||
if (params.type === "lxc") {
|
||||
action[`net${params.netid}`] = `name=${params.name},bridge=${nc.bridge},ip=${nc.ip},ip6=${nc.ip6},tag=${nc.vlan},type=${nc.type},rate=${params.rate}`;
|
||||
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 {
|
||||
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";
|
||||
// commit action
|
||||
@@ -102,15 +96,9 @@ router.post("/:netid/modify", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
netid: Number(req.params.netid.replace("net", "")),
|
||||
netid: req.params.netid,
|
||||
rate: req.body.rate
|
||||
};
|
||||
// check netid is a valid number
|
||||
if (isNaN(params.netid)) {
|
||||
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
||||
@@ -129,14 +117,15 @@ router.post("/:netid/modify", async (req, res) => {
|
||||
};
|
||||
// check resource approval
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
const { approved } = await approveResources(req, userObj, request, params.node);
|
||||
if (!approved) {
|
||||
res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// setup action
|
||||
const action = {};
|
||||
action[`net${params.netid}`] = net.value.replace(`rate=${net.rate}`, `rate=${params.rate}`);
|
||||
action[`${params.netid}`] = net.value.replace(`rate=${net.rate}`, `rate=${params.rate}`);
|
||||
const method = params.type === "qemu" ? "POST" : "PUT";
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
|
||||
@@ -163,14 +152,8 @@ router.delete("/:netid/delete", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
netid: Number(req.params.netid.replace("net", ""))
|
||||
netid: req.params.netid
|
||||
};
|
||||
// check netid is a valid number
|
||||
if (isNaN(params.netid)) {
|
||||
res.status(500).send({ error: `Network interface id must be a number, got ${req.params.netid}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
||||
@@ -187,7 +170,7 @@ router.delete("/:netid/delete", async (req, res) => {
|
||||
// setup action
|
||||
const method = params.type === "qemu" ? "POST" : "PUT";
|
||||
// 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 }, { delete: `${params.netid}` });
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
await global.pve.syncInstance(params.node, params.vmid);
|
||||
});
|
||||
|
||||
+18
-41
@@ -22,14 +22,8 @@ router.get("/:hostpci", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
hostpci: Number(req.params.hostpci.replace("hostpci", ""))
|
||||
hostpci: req.params.hostpci
|
||||
};
|
||||
// check hostpci is a valid number
|
||||
if (isNaN(params.hostpci)) {
|
||||
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check auth for specific instance
|
||||
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
|
||||
const auth = await checkAuth(req.cookies, res, vmpath);
|
||||
@@ -39,7 +33,7 @@ router.get("/:hostpci", async (req, res) => {
|
||||
// get device
|
||||
const device = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||
if (!device) {
|
||||
res.status(500).send({ error: `Could not find hostpci${params.hostpci}=${device} in ${params.node}.` });
|
||||
res.status(500).send({ error: `Could not find ${params.hostpci}=${device} in ${params.node}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
@@ -68,16 +62,10 @@ router.post("/:hostpci/modify", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
hostpci: Number(req.params.hostpci.replace("hostpci", "")),
|
||||
hostpci: req.params.hostpci,
|
||||
device: req.body.device,
|
||||
pcie: req.body.pcie
|
||||
};
|
||||
// check hostpci is a valid number
|
||||
if (isNaN(params.hostpci)) {
|
||||
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check if type is qemu
|
||||
if (params.type !== "qemu") {
|
||||
res.status(500).send({ error: "Type must be qemu (vm)." });
|
||||
@@ -95,13 +83,13 @@ router.post("/:hostpci/modify", async (req, res) => {
|
||||
// device must exist to be modified
|
||||
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||
if (!existingDevice) {
|
||||
res.status(500).send({ error: `No device in hostpci${params.hostpci}.` });
|
||||
res.status(500).send({ error: `No device in ${params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// only check user and node availability if base id is different, we do the split in case of existing partial-function hostpci
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
if (existingDevice.device_id.split(".")[0] !== params.device) {
|
||||
if (existingDevice.device_bus.split(".")[0] !== params.device) {
|
||||
// setup request
|
||||
const node = await global.pve.getNode(params.node);
|
||||
const requestedDevice = node.devices[`${params.device}`];
|
||||
@@ -112,13 +100,14 @@ router.post("/:hostpci/modify", async (req, res) => {
|
||||
return;
|
||||
}
|
||||
// check resource approval
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
const { approved } = await approveResources(req, userObj, request, params.node);
|
||||
if (!approved) {
|
||||
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check node availability
|
||||
if (!Object.values(node.devices).some(element => element.device_id.split(".")[0] === params.device && element.reserved === false)) {
|
||||
if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) {
|
||||
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
|
||||
res.end();
|
||||
return;
|
||||
@@ -126,7 +115,7 @@ router.post("/:hostpci/modify", async (req, res) => {
|
||||
}
|
||||
// setup action
|
||||
const action = {};
|
||||
action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
||||
action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
@@ -153,16 +142,10 @@ router.post("/:hostpci/create", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
hostpci: Number(req.params.hostpci.replace("hostpci", "")),
|
||||
hostpci: req.params.hostpci,
|
||||
device: req.body.device,
|
||||
pcie: req.body.pcie
|
||||
};
|
||||
// check hostpci is a valid number
|
||||
if (isNaN(params.hostpci)) {
|
||||
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check if type is qemu
|
||||
if (params.type !== "qemu") {
|
||||
res.status(500).send({ error: "Type must be qemu (vm)." });
|
||||
@@ -180,7 +163,7 @@ router.post("/:hostpci/create", async (req, res) => {
|
||||
// device must not exist to be added
|
||||
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||
if (existingDevice) {
|
||||
res.status(500).send({ error: `Existing device in hostpci${params.hostpci}.` });
|
||||
res.status(500).send({ error: `Existing device in ${params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
@@ -190,21 +173,21 @@ router.post("/:hostpci/create", async (req, res) => {
|
||||
const request = { pci: requestedDevice.device_name };
|
||||
// check resource approval
|
||||
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
|
||||
if (!await approveResources(req, userObj, request, params.node)) {
|
||||
const { approved } = await approveResources(req, userObj, request, params.node);
|
||||
if (!approved) {
|
||||
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check node availability
|
||||
// const node = await global.pve.getNode(params.node);
|
||||
if (!Object.values(node.devices).some(element => element.device_id.split(".")[0] === params.device && element.reserved === false)) {
|
||||
if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) {
|
||||
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// setup action
|
||||
const action = {};
|
||||
action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
||||
action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
|
||||
// commit action
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
@@ -230,14 +213,8 @@ router.delete("/:hostpci/delete", async (req, res) => {
|
||||
node: req.params.node,
|
||||
type: req.params.type,
|
||||
vmid: req.params.vmid,
|
||||
hostpci: Number(req.params.hostpci.replace("hostpci", ""))
|
||||
hostpci: req.params.hostpci
|
||||
};
|
||||
// check hostpci is a valid number
|
||||
if (isNaN(params.hostpci)) {
|
||||
res.status(500).send({ error: `Hostpci id must be a number, got ${req.params.hostpci}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// check if type is qemu
|
||||
if (params.type !== "qemu") {
|
||||
res.status(500).send({ error: "Type must be qemu (vm)." });
|
||||
@@ -253,12 +230,12 @@ router.delete("/:hostpci/delete", async (req, res) => {
|
||||
// check device is in instance config
|
||||
const device = global.pve.getDevice(params.node, params.vmid, params.hostpci);
|
||||
if (!device) {
|
||||
res.status(500).send({ error: `Could not find hostpci${params.hostpci} in ${params.vmid}.` });
|
||||
res.status(500).send({ error: `Could not find ${params.hostpci} in ${params.vmid}.` });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
// 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
|
||||
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
|
||||
await global.pve.handleResponse(params.node, result, res);
|
||||
|
||||
+12
-2
@@ -73,7 +73,12 @@ router.get("/vm-isos", async (req, res) => {
|
||||
// get user iso config
|
||||
const userIsoConfig = config.useriso;
|
||||
// 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.data;
|
||||
const userIsos = [];
|
||||
isos.forEach((iso) => {
|
||||
iso.name = iso.volid.replace(`${userIsoConfig.storage}:iso/`, "");
|
||||
@@ -98,7 +103,12 @@ router.get("/ct-templates", async (req, res) => {
|
||||
// get user iso config
|
||||
const userIsoConfig = config.useriso;
|
||||
// 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.data;
|
||||
const userIsos = [];
|
||||
isos.forEach((iso) => {
|
||||
iso.name = iso.volid.replace(`${userIsoConfig.storage}:vztmpl/`, "");
|
||||
|
||||
+31
-16
@@ -222,19 +222,24 @@ export async function getUserResources (req, user) {
|
||||
* @param {Object} req ProxmoxAAS API request object.
|
||||
* @param {{id: string, realm: string}} user object of user requesting additional resources.
|
||||
* @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) {
|
||||
const dbResources = global.config.resources;
|
||||
const userResources = await getUserResources(req, user);
|
||||
let approved = true;
|
||||
Object.keys(request).every((key) => {
|
||||
// let approved = true;
|
||||
const reason = {};
|
||||
|
||||
for (const key in request) {
|
||||
// if requested resource is not specified in user resources, assume it's not allowed
|
||||
if (!(key in userResources)) {
|
||||
approved = false;
|
||||
return false;
|
||||
// approved = false;
|
||||
reason[key] = { approved: false, reason: `${key} not allowed` };
|
||||
continue;
|
||||
// return;
|
||||
}
|
||||
|
||||
// use node specific quota if there is one available, otherwise use the global resource quota
|
||||
const inNode = node in userResources[key].nodes;
|
||||
const resourceData = inNode ? userResources[key].nodes[node] : userResources[key].global;
|
||||
|
||||
@@ -244,24 +249,34 @@ export async function approveResources (req, user, request, node) {
|
||||
// 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;
|
||||
if (avail !== dbResources[key].whitelist) {
|
||||
approved = false;
|
||||
return false;
|
||||
// approved = false;
|
||||
reason[key] = { approved: false, reason: `${key} ${dbResources[key].whitelist ? "not in whitelist" : "in blacklist"}` };
|
||||
// return;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// if either the requested or avail resource is NaN, block
|
||||
else if (isNaN(resourceData.avail) || isNaN(request[key])) {
|
||||
approved = false;
|
||||
return false;
|
||||
// if either the requested or avail resource is not strictly a number, block
|
||||
else if (typeof (resourceData.avail) !== "number" || typeof (request[key]) !== "number") {
|
||||
// approved = false;
|
||||
reason[key] = { approved: false, reason: `expected ${key} to be a number but got ${request[key]}` };
|
||||
continue;
|
||||
// return;
|
||||
}
|
||||
// if the avail resources is less than the requested resources, block
|
||||
else if (resourceData.avail - request[key] < 0) {
|
||||
approved = false;
|
||||
return false;
|
||||
// approved = false;
|
||||
reason[key] = { approved: false, reason: `${key} requested ${request[key]} which is more than ${resourceData.avail} available` };
|
||||
continue;
|
||||
// return;
|
||||
}
|
||||
|
||||
return true;
|
||||
reason[key] = { approved: true, reason: "ok" };
|
||||
}
|
||||
|
||||
const approved = Object.values(reason).every((element) => {
|
||||
return element.approved === true;
|
||||
});
|
||||
return approved; // if all requested resources pass, allow
|
||||
return { approved, reason }; // if all requested resources pass, allow
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,7 +331,7 @@ export function readJSONFile (path) {
|
||||
return JSON.parse(readFileSync(path));
|
||||
}
|
||||
catch (e) {
|
||||
console.log(`error: ${path} was not found.`);
|
||||
console.log(`error opening ${path}: ${e}`);
|
||||
exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user