Add StorageClass Parameters for Synology

This commit is contained in:
Kim Wittenburg 2022-04-13 18:44:11 +02:00
parent c76750a303
commit bd620025a0
4 changed files with 274 additions and 71 deletions

View File

@ -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.

View File

@ -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

View File

@ -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);
}

View File

@ -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);