const _ = require("lodash"); const { CsiBaseDriver } = require("../index"); const { GrpcError, grpc } = require("../../utils/grpc"); const sleep = require("../../utils/general").sleep; const getLargestNumber = require("../../utils/general").getLargestNumber; const Handlebars = require("handlebars"); const uuidv4 = require("uuid").v4; const semver = require("semver"); // zfs common properties const MANAGED_PROPERTY_NAME = "democratic-csi:managed_resource"; const SUCCESS_PROPERTY_NAME = "democratic-csi:provision_success"; const VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX = "volume-source-for-volume-"; const VOLUME_SOURCE_DETACHED_SNAPSHOT_PREFIX = "volume-source-for-snapshot-"; const VOLUME_CSI_NAME_PROPERTY_NAME = "democratic-csi:csi_volume_name"; const SHARE_VOLUME_CONTEXT_PROPERTY_NAME = "democratic-csi:csi_share_volume_context"; const VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME = "democratic-csi:csi_volume_content_source_type"; const VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME = "democratic-csi:csi_volume_content_source_id"; const SNAPSHOT_CSI_NAME_PROPERTY_NAME = "democratic-csi:csi_snapshot_name"; const SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME = "democratic-csi:csi_snapshot_source_volume_id"; const VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME = "democratic-csi:volume_context_provisioner_driver"; const VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME = "democratic-csi:volume_context_provisioner_instance_id"; /** * Base driver to provisin zfs assets using zfs cli commands. * Derived drivers only need to implement: * - getExecClient() * - async getZetabyte() * - async setZetabyteCustomOptions(options) // optional * - getDriverZfsResourceType() // return "filesystem" or "volume" * - getFSTypes() // optional * - getAccessModes() // optional * - async getAccessibleTopology() // optional * - async createShare(call, datasetName) // return appropriate volume_context for Node operations * - async deleteShare(call, datasetName) // no return expected * - async expandVolume(call, datasetName) // no return expected, used for restarting services etc if needed */ class ControllerZfsBaseDriver extends CsiBaseDriver { constructor(ctx, options) { super(...arguments); options = options || {}; options.service = options.service || {}; options.service.identity = options.service.identity || {}; options.service.controller = options.service.controller || {}; options.service.node = options.service.node || {}; options.service.identity.capabilities = options.service.identity.capabilities || {}; options.service.controller.capabilities = options.service.controller.capabilities || {}; options.service.node.capabilities = options.service.node.capabilities || {}; if (!("service" in options.service.identity.capabilities)) { this.ctx.logger.debug("setting default identity service caps"); options.service.identity.capabilities.service = [ //"UNKNOWN", "CONTROLLER_SERVICE", //"VOLUME_ACCESSIBILITY_CONSTRAINTS" ]; } if (!("volume_expansion" in options.service.identity.capabilities)) { this.ctx.logger.debug("setting default identity volume_expansion caps"); options.service.identity.capabilities.volume_expansion = [ //"UNKNOWN", "ONLINE", //"OFFLINE" ]; } if (!("rpc" in options.service.controller.capabilities)) { this.ctx.logger.debug("setting default controller caps"); options.service.controller.capabilities.rpc = [ //"UNKNOWN", "CREATE_DELETE_VOLUME", //"PUBLISH_UNPUBLISH_VOLUME", //"LIST_VOLUMES_PUBLISHED_NODES", "LIST_VOLUMES", "GET_CAPACITY", "CREATE_DELETE_SNAPSHOT", "LIST_SNAPSHOTS", "CLONE_VOLUME", //"PUBLISH_READONLY", "EXPAND_VOLUME", ]; if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { options.service.controller.capabilities.rpc.push( //"VOLUME_CONDITION", "GET_VOLUME" ); } if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { options.service.controller.capabilities.rpc.push( "SINGLE_NODE_MULTI_WRITER" ); } } if (!("rpc" in options.service.node.capabilities)) { this.ctx.logger.debug("setting default node caps"); switch (this.getDriverZfsResourceType()) { case "filesystem": options.service.node.capabilities.rpc = [ //"UNKNOWN", "STAGE_UNSTAGE_VOLUME", "GET_VOLUME_STATS", //"EXPAND_VOLUME", //"VOLUME_CONDITION", ]; break; case "volume": options.service.node.capabilities.rpc = [ //"UNKNOWN", "STAGE_UNSTAGE_VOLUME", "GET_VOLUME_STATS", "EXPAND_VOLUME", //"VOLUME_CONDITION", ]; break; } if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { //options.service.node.capabilities.rpc.push("VOLUME_CONDITION"); } if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER"); /** * This is for volumes that support a mount time gid such as smb or fat */ //options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP"); // in k8s is sent in as the security context fsgroup } } } async getWhoAmI() { const driver = this; const execClient = driver.getExecClient(); const command = "whoami"; 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"); } else { return response.stdout.trim(); } } async getSudoPath() { const zb = await this.getZetabyte(); return zb.options.paths.sudo || "/usr/bin/sudo"; } getDatasetParentName() { let datasetParentName = this.options.zfs.datasetParentName; datasetParentName = datasetParentName.replace(/\/$/, ""); return datasetParentName; } getVolumeParentDatasetName() { let datasetParentName = this.getDatasetParentName(); //datasetParentName += "/v"; datasetParentName = datasetParentName.replace(/\/$/, ""); return datasetParentName; } getDetachedSnapshotParentDatasetName() { //let datasetParentName = this.getDatasetParentName(); let datasetParentName = this.options.zfs.detachedSnapshotsDatasetParentName; //datasetParentName += "/s"; datasetParentName = datasetParentName.replace(/\/$/, ""); return datasetParentName; } async removeSnapshotsFromDatatset(datasetName, options = {}) { const zb = await this.getZetabyte(); await zb.zfs.destroy(datasetName + "@%", options); } getFSTypes() { const driverZfsResourceType = this.getDriverZfsResourceType(); switch (driverZfsResourceType) { case "filesystem": return ["nfs", "cifs"]; case "volume": return ["ext3", "ext4", "ext4dev", "xfs"]; } } getAccessModes() { const driverZfsResourceType = this.getDriverZfsResourceType(); switch (driverZfsResourceType) { case "filesystem": return [ "UNKNOWN", "SINGLE_NODE_WRITER", "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 "SINGLE_NODE_READER_ONLY", "MULTI_NODE_READER_ONLY", "MULTI_NODE_SINGLE_WRITER", "MULTI_NODE_MULTI_WRITER", ]; case "volume": return [ "UNKNOWN", "SINGLE_NODE_WRITER", "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 "SINGLE_NODE_READER_ONLY", "MULTI_NODE_READER_ONLY", "MULTI_NODE_SINGLE_WRITER", ]; } } assertCapabilities(capabilities) { const driverZfsResourceType = this.getDriverZfsResourceType(); this.ctx.logger.verbose("validating capabilities: %j", capabilities); let message = null; //[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}] const valid = capabilities.every((capability) => { switch (driverZfsResourceType) { case "filesystem": if (capability.access_type != "mount") { message = `invalid access_type ${capability.access_type}`; return false; } if ( capability.mount.fs_type && !this.getFSTypes().includes(capability.mount.fs_type) ) { message = `invalid fs_type ${capability.mount.fs_type}`; return false; } if (!this.getAccessModes().includes(capability.access_mode.mode)) { message = `invalid access_mode, ${capability.access_mode.mode}`; return false; } return true; case "volume": if (capability.access_type == "mount") { if ( capability.mount.fs_type && !this.getFSTypes().includes(capability.mount.fs_type) ) { message = `invalid fs_type ${capability.mount.fs_type}`; return false; } } if (!this.getAccessModes().includes(capability.access_mode.mode)) { message = `invalid access_mode, ${capability.access_mode.mode}`; return false; } return true; } }); return { valid, message }; } async getVolumeStatus(volume_id) { const driver = this; if (!!!semver.satisfies(driver.ctx.csiVersion, ">=1.2.0")) { return; } let abnormal = false; let message = "OK"; let volume_status = {}; //LIST_VOLUMES_PUBLISHED_NODES if ( semver.satisfies(driver.ctx.csiVersion, ">=1.2.0") && driver.options.service.controller.capabilities.rpc.includes( "LIST_VOLUMES_PUBLISHED_NODES" ) ) { // TODO: let drivers fill this in volume_status.published_node_ids = []; } //VOLUME_CONDITION if ( semver.satisfies(driver.ctx.csiVersion, ">=1.3.0") && driver.options.service.controller.capabilities.rpc.includes( "VOLUME_CONDITION" ) ) { // TODO: let drivers fill ths in volume_condition = { abnormal, message }; volume_status.volume_condition = volume_condition; } return volume_status; } async populateCsiVolumeFromData(row) { const driver = this; const zb = await this.getZetabyte(); const driverZfsResourceType = this.getDriverZfsResourceType(); let datasetName = this.getVolumeParentDatasetName(); // ignore rows were csi_name is empty if (row[MANAGED_PROPERTY_NAME] != "true") { return; } if ( !zb.helpers.isPropertyValueSet(row[SHARE_VOLUME_CONTEXT_PROPERTY_NAME]) ) { driver.ctx.logger.warn(`${row.name} is missing share context`); return; } let volume_content_source; let volume_context = JSON.parse(row[SHARE_VOLUME_CONTEXT_PROPERTY_NAME]); if ( zb.helpers.isPropertyValueSet( row[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME] ) ) { volume_context["provisioner_driver"] = row[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME]; } if ( zb.helpers.isPropertyValueSet( row[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME] ) ) { volume_context["provisioner_driver_instance_id"] = row[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME]; } if ( zb.helpers.isPropertyValueSet( row[VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME] ) ) { volume_content_source = {}; switch (row[VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME]) { case "snapshot": volume_content_source.snapshot = {}; volume_content_source.snapshot.snapshot_id = row[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME]; break; case "volume": volume_content_source.volume = {}; volume_content_source.volume.volume_id = row[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME]; break; } } let accessible_topology; if (typeof this.getAccessibleTopology === "function") { accessible_topology = await this.getAccessibleTopology(); } let volume = { // remove parent dataset info volume_id: row["name"].replace(new RegExp("^" + datasetName + "/"), ""), capacity_bytes: driverZfsResourceType == "filesystem" ? row["refquota"] : row["volsize"], content_source: volume_content_source, volume_context, accessible_topology, }; return volume; } /** * Get the max size a zvol name can be * * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238112 * https://svnweb.freebsd.org/base?view=revision&revision=343485 */ async getMaxZvolNameLength() { const driver = this; const execClient = driver.getExecClient(); let response; let command; let kernel; let kernel_release; // get kernel command = "uname -s"; driver.ctx.logger.verbose("uname command: %s", command); response = await execClient.exec(command); if (response.code !== 0) { throw new Error("failed to run uname to determine max zvol name length"); } else { kernel = response.stdout.trim(); } switch (kernel.toLowerCase().trim()) { // Linux is 255 (probably larger 4096) but scst may have a 255 limit // https://ngelinux.com/what-is-the-maximum-file-name-length-in-linux-and-how-to-see-this-is-this-really-255-characters-answer-is-no/ // https://github.com/dmeister/scst/blob/master/iscsi-scst/include/iscsi_scst.h#L28 case "linux": return 255; case "freebsd": // get kernel_release command = "uname -r"; driver.ctx.logger.verbose("uname command: %s", command); response = await execClient.exec(command); if (response.code !== 0) { throw new Error( "failed to run uname to determine max zvol name length" ); } else { kernel_release = response.stdout; let parts = kernel_release.split("."); let kernel_release_major = parts[0]; if (kernel_release_major >= 13) { return 255; } else { return 63; } } default: throw new Error(`unknown kernel: ${kernel}`); } } async setFilesystemMode(path, mode) { const driver = this; const execClient = this.getExecClient(); let command = execClient.buildCommand("chmod", [mode, path]); if ((await driver.getWhoAmI()) != "root") { command = (await driver.getSudoPath()) + " " + command; } driver.ctx.logger.verbose("set permission command: %s", command); let response = await execClient.exec(command); if (response.code != 0) { throw new GrpcError( grpc.status.UNKNOWN, `error setting permissions on dataset: ${JSON.stringify(response)}` ); } } async setFilesystemOwnership(path, user = false, group = false) { const driver = this; const execClient = this.getExecClient(); if (user === false || typeof user == "undefined" || user === null) { user = ""; } if (group === false || typeof group == "undefined" || group === null) { group = ""; } user = String(user); group = String(group); if (user.length < 1 && group.length < 1) { return; } let command = execClient.buildCommand("chown", [ (user.length > 0 ? user : "") + ":" + (group.length > 0 ? group : ""), path, ]); if ((await driver.getWhoAmI()) != "root") { command = (await driver.getSudoPath()) + " " + command; } driver.ctx.logger.verbose("set ownership command: %s", command); let response = await execClient.exec(command); if (response.code != 0) { throw new GrpcError( grpc.status.UNKNOWN, `error setting ownership on dataset: ${JSON.stringify(response)}` ); } } /** * Ensure sane options are used etc * true = ready * false = not ready, but progressiong towards ready * throw error = faulty setup * * @param {*} call */ async Probe(call) { const driver = this; if (driver.ctx.args.csiMode.includes("controller")) { let datasetParentName = this.getVolumeParentDatasetName() + "/"; let snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName() + "/"; if ( datasetParentName.startsWith(snapshotParentDatasetName) || snapshotParentDatasetName.startsWith(datasetParentName) ) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `datasetParentName and detachedSnapshotsDatasetParentName must not overlap` ); } const timerEnabled = false; /** * limit the actual checks semi sanely * health checks should kick in an restart the pod * this process is 2 checks in 1 * - ensure basic exec connectivity * - ensure csh is not the operative shell */ if (!driver.currentExecShell || timerEnabled === false) { const execClient = this.getExecClient(); driver.ctx.logger.debug("performing exec sanity check.."); const response = await execClient.exec("echo $0"); driver.currentExecShell = response.stdout.split("\n")[0]; } // update in the background every X interval to prevent incessant checks if (timerEnabled && !driver.currentExecShellInterval) { const intervalTime = 60000; driver.currentExecShellInterval = setInterval(async () => { try { driver.ctx.logger.debug("performing exec sanity check.."); const execClient = this.getExecClient(); const response = await execClient.exec("echo $0"); driver.currentExecShell = response.stdout.split("\n")[0]; } catch (e) { delete driver.currentExecShell; } }, intervalTime); } if (driver.currentExecShell.includes("csh")) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `csh is an unsupported shell, please update the default shell of your exec user` ); } return { ready: { value: true } }; } else { return { ready: { value: true } }; } } /** * Create a volume doing in essence the following: * 1. create dataset * 2. create nfs share * * Should return 2 parameters * 1. `server` - host/ip of the nfs server * 2. `share` - path of the mount shared * * @param {*} call */ async CreateVolume(call) { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const execClient = this.getExecClient(); const zb = await this.getZetabyte(); let datasetParentName = this.getVolumeParentDatasetName(); let snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName(); let zvolBlocksize = this.options.zfs.zvolBlocksize || "16K"; let name = call.request.name; let volume_content_source = call.request.volume_content_source; if (!datasetParentName) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing datasetParentName` ); } if (!name) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume name is required` ); } if ( call.request.volume_capabilities && call.request.volume_capabilities.length > 0 ) { const result = this.assertCapabilities(call.request.volume_capabilities); if (result.valid !== true) { throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); } } else { throw new GrpcError( grpc.status.INVALID_ARGUMENT, "missing volume_capabilities" ); } // if no capacity_range specified set a required_bytes at least if ( !call.request.capacity_range || Object.keys(call.request.capacity_range).length === 0 ) { call.request.capacity_range = { required_bytes: 1073741824, }; } if ( call.request.capacity_range.required_bytes > 0 && call.request.capacity_range.limit_bytes > 0 && call.request.capacity_range.required_bytes > call.request.capacity_range.limit_bytes ) { throw new GrpcError( grpc.status.OUT_OF_RANGE, `required_bytes is greather than limit_bytes` ); } let capacity_bytes = call.request.capacity_range.required_bytes || call.request.capacity_range.limit_bytes; if (!capacity_bytes) { //should never happen, value must be set throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume capacity is required (either required_bytes or limit_bytes)` ); } if (capacity_bytes && driverZfsResourceType == "volume") { //make sure to align capacity_bytes with zvol blocksize //volume size must be a multiple of volume block size capacity_bytes = zb.helpers.generateZvolSize( capacity_bytes, zvolBlocksize ); } // ensure *actual* capacity is not greater than limit if ( call.request.capacity_range.limit_bytes && call.request.capacity_range.limit_bytes > 0 && capacity_bytes > call.request.capacity_range.limit_bytes ) { throw new GrpcError( grpc.status.OUT_OF_RANGE, `required volume capacity is greater than limit` ); } /** * NOTE: avoid the urge to templatize this given the name length limits for zvols * ie: namespace-name may quite easily exceed 58 chars */ const datasetName = datasetParentName + "/" + name; // ensure volumes with the same name being requested a 2nd time but with a different size fails try { let properties = await zb.zfs.get(datasetName, ["volsize", "refquota"]); properties = properties[datasetName]; let size; switch (driverZfsResourceType) { case "volume": size = properties["volsize"].value; break; case "filesystem": size = properties["refquota"].value; break; default: throw new Error( `unknown zfs resource type: ${driverZfsResourceType}` ); } let check = false; if (driverZfsResourceType == "volume") { check = true; } if ( driverZfsResourceType == "filesystem" && this.options.zfs.datasetEnableQuotas ) { check = true; } if (check) { if ( (call.request.capacity_range.required_bytes && call.request.capacity_range.required_bytes > 0 && size < call.request.capacity_range.required_bytes) || (call.request.capacity_range.limit_bytes && call.request.capacity_range.limit_bytes > 0 && size > call.request.capacity_range.limit_bytes) ) { throw new GrpcError( grpc.status.ALREADY_EXISTS, `volume has already been created with a different size, existing size: ${size}, required_bytes: ${call.request.capacity_range.required_bytes}, limit_bytes: ${call.request.capacity_range.limit_bytes}` ); } } } catch (err) { if (err.toString().includes("dataset does not exist")) { // does NOT already exist } else { throw err; } } /** * This is specifically a FreeBSD limitation, not sure what linux limit is * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab * https://www.ixsystems.com/documentation/freenas/11.3-BETA1/intro.html#path-and-name-lengths * https://www.freebsd.org/cgi/man.cgi?query=devfs * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238112 */ if (driverZfsResourceType == "volume") { let extentDiskName = "zvol/" + datasetName; let maxZvolNameLength = await driver.getMaxZvolNameLength(); driver.ctx.logger.debug("max zvol name length: %s", maxZvolNameLength); if (extentDiskName.length > maxZvolNameLength) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` ); } } let response, command; let volume_content_source_snapshot_id; 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] = driver.options.driver; if (driver.options.instance_id) { volumeProperties[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME] = driver.options.instance_id; } // TODO: also set access_mode as property? // TODO: also set fsType as property? // zvol enables reservation by default // this implements 'sparse' zvols if (driverZfsResourceType == "volume") { if (!this.options.zfs.zvolEnableReservation) { volumeProperties.refreservation = 0; } } let detachedClone = false; // create dataset if (volume_content_source) { volumeProperties[VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME] = volume_content_source.type; switch (volume_content_source.type) { // must be available when adverstising CREATE_DELETE_SNAPSHOT // simply clone case "snapshot": try { let tmpDetachedClone = JSON.parse( driver.getNormalizedParameterValue( call.request.parameters, "detachedVolumesFromSnapshots" ) ); if (typeof tmpDetachedClone === "boolean") { detachedClone = tmpDetachedClone; } } catch (e) {} volumeProperties[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME] = volume_content_source.snapshot.snapshot_id; volume_content_source_snapshot_id = volume_content_source.snapshot.snapshot_id; // zfs origin property contains parent info, ie: pool0/k8s/test/PVC-111@clone-test if (zb.helpers.isZfsSnapshot(volume_content_source_snapshot_id)) { fullSnapshotName = datasetParentName + "/" + volume_content_source_snapshot_id; } else { fullSnapshotName = snapshotParentDatasetName + "/" + volume_content_source_snapshot_id + "@" + VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + name; } driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); if (!zb.helpers.isZfsSnapshot(volume_content_source_snapshot_id)) { try { await zb.zfs.snapshot(fullSnapshotName); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( grpc.status.NOT_FOUND, `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` ); } throw err; } } if (detachedClone) { try { response = await zb.zfs.send_receive( fullSnapshotName, [], datasetName, [] ); response = await zb.zfs.set(datasetName, volumeProperties); } catch (err) { if ( err.toString().includes("destination") && err.toString().includes("exists") ) { // move along } else { throw err; } } // remove snapshots from target await this.removeSnapshotsFromDatatset(datasetName, { force: true, }); } else { try { response = await zb.zfs.clone(fullSnapshotName, datasetName, { properties: volumeProperties, }); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( grpc.status.NOT_FOUND, "dataset does not exists" ); } throw err; } } if (!zb.helpers.isZfsSnapshot(volume_content_source_snapshot_id)) { try { // schedule snapshot removal from source await zb.zfs.destroy(fullSnapshotName, { recurse: true, force: true, defer: true, }); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( grpc.status.NOT_FOUND, `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` ); } throw err; } } break; // must be available when adverstising CLONE_VOLUME // create snapshot first, then clone case "volume": try { let tmpDetachedClone = JSON.parse( driver.getNormalizedParameterValue( call.request.parameters, "detachedVolumesFromVolumes" ) ); if (typeof tmpDetachedClone === "boolean") { detachedClone = tmpDetachedClone; } } catch (e) {} volumeProperties[VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME] = volume_content_source.volume.volume_id; volume_content_source_volume_id = volume_content_source.volume.volume_id; fullSnapshotName = datasetParentName + "/" + volume_content_source_volume_id + "@" + VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + name; driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); // create snapshot try { response = await zb.zfs.snapshot(fullSnapshotName); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( grpc.status.NOT_FOUND, "dataset does not exists" ); } throw err; } if (detachedClone) { try { response = await zb.zfs.send_receive( fullSnapshotName, [], datasetName, [] ); } catch (err) { if ( err.toString().includes("destination") && err.toString().includes("exists") ) { // move along } else { throw err; } } response = await zb.zfs.set(datasetName, volumeProperties); // remove snapshots from target await this.removeSnapshotsFromDatatset(datasetName, { force: true, }); // remove snapshot from source await zb.zfs.destroy(fullSnapshotName, { recurse: true, force: true, defer: true, }); } else { // create clone // zfs origin property contains parent info, ie: pool0/k8s/test/PVC-111@clone-test try { response = await zb.zfs.clone(fullSnapshotName, datasetName, { properties: volumeProperties, }); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( grpc.status.NOT_FOUND, "dataset does not exists" ); } throw err; } } break; default: throw new GrpcError( grpc.status.INVALID_ARGUMENT, `invalid volume_content_source type: ${volume_content_source.type}` ); break; } } else { // force blocksize on newly created zvols if (driverZfsResourceType == "volume") { volumeProperties.volblocksize = zvolBlocksize; } await zb.zfs.create(datasetName, { parents: true, properties: volumeProperties, size: driverZfsResourceType == "volume" ? capacity_bytes : false, }); } let setProps = false; let properties = {}; let volume_context = {}; switch (driverZfsResourceType) { case "filesystem": // set quota if (this.options.zfs.datasetEnableQuotas) { setProps = true; properties.refquota = capacity_bytes; } // set reserve if (this.options.zfs.datasetEnableReservation) { setProps = true; properties.refreservation = capacity_bytes; } // quota for dataset and all children // reserved for dataset and all children // dedup // ro? // record size // set properties if (setProps) { await zb.zfs.set(datasetName, properties); } // get properties needed for remaining calls properties = await zb.zfs.get(datasetName, [ "mountpoint", "refquota", "compression", VOLUME_CSI_NAME_PROPERTY_NAME, VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME, VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME, ]); properties = properties[datasetName]; driver.ctx.logger.debug("zfs props data: %j", properties); // set mode if (this.options.zfs.datasetPermissionsMode) { await driver.setFilesystemMode( properties.mountpoint.value, this.options.zfs.datasetPermissionsMode ); } // set ownership if ( String(_.get(this.options, "zfs.datasetPermissionsUser", "")).length > 0 || String(_.get(this.options, "zfs.datasetPermissionsGroup", "")) .length > 0 ) { await driver.setFilesystemOwnership( properties.mountpoint.value, this.options.zfs.datasetPermissionsUser, this.options.zfs.datasetPermissionsGroup ); } // set acls // TODO: this is unsfafe approach, make it better // probably could see if ^-.*\s and split and then shell escape if (this.options.zfs.datasetPermissionsAcls) { for (const acl of this.options.zfs.datasetPermissionsAcls) { command = execClient.buildCommand("setfacl", [ acl, properties.mountpoint.value, ]); if ((await this.getWhoAmI()) != "root") { command = (await this.getSudoPath()) + " " + command; } driver.ctx.logger.verbose("set acl command: %s", command); response = await execClient.exec(command); if (response.code != 0) { throw new GrpcError( grpc.status.UNKNOWN, `error setting acl on dataset: ${JSON.stringify(response)}` ); } } } break; case "volume": // set properties // set reserve setProps = true; // this should be already set, but when coming from a volume source // it may not match that of the source // TODO: probably need to recalculate size based on *actual* volume source blocksize in case of difference from currently configured properties.volsize = capacity_bytes; //dedup //compression if (setProps) { await zb.zfs.set(datasetName, properties); } break; } volume_context = await this.createShare(call, datasetName); await zb.zfs.set(datasetName, { [SHARE_VOLUME_CONTEXT_PROPERTY_NAME]: JSON.stringify(volume_context), }); volume_context["provisioner_driver"] = driver.options.driver; if (driver.options.instance_id) { volume_context["provisioner_driver_instance_id"] = driver.options.instance_id; } // set this just before sending out response so we know if volume completed // this should give us a relatively sane way to clean up artifacts over time await zb.zfs.set(datasetName, { [SUCCESS_PROPERTY_NAME]: "true" }); let accessible_topology; if (typeof this.getAccessibleTopology === "function") { accessible_topology = await this.getAccessibleTopology(); } const res = { volume: { volume_id: name, //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 capacity_bytes: this.options.zfs.datasetEnableQuotas || driverZfsResourceType == "volume" ? capacity_bytes : 0, content_source: volume_content_source, volume_context, accessible_topology, }, }; return res; } /** * Delete a volume * * Deleting a volume consists of the following steps: * 1. delete the nfs share * 2. delete the dataset * * @param {*} call */ async DeleteVolume(call) { const driver = this; const zb = await this.getZetabyte(); let datasetParentName = this.getVolumeParentDatasetName(); let name = call.request.volume_id; if (!datasetParentName) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing datasetParentName` ); } if (!name) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume_id is required` ); } const datasetName = datasetParentName + "/" + name; let properties; // get properties needed for remaining calls try { properties = await zb.zfs.get(datasetName, [ "mountpoint", "origin", "refquota", "compression", VOLUME_CSI_NAME_PROPERTY_NAME, ]); properties = properties[datasetName]; } catch (err) { let ignore = false; if (err.toString().includes("dataset does not exist")) { ignore = true; } if (!ignore) { throw err; } } driver.ctx.logger.debug("dataset properties: %j", properties); // remove share resources await this.deleteShare(call, datasetName); // remove parent snapshot if appropriate with defer if ( properties && properties.origin && properties.origin.value != "-" && zb.helpers .extractSnapshotName(properties.origin.value) .startsWith(VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX) ) { driver.ctx.logger.debug( "removing with defer source snapshot: %s", properties.origin.value ); try { await zb.zfs.destroy(properties.origin.value, { recurse: true, force: true, defer: true, }); } catch (err) { if (err.toString().includes("snapshot has dependent clones")) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, "snapshot has dependent clones" ); } throw err; } } // NOTE: -f does NOT allow deletes if dependent filesets exist // NOTE: -R will recursively delete items + dependent filesets // delete dataset try { let max_tries = 5; let sleep_time = 3000; let current_try = 1; let success = false; while (!success && current_try <= max_tries) { try { await zb.zfs.destroy(datasetName, { recurse: true, force: true }); success = true; } catch (err) { if (err.toString().includes("dataset is busy")) { current_try++; if (current_try > max_tries) { throw err; } else { await sleep(sleep_time); } } else { throw err; } } } } catch (err) { if (err.toString().includes("filesystem has dependent clones")) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, "filesystem has dependent clones" ); } throw err; } return {}; } /** * * @param {*} call */ async ControllerExpandVolume(call) { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const zb = await this.getZetabyte(); let datasetParentName = this.getVolumeParentDatasetName(); let name = call.request.volume_id; if (!datasetParentName) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing datasetParentName` ); } if (!name) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume_id is required` ); } const datasetName = datasetParentName + "/" + name; let capacity_bytes = call.request.capacity_range.required_bytes || call.request.capacity_range.limit_bytes; if (!capacity_bytes) { //should never happen, value must be set throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume capacity is required (either required_bytes or limit_bytes)` ); } if (capacity_bytes && driverZfsResourceType == "volume") { //make sure to align capacity_bytes with zvol blocksize //volume size must be a multiple of volume block size let properties = await zb.zfs.get(datasetName, ["volblocksize"]); properties = properties[datasetName]; capacity_bytes = zb.helpers.generateZvolSize( capacity_bytes, properties.volblocksize.value ); } if ( call.request.capacity_range.required_bytes > 0 && call.request.capacity_range.limit_bytes > 0 && call.request.capacity_range.required_bytes > call.request.capacity_range.limit_bytes ) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `required_bytes is greather than limit_bytes` ); } // ensure *actual* capacity is not greater than limit if ( call.request.capacity_range.limit_bytes && call.request.capacity_range.limit_bytes > 0 && capacity_bytes > call.request.capacity_range.limit_bytes ) { throw new GrpcError( grpc.status.OUT_OF_RANGE, `required volume capacity is greater than limit` ); } let setProps = false; let properties = {}; switch (driverZfsResourceType) { case "filesystem": // set quota if (this.options.zfs.datasetEnableQuotas) { setProps = true; properties.refquota = capacity_bytes; } // set reserve if (this.options.zfs.datasetEnableReservation) { setProps = true; properties.refreservation = capacity_bytes; } break; case "volume": properties.volsize = capacity_bytes; setProps = true; // managed automatically for zvols //if (this.options.zfs.zvolEnableReservation) { // properties.refreservation = capacity_bytes; //} break; } if (setProps) { await zb.zfs.set(datasetName, properties); } await this.expandVolume(call, datasetName); return { capacity_bytes: this.options.zfs.datasetEnableQuotas || driverZfsResourceType == "volume" ? capacity_bytes : 0, node_expansion_required: driverZfsResourceType == "volume" ? true : false, }; } /** * TODO: consider volume_capabilities? * * @param {*} call */ async GetCapacity(call) { const driver = this; const zb = await this.getZetabyte(); let datasetParentName = this.getVolumeParentDatasetName(); if (!datasetParentName) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing datasetParentName` ); } if (call.request.volume_capabilities) { const result = this.assertCapabilities(call.request.volume_capabilities); if (result.valid !== true) { return { available_capacity: 0 }; } } const datasetName = datasetParentName; await zb.zfs.create(datasetName, { parents: true, }); let properties; try { properties = await zb.zfs.get(datasetName, ["avail"]); properties = properties[datasetName]; return { available_capacity: properties.available.value }; } catch (err) { throw err; // gracefully handle csi-test suite when parent dataset does not yet exist if (err.toString().includes("dataset does not exist")) { return { available_capacity: 0 }; } else { throw err; } } } /** * Get a single volume * * @param {*} call */ async ControllerGetVolume(call) { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const zb = await this.getZetabyte(); let datasetParentName = this.getVolumeParentDatasetName(); let response; let name = call.request.volume_id; if (!datasetParentName) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing datasetParentName` ); } if (!name) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `volume_id is required` ); } const datasetName = datasetParentName + "/" + name; let types = []; switch (driverZfsResourceType) { case "filesystem": types = ["filesystem"]; break; case "volume": types = ["volume"]; break; } try { response = await zb.zfs.list( datasetName, [ "name", "mountpoint", "refquota", "avail", "used", VOLUME_CSI_NAME_PROPERTY_NAME, VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME, VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME, "volsize", MANAGED_PROPERTY_NAME, SHARE_VOLUME_CONTEXT_PROPERTY_NAME, SUCCESS_PROPERTY_NAME, VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME, VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME, ], { types, recurse: false } ); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError(grpc.status.NOT_FOUND, `volume_id is missing`); } throw err; } driver.ctx.logger.debug("list volumes result: %j", response); let volume = await driver.populateCsiVolumeFromData(response.indexed[0]); let status = await driver.getVolumeStatus(datasetName); let res = { volume }; if (status) { res.status = status; } return res; } /** * * TODO: check capability to ensure not asking about block volumes * * @param {*} call */ async ListVolumes(call) { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const zb = await this.getZetabyte(); let datasetParentName = this.getVolumeParentDatasetName(); let entries = []; let entries_length = 0; let next_token; let uuid; let response; const max_entries = call.request.max_entries; const starting_token = call.request.starting_token; // get data from cache and return immediately if (starting_token) { let parts = starting_token.split(":"); uuid = parts[0]; let start_position = parseInt(parts[1]); let end_position; if (max_entries > 0) { end_position = start_position + max_entries; } entries = this.ctx.cache.get(`ListVolumes:result:${uuid}`); if (entries) { entries_length = entries.length; entries = entries.slice(start_position, end_position); if (max_entries > 0 && end_position > entries_length) { next_token = `${uuid}:${end_position}`; } else { next_token = null; } const data = { entries: entries, next_token: next_token, }; return data; } else { throw new GrpcError( grpc.status.ABORTED, `invalid starting_token: ${starting_token}` ); } } if (!datasetParentName) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing datasetParentName` ); } const datasetName = datasetParentName; let types = []; switch (driverZfsResourceType) { case "filesystem": types = ["filesystem"]; break; case "volume": types = ["volume"]; break; } try { response = await zb.zfs.list( datasetName, [ "name", "mountpoint", "refquota", "avail", "used", VOLUME_CSI_NAME_PROPERTY_NAME, VOLUME_CONTENT_SOURCE_TYPE_PROPERTY_NAME, VOLUME_CONTENT_SOURCE_ID_PROPERTY_NAME, "volsize", MANAGED_PROPERTY_NAME, SHARE_VOLUME_CONTEXT_PROPERTY_NAME, SUCCESS_PROPERTY_NAME, VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME, VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME, ], { types, recurse: true } ); } catch (err) { if (err.toString().includes("dataset does not exist")) { return { entries: [], next_token: null, }; } throw err; } driver.ctx.logger.debug("list volumes result: %j", response); // remove parent dataset from results if (driverZfsResourceType == "filesystem") { response.data.shift(); } entries = []; for (let row of response.indexed) { // ignore rows were csi_name is empty if (row[MANAGED_PROPERTY_NAME] != "true") { continue; } let volume = await driver.populateCsiVolumeFromData(row); if (volume) { let status = await driver.getVolumeStatus(datasetName); entries.push({ volume, status, }); } } if (max_entries && entries.length > max_entries) { uuid = uuidv4(); this.ctx.cache.set(`ListVolumes:result:${uuid}`, entries); next_token = `${uuid}:${max_entries}`; entries = entries.slice(0, max_entries); } const data = { entries: entries, next_token: next_token, }; return data; } /** * * @param {*} call */ async ListSnapshots(call) { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const zb = await this.getZetabyte(); let entries = []; let entries_length = 0; let next_token; let uuid; const max_entries = call.request.max_entries; const starting_token = call.request.starting_token; let types = []; const volumeParentDatasetName = this.getVolumeParentDatasetName(); const snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName(); // get data from cache and return immediately if (starting_token) { let parts = starting_token.split(":"); uuid = parts[0]; let start_position = parseInt(parts[1]); let end_position; if (max_entries > 0) { end_position = start_position + max_entries; } entries = this.ctx.cache.get(`ListSnapshots:result:${uuid}`); if (entries) { entries_length = entries.length; entries = entries.slice(start_position, end_position); if (max_entries > 0 && end_position > entries_length) { next_token = `${uuid}:${end_position}`; } else { next_token = null; } const data = { entries: entries, next_token: next_token, }; return data; } else { throw new GrpcError( grpc.status.ABORTED, `invalid starting_token: ${starting_token}` ); } } if (!volumeParentDatasetName) { // throw error throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing datasetParentName` ); } let snapshot_id = call.request.snapshot_id; let source_volume_id = call.request.source_volume_id; entries = []; for (let loopType of ["snapshot", "filesystem"]) { let response, operativeFilesystem, operativeFilesystemType; let datasetParentName; switch (loopType) { case "snapshot": datasetParentName = volumeParentDatasetName; types = ["snapshot"]; // should only send 1 of snapshot_id or source_volume_id, preferring the former if sent if (snapshot_id) { if (!zb.helpers.isZfsSnapshot(snapshot_id)) { continue; } operativeFilesystem = volumeParentDatasetName + "/" + snapshot_id; operativeFilesystemType = 3; } else if (source_volume_id) { operativeFilesystem = volumeParentDatasetName + "/" + source_volume_id; operativeFilesystemType = 2; } else { operativeFilesystem = volumeParentDatasetName; operativeFilesystemType = 1; } break; case "filesystem": datasetParentName = snapshotParentDatasetName; if (!datasetParentName) { continue; } if (driverZfsResourceType == "filesystem") { types = ["filesystem"]; } else { types = ["volume"]; } // should only send 1 of snapshot_id or source_volume_id, preferring the former if sent if (snapshot_id) { if (zb.helpers.isZfsSnapshot(snapshot_id)) { continue; } operativeFilesystem = snapshotParentDatasetName + "/" + snapshot_id; operativeFilesystemType = 3; } else if (source_volume_id) { operativeFilesystem = snapshotParentDatasetName + "/" + source_volume_id; operativeFilesystemType = 2; } else { operativeFilesystem = snapshotParentDatasetName; operativeFilesystemType = 1; } break; } try { response = await zb.zfs.list( operativeFilesystem, [ "name", "creation", "mountpoint", "refquota", "avail", "used", "volsize", "referenced", "logicalreferenced", VOLUME_CSI_NAME_PROPERTY_NAME, SNAPSHOT_CSI_NAME_PROPERTY_NAME, MANAGED_PROPERTY_NAME, ], { types, recurse: true } ); } catch (err) { let message; if (err.toString().includes("dataset does not exist")) { switch (operativeFilesystemType) { case 1: //message = `invalid configuration: datasetParentName ${datasetParentName} does not exist`; continue; break; case 2: message = `source_volume_id ${source_volume_id} does not exist`; continue; break; case 3: message = `snapshot_id ${snapshot_id} does not exist`; continue; break; } throw new GrpcError(grpc.status.NOT_FOUND, message); } throw new GrpcError(grpc.status.FAILED_PRECONDITION, err.toString()); } response.indexed.forEach((row) => { // skip any snapshots not explicitly created by CO if (row[MANAGED_PROPERTY_NAME] != "true") { return; } // ignore snapshots that are not explicit CO snapshots if ( !zb.helpers.isPropertyValueSet(row[SNAPSHOT_CSI_NAME_PROPERTY_NAME]) ) { return; } // strip parent dataset let source_volume_id = row["name"].replace( new RegExp("^" + datasetParentName + "/"), "" ); // strip snapshot details (@snapshot-name) if (source_volume_id.includes("@")) { source_volume_id = source_volume_id.substring( 0, source_volume_id.indexOf("@") ); } else { source_volume_id = source_volume_id.replace( new RegExp("/" + row[SNAPSHOT_CSI_NAME_PROPERTY_NAME] + "$"), "" ); } if (source_volume_id == datasetParentName) { return; } // TODO: properly handle use-case where datasetEnableQuotas is not turned on let size_bytes = 0; if (driverZfsResourceType == "filesystem") { // independent of detached snapshots when creating a volume from a 'snapshot' // we could be using detached clones (ie: send/receive) // so we must be cognizant and use the highest possible value here // note that whatever value is returned here can/will essentially impact the refquota // value of a derived volume size_bytes = getLargestNumber(row.referenced, row.logicalreferenced); } else { // get the size of the parent volume size_bytes = row.volsize; } if (source_volume_id) entries.push({ snapshot: { /** * The purpose of this field is to give CO guidance on how much space * is needed to create a volume from this snapshot. * * In that vein, I think it's best to return 0 here given the * unknowns of 'cow' implications. */ size_bytes, // remove parent dataset details snapshot_id: row["name"].replace( new RegExp("^" + datasetParentName + "/"), "" ), source_volume_id: source_volume_id, //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto creation_time: { seconds: row["creation"], nanos: 0, }, ready_to_use: true, }, }); }); } if (max_entries && entries.length > max_entries) { uuid = uuidv4(); this.ctx.cache.set(`ListSnapshots:result:${uuid}`, entries); next_token = `${uuid}:${max_entries}`; entries = entries.slice(0, max_entries); } const data = { entries: entries, next_token: next_token, }; return data; } /** * * @param {*} call */ async CreateSnapshot(call) { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const zb = await this.getZetabyte(); let size_bytes = 0; let detachedSnapshot = false; try { let tmpDetachedSnapshot = JSON.parse( driver.getNormalizedParameterValue( call.request.parameters, "detachedSnapshots" ) ); // snapshot class parameter if (typeof tmpDetachedSnapshot === "boolean") { detachedSnapshot = tmpDetachedSnapshot; } } catch (e) {} let response; const volumeParentDatasetName = this.getVolumeParentDatasetName(); let datasetParentName; let snapshotProperties = {}; let types = []; if (detachedSnapshot) { datasetParentName = this.getDetachedSnapshotParentDatasetName(); if (driverZfsResourceType == "filesystem") { types.push("filesystem"); } else { types.push("volume"); } } else { datasetParentName = this.getVolumeParentDatasetName(); types.push("snapshot"); } if (!datasetParentName) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing datasetParentName` ); } // both these are required let source_volume_id = call.request.source_volume_id; let name = call.request.name; if (!source_volume_id) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `snapshot source_volume_id is required` ); } if (!name) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `snapshot name is required` ); } const volumeDatasetName = volumeParentDatasetName + "/" + source_volume_id; const datasetName = datasetParentName + "/" + source_volume_id; snapshotProperties[SNAPSHOT_CSI_NAME_PROPERTY_NAME] = name; snapshotProperties[SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME] = source_volume_id; snapshotProperties[MANAGED_PROPERTY_NAME] = "true"; driver.ctx.logger.verbose("requested snapshot name: %s", name); let invalid_chars; invalid_chars = name.match(/[^a-z0-9_\-:.+]+/gi); if (invalid_chars) { invalid_chars = String.prototype.concat( ...new Set(invalid_chars.join("")) ); throw new GrpcError( grpc.status.INVALID_ARGUMENT, `snapshot name contains invalid characters: ${invalid_chars}` ); } // https://stackoverflow.com/questions/32106243/regex-to-remove-all-non-alpha-numeric-and-replace-spaces-with/32106277 name = name.replace(/[^a-z0-9_\-:.+]+/gi, ""); driver.ctx.logger.verbose("cleansed snapshot name: %s", name); // check for other snapshopts with the same name on other volumes and fail as appropriate { try { let datasets = []; datasets = await zb.zfs.list( this.getDetachedSnapshotParentDatasetName(), [], { recurse: true, types } ); for (let dataset of datasets.indexed) { let parts = dataset.name.split("/").slice(-2); if (parts[1] != name) { continue; } if (parts[0] != source_volume_id) { throw new GrpcError( grpc.status.ALREADY_EXISTS, `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` ); } } } catch (err) { if (!err.toString().includes("dataset does not exist")) { throw err; } } let snapshots = []; snapshots = await zb.zfs.list(this.getVolumeParentDatasetName(), [], { recurse: true, types, }); for (let snapshot of snapshots.indexed) { let parts = zb.helpers.extractLeafName(snapshot.name).split("@"); if (parts[1] != name) { continue; } if (parts[0] != source_volume_id) { throw new GrpcError( grpc.status.ALREADY_EXISTS, `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` ); } } } let fullSnapshotName; let snapshotDatasetName; let tmpSnapshotName; if (detachedSnapshot) { fullSnapshotName = datasetName + "/" + name; } else { fullSnapshotName = datasetName + "@" + name; } driver.ctx.logger.verbose("full snapshot name: %s", fullSnapshotName); if (detachedSnapshot) { tmpSnapshotName = volumeDatasetName + "@" + VOLUME_SOURCE_DETACHED_SNAPSHOT_PREFIX + name; snapshotDatasetName = datasetName + "/" + name; await zb.zfs.create(datasetName, { parents: true }); try { await zb.zfs.snapshot(tmpSnapshotName); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `snapshot source_volume_id ${source_volume_id} does not exist` ); } throw err; } try { response = await zb.zfs.send_receive( tmpSnapshotName, [], snapshotDatasetName, [] ); response = await zb.zfs.set(snapshotDatasetName, snapshotProperties); } catch (err) { if ( err.toString().includes("destination") && err.toString().includes("exists") ) { // move along } else { throw err; } } // remove snapshot from target await zb.zfs.destroy( snapshotDatasetName + "@" + zb.helpers.extractSnapshotName(tmpSnapshotName), { recurse: true, force: true, defer: true, } ); // remove snapshot from source await zb.zfs.destroy(tmpSnapshotName, { recurse: true, force: true, defer: true, }); // let things settle down //await sleep(3000); } else { try { await zb.zfs.snapshot(fullSnapshotName, { properties: snapshotProperties, }); // let things settle down //await sleep(3000); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `snapshot source_volume_id ${source_volume_id} does not exist` ); } throw err; } } // TODO: let things settle to ensure proper size_bytes is reported // sysctl -d vfs.zfs.txg.timeout # vfs.zfs.txg.timeout: Max seconds worth of delta per txg let properties; properties = await zb.zfs.get( fullSnapshotName, [ "name", "creation", "mountpoint", "refquota", "avail", "used", "volsize", "referenced", "refreservation", "logicalused", "logicalreferenced", VOLUME_CSI_NAME_PROPERTY_NAME, SNAPSHOT_CSI_NAME_PROPERTY_NAME, SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME, MANAGED_PROPERTY_NAME, ], { types } ); properties = properties[fullSnapshotName]; driver.ctx.logger.verbose("snapshot properties: %j", properties); // TODO: properly handle use-case where datasetEnableQuotas is not turned on if (driverZfsResourceType == "filesystem") { // independent of detached snapshots when creating a volume from a 'snapshot' // we could be using detached clones (ie: send/receive) // so we must be cognizant and use the highest possible value here // note that whatever value is returned here can/will essentially impact the refquota // value of a derived volume size_bytes = getLargestNumber( properties.referenced.value, properties.logicalreferenced.value ); } else { // get the size of the parent volume size_bytes = properties.volsize.value; } // set this just before sending out response so we know if volume completed // this should give us a relatively sane way to clean up artifacts over time await zb.zfs.set(fullSnapshotName, { [SUCCESS_PROPERTY_NAME]: "true" }); return { snapshot: { /** * The purpose of this field is to give CO guidance on how much space * is needed to create a volume from this snapshot. * * In that vein, I think it's best to return 0 here given the * unknowns of 'cow' implications. */ size_bytes, // remove parent dataset details snapshot_id: properties.name.value.replace( new RegExp("^" + datasetParentName + "/"), "" ), source_volume_id: source_volume_id, //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto creation_time: { seconds: properties.creation.value, nanos: 0, }, ready_to_use: true, }, }; } /** * In addition, if clones have been created from a snapshot, then they must * be destroyed before the snapshot can be destroyed. * * @param {*} call */ async DeleteSnapshot(call) { const driver = this; const zb = await this.getZetabyte(); const snapshot_id = call.request.snapshot_id; if (!snapshot_id) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, `snapshot_id is required` ); } const detachedSnapshot = !zb.helpers.isZfsSnapshot(snapshot_id); let datasetParentName; if (detachedSnapshot) { datasetParentName = this.getDetachedSnapshotParentDatasetName(); } else { datasetParentName = this.getVolumeParentDatasetName(); } if (!datasetParentName) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing datasetParentName` ); } const fullSnapshotName = datasetParentName + "/" + snapshot_id; driver.ctx.logger.verbose("deleting snapshot: %s", fullSnapshotName); try { await zb.zfs.destroy(fullSnapshotName, { recurse: true, force: true, defer: zb.helpers.isZfsSnapshot(snapshot_id), // only defer when snapshot }); } catch (err) { if (err.toString().includes("snapshot has dependent clones")) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, "snapshot has dependent clones" ); } throw err; } // cleanup parent dataset if possible if (detachedSnapshot) { let containerDataset = zb.helpers.extractParentDatasetName(fullSnapshotName); try { await this.removeSnapshotsFromDatatset(containerDataset); await zb.zfs.destroy(containerDataset); } catch (err) { if (!err.toString().includes("filesystem has children")) { throw err; } } } return {}; } /** * * @param {*} call */ async ValidateVolumeCapabilities(call) { const driver = this; const zb = await this.getZetabyte(); const volume_id = call.request.volume_id; if (!volume_id) { throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); } const capabilities = call.request.volume_capabilities; if (!capabilities || capabilities.length === 0) { throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); } let datasetParentName = this.getVolumeParentDatasetName(); let name = volume_id; if (!datasetParentName) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing datasetParentName` ); } const datasetName = datasetParentName + "/" + name; try { await zb.zfs.get(datasetName, []); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( grpc.status.NOT_FOUND, `invalid volume_id: ${volume_id}` ); } else { throw err; } } const result = this.assertCapabilities(call.request.volume_capabilities); if (result.valid !== true) { return { message: result.message }; } return { confirmed: { volume_context: call.request.volume_context, volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested parameters: call.request.parameters, }, }; } } module.exports.ControllerZfsBaseDriver = ControllerZfsBaseDriver;