improve method comments relating to pve builtin return objects,

add paths to create and delete network interfaces,
check for interface existence in modify interface,
add vlan specification to user config and use in network interface creation,
fix disk path security by checking disk existence or nonexistence,
TODO: check create and mount disk against allowed bus types

Signed-off-by: Arthur Lu <learthurgo@gmail.com>
This commit is contained in:
Arthur Lu 2023-06-08 23:33:32 +00:00
parent 3f98355d32
commit 825f7ccea1
2 changed files with 158 additions and 29 deletions

View File

@ -77,6 +77,7 @@
"max": 199 "max": 199
}, },
"pool": "examplepool", "pool": "examplepool",
"vlan": "100",
"templates": { "templates": {
"lxc": { "lxc": {
"net0": { "net0": {

186
main.js
View File

@ -157,16 +157,25 @@ app.get("/api/user/nodes", async (req, res) => {
* - vmid: Number - vm id number * - vmid: Number - vm id number
* - disk: String - disk id (sata0, NOT unused) * - disk: String - disk id (sata0, NOT unused)
* responses: * responses:
* - 200: Object(pve_task_object) * - 200: PVE Task Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {error: String} * - 500: {error: String}
* - 500: Object(pve_task_object) * - 500: PVE Task Object
*/ */
app.post("/api/instance/disk/detach", async (req, res) => { app.post("/api/instance/disk/detach", async (req, res) => {
// check auth for specific instance // check auth for specific instance
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
let auth = await checkAuth(req.cookies, res, vmpath); let auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { return; } if (!auth) { return; }
// get current config
let config = (await requestPVE(`${vmpath}/config`, "GET", req.cookies, null, null)).data.data;
// disk must exist
if (!config[req.body.disk]) {
res.status(500).send({ error: `Disk ${req.body.disk} does not exist.` });
res.end();
return;
}
// disk cannot be unused
if (req.body.disk.includes("unused")) { if (req.body.disk.includes("unused")) {
res.status(500).send({ error: `Requested disk ${req.body.disk} cannot be unused. Use /disk/delete to permanently delete unused disks.` }); res.status(500).send({ error: `Requested disk ${req.body.disk} cannot be unused. Use /disk/delete to permanently delete unused disks.` });
res.end(); res.end();
@ -187,23 +196,25 @@ app.post("/api/instance/disk/detach", async (req, res) => {
* - disk: String - disk id (sata0) * - disk: String - disk id (sata0)
* - source: Number - source unused disk number (0 => unused0) * - source: Number - source unused disk number (0 => unused0)
* responses: * responses:
* - 200: Object(pve_task_object) * - 200: PVE Task Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {error: String} * - 500: {error: String}
* - 500: Object(pve_task_object) * - 500: PVE Task Object
*/ */
app.post("/api/instance/disk/attach", async (req, res) => { app.post("/api/instance/disk/attach", async (req, res) => {
// check auth for specific instance // check auth for specific instance
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
let auth = await checkAuth(req.cookies, res, vmpath); let auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { return; } if (!auth) { return; }
// get current config and check if unused disk exists // get current config
let config = await requestPVE(`${vmpath}/config`, "GET", req.cookies, null, null); let config = (await requestPVE(`${vmpath}/config`, "GET", req.cookies, null, null)).data.data;
if (!config.data.data[`unused${req.body.source}`]) { // disk must exist
res.status(403).send({ error: `requested disk unused${req.body.source} does not exist` }); if (!config[`unused${req.body.source}`]) {
res.status(403).send({ error: `Requested disk unused${req.body.source} does not exist.` });
res.end(); res.end();
return; return;
} }
// TODO: check create and mount disk against allowed bus types
let sourceDisk = config.data.data[`unused${req.body.source}`]; let sourceDisk = config.data.data[`unused${req.body.source}`];
// setup action using source disk info from vm config // setup action using source disk info from vm config
let action = {}; let action = {};
@ -224,11 +235,11 @@ app.post("/api/instance/disk/attach", async (req, res) => {
* - disk: String - disk id (sata0) * - disk: String - disk id (sata0)
* - size: Number - increase size in GiB * - size: Number - increase size in GiB
* responses: * responses:
* - 200: Object(pve_task_object) * - 200: PVE Task Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {error: String} * - 500: {error: String}
* - 500: {request: Object, error: String} * - 500: {request: Object, error: String}
* - 500: Object(pve_task_object) * - 500: PVE Task Object
*/ */
app.post("/api/instance/disk/resize", async (req, res) => { app.post("/api/instance/disk/resize", async (req, res) => {
// check auth for specific instance // check auth for specific instance
@ -268,11 +279,11 @@ app.post("/api/instance/disk/resize", async (req, res) => {
* - storage: String - target storage to move disk * - storage: String - target storage to move disk
* - delete: Number - delete original disk (0, 1) * - delete: Number - delete original disk (0, 1)
* responses: * responses:
* - 200: Object(pve_task_object) * - 200: PVE Task Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {error: String} * - 500: {error: String}
* - 500: {request: Object, error: String} * - 500: {request: Object, error: String}
* - 500: Object(pve_task_object) * - 500: PVE Task Object
*/ */
app.post("/api/instance/disk/move", async (req, res) => { app.post("/api/instance/disk/move", async (req, res) => {
// check auth for specific instance // check auth for specific instance
@ -318,23 +329,31 @@ app.post("/api/instance/disk/move", async (req, res) => {
}); });
/** /**
* POST - delete unused disk permanently * DELETE - delete unused disk permanently
* request: * request:
* - node: String - vm host node id * - node: String - vm host node id
* - type: String - vm type (lxc, qemu) * - type: String - vm type (lxc, qemu)
* - vmid: Number - vm id number * - vmid: Number - vm id number
* - disk: String - disk id (sata0) * - disk: String - disk id (unused0 or ide0)
* responses: * responses:
* - 200: Object(pve_task_object) * - 200: PVE Task Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {error: String} * - 500: {error: String}
* - 500: Object(pve_task_object) * - 500: PVE Task Object
*/ */
app.post("/api/instance/disk/delete", async (req, res) => { app.delete("/api/instance/disk/delete", async (req, res) => {
// check auth for specific instance // check auth for specific instance
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
let auth = await checkAuth(req.cookies, res, vmpath); let auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { return; } if (!auth) { return; }
// get current config
let config = (await requestPVE(`${vmpath}/config`, "GET", req.cookies, null, null)).data.data;
// disk must exist
if (!config[req.body.disk]) {
res.status(403).send({ error: `Requested disk unused${req.body.source} does not exist.` });
res.end();
return;
}
// only ide or unused are allowed to be deleted // only ide or unused are allowed to be deleted
if (!req.body.disk.includes("unused") && !req.body.disk.includes("ide")) { // must be ide or unused if (!req.body.disk.includes("unused") && !req.body.disk.includes("ide")) { // must be ide or unused
res.status(500).send({ error: `Requested disk ${req.body.disk} must be unused or ide. Use /disk/detach to detach disks in use.` }); res.status(500).send({ error: `Requested disk ${req.body.disk} must be unused or ide. Use /disk/detach to detach disks in use.` });
@ -359,17 +378,26 @@ app.post("/api/instance/disk/delete", async (req, res) => {
* - storage: String - storage to hold disk * - storage: String - storage to hold disk
* - size: Number size of disk in GiB * - size: Number size of disk in GiB
* responses: * responses:
* - 200: Object(pve_task_object) * - 200: PVE Task Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {request: Object, error: String} * - 500: {request: Object, error: String}
* - 500: Object(pve_task_object) * - 500: PVE Task Object
*/ */
app.post("/api/instance/disk/create", async (req, res) => { app.post("/api/instance/disk/create", async (req, res) => {
// check auth for specific instance // check auth for specific instance
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
let auth = await checkAuth(req.cookies, res, vmpath); let auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { return; } if (!auth) { return; }
// get current config
let config = (await requestPVE(`${vmpath}/config`, "GET", req.cookies, null, null)).data.data;
// disk must not exist
if (config[req.body.disk]) {
res.status(403).send({ error: `Requested disk ${req.body.disk} already exists.` });
res.end();
return;
}
// setup request // setup request
// TODO: check create and mount disk against allowed bus types
let request = {}; let request = {};
if (!req.body.disk.includes("ide")) { if (!req.body.disk.includes("ide")) {
request[req.body.storage] = Number(req.body.size * 1024 ** 3); // setup request object request[req.body.storage] = Number(req.body.size * 1024 ** 3); // setup request object
@ -398,6 +426,65 @@ app.post("/api/instance/disk/create", async (req, res) => {
await handleResponse(req.body.node, result, res); await handleResponse(req.body.node, result, res);
}); });
/**
* POST - create new virtual network interface
* request:
* - node: String - vm host node id
* - type: String - vm type (lxc, qemu)
* - vmid: Number - vm id number
* - netid: Number - network interface id number (0 => net0)
* - rate: Number - new bandwidth rate for interface in MB/s
* - name: String, optional - required interface name for lxc only
* responses:
* - 200: PVE Task Object
* - 401: {auth: false, path: String}
* - 500: {error: String}
* - 500: {request: Object, error: String}
* - 500: PVE Task Object
*/
app.post("/api/instance/network/create", async (req, res) => {
// check auth for specific instance
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
let auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { return; }
// get current config
let currentConfig = await requestPVE(`/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}/config`, "GET", null, null, pveAPIToken);
// net interface must not exist
if (currentConfig.data.data[`net${req.body.netid}`]) {
res.status(500).send({ error: `Network interface net${req.body.netid} already exists. Use /api/instance.network/modify to modify existing network interface.` });
res.end();
return;
}
if (req.body.type === "lxc" && !req.body.name) {
res.status(500).send({ error: `Network interface must have name parameter.` });
res.end();
return;
}
let request = {
network: Number(req.body.rate)
};
// check resource approval
if (!await approveResources(req, req.cookies.username, request)) {
res.status(500).send({ request: request, error: `Could not fulfil network request of ${req.body.rate}MB/s.` });
res.end();
return;
}
// setup action
let vlan = getUserConfig().instances.vlan;
let action = {};
if (type === "lxc") {
action[`net${req.body.netid}`] = `name=${req.body.name},bridge=vmbr0,ip=dhcp,ip6=dhcp,tag=${vlan},type=veth,rate=${req.body.rate}`;
}
else {
action[`new${req.body.netid}`] = `virtio,bridge=vmbr0,tag=${vlan},rate=${req.body.rate}`;
}
action = JSON.stringify(action);
let method = req.body.type === "qemu" ? "POST" : "PUT";
// commit action
let result = await requestPVE(`${vmpath}/config`, method, req.cookies, action, pveAPIToken);
await handleResponse(req.body.node, result, res);
});
/** /**
* POST - modify virtual network interface * POST - modify virtual network interface
* request: * request:
@ -407,18 +494,25 @@ app.post("/api/instance/disk/create", async (req, res) => {
* - netid: Number - network interface id number (0 => net0) * - netid: Number - network interface id number (0 => net0)
* - rate: Number - new bandwidth rate for interface in MB/s * - rate: Number - new bandwidth rate for interface in MB/s
* responses: * responses:
* - 200: Object(pve_task_object) * - 200: PVE Task Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {error: String}
* - 500: {request: Object, error: String} * - 500: {request: Object, error: String}
* - 500: Object(pve_task_object) * - 500: PVE Task Object
*/ */
app.post("/api/instance/network", async (req, res) => { app.post("/api/instance/network/modify", async (req, res) => {
// check auth for specific instance // check auth for specific instance
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`; let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
let auth = await checkAuth(req.cookies, res, vmpath); let auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { return; } if (!auth) { return; }
// get current config // get current config
let currentConfig = await requestPVE(`/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}/config`, "GET", null, null, pveAPIToken); let currentConfig = await requestPVE(`/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}/config`, "GET", null, null, pveAPIToken);
// net interface must already exist
if (!currentConfig.data.data[`net${req.body.netid}`]) {
res.status(500).send({ error: `Network interface net${req.body.netid} does not exist. Use /api/instance/network/create to create a new network interface.` });
res.end();
return;
}
let currentNetworkConfig = currentConfig.data.data[`net${req.body.netid}`]; let currentNetworkConfig = currentConfig.data.data[`net${req.body.netid}`];
let currentNetworkRate = currentNetworkConfig.split("rate=")[1].split(",")[0]; let currentNetworkRate = currentNetworkConfig.split("rate=")[1].split(",")[0];
let request = { let request = {
@ -440,6 +534,40 @@ app.post("/api/instance/network", async (req, res) => {
await handleResponse(req.body.node, result, res); await handleResponse(req.body.node, result, res);
}); });
/**
* DELETE - delete virtual network interface
* request:
* - node: String - vm host node id
* - type: String - vm type (lxc, qemu)
* - vmid: Number - vm id number
* - netid: Number - network interface id number (0 => net0)
* responses:
* - 200: PVE Task Object
* - 401: {auth: false, path: String}
* - 500: {error: String}
* - 500: PVE Task Object
*/
app.delete("/api/instance/network/delete", async (req, res) => {
// check auth for specific instance
let vmpath = `/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}`;
let auth = await checkAuth(req.cookies, res, vmpath);
if (!auth) { return; }
// get current config
let currentConfig = await requestPVE(`/nodes/${req.body.node}/${req.body.type}/${req.body.vmid}/config`, "GET", null, null, pveAPIToken);
// net interface must already exist
if (!currentConfig.data.data[`net${req.body.netid}`]) {
res.status(500).send({ error: `Network interface net${req.body.netid} does not exist.` });
res.end();
return;
}
// setup action
let action = { delete: `net${req.body.netid}`};
let method = req.body.type === "qemu" ? "POST" : "PUT";
// commit action
let result = await requestPVE(`${vmpath}/config`, method, req.cookies, action, pveAPIToken);
await handleResponse(req.body.node, result, res);
});
/** /**
* GET - get instance pcie device data * GET - get instance pcie device data
* request: * request:
@ -448,7 +576,7 @@ app.post("/api/instance/network", async (req, res) => {
* - vmid: Number - vm id number to destroy * - vmid: Number - vm id number to destroy
* - hostpci: String - hostpci number * - hostpci: String - hostpci number
* responses: * responses:
* - 200: Object(pve_pci_device_object) * - 200: PVE PCI Device Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {error: String} * - 500: {error: String}
*/ */
@ -489,10 +617,10 @@ app.get("/api/instance/pci", async (req, res) => {
* - memory: Number - new amount of memory for instance * - memory: Number - new amount of memory for instance
* - swap: Number, optional - new amount of swap for instance * - swap: Number, optional - new amount of swap for instance
* responses: * responses:
* - 200: Object(pve_task_object) * - 200: PVE Task Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {request: Object, error: String} * - 500: {request: Object, error: String}
* - 500: Object(pve_task_object) * - 500: PVE Task Object
*/ */
app.post("/api/instance/resources", async (req, res) => { app.post("/api/instance/resources", async (req, res) => {
// check auth for specific instance // check auth for specific instance
@ -542,11 +670,11 @@ app.post("/api/instance/resources", async (req, res) => {
* - rootfslocation: String, optional - storage name for lxc instance rootfs * - rootfslocation: String, optional - storage name for lxc instance rootfs
* - rootfssize: Number, optional, - size of lxc instance rootfs * - rootfssize: Number, optional, - size of lxc instance rootfs
* responses: * responses:
* - 200: Object(pve_task_object) * - 200: PVE Task Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: {error: String} * - 500: {error: String}
* - 500: {request: Object, error: String} * - 500: {request: Object, error: String}
* - 500: Object(pve_task_object) * - 500: PVE Task Object
*/ */
app.post("/api/instance", async (req, res) => { app.post("/api/instance", async (req, res) => {
// check auth // check auth
@ -629,9 +757,9 @@ app.post("/api/instance", async (req, res) => {
* - type: String - vm type (lxc, qemu) * - type: String - vm type (lxc, qemu)
* - vmid: Number - vm id number to destroy * - vmid: Number - vm id number to destroy
* responses: * responses:
* - 200: Object(pve_task_object) * - 200: PVE Task Object
* - 401: {auth: false, path: String} * - 401: {auth: false, path: String}
* - 500: Object(pve_task_object) * - 500: PVE Task Object
*/ */
app.delete("/api/instance", async (req, res) => { app.delete("/api/instance", async (req, res) => {
// check auth for specific instance // check auth for specific instance