From 5ae0248ba8d7044c4241bf088b5bd8f1d40957b5 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 8 Sep 2020 10:06:09 -0600 Subject: [PATCH] preview support for cifs/smb --- README.md | 2 +- src/driver/controller-zfs-ssh/index.js | 16 +- src/driver/factory.js | 2 + src/driver/freenas/index.js | 249 ++++++++++++++++++++++++- src/driver/index.js | 55 +++--- 5 files changed, 293 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 17d727d..ff2c05d 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ You may install multiple deployments of each/any driver. It requires the followi Install beta (v1.17+) CRDs (once per cluster): -- https://github.com/kubernetes-csi/external-snapshotter/tree/master/config/crd +- https://github.com/kubernetes-csi/external-snapshotter/tree/master/client/config/crd ``` kubectl apply -f snapshot.storage.k8s.io_volumesnapshotclasses.yaml diff --git a/src/driver/controller-zfs-ssh/index.js b/src/driver/controller-zfs-ssh/index.js index 92bb666..18b1fde 100644 --- a/src/driver/controller-zfs-ssh/index.js +++ b/src/driver/controller-zfs-ssh/index.js @@ -181,7 +181,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { if ( capability.mount.fs_type && - !["nfs"].includes(capability.mount.fs_type) + !["nfs", "cifs"].includes(capability.mount.fs_type) ) { message = `invalid fs_type ${capability.mount.fs_type}`; return false; @@ -694,9 +694,21 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { response = await sshClient.exec(command); } + // set acls + // TODO: this is unsfafe approach, make it better + if (this.options.zfs.datasetPermissionsAcls) { + for (const acl of this.options.zfs.datasetPermissionsAcls) { + command = sshClient.buildCommand("setfacl", [ + acl, + properties.mountpoint.value, + ]); + driver.ctx.logger.verbose("set acl command: %s", command); + response = await sshClient.exec(command); + } + } + break; case "volume": - // TODO: create all the necessary iscsi stuff // set properties // set reserve setProps = true; diff --git a/src/driver/factory.js b/src/driver/factory.js index a8d581a..48408fa 100644 --- a/src/driver/factory.js +++ b/src/driver/factory.js @@ -9,8 +9,10 @@ const { ControllerNfsClientDriver } = require("./controller-nfs-client"); function factory(ctx, options) { switch (options.driver) { case "freenas-nfs": + case "freenas-smb": case "freenas-iscsi": case "truenas-nfs": + case "truenas-smb": case "truenas-iscsi": return new FreeNASDriver(ctx, options); case "zfs-generic-nfs": diff --git a/src/driver/freenas/index.js b/src/driver/freenas/index.js index bd25d3a..7df05b0 100644 --- a/src/driver/freenas/index.js +++ b/src/driver/freenas/index.js @@ -6,6 +6,7 @@ const Handlebars = require("handlebars"); // freenas properties const FREENAS_NFS_SHARE_PROPERTY_NAME = "democratic-csi:freenas_nfs_share_id"; +const FREENAS_SMB_SHARE_PROPERTY_NAME = "democratic-csi:freenas_smb_share_id"; const FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME = "democratic-csi:freenas_iscsi_target_id"; const FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME = @@ -22,6 +23,8 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { switch (this.options.driver) { case "freenas-nfs": case "truenas-nfs": + case "freenas-smb": + case "truenas-smb": return "filesystem"; case "freenas-iscsi": case "truenas-iscsi": @@ -45,6 +48,9 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { case "freenas-iscsi": case "truenas-iscsi": return "iscsi"; + case "freenas-smb": + case "truenas-smb": + return "smb"; default: throw new Error("unknown driver: " + this.ctx.args.driver); } @@ -123,6 +129,7 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { const zb = this.getZetabyte(); let properties; + let endpoint; let response; let share = {}; @@ -207,7 +214,9 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { } else { throw new GrpcError( grpc.status.UNKNOWN, - `received error creating nfs share - code: ${response.statusCode} body: ${response.body}` + `received error creating nfs share - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` ); } } @@ -234,6 +243,179 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { return volume_context; } break; + case "smb": + properties = await zb.zfs.get(datasetName, [ + "mountpoint", + FREENAS_SMB_SHARE_PROPERTY_NAME, + ]); + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); + + let smbName; + + if (this.options.smb.nameTemplate) { + smbName = Handlebars.compile(this.options.smb.nameTemplate)({ + name: call.request.name, + parameters: call.request.parameters, + }); + } else { + smbName = zb.helpers.extractLeafName(datasetName); + } + + if (this.options.smb.namePrefix) { + smbName = this.options.smb.namePrefix + smbName; + } + + if (this.options.smb.nameSuffix) { + smbName += this.options.smb.nameSuffix; + } + + smbName = smbName.toLowerCase(); + + this.ctx.logger.info( + "FreeNAS creating smb share with name: " + smbName + ); + + // create smb share + if ( + !zb.helpers.isPropertyValueSet( + properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value + ) + ) { + /** + * The only required parameters are: + * - path + * - name + * + * Note that over time it appears the list of available parameters has increased + * so in an effort to best support old versions of FreeNAS we should check the + * presense of each parameter in the config and set the corresponding parameter in + * the API request *only* if present in the config. + */ + switch (apiVersion) { + case 1: + case 2: + share = { + name: smbName, + path: properties.mountpoint.value, + }; + + let propertyMapping = { + shareTemplate: "auxsmbconf", + shareHome: "home", + shareAllowedHosts: "hostsallow", + shareDeniedHosts: "hostsdeny", + shareDefaultPermissions: "default_permissions", + shareGuestOk: "guestok", + shareGuestOnly: "guestonly", + shareShowHiddenFiles: "showhiddenfiles", + shareRecycleBin: "recyclebin", + shareBrowsable: "browsable", + shareAccessBasedEnumeration: "abe", + shareTimeMachine: "timemachine", + shareStorageTask: "storage_task", + }; + + for (const key in propertyMapping) { + if (this.options.smb.hasOwnProperty(key)) { + let value; + switch (key) { + case "shareTemplate": + value = Handlebars.compile( + this.options.smb.shareTemplate + )({ + name: call.request.name, + parameters: call.request.parameters, + }); + break; + default: + value = this.options.smb[key]; + break; + } + share[propertyMapping[key]] = value; + } + } + + switch (apiVersion) { + case 1: + endpoint = "/sharing/cifs"; + + // rename keys with cifs_ prefix + for (const key in share) { + share["cifs_" + key] = share[key]; + delete share[key]; + } + + // convert to comma-separated list + if (share.cifs_hostsallow) { + share.cifs_hostsallow = share.cifs_hostsallow.join(","); + } + + // convert to comma-separated list + if (share.cifs_hostsdeny) { + share.cifs_hostsdeny = share.cifs_hostsdeny.join(","); + } + break; + case 2: + endpoint = "/sharing/smb"; + break; + } + + response = await httpClient.post(endpoint, share); + + /** + * v1 = 201 + * v2 = 200 + */ + if ([200, 201].includes(response.statusCode)) { + //set zfs property + await zb.zfs.set(datasetName, { + [FREENAS_SMB_SHARE_PROPERTY_NAME]: response.body.id, + }); + } else { + /** + * v1 = 409 + * v2 = 422 + */ + if ( + [409, 422].includes(response.statusCode) && + JSON.stringify(response.body).includes( + "You can't share same filesystem with all hosts twice." + ) + ) { + // move along + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating smb share - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } + + let volume_context = { + node_attach_driver: "smb", + server: this.options.smb.shareHost, + share: smbName, + }; + return volume_context; + + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } + } else { + let volume_context = { + node_attach_driver: "smb", + server: this.options.smb.shareHost, + share: smbName, + }; + return volume_context; + } + break; case "iscsi": properties = await zb.zfs.get(datasetName, [ FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, @@ -835,6 +1017,7 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { let properties; let response; let endpoint; + let shareId; switch (driverShareType) { case "nfs": @@ -851,7 +1034,7 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { properties = properties[datasetName]; this.ctx.logger.debug("zfs props data: %j", properties); - let shareId = properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value; + shareId = properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value; // remove nfs share if ( @@ -896,6 +1079,68 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { } } break; + case "smb": + try { + properties = await zb.zfs.get(datasetName, [ + FREENAS_SMB_SHARE_PROPERTY_NAME, + ]); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + return; + } + throw err; + } + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); + + shareId = properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value; + + // remove smb share + if ( + properties && + properties[FREENAS_SMB_SHARE_PROPERTY_NAME] && + properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value != "-" + ) { + switch (apiVersion) { + case 1: + case 2: + switch (apiVersion) { + case 1: + endpoint = `/sharing/cifs/${shareId}`; + break; + case 2: + endpoint = `/sharing/smb/id/${shareId}`; + break; + } + + response = await httpClient.get(endpoint); + + // assume share is gone for now + if ([404, 500].includes(response.statusCode)) { + } else { + response = await httpClient.delete(endpoint); + + // returns a 500 if does not exist + // v1 = 204 + // v2 = 200 + if (![200, 204].includes(response.statusCode)) { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error deleting smb share - share: ${shareId} code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } + } + break; case "iscsi": // Delete target // NOTE: deletting a target inherently deletes associated targetgroup(s) and targettoextent(s) diff --git a/src/driver/index.js b/src/driver/index.js index dc866c1..dc974f5 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -75,14 +75,14 @@ class CsiBaseDriver { async GetPluginInfo(call) { return { name: this.ctx.args.csiName, - vendor_version: this.ctx.args.version + vendor_version: this.ctx.args.version, }; } async GetPluginCapabilities(call) { let capabilities; const response = { - capabilities: [] + capabilities: [], }; //UNKNOWN = 0; @@ -104,12 +104,12 @@ class CsiBaseDriver { // accessible from a given node when scheduling workloads. //VOLUME_ACCESSIBILITY_CONSTRAINTS = 2; capabilities = this.options.service.identity.capabilities.service || [ - "UNKNOWN" + "UNKNOWN", ]; - capabilities.forEach(item => { + capabilities.forEach((item) => { response.capabilities.push({ - service: { type: item } + service: { type: item }, }); }); @@ -155,9 +155,9 @@ class CsiBaseDriver { capabilities = this.options.service.identity.capabilities .volume_expansion || ["UNKNOWN"]; - capabilities.forEach(item => { + capabilities.forEach((item) => { response.capabilities.push({ - volume_expansion: { type: item } + volume_expansion: { type: item }, }); }); @@ -171,7 +171,7 @@ class CsiBaseDriver { async ControllerGetCapabilities(call) { let capabilities; const response = { - capabilities: [] + capabilities: [], }; //UNKNOWN = 0; @@ -199,12 +199,12 @@ class CsiBaseDriver { // See VolumeExpansion for details. //EXPAND_VOLUME = 9; capabilities = this.options.service.controller.capabilities.rpc || [ - "UNKNOWN" + "UNKNOWN", ]; - capabilities.forEach(item => { + capabilities.forEach((item) => { response.capabilities.push({ - rpc: { type: item } + rpc: { type: item }, }); }); @@ -214,7 +214,7 @@ class CsiBaseDriver { async NodeGetCapabilities(call) { let capabilities; const response = { - capabilities: [] + capabilities: [], }; //UNKNOWN = 0; @@ -227,9 +227,9 @@ class CsiBaseDriver { //EXPAND_VOLUME = 3; capabilities = this.options.service.node.capabilities.rpc || ["UNKNOWN"]; - capabilities.forEach(item => { + capabilities.forEach((item) => { response.capabilities.push({ - rpc: { type: item } + rpc: { type: item }, }); }); @@ -239,7 +239,7 @@ class CsiBaseDriver { async NodeGetInfo(call) { return { node_id: process.env.CSI_NODE_ID || os.hostname(), - max_volumes_per_node: 0 + max_volumes_per_node: 0, }; } @@ -296,12 +296,15 @@ class CsiBaseDriver { case "nfs": device = `${volume_context.server}:${volume_context.share}`; break; + case "smb": + device = `//${volume_context.server}/${volume_context.share}`; + break; case "iscsi": // create DB entry // https://library.netapp.com/ecmdocs/ECMP1654943/html/GUID-8EC685B4-8CB6-40D8-A8D5-031A3899BCDC.html // put these options in place to force targets managed by csi to be explicitly attached (in the case of unclearn shutdown etc) let nodeDB = { - "node.startup": "manual" + "node.startup": "manual", }; const nodeDBKeyPrefix = "node-db."; const normalizedSecrets = this.getNormalizedParameters( @@ -420,7 +423,7 @@ class CsiBaseDriver { await mount.bindMount(device, block_path, [ "-o", - bind_mount_flags.join(",") + bind_mount_flags.join(","), ]); } break; @@ -506,7 +509,7 @@ class CsiBaseDriver { session.attached_scsi_devices.host.devices ) { is_attached_to_session = session.attached_scsi_devices.host.devices.some( - device => { + (device) => { if (device.attached_scsi_disk == block_device_info.name) { return true; } @@ -525,7 +528,7 @@ class CsiBaseDriver { while (!loggedOut) { try { await iscsi.iscsiadm.logout(session.target, [ - session.persistent_portal + session.persistent_portal, ]); loggedOut = true; } catch (err) { @@ -659,7 +662,7 @@ class CsiBaseDriver { if (!result) { await mount.bindMount(normalized_staging_path, target_path, [ "-o", - bind_mount_flags.join(",") + bind_mount_flags.join(","), ]); } else { // if is mounted, ensure proper source @@ -760,9 +763,9 @@ class CsiBaseDriver { available: result.avail, total: result.size, used: result.used, - unit: "BYTES" - } - ] + unit: "BYTES", + }, + ], }; case "block": result = await filesystem.getBlockDevice(device_path); @@ -771,9 +774,9 @@ class CsiBaseDriver { usage: [ { total: result.size, - unit: "BYTES" - } - ] + unit: "BYTES", + }, + ], }; default: throw new GrpcError(