diff --git a/examples/freenas-iscsi.yaml b/examples/freenas-iscsi.yaml index 0b72554..15e29f6 100644 --- a/examples/freenas-iscsi.yaml +++ b/examples/freenas-iscsi.yaml @@ -18,6 +18,22 @@ sshConnection: ... -----END RSA PRIVATE KEY----- zfs: + # can be used to override defaults if necessary + # the example below is useful for TrueNAS 12 + #cli: + # paths: + # zfs: /usr/local/sbin/zfs + # zpool: /usr/local/sbin/zpool + # sudo: /usr/local/bin/sudo + # chroot: /usr/sbin/chroot + + # can be used to set arbitrary values on the dataset/zvol + # can use handlebars templates with the parameters from the storage class/CO + #datasetProperties: + # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" + # "org.freenas:test": "{{ parameters.foo }}" + # "org.freenas:test2": "some value" + # total volume name (zvol//) length cannot exceed 63 chars # https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab # standard volume naming overhead is 46 chars diff --git a/examples/freenas-nfs.yaml b/examples/freenas-nfs.yaml index e5da6d8..a65e9de 100644 --- a/examples/freenas-nfs.yaml +++ b/examples/freenas-nfs.yaml @@ -18,6 +18,22 @@ sshConnection: ... -----END RSA PRIVATE KEY----- zfs: + # can be used to override defaults if necessary + # the example below is useful for TrueNAS 12 + #cli: + # paths: + # zfs: /usr/local/sbin/zfs + # zpool: /usr/local/sbin/zpool + # sudo: /usr/local/bin/sudo + # chroot: /usr/sbin/chroot + + # can be used to set arbitrary values on the dataset/zvol + # can use handlebars templates with the parameters from the storage class/CO + #datasetProperties: + # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" + # "org.freenas:test": "{{ parameters.foo }}" + # "org.freenas:test2": "some value" + datasetParentName: tank/k8s/a/vols detachedSnapshotsDatasetParentName: tank/k8s/a/snaps datasetEnableQuotas: true diff --git a/examples/nfs-client.yaml b/examples/nfs-client.yaml index f0bd490..0f3110b 100644 --- a/examples/nfs-client.yaml +++ b/examples/nfs-client.yaml @@ -3,6 +3,7 @@ instance_id: nfs: shareHost: server address shareBasePath: "/some/path" + # shareHost:shareBasePath should be mounted at this location in the controller container controllerBasePath: "/storage" dirPermissionsMode: "0777" dirPermissionsUser: root diff --git a/examples/zfs-generic-iscsi.yaml b/examples/zfs-generic-iscsi.yaml index 26af41f..cda916d 100644 --- a/examples/zfs-generic-iscsi.yaml +++ b/examples/zfs-generic-iscsi.yaml @@ -14,6 +14,22 @@ service: controller: {} node: {} zfs: + # can be used to override defaults if necessary + # the example below is useful for TrueNAS 12 + #cli: + # paths: + # zfs: /usr/local/sbin/zfs + # zpool: /usr/local/sbin/zpool + # sudo: /usr/local/bin/sudo + # chroot: /usr/sbin/chroot + + # can be used to set arbitrary values on the dataset/zvol + # can use handlebars templates with the parameters from the storage class/CO + #datasetProperties: + # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" + # "org.freenas:test": "{{ parameters.foo }}" + # "org.freenas:test2": "some value" + datasetParentName: tank/k8s/test detachedSnapshotsDatasetParentName: tanks/k8s/test-snapshots diff --git a/examples/zfs-generic-nfs.yaml b/examples/zfs-generic-nfs.yaml index b06cf0a..b94c9f7 100644 --- a/examples/zfs-generic-nfs.yaml +++ b/examples/zfs-generic-nfs.yaml @@ -14,6 +14,22 @@ service: controller: {} node: {} zfs: + # can be used to override defaults if necessary + # the example below is useful for TrueNAS 12 + #cli: + # paths: + # zfs: /usr/local/sbin/zfs + # zpool: /usr/local/sbin/zpool + # sudo: /usr/local/bin/sudo + # chroot: /usr/sbin/chroot + + # can be used to set arbitrary values on the dataset/zvol + # can use handlebars templates with the parameters from the storage class/CO + #datasetProperties: + # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" + # "org.freenas:test": "{{ parameters.foo }}" + # "org.freenas:test2": "some value" + datasetParentName: tank/k8s/test detachedSnapshotsDatasetParentName: tanks/k8s/test-snapshots diff --git a/package-lock.json b/package-lock.json index 2e7e573..c5bbfa2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -820,6 +820,18 @@ "protobufjs": "^5.0.3" } }, + "handlebars": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", + "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "requires": { + "minimist": "^1.2.5", + "neo-async": "^2.6.0", + "source-map": "^0.6.1", + "uglify-js": "^3.1.4", + "wordwrap": "^1.0.0" + } + }, "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -1127,6 +1139,11 @@ "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", "optional": true }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -1502,6 +1519,11 @@ } } }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -1696,6 +1718,12 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" }, + "uglify-js": { + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.3.tgz", + "integrity": "sha512-Lh00i69Uf6G74mvYpHCI9KVVXLcHW/xu79YTvH7Mkc9zyKUeSPz0owW0dguj0Scavns3ZOh3wY63J0Zb97Za2g==", + "optional": true + }, "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", @@ -1806,6 +1834,11 @@ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" + }, "wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", diff --git a/package.json b/package.json index 8132aae..8cf70fd 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "bunyan": "^1.8.14", "eslint": "^7.4.0", "grpc-uds": "^0.1.4", + "handlebars": "^4.7.6", "js-yaml": "^3.14.0", "lru-cache": "^5.1.1", "request": "^2.88.2", diff --git a/src/driver/controller-zfs-ssh/index.js b/src/driver/controller-zfs-ssh/index.js index b432a36..39de53d 100644 --- a/src/driver/controller-zfs-ssh/index.js +++ b/src/driver/controller-zfs-ssh/index.js @@ -3,6 +3,8 @@ const SshClient = require("../../utils/ssh").SshClient; const { GrpcError, grpc } = require("../../utils/grpc"); const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); + +const Handlebars = require("handlebars"); const uuidv4 = require("uuid").v4; // zfs common properties @@ -122,10 +124,18 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { getZetabyte() { const sshClient = this.getSshClient(); - return new Zetabyte({ - executor: new ZfsSshProcessManager(sshClient), - idempotent: true, - }); + const options = {}; + options.executor = new ZfsSshProcessManager(sshClient); + options.idempotent = true; + + if ( + this.options.zfs.hasOwnProperty("cli") && + this.options.zfs.cli.hasOwnProperty("paths") + ) { + options.paths = this.options.zfs.cli.paths; + } + + return new Zetabyte(options); } getDatasetParentName() { @@ -335,6 +345,20 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { let volume_content_source_volume_id; let fullSnapshotName; let volumeProperties = {}; + + // user-supplied properties + // put early to prevent stupid (user-supplied values overwriting system values) + if (driver.options.zfs.datasetProperties) { + for (let property in driver.options.zfs.datasetProperties) { + let value = driver.options.zfs.datasetProperties[property]; + const template = Handlebars.compile(value); + + volumeProperties[property] = template({ + parameters: call.request.parameters, + }); + } + } + volumeProperties[VOLUME_CSI_NAME_PROPERTY_NAME] = name; volumeProperties[MANAGED_PROPERTY_NAME] = "true"; volumeProperties[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME] = @@ -346,7 +370,6 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { // TODO: also set access_mode as property? // TODO: also set fsType as property? - // TODO: allow for users to configure arbitrary/custom properties to add // zvol enables reservation by default // this implements 'sparse' zvols @@ -691,8 +714,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { volume_context = await this.createShare(call, datasetName); await zb.zfs.set(datasetName, { - [SHARE_VOLUME_CONTEXT_PROPERTY_NAME]: - "'" + JSON.stringify(volume_context) + "'", + [SHARE_VOLUME_CONTEXT_PROPERTY_NAME]: JSON.stringify(volume_context), }); volume_context["provisioner_driver"] = driver.options.driver; diff --git a/src/utils/zfs.js b/src/utils/zfs.js index b47a972..040a70d 100644 --- a/src/utils/zfs.js +++ b/src/utils/zfs.js @@ -1,6 +1,11 @@ const events = require("events"); const cp = require("child_process"); +const escapeShell = function (cmd) { + cmd = String(cmd); + return '"' + cmd.replace(/(["$`\\])/g, "\\$1") + '"'; +}; + class Zetabyte { constructor(options = {}) { const zb = this; @@ -283,7 +288,8 @@ class Zetabyte { } } if (options.fsProperties) { - for (const [key, value] of Object.entries(options.fsProperties)) { + for (let [key, value] of Object.entries(options.fsProperties)) { + value = escapeShell(value); args.push("-O"); args.push(`${key}=${value}`); } @@ -774,6 +780,7 @@ class Zetabyte { * @param {*} value */ set: function (pool, property, value) { + value = escapeShell(value); return new Promise((resolve, reject) => { let args = []; args.push("set"); @@ -913,7 +920,8 @@ class Zetabyte { if (options.unmounted) args.push("-u"); if (options.blocksize) args = args.concat(["-b", options.blocksize]); if (options.properties) { - for (const [key, value] of Object.entries(options.properties)) { + for (let [key, value] of Object.entries(options.properties)) { + value = escapeShell(value); args.push("-o"); args.push(`${key}=${value}`); } @@ -1012,7 +1020,8 @@ class Zetabyte { args.push("snapshot"); if (options.recurse) args.push("-r"); if (options.properties) { - for (const [key, value] of Object.entries(options.properties)) { + for (let [key, value] of Object.entries(options.properties)) { + value = escapeShell(value); args.push("-o"); args.push(`${key}=${value}`); } @@ -1093,7 +1102,8 @@ class Zetabyte { args.push("clone"); if (options.parents) args.push("-p"); if (options.properties) { - for (const [key, value] of Object.entries(options.properties)) { + for (let [key, value] of Object.entries(options.properties)) { + value = escapeShell(value); args.push("-o"); args.push(`${key}=${value}`); } @@ -1301,7 +1311,8 @@ class Zetabyte { args.push("set"); if (properties) { - for (const [key, value] of Object.entries(properties)) { + for (let [key, value] of Object.entries(properties)) { + value = escapeShell(value); args.push(`${key}=${value}`); } }