add user backup endpoints

This commit is contained in:
2025-06-25 19:41:17 +00:00
parent 3a41afb696
commit b098a173fa
4 changed files with 167 additions and 2 deletions

View File

@@ -15,13 +15,20 @@ 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 the `ProxmoxAAS-API` host
@@ -43,6 +50,8 @@ In the Proxmox web GUI, perform the following steps:
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
# Backends

View File

@@ -60,6 +60,9 @@
"node": "examplenode1",
"storage": "cephfs"
},
"backups": {
"storage": "cephfs"
},
"resources": {
"cpu": {
"type": "list",

View File

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

View File

@@ -85,6 +85,9 @@
"pools": {
"example-pool-1": true,
"example-pool-2": true
},
"backups": {
"max": 5
}
},
"templates": {