diff --git a/docs/storage-class-parameters.md b/docs/storage-class-parameters.md new file mode 100644 index 0000000..d2a1684 --- /dev/null +++ b/docs/storage-class-parameters.md @@ -0,0 +1,100 @@ +# Storage Class Parameters + +Some drivers support different settings for volumes. These can be configured via the driver configuration and/or storage classes. + +## `synology-iscsi` +The `synology-iscsi` driver supports several storage class parameters. Note however that not all parameters/values are supported for all backing file systems and LUN type. The following options are available: + +### Configure Storage Classes +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: synology-iscsi +parameters: + fsType: ext4 + # The following options affect the LUN representing the volume + volume: /volume2 # Optional. Override the volume on which the LUN will be created. + lunType: BLUN # Btrfs thin provisioning + lunType: BLUN_THICK # Btrfs thick provisioning + lunType: THIN # Ext4 thin provisioning + lunType: ADV # Ext4 thin provisioning with legacy advanced feature set + lunType: FILE # Ext4 thick provisioning + lunDescription: Some Description + hardwareAssistedZeroing: true + hardwareAssistedLocking: true + hardwareAssistedDataTransfer: true + spaceReclamation: true + allowSnapshots: true + enableFuaWrite: false + enableSyncCache: false + ioPolicy: Buffered # or Direct + # The following options affect the iSCSI target + headerDigenst: false + dataDigest: false + maxSessions: 1 # Note that this option requires a compatible filesystem + maxRecieveSegmentBytes: 262144 + maxSendSegmentBytes: 262144 +... +``` + +About extended features: +- For `BLUN_THICK` volumes only hardware assisted zeroing and locking can be configured. +- For `THIN` volumes none of the extended features can be configured. +- For `ADV` volumes only space reclamation can be configured. +- For `FILE` volumes only hardware assisted locking can be configured. +- `ioPolicy` is only available for thick provisioned volumes. + +### Configure Snapshot Classes +`synology-iscsi` can also configure different parameters on snapshot classes: + +```yaml +apiVersion: snapshot.storage.k8s.io/v1 +kind: VolumeSnapshotClass +metadata: + name: synology-iscsi-snapshot +parameters: + isLocked: true + # https://kb.synology.com/en-me/DSM/tutorial/What_is_file_system_consistent_snapshot + consistency: AppConsistent # Or CrashConsistent +... +``` + +### Enabling CHAP Authentication +You can enable CHAP Authentication for `StorageClass`es by supplying an appropriate `StorageClass` secret (see the [documentation](https://kubernetes-csi.github.io/docs/secrets-and-credentials-storage-class.html) for more details). You can use the same password for alle volumes of a `StorageClass` or use different passwords per volume. + +```yaml +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: synology-iscsi-chap +parameters: + fsType: ext4 + lunType: BLUN + lunDescription: iSCSI volumes with CHAP Authentication +secrets: + # Use this to configure a single set of credentials for all volumes of this StorageClass + csi.storage.k8s.io/provisioner-secret-name: chap-secret + csi.storage.k8s.io/provisioner-secret-namespace: default + # Use substitutions to use different credentials for volumes based on the PVC + csi.storage.k8s.io/provisioner-secret-name: "${pvc.name}-chap-secret" + csi.storage.k8s.io/provisioner-secret-namespace: "${pvc.namespace}" +... +--- +# Use a secret like this to supply CHAP credentials. +apiVersion: v1 +kind: Secret +metadata: + name: chap-secret +stringData: + # Client Credentials + user: client + password: MySecretPassword + # Mutual CHAP Credentials. If these are specified mutual CHAP will be enabled. + mutualUser: server + password: MyOtherPassword +``` + +Note that CHAP authentication will only be enabled if the secret is correctly configured. If e.g. a password is missing CHAP authentication will not be enabled (but the volume will still be created). You cannot automatically enable/disable CHAP or change the password after the volume has been created. + +If the secret itself is referenced but not present, the volume will not be created. diff --git a/examples/synology-iscsi.yaml b/examples/synology-iscsi.yaml index b8cd825..64629cb 100644 --- a/examples/synology-iscsi.yaml +++ b/examples/synology-iscsi.yaml @@ -10,9 +10,10 @@ httpConnection: session: "democratic-csi" serialize: true -synology: - # choose the proper volume for your system - volume: /volume1 +# choose the default volume for your system. The default value is /volume1. +# This can also be overridden by StorageClasses +# synology: +# volume: /volume1 iscsi: targetPortal: "server[:port]" @@ -27,63 +28,5 @@ iscsi: # full iqn limit is 223 bytes, plan accordingly namePrefix: "" nameSuffix: "" - - # documented below are several blocks - # pick the option appropriate for you based on what your backing fs is and desired features - # you do not need to alter dev_attribs under normal circumstances but they may be altered in advanced use-cases - lunTemplate: - # btrfs thin provisioning - type: "BLUN" - # tpws = Hardware-assisted zeroing - # caw = Hardware-assisted locking - # 3pc = Hardware-assisted data transfer - # tpu = Space reclamation - # can_snapshot = Snapshot - #dev_attribs: - #- dev_attrib: emulate_tpws - # enable: 1 - #- dev_attrib: emulate_caw - # enable: 1 - #- dev_attrib: emulate_3pc - # enable: 1 - #- dev_attrib: emulate_tpu - # enable: 0 - #- dev_attrib: can_snapshot - # enable: 1 - - # btfs thick provisioning - # only zeroing and locking supported - #type: "BLUN_THICK" - # tpws = Hardware-assisted zeroing - # caw = Hardware-assisted locking - #dev_attribs: - #- dev_attrib: emulate_tpws - # enable: 1 - #- dev_attrib: emulate_caw - # enable: 1 - - # ext4 thinn provisioning UI sends everything with enabled=0 - #type: "THIN" - - # ext4 thin with advanced legacy features set - # can only alter tpu (all others are set as enabled=1) - #type: "ADV" - #dev_attribs: - #- dev_attrib: emulate_tpu - # enable: 1 - - # ext4 thick - # can only alter caw - #type: "FILE" - #dev_attribs: - #- dev_attrib: emulate_caw - # enable: 1 - - lunSnapshotTemplate: - is_locked: true - # https://kb.synology.com/en-me/DSM/tutorial/What_is_file_system_consistent_snapshot - is_app_consistent: true - - targetTemplate: - auth_type: 0 - max_sessions: 0 + # LUN options and CHAP authentication can be configured using StorageClasses. + # See https://github.com/democratic-csi/democratic-csi/blob/master/docs/storage-class-parameters.md diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index f25e3b3..2651f79 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -612,16 +612,20 @@ class SynologyHttpClient { ); } - async CreateClonedVolume(src_lun_uuid, dst_lun_name) { + async CreateClonedVolume(src_lun_uuid, dst_lun_name, dst_location, description) { const create_cloned_volume = { api: "SYNO.Core.ISCSI.LUN", version: 1, method: "clone", src_lun_uuid: JSON.stringify(src_lun_uuid), // src lun uuid dst_lun_name: dst_lun_name, // dst lun name + dst_location: dst_location, is_same_pool: true, // always true? string? clone_type: "democratic-csi", // check }; + if (description) { + create_cloned_volume.description = description; + } return await this.do_request("GET", "entry.cgi", create_cloned_volume); } diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index 6db629f..f00a0de 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -142,6 +142,24 @@ class ControllerSynologyDriver extends CsiBaseDriver { } } + /** + * Parses a boolean value (e.g. from the value of a parameter. This recognizes + * strings containing boolean literals as well as the numbers 1 and 0. + * + * @param {String} value - The value to be parsed. + * @returns {boolean} The parsed boolean value. + */ + parseBoolean(value) { + if (value === undefined) { + return undefined; + } + const parsed = parseInt(value) + if (!isNaN(parsed)) { + return Boolean(parsed) + } + return "true".localeCompare(value, undefined, {sensitivity: "accent"}) === 0 + } + buildIscsiName(name) { let iscsiName = name; if (this.options.iscsi.namePrefix) { @@ -155,6 +173,25 @@ class ControllerSynologyDriver extends CsiBaseDriver { return iscsiName.toLowerCase(); } + /** + * Returns the value for the 'location' parameter indicating on which volume + * a LUN is to be created. + * + * @param {Object} parameters - Parameters received from a StorageClass + * @param {String} parameters.volume - The volume specified by the StorageClass + * @returns {String} The location of the volume. + */ + getLocation({volume}) { + let location = volume ?? this.options?.synology?.volume + if (location === undefined) { + location = "volume1" + } + if (!location.startsWith('/')) { + location = "/" + location + } + return location + } + assertCapabilities(capabilities) { const driverResourceType = this.getDriverResourceType(); this.ctx.logger.verbose("validating capabilities: %j", capabilities); @@ -310,6 +347,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { } let volume_context = {}; + const normalizedParameters = driver.getNormalizedParameters(call.request.parameters); switch (driver.getDriverShareType()) { case "nfs": // TODO: create volume here @@ -425,7 +463,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { `invalid volume_id: ${volume_content_source.volume.volume_id}` ); } - await httpClient.CreateClonedVolume(src_lun_uuid, iscsiName); + await httpClient.CreateClonedVolume(src_lun_uuid, iscsiName, driver.getLocation(normalizedParameters)); } break; default: @@ -446,9 +484,59 @@ class ControllerSynologyDriver extends CsiBaseDriver { // create lun data = Object.assign({}, driver.options.iscsi.lunTemplate, { name: iscsiName, - location: driver.options.synology.volume, - size: capacity_bytes, + location: driver.getLocation(normalizedParameters), + size: capacity_bytes }); + data.type = normalizedParameters.lunType ?? data.type; + if ('lunDescription' in normalizedParameters) { + data.description = normalizedParameters.lunDescription; + } + if (normalizedParameters.ioPolicy === "Direct") { + data.direct_io_pattern = 3; + } else if (normalizedParameters.ioPolicy === "Buffered") { + data.direct_io_pattern = 0; + } else if (normalizedParameters.ioPolicy !== undefined) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `snapshot consistency must be either CrashConsistent or AppConsistent` + ); + } + + const dev_attribs = (data.dev_attribs ?? []).reduce( + (obj, item) => Object.assign(obj, {[item.dev_attrib]: driver.parseBoolean(item.enable)}), {} + ); + dev_attribs.emulate_tpws = driver.parseBoolean(normalizedParameters.hardwareAssistedZeroing) ?? dev_attribs.emulate_tpws; + dev_attribs.emulate_caw = driver.parseBoolean(normalizedParameters.hardwareAssistedLocking) ?? dev_attribs.emulate_caw; + dev_attribs.emulate_3pc = driver.parseBoolean(normalizedParameters.hardwareAssistedDataTransfer) ?? dev_attribs.emulate_3pc; + dev_attribs.emulate_tpu = driver.parseBoolean(normalizedParameters.spaceReclamation) ?? dev_attribs.emulate_tpu; + dev_attribs.emulate_fua_write = driver.parseBoolean(normalizedParameters.enableFuaWrite) ?? dev_attribs.emulate_fua_write; + dev_attribs.emulate_sync_cache = driver.parseBoolean(normalizedParameters.enableSyncCache) ?? dev_attribs.emulate_sync_cache; + dev_attribs.can_snapshot = driver.parseBoolean(normalizedParameters.allowSnapshots) ?? dev_attribs.can_snapshot; + data.dev_attribs = Object.entries(dev_attribs).filter( + e => e[1] !== undefined + ).map( + e => ({dev_attrib: e[0], enable: Number(e[1])}) + ); + + if (["BLUN", "THIN", "ADV"].includes(data.type) && 'direct_io_pattern' in data) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `ioPolicy can only be used with thick provisioning.` + ); + } + if (["BLUN_THICK", "FILE"].includes(data.type) && dev_attribs.emulate_tpu) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `spaceReclamation can only be used with thin provisioning.` + ); + } + if (["BLUN_THICK", "FILE"].includes(data.type) && dev_attribs.can_snapshot) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `allowSnapshots can only be used with thin provisioning.` + ); + } + lun_uuid = await httpClient.CreateLun(data); } @@ -458,6 +546,60 @@ class ControllerSynologyDriver extends CsiBaseDriver { name: iscsiName, iqn, }); + if ('headerChecksum' in normalizedParameters) { + data.has_data_checksum = normalizedParameters['headerChecksum']; + } + if ('dataChecksum' in normalizedParameters) { + data.has_data_checksum = normalizedParameters['dataChecksum']; + } + if ('maxSessions' in normalizedParameters) { + data.max_sessions = Number(normalizedParameters['maxSessions']); + if (isNaN(data.max_sessions)) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `maxSessions must be a number.` + ); + } + } + if (!('multi_sessions' in data) && 'max_sessions' in data) { + data.multi_sessions = data.max_sessions == 1; + } + if ('maxReceiveSegmentBytes' in normalizedParameters) { + data.max_recv_seg_bytes = Number(normalizedParameters['maxReceiveSegmentBytes']); + if (isNaN(data.max_recv_seg_bytes)) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `maxReceiveSegmentBytes must be a number.` + ); + } + } + if ('maxSendSegmentBytes' in normalizedParameters) { + data.max_send_seg_bytes = Number(normalizedParameters['maxSendSegmentBytes']); + if (isNaN(data.max_send_seg_bytes)) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `maxSendSegmentBytes must be a number.` + ); + } + } + + if ('user' in call.request.secrets && 'password' in call.request.secrets) { + data.user = call.request.secrets.user; + data.password = call.request.secrets.password; + data['chap'] = true; + if ('mutualUser' in call.request.secrets && 'mutualPassword' in call.request.secrets) { + data.mutual_user = call.request.secrets.mutualUser; + data.mutual_password = call.request.secrets.mutualPassword; + data.auth_type = 2; + data.mutual_chap = true; + } else { + data.auth_type = 1; + data.mutual_chap = false; + } + } else { + data.auth_type ??= 0; + data.chap ??= false; + } let target_id = await httpClient.CreateTarget(data); //target = await httpClient.GetTargetByTargetID(target_id); target = await httpClient.GetTargetByIQN(iqn); @@ -737,8 +879,10 @@ class ControllerSynologyDriver extends CsiBaseDriver { async GetCapacity(call) { const driver = this; const httpClient = await driver.getHttpClient(); + const normalizedParameters = driver.getNormalizedParameters(call.request.parameters) + const location = driver.getLocation(normalizedParameters); - if (!driver.options.synology.volume) { + if (!location) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `invalid configuration: missing volume` @@ -753,9 +897,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { } } - let response = await httpClient.GetVolumeInfo( - driver.options.synology.volume - ); + let response = await httpClient.GetVolumeInfo(location); return { available_capacity: response.body.data.volume.size_free_byte }; } @@ -850,11 +992,25 @@ class ControllerSynologyDriver extends CsiBaseDriver { let snapshot; snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name); if (!snapshot) { + const normalizedParameters = driver.getNormalizedParameters(call.request.parameters); let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, { src_lun_uuid: lun.uuid, taken_by: "democratic-csi", description: name, //check }); + if ('isLocked' in normalizedParameters) { + data['is_locked'] = driver.parseBoolean(normalizedParameters.isLocked); + } + if (normalizedParameters.consistency === "AppConsistent") { + data['is_app_consistent'] = true; + } else if (normalizedParameters.consistency === 'CrashConsistent') { + data['is_app_consistent'] = false; + } else if ('consistency' in normalizedParameters.consistency) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `snapshot consistency must be either CrashConsistent or AppConsistent` + ); + } await httpClient.CreateSnapshot(data); snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name);