308 lines
10 KiB
JavaScript
308 lines
10 KiB
JavaScript
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 });
|
|
}
|
|
}
|
|
});
|