From 8ef05936d82d7cd7d56635b0eed638b091b395b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Su=C5=A1nik?= Date: Mon, 18 Jul 2022 14:40:28 +0200 Subject: [PATCH] ZFS and sudo permissions related improvements --- examples/zfs-generic-nfs.yaml | 9 + src/driver/controller-zfs-generic/index.js | 56 ++++-- src/driver/controller-zfs-local/index.js | 1 + src/driver/controller-zfs/index.js | 5 +- src/driver/freenas/ssh.js | 1 + src/utils/zfs.js | 214 +++++++++++++++++++-- 6 files changed, 243 insertions(+), 43 deletions(-) diff --git a/examples/zfs-generic-nfs.yaml b/examples/zfs-generic-nfs.yaml index e068c29..fd8a531 100644 --- a/examples/zfs-generic-nfs.yaml +++ b/examples/zfs-generic-nfs.yaml @@ -15,6 +15,15 @@ zfs: # the example below is useful for TrueNAS 12 #cli: # sudoEnabled: true + # + # instead of setting sudoEnabled to true, sudo can be + # reduced only to specific (supported) zfs commands + # sudoEnabledCommands: + # - mount + # - unmount + # - share + # - unshare + # # paths: # zfs: /usr/local/sbin/zfs # zpool: /usr/local/sbin/zpool diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 991a922..f42dbb2 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -36,6 +36,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { } options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); + options.sudoCommands = _.get(this.options, "zfs.cli.sudoEnabledCommands", []); if (typeof this.setZetabyteCustomOptions === "function") { await this.setZetabyteCustomOptions(options); @@ -79,6 +80,37 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { return name; } + async setShareProperty(zb, datasetName, propertyDict) { + try { + await zb.zfs.set(datasetName, propertyDict); + } catch (err) { + if ( + err.toString().includes("unable to reshare") && + zb.options.sudoCommands.includes("mount") + ) { + await zb.zfs.share(datasetName); + } else { + throw err; + } + } + } + + async unsetShareProperty(zb, datasetName, propertyKey) { + if (zb.options.sudoCommands.includes("unshare")) { + await zb.zfs.unshare(datasetName, { idempotent: true }); + } + + try { + await zb.zfs.inherit(datasetName, propertyKey); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + // do nothing + } else { + throw err; + } + } + } + /** * should create any necessary share resources * should set the SHARE_VOLUME_CONTEXT_PROPERTY_NAME propery @@ -105,7 +137,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { key ] ) { - await zb.zfs.set(datasetName, { + await this.setShareProperty(zb, datasetName, { [key]: this.options.nfs.shareStrategySetDatasetProperties .properties[key], @@ -139,7 +171,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { key ] ) { - await zb.zfs.set(datasetName, { + await this.setShareProperty(zb, datasetName, { [key]: this.options.smb.shareStrategySetDatasetProperties .properties[key], @@ -316,15 +348,7 @@ create /backstores/block/${iscsiName} key ] ) { - try { - await zb.zfs.inherit(datasetName, key); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - // do nothing - } else { - throw err; - } - } + await this.unsetShareProperty(zb, datasetName, key); } } await GeneralUtils.sleep(2000); // let things settle @@ -346,15 +370,7 @@ create /backstores/block/${iscsiName} key ] ) { - try { - await zb.zfs.inherit(datasetName, key); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - // do nothing - } else { - throw err; - } - } + await this.unsetShareProperty(zb, datasetName, key); } } await GeneralUtils.sleep(2000); // let things settle diff --git a/src/driver/controller-zfs-local/index.js b/src/driver/controller-zfs-local/index.js index 3c1911b..f3a01a3 100644 --- a/src/driver/controller-zfs-local/index.js +++ b/src/driver/controller-zfs-local/index.js @@ -66,6 +66,7 @@ class ControllerZfsLocalDriver extends ControllerZfsBaseDriver { }; options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); + options.sudoCommands = _.get(this.options, "zfs.cli.sudoEnabledCommands", []); if (typeof this.setZetabyteCustomOptions === "function") { await this.setZetabyteCustomOptions(options); diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index ccc5b01..1546660 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -159,7 +159,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { driver.ctx.logger.verbose("whoami command: %s", command); const response = await execClient.exec(command); if (response.code !== 0) { - throw new Error("failed to run uname to determine max zvol name length"); + throw new Error("failed to run whoami to determine name of user"); } else { return response.stdout.trim(); } @@ -475,9 +475,10 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { async setFilesystemMode(path, mode) { const driver = this; const execClient = this.getExecClient(); + const sudoEnabled = _.get(this.options, "zfs.cli.sudoEnabled", false); let command = execClient.buildCommand("chmod", [mode, path]); - if ((await driver.getWhoAmI()) != "root") { + if ((await driver.getWhoAmI()) != "root" && sudoEnabled) { command = (await driver.getSudoPath()) + " " + command; } diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 1920202..b34f126 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -53,6 +53,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { } options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); + options.sudoCommands = _.get(this.options, "zfs.cli.sudoEnabledCommands", []); if (typeof this.setZetabyteCustomOptions === "function") { await this.setZetabyteCustomOptions(options); diff --git a/src/utils/zfs.js b/src/utils/zfs.js index a26db89..bce3804 100644 --- a/src/utils/zfs.js +++ b/src/utils/zfs.js @@ -904,6 +904,13 @@ class Zetabyte { }; zb.zfs = { + SUDO_COMMANDS: [ + "mount", + "unmount", + "share", + "unshare" + ], + /** * zfs create [-pu] [-o property=value]... filesystem * zfs create [-ps] [-b blocksize] [-o property=value]... -V size volume @@ -913,6 +920,7 @@ class Zetabyte { */ create: function (dataset, options = {}) { if (!(arguments.length >= 1)) throw new (Error("Invalid arguments"))(); + const zfs = this; return new Promise((resolve, reject) => { const idempotent = @@ -921,11 +929,12 @@ class Zetabyte { : "idempotent" in zb.options ? zb.options.idempotent : false; + const shouldSudoMount = !zb.options.sudo && zb.options.sudoCommands.includes("mount"); let args = []; args.push("create"); if (options.parents) args.push("-p"); - if (options.unmounted) args.push("-u"); + if (options.unmounted || shouldSudoMount) args.push("-u"); if (options.blocksize) args = args.concat(["-b", options.blocksize]); if (options.properties) { for (let [key, value] of Object.entries(options.properties)) { @@ -945,9 +954,17 @@ class Zetabyte { if ( error && !(idempotent && stderr.includes("dataset already exists")) - ) + ) { return reject(zb.helpers.zfsError(error, stderr)); - return resolve(stdout); + } + + if (shouldSudoMount) { + zfs.mount(dataset, { idempotent }) + .then(out => resolve(stdout + "; " + out)) + .catch(err => reject(err)); + } else { + return resolve(stdout); + } } ); }); @@ -964,6 +981,7 @@ class Zetabyte { */ destroy: function (dataset, options = {}) { if (!(arguments.length >= 1)) throw Error("Invalid arguments"); + const zfs = this; return new Promise((resolve, reject) => { const idempotent = @@ -985,23 +1003,32 @@ class Zetabyte { if (options.defer) args.push("-d"); args.push(dataset); - zb.exec( - zb.options.paths.zfs, - args, - { timeout: zb.options.timeout }, - function (error, stdout, stderr) { - if ( - error && - !( - idempotent && - (stderr.includes("dataset does not exist") || - stderr.includes("could not find any snapshots to destroy")) + const destroyExec = function() { + zb.exec( + zb.options.paths.zfs, + args, + { timeout: zb.options.timeout }, + function (error, stdout, stderr) { + if ( + error && + !( + idempotent && + (stderr.includes("dataset does not exist") || + stderr.includes("could not find any snapshots to destroy")) + ) ) - ) - return reject(zb.helpers.zfsError(error, stderr)); - return resolve(stdout); - } - ); + return reject(zb.helpers.zfsError(error, stderr)); + return resolve(stdout); + } + ); + }; + if (zb.options.sudoCommands.includes("unmount")) { + zfs.unmount(dataset, { idempotent }) + .then(() => destroyExec()) + .catch(err => reject(err)); + } else { + destroyExec(); + } }); }, @@ -1224,7 +1251,7 @@ class Zetabyte { let args = []; args.push("rename"); if (options.parents) args.push("-p"); - if (options.unmounted) args.push("-u"); + if (options.unmounted || (!zb.options.sudo && zb.options.sudoCommands.includes("mount"))) args.push("-u"); if (options.force) args.push("-f"); if (options.recurse) args.push("-r"); args.push(source); @@ -1522,6 +1549,145 @@ class Zetabyte { ); }); }, + + /** + * zfs mount filesystem + * + * @param {*} dataset + * @param {*} options + */ + mount: function (dataset, options = {}) { + if (!(arguments.length >= 1)) throw Error("Invalid arguments"); + + return new Promise((resolve, reject) => { + const idempotent = + "idempotent" in options + ? options.idempotent + : "idempotent" in zb.options + ? zb.options.idempotent + : false; + + let args = []; + args.push("mount"); + args.push(dataset); + + zb.exec( + zb.options.paths.zfs, + args, + { timeout: zb.options.timeout }, + function (error, stdout, stderr) { + if ( + error && + !(idempotent && stderr.includes("filesystem already mounted")) + ) { + return reject(zb.helpers.zfsError(error, stderr)); + } + return resolve(stdout); + } + ); + }); + }, + + /** + * zfs unmount filesystem + * + * @param {*} dataset + * @param {*} options + */ + unmount: function (dataset, options = {}) { + if (!(arguments.length >= 1)) throw Error("Invalid arguments"); + + return new Promise((resolve, reject) => { + const idempotent = + "idempotent" in options + ? options.idempotent + : "idempotent" in zb.options + ? zb.options.idempotent + : false; + + let args = []; + args.push("unmount"); + args.push(dataset); + + zb.exec( + zb.options.paths.zfs, + args, + { timeout: zb.options.timeout }, + function (error, stdout, stderr) { + if ( + error && + !(idempotent && stderr.includes("not currently mounted")) + ) { + return reject(zb.helpers.zfsError(error, stderr)); + } + return resolve(stdout); + } + ); + }); + }, + + /** + * zfs share filesystem + * + * @param {*} dataset + */ + share: function (dataset) { + if (arguments.length != 1) throw Error("Invalid arguments"); + + return new Promise((resolve, reject) => { + let args = []; + args.push("share"); + args.push(dataset); + + zb.exec( + zb.options.paths.zfs, + args, + { timeout: zb.options.timeout }, + function (error, stdout, stderr) { + if (error) return reject(zb.helpers.zfsError(error, stderr)); + return resolve(stdout); + } + ); + }); + }, + + /** + * zfs unshare filesystem + * + * @param {*} dataset + * @param {*} options + */ + unshare: function (dataset, options = {}) { + if (!(arguments.length >= 1)) throw Error("Invalid arguments"); + + return new Promise((resolve, reject) => { + const idempotent = + "idempotent" in options + ? options.idempotent + : "idempotent" in zb.options + ? zb.options.idempotent + : false; + + let args = []; + args.push("unshare"); + args.push(dataset); + + zb.exec( + zb.options.paths.zfs, + args, + { timeout: zb.options.timeout }, + function (error, stdout, stderr) { + if ( + error && + !(idempotent && stderr.includes("not currently shared")) + ) { + return reject(zb.helpers.zfsError(error, stderr)); + } + return resolve(stdout); + } + ); + }); + }, }; } @@ -1564,7 +1730,13 @@ class Zetabyte { use_sudo = options.sudo; } - if (use_sudo) { + if ( + use_sudo || ( + Array.isArray(args) && args.length > 0 && + zb.zfs.SUDO_COMMANDS.includes(args[0]) && + zb.options.sudoCommands.includes(args[0]) + ) + ) { args = args || []; args.unshift(command); command = zb.options.paths.sudo;