From c76750a30343f5c5f4d3fa1a857e2024b084fd17 Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Wed, 13 Apr 2022 17:53:25 +0200 Subject: [PATCH 01/71] Improve Synology error handling --- src/driver/controller-synology/http/index.js | 50 ++++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index f1e7c0a..f25e3b3 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -4,10 +4,50 @@ const https = require("https"); const { axios_request, stringify } = require("../../../utils/general"); const Mutex = require("async-mutex").Mutex; const registry = require("../../../utils/registry"); +const { GrpcError, grpc } = require("../../../utils/grpc"); const USER_AGENT = "democratic-csi"; const __REGISTRY_NS__ = "SynologyHttpClient"; +SYNO_ERROR_MESSAGES = { + 18990002: "The synology volume is out of disk space.", + 18990538: "A LUN with this name already exists.", + 18990541: "The maximum number of LUNS has been reached.", + 18990542: "The maximum number if iSCSI target has been reached.", + 18990744: "An iSCSI target with this name already exists.", + 18990532: "No such snapshot.", + 18990500: "Bad LUN type", + 18990543: "Maximum number of snapshots reached.", + 18990635: "Invalid ioPolicy." +} + +SYNO_GRPC_CODES = { + 18990002: grpc.status.RESOURCE_EXHAUSTED, + 18990538: grpc.status.ALREADY_EXISTS, + 18990541: grpc.status.RESOURCE_EXHAUSTED, + 18990542: grpc.status.RESOURCE_EXHAUSTED, + 18990744: grpc.status.ALREADY_EXISTS, + 18990532: grpc.status.NOT_FOUND, + 18990500: grpc.status.INVALID_ARGUMENT, + 18990543: grpc.status.RESOURCE_EXHAUSTED, + 18990635: grpc.status.INVALID_ARGUMENT +} + +class SynologyError extends GrpcError { + constructor(code, httpCode = undefined) { + super(0, ""); + this.synoCode = code; + this.httpCode = httpCode; + if (code > 0) { + this.code = SYNO_GRPC_CODES[code] ?? grpc.status.UNKNOWN; + this.message = SYNO_ERROR_MESSAGES[code] ?? `An unknown error occurred when executing a synology command (code = ${code}).`; + } else { + this.code = grpc.status.UNKNOWN; + this.message = `The synology webserver returned a status code ${httpCode}`; + } + } +} + class SynologyHttpClient { constructor(options = {}) { this.options = JSON.parse(JSON.stringify(options)); @@ -149,7 +189,7 @@ class SynologyHttpClient { } if (response.statusCode > 299 || response.statusCode < 200) { - reject(response); + reject(new SynologyError(-1, response.statusCode)) } if (response.body.success === false) { @@ -157,7 +197,7 @@ class SynologyHttpClient { if (response.body.error.code == 119 && sid == client.sid) { client.sid = null; } - reject(response); + reject(new SynologyError(response.body.error.code, response.statusCode)); } resolve(response); @@ -412,7 +452,7 @@ class SynologyHttpClient { response = await this.do_request("GET", "entry.cgi", iscsi_lun_create); return response.body.data.uuid; } catch (err) { - if ([18990538].includes(err.body.error.code)) { + if (err.synoCode === 18990538) { response = await this.do_request("GET", "entry.cgi", lun_list); let lun = response.body.data.luns.find((i) => { return i.name == iscsi_lun_create.name; @@ -503,7 +543,7 @@ class SynologyHttpClient { return response.body.data.target_id; } catch (err) { - if ([18990744].includes(err.body.error.code)) { + if (err.synoCode === 18990744) { //do lookup const iscsi_target_list = { api: "SYNO.Core.ISCSI.Target", @@ -549,7 +589,7 @@ class SynologyHttpClient { /** * 18990710 = non-existant */ - //if (![18990710].includes(err.body.error.code)) { + //if (err.synoCode !== 18990710) { throw err; //} } From bd620025a06f0d64f8a95a163866d771b3debb8b Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Wed, 13 Apr 2022 18:44:11 +0200 Subject: [PATCH 02/71] Add StorageClass Parameters for Synology --- docs/storage-class-parameters.md | 100 +++++++++++ examples/synology-iscsi.yaml | 69 +------- src/driver/controller-synology/http/index.js | 6 +- src/driver/controller-synology/index.js | 170 ++++++++++++++++++- 4 files changed, 274 insertions(+), 71 deletions(-) create mode 100644 docs/storage-class-parameters.md 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); From 4de638b596cf91bb3dd10ff055bcbad71fe144cc Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Fri, 15 Apr 2022 23:24:18 +0200 Subject: [PATCH 03/71] Fix Snapshots for DSM 7 --- README.md | 2 +- src/driver/controller-synology/http/index.js | 26 +++++++------- src/driver/controller-synology/index.js | 37 ++++++++++++-------- 3 files changed, 36 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index f934689..dc8d43b 100644 --- a/README.md +++ b/README.md @@ -283,7 +283,7 @@ Ensure ssh and zfs is installed on the nfs/iscsi server and that you have instal ### Synology (synology-iscsi) -Ensure iscsi manager has been installed and is generally setup/configured. +Ensure iscsi manager has been installed and is generally setup/configured. DSM 6.3+ is supported. ## Helm Installation diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index 2651f79..0412924 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -333,19 +333,19 @@ class SynologyHttpClient { return snapshots; } - async GetSnapshotByLunIDAndName(lun_id, name) { + async GetSnapshotByLunUUIDAndName(lun_uuid, name) { const get_snapshot_info = { - lid: lun_id, //check? - api: "SYNO.Core.Storage.iSCSILUN", - method: "load_snapshot", + api: "SYNO.Core.ISCSI.LUN", + method: "list_snapshot", version: 1, + src_lun_uuid: JSON.stringify(lun_uuid), }; let response = await this.do_request("GET", "entry.cgi", get_snapshot_info); - if (response.body.data) { - let snapshot = response.body.data.find((i) => { - return i.desc == name; + if (response.body.data.snapshots) { + let snapshot = response.body.data.snapshots.find((i) => { + return i.description == name; }); if (snapshot) { @@ -354,18 +354,18 @@ class SynologyHttpClient { } } - async GetSnapshotByLunIDAndSnapshotUUID(lun_id, snapshot_uuid) { + async GetSnapshotByLunUUIDAndSnapshotUUID(lun_uuid, snapshot_uuid) { const get_snapshot_info = { - lid: lun_id, //check? - api: "SYNO.Core.Storage.iSCSILUN", - method: "load_snapshot", + api: "SYNO.Core.ISCSI.LUN", + method: "list_snapshot", version: 1, + src_lun_uuid: JSON.stringify(lun_uuid), }; let response = await this.do_request("GET", "entry.cgi", get_snapshot_info); - if (response.body.data) { - let snapshot = response.body.data.find((i) => { + if (response.body.data.snapshots) { + let snapshot = response.body.data.snapshots.find((i) => { return i.uuid == snapshot_uuid; }); diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index f00a0de..dead5dd 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -399,13 +399,12 @@ class ControllerSynologyDriver extends CsiBaseDriver { if (volume_content_source) { let src_lun_uuid; - let src_lun_id; switch (volume_content_source.type) { case "snapshot": let parts = volume_content_source.snapshot.snapshot_id.split("/"); - src_lun_id = parts[2]; - if (!src_lun_id) { + src_lun_uuid = parts[2]; + if (!src_lun_uuid) { throw new GrpcError( grpc.status.NOT_FOUND, `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` @@ -420,11 +419,14 @@ class ControllerSynologyDriver extends CsiBaseDriver { ); } - let src_lun = await httpClient.GetLunByID(src_lun_id); - src_lun_uuid = src_lun.uuid; + // This is for backwards compatibility. Previous versions of this driver used the LUN ID instead of the + // UUID. If this is the case we need to get the LUN UUID before we can proceed. + if (!src_lun_uuid.includes("-")) { + src_lun_uuid = await httpClient.GetLunByID(src_lun_uuid).uuid; + } - let snapshot = await httpClient.GetSnapshotByLunIDAndSnapshotUUID( - src_lun_id, + let snapshot = await httpClient.GetSnapshotByLunUUIDAndSnapshotUUID( + src_lun_uuid, snapshot_uuid ); if (!snapshot) { @@ -990,7 +992,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { // check for already exists let snapshot; - snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name); + snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); if (!snapshot) { const normalizedParameters = driver.getNormalizedParameters(call.request.parameters); let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, { @@ -1013,7 +1015,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { } await httpClient.CreateSnapshot(data); - snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name); + snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); if (!snapshot) { throw new Error(`failed to create snapshot`); @@ -1027,7 +1029,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { * is needed to create a volume from this snapshot. */ size_bytes: snapshot.total_size, - snapshot_id: `/lun/${lun.lun_id}/${snapshot.uuid}`, // add shanpshot_uuid //fixme + snapshot_id: `/lun/${lun.uuid}/${snapshot.uuid}`, source_volume_id: source_volume_id, //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto creation_time: { @@ -1064,8 +1066,8 @@ class ControllerSynologyDriver extends CsiBaseDriver { } let parts = snapshot_id.split("/"); - let lun_id = parts[2]; - if (!lun_id) { + let lun_uuid = parts[2]; + if (!lun_uuid) { return {}; } @@ -1074,9 +1076,14 @@ class ControllerSynologyDriver extends CsiBaseDriver { return {}; } - // TODO: delete snapshot - let snapshot = await httpClient.GetSnapshotByLunIDAndSnapshotUUID( - lun_id, + // This is for backwards compatibility. Previous versions of this driver used the LUN ID instead of the UUID. If + // this is the case we need to get the LUN UUID before we can proceed. + if (!lun_uuid.includes("-")) { + lun_uuid = await httpClient.GetLunByID(lun_uuid).uuid; + } + + let snapshot = await httpClient.GetSnapshotByLunUUIDAndSnapshotUUID( + lun_uuid, snapshot_uuid ); From 20d4b3a7a3e3b51abcd123b35ee87d83a219aaad Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Fri, 15 Apr 2022 23:40:40 +0200 Subject: [PATCH 04/71] Update docs --- docs/storage-class-parameters.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/storage-class-parameters.md b/docs/storage-class-parameters.md index d2a1684..e7e6ba9 100644 --- a/docs/storage-class-parameters.md +++ b/docs/storage-class-parameters.md @@ -1,9 +1,11 @@ # Storage Class Parameters -Some drivers support different settings for volumes. These can be configured via the driver configuration and/or storage classes. +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: +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 @@ -56,12 +58,16 @@ metadata: parameters: isLocked: true # https://kb.synology.com/en-me/DSM/tutorial/What_is_file_system_consistent_snapshot + # Note that AppConsistent snapshots require a working Synology Storage Console. Otherwise both values will have + # equivalent behavior. 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. +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 @@ -95,6 +101,8 @@ stringData: 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. +Note that CHAP authentication will only be enabled if the secret contains a username and password. 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. From c8f50f3c6be6ddaf9c528ed243dea16f56f6bebc Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Sat, 16 Apr 2022 00:05:49 +0200 Subject: [PATCH 05/71] Consolidate Synology API errors --- src/driver/controller-synology/http/index.js | 37 +++++++------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index 0412924..cbb2c08 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -9,28 +9,16 @@ const { GrpcError, grpc } = require("../../../utils/grpc"); const USER_AGENT = "democratic-csi"; const __REGISTRY_NS__ = "SynologyHttpClient"; -SYNO_ERROR_MESSAGES = { - 18990002: "The synology volume is out of disk space.", - 18990538: "A LUN with this name already exists.", - 18990541: "The maximum number of LUNS has been reached.", - 18990542: "The maximum number if iSCSI target has been reached.", - 18990744: "An iSCSI target with this name already exists.", - 18990532: "No such snapshot.", - 18990500: "Bad LUN type", - 18990543: "Maximum number of snapshots reached.", - 18990635: "Invalid ioPolicy." -} - -SYNO_GRPC_CODES = { - 18990002: grpc.status.RESOURCE_EXHAUSTED, - 18990538: grpc.status.ALREADY_EXISTS, - 18990541: grpc.status.RESOURCE_EXHAUSTED, - 18990542: grpc.status.RESOURCE_EXHAUSTED, - 18990744: grpc.status.ALREADY_EXISTS, - 18990532: grpc.status.NOT_FOUND, - 18990500: grpc.status.INVALID_ARGUMENT, - 18990543: grpc.status.RESOURCE_EXHAUSTED, - 18990635: grpc.status.INVALID_ARGUMENT +SYNO_ERRORS = { + 18990002: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The synology volume is out of disk space." }, + 18990538: { status: grpc.status.ALREADY_EXISTS, message: "A LUN with this name already exists." }, + 18990541: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The maximum number of LUNS has been reached." }, + 18990542: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The maximum number if iSCSI target has been reached." }, + 18990744: { status: grpc.status.ALREADY_EXISTS, message: "An iSCSI target with this name already exists." }, + 18990532: { status: grpc.status.NOT_FOUND, message: "No such snapshot." }, + 18990500: { status: grpc.status.INVALID_ARGUMENT, message: "Bad LUN type" }, + 18990543: { status: grpc.status.RESOURCE_EXHAUSTED, message: "Maximum number of snapshots reached." }, + 18990635: { status: grpc.status.INVALID_ARGUMENT, message: "Invalid ioPolicy." } } class SynologyError extends GrpcError { @@ -39,8 +27,9 @@ class SynologyError extends GrpcError { this.synoCode = code; this.httpCode = httpCode; if (code > 0) { - this.code = SYNO_GRPC_CODES[code] ?? grpc.status.UNKNOWN; - this.message = SYNO_ERROR_MESSAGES[code] ?? `An unknown error occurred when executing a synology command (code = ${code}).`; + const error = SYNO_ERRORS[code] + this.code = error?.status ?? grpc.status.UNKNOWN; + this.message = error?.message ?? `An unknown error occurred when executing a synology command (code = ${code}).`; } else { this.code = grpc.status.UNKNOWN; this.message = `The synology webserver returned a status code ${httpCode}`; From 8dbe45a789fe0abd814f97a77b147668f115b06c Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Sat, 16 Apr 2022 00:12:09 +0200 Subject: [PATCH 06/71] Fix null value --- src/driver/controller-synology/http/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index cbb2c08..328b5bd 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -178,7 +178,7 @@ class SynologyHttpClient { } if (response.statusCode > 299 || response.statusCode < 200) { - reject(new SynologyError(-1, response.statusCode)) + reject(new SynologyError(null, response.statusCode)) } if (response.body.success === false) { From 7f998ebec2f1be2ae5024f610c76081ae77f10a3 Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Sat, 16 Apr 2022 15:56:39 +0200 Subject: [PATCH 07/71] Default Values & Code Style --- docs/storage-class-parameters.md | 3 +++ src/driver/controller-synology/http/index.js | 12 +++++++-- src/driver/controller-synology/index.js | 28 ++++++++++++-------- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/docs/storage-class-parameters.md b/docs/storage-class-parameters.md index e7e6ba9..da50195 100644 --- a/docs/storage-class-parameters.md +++ b/docs/storage-class-parameters.md @@ -64,6 +64,9 @@ parameters: ... ``` +Note that it is currently not supported by Synology devices to restore a snapshot onto a different volume. You can +create volumes from snapshots, but you should use the same `StorageClass` as the original volume of the snapshot did. + ### 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 diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index 328b5bd..03d6013 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -39,7 +39,12 @@ class SynologyError extends GrpcError { class SynologyHttpClient { constructor(options = {}) { - this.options = JSON.parse(JSON.stringify(options)); + this.options = Object.assign({ + protocol: "https", + port: 5001, + allowInsecure: false, + session: "democratic-csi" + }, JSON.parse(JSON.stringify(options))); this.logger = console; this.doLoginMutex = new Mutex(); this.apiSerializeMutex = new Mutex(); @@ -618,7 +623,7 @@ class SynologyHttpClient { return await this.do_request("GET", "entry.cgi", create_cloned_volume); } - async CreateVolumeFromSnapshot(src_lun_uuid, snapshot_uuid, cloned_lun_name) { + async CreateVolumeFromSnapshot(src_lun_uuid, snapshot_uuid, cloned_lun_name, description) { const create_volume_from_snapshot = { api: "SYNO.Core.ISCSI.LUN", version: 1, @@ -628,6 +633,9 @@ class SynologyHttpClient { cloned_lun_name: cloned_lun_name, // cloned lun name clone_type: "democratic-csi", // check }; + if (description) { + create_volume_from_snapshot.description = description; + } return await this.do_request( "GET", "entry.cgi", diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index dead5dd..6670422 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -441,7 +441,8 @@ class ControllerSynologyDriver extends CsiBaseDriver { await httpClient.CreateVolumeFromSnapshot( src_lun_uuid, snapshot_uuid, - iscsiName + iscsiName, + normalizedParameters.description ); } break; @@ -465,7 +466,12 @@ class ControllerSynologyDriver extends CsiBaseDriver { `invalid volume_id: ${volume_content_source.volume.volume_id}` ); } - await httpClient.CreateClonedVolume(src_lun_uuid, iscsiName, driver.getLocation(normalizedParameters)); + await httpClient.CreateClonedVolume( + src_lun_uuid, + iscsiName, + driver.getLocation(normalizedParameters), + normalizedParameters.description + ); } break; default: @@ -549,13 +555,13 @@ class ControllerSynologyDriver extends CsiBaseDriver { iqn, }); if ('headerChecksum' in normalizedParameters) { - data.has_data_checksum = normalizedParameters['headerChecksum']; + data.has_data_checksum = normalizedParameters.headerChecksum; } if ('dataChecksum' in normalizedParameters) { - data.has_data_checksum = normalizedParameters['dataChecksum']; + data.has_data_checksum = normalizedParameters.dataChecksum; } if ('maxSessions' in normalizedParameters) { - data.max_sessions = Number(normalizedParameters['maxSessions']); + data.max_sessions = Number(normalizedParameters.maxSessions); if (isNaN(data.max_sessions)) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, @@ -567,7 +573,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { data.multi_sessions = data.max_sessions == 1; } if ('maxReceiveSegmentBytes' in normalizedParameters) { - data.max_recv_seg_bytes = Number(normalizedParameters['maxReceiveSegmentBytes']); + data.max_recv_seg_bytes = Number(normalizedParameters.maxReceiveSegmentBytes); if (isNaN(data.max_recv_seg_bytes)) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, @@ -576,7 +582,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { } } if ('maxSendSegmentBytes' in normalizedParameters) { - data.max_send_seg_bytes = Number(normalizedParameters['maxSendSegmentBytes']); + data.max_send_seg_bytes = Number(normalizedParameters.maxSendSegmentBytes); if (isNaN(data.max_send_seg_bytes)) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, @@ -588,7 +594,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { 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; + 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; @@ -1001,12 +1007,12 @@ class ControllerSynologyDriver extends CsiBaseDriver { description: name, //check }); if ('isLocked' in normalizedParameters) { - data['is_locked'] = driver.parseBoolean(normalizedParameters.isLocked); + data.is_locked = driver.parseBoolean(normalizedParameters.isLocked); } if (normalizedParameters.consistency === "AppConsistent") { - data['is_app_consistent'] = true; + data.is_app_consistent = true; } else if (normalizedParameters.consistency === 'CrashConsistent') { - data['is_app_consistent'] = false; + data.is_app_consistent = false; } else if ('consistency' in normalizedParameters.consistency) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, From 7b4d6a9a059ae4b70207f267ec6e72c0eb68c67a Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Sat, 16 Apr 2022 17:01:59 +0200 Subject: [PATCH 08/71] Custom templates, remove volume from StorageClass --- docs/storage-class-parameters.md | 17 ++++++++++++++--- examples/synology-iscsi.yaml | 3 +-- src/driver/controller-synology/http/index.js | 7 +------ src/driver/controller-synology/index.js | 18 +++++++++--------- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/docs/storage-class-parameters.md b/docs/storage-class-parameters.md index da50195..b9a6bd9 100644 --- a/docs/storage-class-parameters.md +++ b/docs/storage-class-parameters.md @@ -16,7 +16,6 @@ metadata: 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 @@ -34,10 +33,18 @@ parameters: # The following options affect the iSCSI target headerDigenst: false dataDigest: false - maxSessions: 1 # Note that this option requires a compatible filesystem + maxSessions: 1 # Note that this option requires a compatible filesystem. Use 0 for unlimited sessions (default). maxRecieveSegmentBytes: 262144 maxSendSegmentBytes: 262144 -... + lunTemplate: | + # This inline yaml object will be passed to the Synology API when creating the LUN. Use this for custom options. + dev_attribs: + - dev_attrib: emulate_caw + enable: 1 + targetTemplate: | + # This inline yaml object will be passed to the Synology API when creating the target. Use this for custom + # options. + max_sessions: 0 ``` About extended features: @@ -61,6 +68,10 @@ parameters: # Note that AppConsistent snapshots require a working Synology Storage Console. Otherwise both values will have # equivalent behavior. consistency: AppConsistent # Or CrashConsistent + lunSnapshotTemplate: | + # This inline yaml object will be passed to the Synology API when creating the snapshot. Use this for custom + # options. + is_locked: true ... ``` diff --git a/examples/synology-iscsi.yaml b/examples/synology-iscsi.yaml index 64629cb..0fba420 100644 --- a/examples/synology-iscsi.yaml +++ b/examples/synology-iscsi.yaml @@ -10,8 +10,7 @@ httpConnection: session: "democratic-csi" serialize: true -# choose the default volume for your system. The default value is /volume1. -# This can also be overridden by StorageClasses +# Choose the DSM volume this driver operates on. The default value is /volume1. # synology: # volume: /volume1 diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index 03d6013..9e00362 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -39,12 +39,7 @@ class SynologyError extends GrpcError { class SynologyHttpClient { constructor(options = {}) { - this.options = Object.assign({ - protocol: "https", - port: 5001, - allowInsecure: false, - session: "democratic-csi" - }, JSON.parse(JSON.stringify(options))); + this.options = JSON.parse(JSON.stringify(options)); this.logger = console; this.doLoginMutex = new Mutex(); this.apiSerializeMutex = new Mutex(); diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index 6670422..e1da1bf 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -4,6 +4,7 @@ const registry = require("../../utils/registry"); const SynologyHttpClient = require("./http").SynologyHttpClient; const semver = require("semver"); const sleep = require("../../utils/general").sleep; +const yaml = require("js-yaml"); const __REGISTRY_NS__ = "ControllerSynologyDriver"; @@ -181,8 +182,8 @@ class ControllerSynologyDriver extends CsiBaseDriver { * @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 + getLocation() { + let location = this.options?.synology?.volume; if (location === undefined) { location = "volume1" } @@ -469,7 +470,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { await httpClient.CreateClonedVolume( src_lun_uuid, iscsiName, - driver.getLocation(normalizedParameters), + driver.getLocation(), normalizedParameters.description ); } @@ -490,9 +491,9 @@ class ControllerSynologyDriver extends CsiBaseDriver { } } else { // create lun - data = Object.assign({}, driver.options.iscsi.lunTemplate, { + data = Object.assign({}, driver.options.iscsi.lunTemplate, yaml.load(normalizedParameters.lunTemplate), { name: iscsiName, - location: driver.getLocation(normalizedParameters), + location: driver.getLocation(), size: capacity_bytes }); data.type = normalizedParameters.lunType ?? data.type; @@ -550,7 +551,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { // create target let iqn = driver.options.iscsi.baseiqn + iscsiName; - data = Object.assign({}, driver.options.iscsi.targetTemplate, { + data = Object.assign({}, driver.options.iscsi.targetTemplate, yaml.load(normalizedParameters.targetTemplate), { name: iscsiName, iqn, }); @@ -887,8 +888,7 @@ 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); + const location = driver.getLocation(); if (!location) { throw new GrpcError( @@ -1001,7 +1001,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); if (!snapshot) { const normalizedParameters = driver.getNormalizedParameters(call.request.parameters); - let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, { + let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, yaml.load(normalizedParameters.lunSnapshotTemplate), { src_lun_uuid: lun.uuid, taken_by: "democratic-csi", description: name, //check From 3f51f5b7a6957b0fa144270bb1138c27d8d6fa2a Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sun, 17 Apr 2022 10:57:57 -0600 Subject: [PATCH 09/71] windows node support windows node support for smb/iscsi (csi-proxy) better logging support (for testing) generating volume_id from name wait for chown/chmod jobs to complete (freenas) general improvement to smb behavior throughout Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 2 +- bin/democratic-csi | 30 +- csi_proxy_proto/disk/v1/api.proto | 111 + csi_proxy_proto/disk/v1alpha1/api.proto | 62 + csi_proxy_proto/disk/v1beta1/api.proto | 81 + csi_proxy_proto/disk/v1beta2/api.proto | 109 + csi_proxy_proto/disk/v1beta3/api.proto | 111 + csi_proxy_proto/errors.proto | 17 + csi_proxy_proto/filesystem/v1/api.proto | 136 + csi_proxy_proto/filesystem/v1alpha1/api.proto | 168 + csi_proxy_proto/filesystem/v1beta1/api.proto | 168 + csi_proxy_proto/filesystem/v1beta2/api.proto | 136 + csi_proxy_proto/filesystem/v2alpha1/api.proto | 163 + csi_proxy_proto/iscsi/v1alpha1/api.proto | 153 + csi_proxy_proto/iscsi/v1alpha2/api.proto | 175 ++ csi_proxy_proto/smb/v1/api.proto | 58 + csi_proxy_proto/smb/v1alpha1/api.proto | 59 + csi_proxy_proto/smb/v1beta1/api.proto | 59 + csi_proxy_proto/smb/v1beta2/api.proto | 58 + csi_proxy_proto/system/v1alpha1/api.proto | 93 + csi_proxy_proto/volume/v1/api.proto | 143 + csi_proxy_proto/volume/v1alpha1/api.proto | 69 + csi_proxy_proto/volume/v1beta1/api.proto | 121 + csi_proxy_proto/volume/v1beta2/api.proto | 132 + csi_proxy_proto/volume/v1beta3/api.proto | 143 + csi_proxy_proto/volume/v2alpha1/api.proto | 158 + examples/freenas-api-nfs.yaml | 2 + examples/freenas-api-smb.yaml | 13 +- examples/freenas-smb.yaml | 45 +- examples/node-common.yaml | 3 + examples/private.yaml | 18 + package-lock.json | 181 +- package.json | 2 +- src/driver/controller-synology/index.js | 12 +- src/driver/controller-zfs-local/index.js | 3 +- src/driver/controller-zfs/index.js | 43 +- src/driver/freenas/api.js | 45 +- src/driver/freenas/http/api.js | 50 +- src/driver/freenas/http/index.js | 3 + src/driver/freenas/ssh.js | 12 + src/driver/index.js | 2709 +++++++++++------ src/utils/csi_proxy_client.js | 257 ++ src/utils/filesystem.js | 9 +- src/utils/general.js | 80 +- 44 files changed, 5204 insertions(+), 998 deletions(-) create mode 100644 csi_proxy_proto/disk/v1/api.proto create mode 100644 csi_proxy_proto/disk/v1alpha1/api.proto create mode 100644 csi_proxy_proto/disk/v1beta1/api.proto create mode 100644 csi_proxy_proto/disk/v1beta2/api.proto create mode 100644 csi_proxy_proto/disk/v1beta3/api.proto create mode 100644 csi_proxy_proto/errors.proto create mode 100644 csi_proxy_proto/filesystem/v1/api.proto create mode 100644 csi_proxy_proto/filesystem/v1alpha1/api.proto create mode 100644 csi_proxy_proto/filesystem/v1beta1/api.proto create mode 100644 csi_proxy_proto/filesystem/v1beta2/api.proto create mode 100644 csi_proxy_proto/filesystem/v2alpha1/api.proto create mode 100644 csi_proxy_proto/iscsi/v1alpha1/api.proto create mode 100644 csi_proxy_proto/iscsi/v1alpha2/api.proto create mode 100644 csi_proxy_proto/smb/v1/api.proto create mode 100644 csi_proxy_proto/smb/v1alpha1/api.proto create mode 100644 csi_proxy_proto/smb/v1beta1/api.proto create mode 100644 csi_proxy_proto/smb/v1beta2/api.proto create mode 100644 csi_proxy_proto/system/v1alpha1/api.proto create mode 100644 csi_proxy_proto/volume/v1/api.proto create mode 100644 csi_proxy_proto/volume/v1alpha1/api.proto create mode 100644 csi_proxy_proto/volume/v1beta1/api.proto create mode 100644 csi_proxy_proto/volume/v1beta2/api.proto create mode 100644 csi_proxy_proto/volume/v1beta3/api.proto create mode 100644 csi_proxy_proto/volume/v2alpha1/api.proto create mode 100644 examples/private.yaml create mode 100644 src/utils/csi_proxy_client.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5bff4fa..528fb9d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -250,7 +250,7 @@ jobs: run: | export ARCH=$([ $(uname -m) = "x86_64" ] && echo "amd64" || echo "arm64") mkdir -p ~/.docker/cli-plugins/ - wget -qO ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.5.1/buildx-v0.5.1.linux-${ARCH} + wget -qO ~/.docker/cli-plugins/docker-buildx https://github.com/docker/buildx/releases/download/v0.8.2/buildx-v0.8.2.linux-${ARCH} chmod a+x ~/.docker/cli-plugins/docker-buildx docker info docker buildx version diff --git a/bin/democratic-csi b/bin/democratic-csi index 2a2148e..df94d93 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -136,6 +136,14 @@ let operationLock = new Set(); async function requestHandlerProxy(call, callback, serviceMethodName) { const cleansedCall = JSON.parse(stringify(call)); + + delete cleansedCall.call; + delete cleansedCall.canceled; + for (const key in cleansedCall) { + if (key.startsWith("_")) { + delete cleansedCall[key]; + } + } for (const key in cleansedCall.request) { if (key.includes("secret")) { cleansedCall.request[key] = "redacted"; @@ -165,6 +173,17 @@ async function requestHandlerProxy(call, callback, serviceMethodName) { }); } + // for testing purposes + //await GeneralUtils.sleep(10000); + + // for CI/testing purposes + if (["NodePublishVolume", "NodeStageVolume"].includes(serviceMethodName)) { + await driver.setVolumeContextCache( + call.request.volume_id, + call.request.volume_context + ); + } + let response; let responseError; try { @@ -190,12 +209,21 @@ async function requestHandlerProxy(call, callback, serviceMethodName) { throw responseError; } + // for CI/testing purposes + if (serviceMethodName == "CreateVolume") { + await driver.setVolumeContextCache( + response.volume.volume_id, + response.volume.volume_context + ); + } + logger.info( "new response - driver: %s method: %s response: %j", driver.constructor.name, serviceMethodName, response ); + callback(null, response); } catch (e) { let message; @@ -205,7 +233,7 @@ async function requestHandlerProxy(call, callback, serviceMethodName) { message += ` ${e.stack}`; } } else { - message = JSON.stringify(e); + message = stringify(e); } logger.error( diff --git a/csi_proxy_proto/disk/v1/api.proto b/csi_proxy_proto/disk/v1/api.proto new file mode 100644 index 0000000..f73f412 --- /dev/null +++ b/csi_proxy_proto/disk/v1/api.proto @@ -0,0 +1,111 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/disk/v1"; + +service Disk { + // ListDiskLocations returns locations of all + // disk devices enumerated by the host. + rpc ListDiskLocations(ListDiskLocationsRequest) returns (ListDiskLocationsResponse) {} + + // PartitionDisk initializes and partitions a disk device with the GPT partition style + // (if the disk has not been partitioned already) and returns the resulting volume device ID. + rpc PartitionDisk(PartitionDiskRequest) returns (PartitionDiskResponse) {} + + // Rescan refreshes the host's storage cache. + rpc Rescan(RescanRequest) returns (RescanResponse) {} + + // ListDiskIDs returns a map of DiskID objects where the key is the disk number. + rpc ListDiskIDs(ListDiskIDsRequest) returns (ListDiskIDsResponse) {} + + // GetDiskStats returns the stats of a disk (currently it returns the disk size). + rpc GetDiskStats(GetDiskStatsRequest) returns (GetDiskStatsResponse) {} + + // SetDiskState sets the offline/online state of a disk. + rpc SetDiskState(SetDiskStateRequest) returns (SetDiskStateResponse) {} + + // GetDiskState gets the offline/online state of a disk. + rpc GetDiskState(GetDiskStateRequest) returns (GetDiskStateResponse) {} +} + +message ListDiskLocationsRequest { + // Intentionally empty. +} + +message DiskLocation { + string Adapter = 1; + string Bus = 2; + string Target = 3; + string LUNID = 4; +} + +message ListDiskLocationsResponse { + // Map of disk number and associated with each disk device. + map disk_locations = 1; +} + +message PartitionDiskRequest { + // Disk device number of the disk to partition. + uint32 disk_number = 1; +} + +message PartitionDiskResponse { + // Intentionally empty. +} + +message RescanRequest { + // Intentionally empty. +} + +message RescanResponse { + // Intentionally empty. +} + +message ListDiskIDsRequest { + // Intentionally empty. +} + +message DiskIDs { + // The disk page83 id. + string page83 = 1; + // The disk serial number. + string serial_number = 2; +} + +message ListDiskIDsResponse { + // Map of disk numbers and disk identifiers associated with each disk device. + map diskIDs = 1; // the case is intentional for protoc to generate the field as DiskIDs +} + +message GetDiskStatsRequest { + // Disk device number of the disk to get the stats from. + uint32 disk_number = 1; +} + +message GetDiskStatsResponse { + // Total size of the volume. + int64 total_bytes = 1; +} + +message SetDiskStateRequest { + // Disk device number of the disk. + uint32 disk_number = 1; + + // Online state to set for the disk. true for online, false for offline. + bool is_online = 2; +} + +message SetDiskStateResponse { + // Intentionally empty. +} + +message GetDiskStateRequest { + // Disk device number of the disk. + uint32 disk_number = 1; +} + +message GetDiskStateResponse { + // Online state of the disk. true for online, false for offline. + bool is_online = 1; +} diff --git a/csi_proxy_proto/disk/v1alpha1/api.proto b/csi_proxy_proto/disk/v1alpha1/api.proto new file mode 100644 index 0000000..03ef612 --- /dev/null +++ b/csi_proxy_proto/disk/v1alpha1/api.proto @@ -0,0 +1,62 @@ +syntax = "proto3"; + +package v1alpha1; + +service Disk { + // ListDiskLocations returns locations of all + // disk devices enumerated by the host + rpc ListDiskLocations(ListDiskLocationsRequest) returns (ListDiskLocationsResponse) {} + + // PartitionDisk initializes and partitions a disk device (if the disk has not + // been partitioned already) and returns the resulting volume device ID + rpc PartitionDisk(PartitionDiskRequest) returns (PartitionDiskResponse) {} + + // Rescan refreshes the host's storage cache + rpc Rescan(RescanRequest) returns (RescanResponse) {} + + // GetDiskNumberByName returns disk number based on the passing disk name information + rpc GetDiskNumberByName(GetDiskNumberByNameRequest) returns (GetDiskNumberByNameResponse) {} +} + +message ListDiskLocationsRequest { + // Intentionally empty +} + +message DiskLocation { + string Adapter = 1; + string Bus = 2; + string Target = 3; + string LUNID = 4; +} + +message ListDiskLocationsResponse { + // Map of disk device IDs and associated with each disk device + map disk_locations = 1; +} + +message PartitionDiskRequest { + // Disk device ID of the disk to partition + string diskID = 1; +} + +message PartitionDiskResponse { + // Intentionally empty +} + +message RescanRequest { + // Intentionally empty +} + +message RescanResponse { + // Intentionally empty +} + +message GetDiskNumberByNameRequest { + // Disk ID + string disk_name = 1; +} + +message GetDiskNumberByNameResponse { + // Disk number + string disk_number = 1; +} diff --git a/csi_proxy_proto/disk/v1beta1/api.proto b/csi_proxy_proto/disk/v1beta1/api.proto new file mode 100644 index 0000000..4673b2c --- /dev/null +++ b/csi_proxy_proto/disk/v1beta1/api.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; + +package v1beta1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/disk/v1beta1"; + +service Disk { + // ListDiskLocations returns locations of all + // disk devices enumerated by the host + rpc ListDiskLocations(ListDiskLocationsRequest) returns (ListDiskLocationsResponse) {} + + // PartitionDisk initializes and partitions a disk device (if the disk has not + // been partitioned already) and returns the resulting volume device ID + rpc PartitionDisk(PartitionDiskRequest) returns (PartitionDiskResponse) {} + + // Rescan refreshes the host's storage cache + rpc Rescan(RescanRequest) returns (RescanResponse) {} + + // ListDiskIDs returns a map of DiskID objects where the key is the disk number + rpc ListDiskIDs(ListDiskIDsRequest) returns (ListDiskIDsResponse) {} + + // DiskStats returns the stats for the disk + rpc DiskStats(DiskStatsRequest) returns (DiskStatsResponse) {} +} + +message ListDiskLocationsRequest { + // Intentionally empty +} + +message DiskLocation { + string Adapter = 1; + string Bus = 2; + string Target = 3; + string LUNID = 4; +} + +message ListDiskLocationsResponse { + // Map of disk device IDs and associated with each disk device + map disk_locations = 1; +} + +message PartitionDiskRequest { + // Disk device ID of the disk to partition + string diskID = 1; +} + +message PartitionDiskResponse { + // Intentionally empty +} + +message RescanRequest { + // Intentionally empty +} + +message RescanResponse { + // Intentionally empty +} + +message ListDiskIDsRequest { + // Intentionally empty +} + +message DiskIDs { + // Map of Disk ID types and Disk ID values + map identifiers = 1; +} + +message ListDiskIDsResponse { + // Map of disk device numbers and IDs associated with each disk device + map diskIDs = 1; +} + +message DiskStatsRequest { + // Disk device ID of the disk to get the size from + string diskID = 1; +} + +message DiskStatsResponse { + //Total size of the volume + int64 diskSize = 1; +} diff --git a/csi_proxy_proto/disk/v1beta2/api.proto b/csi_proxy_proto/disk/v1beta2/api.proto new file mode 100644 index 0000000..c9f6c8f --- /dev/null +++ b/csi_proxy_proto/disk/v1beta2/api.proto @@ -0,0 +1,109 @@ +syntax = "proto3"; + +package v1beta2; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/disk/v1beta2"; + +service Disk { + // ListDiskLocations returns locations of all + // disk devices enumerated by the host + rpc ListDiskLocations(ListDiskLocationsRequest) returns (ListDiskLocationsResponse) {} + + // PartitionDisk initializes and partitions a disk device (if the disk has not + // been partitioned already) and returns the resulting volume device ID + rpc PartitionDisk(PartitionDiskRequest) returns (PartitionDiskResponse) {} + + // Rescan refreshes the host's storage cache + rpc Rescan(RescanRequest) returns (RescanResponse) {} + + // ListDiskIDs returns a map of DiskID objects where the key is the disk number + rpc ListDiskIDs(ListDiskIDsRequest) returns (ListDiskIDsResponse) {} + + // DiskStats returns the stats for the disk + rpc DiskStats(DiskStatsRequest) returns (DiskStatsResponse) {} + + // SetAttachState sets the offline/online state of a disk + rpc SetAttachState(SetAttachStateRequest) returns (SetAttachStateResponse) {} + + // GetAttachState gets the offline/online state of a disk + rpc GetAttachState(GetAttachStateRequest) returns (GetAttachStateResponse) {} +} + +message ListDiskLocationsRequest { + // Intentionally empty +} + +message DiskLocation { + string Adapter = 1; + string Bus = 2; + string Target = 3; + string LUNID = 4; +} + +message ListDiskLocationsResponse { + // Map of disk device IDs and associated with each disk device + map disk_locations = 1; +} + +message PartitionDiskRequest { + // Disk device ID of the disk to partition + string diskID = 1; +} + +message PartitionDiskResponse { + // Intentionally empty +} + +message RescanRequest { + // Intentionally empty +} + +message RescanResponse { + // Intentionally empty +} + +message ListDiskIDsRequest { + // Intentionally empty +} + +message DiskIDs { + // Map of Disk ID types and Disk ID values + map identifiers = 1; +} + +message ListDiskIDsResponse { + // Map of disk device numbers and IDs associated with each disk device + map diskIDs = 1; +} + +message DiskStatsRequest { + // Disk device ID of the disk to get the size from + string diskID = 1; +} + +message DiskStatsResponse { + //Total size of the volume + int64 diskSize = 1; +} + +message SetAttachStateRequest { + // Disk device ID (number) of the disk which state will change + string diskID = 1; + + // Online state to set for the disk. true for online, false for offline + bool isOnline = 2; +} + +message SetAttachStateResponse { +} + +message GetAttachStateRequest { + // Disk device ID (number) of the disk + string diskID = 1; +} + +message GetAttachStateResponse { + // Online state of the disk. true for online, false for offline + bool isOnline = 1; +} + diff --git a/csi_proxy_proto/disk/v1beta3/api.proto b/csi_proxy_proto/disk/v1beta3/api.proto new file mode 100644 index 0000000..8f8283e --- /dev/null +++ b/csi_proxy_proto/disk/v1beta3/api.proto @@ -0,0 +1,111 @@ +syntax = "proto3"; + +package v1beta3; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/disk/v1beta3"; + +service Disk { + // ListDiskLocations returns locations of all + // disk devices enumerated by the host. + rpc ListDiskLocations(ListDiskLocationsRequest) returns (ListDiskLocationsResponse) {} + + // PartitionDisk initializes and partitions a disk device with the GPT partition style + // (if the disk has not been partitioned already) and returns the resulting volume device ID. + rpc PartitionDisk(PartitionDiskRequest) returns (PartitionDiskResponse) {} + + // Rescan refreshes the host's storage cache. + rpc Rescan(RescanRequest) returns (RescanResponse) {} + + // ListDiskIDs returns a map of DiskID objects where the key is the disk number. + rpc ListDiskIDs(ListDiskIDsRequest) returns (ListDiskIDsResponse) {} + + // GetDiskStats returns the stats of a disk (currently it returns the disk size). + rpc GetDiskStats(GetDiskStatsRequest) returns (GetDiskStatsResponse) {} + + // SetDiskState sets the offline/online state of a disk. + rpc SetDiskState(SetDiskStateRequest) returns (SetDiskStateResponse) {} + + // GetDiskState gets the offline/online state of a disk. + rpc GetDiskState(GetDiskStateRequest) returns (GetDiskStateResponse) {} +} + +message ListDiskLocationsRequest { + // Intentionally empty. +} + +message DiskLocation { + string Adapter = 1; + string Bus = 2; + string Target = 3; + string LUNID = 4; +} + +message ListDiskLocationsResponse { + // Map of disk number and associated with each disk device. + map disk_locations = 1; +} + +message PartitionDiskRequest { + // Disk device number of the disk to partition. + uint32 disk_number = 1; +} + +message PartitionDiskResponse { + // Intentionally empty. +} + +message RescanRequest { + // Intentionally empty. +} + +message RescanResponse { + // Intentionally empty. +} + +message ListDiskIDsRequest { + // Intentionally empty. +} + +message DiskIDs { + // The disk page83 id. + string page83 = 1; + // The disk serial number. + string serial_number = 2; +} + +message ListDiskIDsResponse { + // Map of disk numbers and disk identifiers associated with each disk device. + map diskIDs = 1; // the case is intentional for protoc to generate the field as DiskIDs +} + +message GetDiskStatsRequest { + // Disk device number of the disk to get the stats from. + uint32 disk_number = 1; +} + +message GetDiskStatsResponse { + // Total size of the volume. + int64 total_bytes = 1; +} + +message SetDiskStateRequest { + // Disk device number of the disk. + uint32 disk_number = 1; + + // Online state to set for the disk. true for online, false for offline. + bool is_online = 2; +} + +message SetDiskStateResponse { + // Intentionally empty. +} + +message GetDiskStateRequest { + // Disk device number of the disk. + uint32 disk_number = 1; +} + +message GetDiskStateResponse { + // Online state of the disk. true for online, false for offline. + bool is_online = 1; +} diff --git a/csi_proxy_proto/errors.proto b/csi_proxy_proto/errors.proto new file mode 100644 index 0000000..60c74cd --- /dev/null +++ b/csi_proxy_proto/errors.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package api; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api"; + +// CommandError details errors yielded by cmdlet calls. +message CmdletError { + // Name of the cmdlet that errored out. + string cmdlet_name = 1; + + // Error code that got returned. + uint32 code = 2; + + // Human-readable error message - can be empty. + string message = 3; +} diff --git a/csi_proxy_proto/filesystem/v1/api.proto b/csi_proxy_proto/filesystem/v1/api.proto new file mode 100644 index 0000000..151a1ff --- /dev/null +++ b/csi_proxy_proto/filesystem/v1/api.proto @@ -0,0 +1,136 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/filesystem/v1"; + +service Filesystem { + // PathExists checks if the requested path exists in the host filesystem. + rpc PathExists(PathExistsRequest) returns (PathExistsResponse) {} + + // Mkdir creates a directory at the requested path in the host filesystem. + rpc Mkdir(MkdirRequest) returns (MkdirResponse) {} + + // Rmdir removes the directory at the requested path in the host filesystem. + // This may be used for unlinking a symlink created through CreateSymlink. + rpc Rmdir(RmdirRequest) returns (RmdirResponse) {} + + // CreateSymlink creates a symbolic link called target_path that points to source_path + // in the host filesystem (target_path is the name of the symbolic link created, + // source_path is the existing path). + rpc CreateSymlink(CreateSymlinkRequest) returns (CreateSymlinkResponse) {} + + // IsSymlink checks if a given path is a symlink. + rpc IsSymlink(IsSymlinkRequest) returns (IsSymlinkResponse) {} +} + +message PathExistsRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; +} + +message PathExistsResponse { + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool exists = 1; +} + +message MkdirRequest { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist in the host's filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + string path = 1; +} + +message MkdirResponse { + // Intentionally empty. +} + +message RmdirRequest { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Force remove all contents under path (if any). + bool force = 2; +} + +message RmdirResponse { + // Intentionally empty. +} + +message CreateSymlinkRequest { + // The path of the existing directory to be linked. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + string source_path = 1; + + // Target path is the location of the new directory entry to be created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + string target_path = 2; +} + +message CreateSymlinkResponse { + // Intentionally empty. +} + +message IsSymlinkRequest { + // The path whose existence as a symlink we want to check in the host's filesystem. + string path = 1; +} + +message IsSymlinkResponse { + // Indicates whether the path in IsSymlinkRequest is a symlink. + bool is_symlink = 1; +} diff --git a/csi_proxy_proto/filesystem/v1alpha1/api.proto b/csi_proxy_proto/filesystem/v1alpha1/api.proto new file mode 100644 index 0000000..8856da5 --- /dev/null +++ b/csi_proxy_proto/filesystem/v1alpha1/api.proto @@ -0,0 +1,168 @@ +syntax = "proto3"; + +package v1alpha1; + +service Filesystem { + // PathExists checks if the requested path exists in the host's filesystem + rpc PathExists(PathExistsRequest) returns (PathExistsResponse) {} + + // Mkdir creates a directory at the requested path in the host's filesystem + rpc Mkdir(MkdirRequest) returns (MkdirResponse) {} + + // Rmdir removes the directory at the requested path in the host's filesystem. + // This may be used for unlinking a symlink created through LinkPath + rpc Rmdir(RmdirRequest) returns (RmdirResponse) {} + + // LinkPath creates a local directory symbolic link between a source path + // and target path in the host's filesystem + rpc LinkPath(LinkPathRequest) returns (LinkPathResponse) {} + + //IsMountPoint checks if a given path is mount or not + rpc IsMountPoint(IsMountPointRequest) returns (IsMountPointResponse) {} +} + +// Context of the paths used for path prefix validation +enum PathContext { + // Indicates the kubelet-csi-plugins-path parameter of csi-proxy be used as + // the path context. This may be used while handling NodeStageVolume where + // a volume may need to be mounted at a plugin-specific path like: + // kubelet\plugins\kubernetes.io\csi\pv\\globalmount + PLUGIN = 0; + // Indicates the kubelet-pod-path parameter of csi-proxy be used as the path + // context. This may be used while handling NodePublishVolume where a staged + // volume may be need to be symlinked to a pod-specific path like: + // kubelet\pods\\volumes\kubernetes.io~csi\\mount + POD = 1; +} + +message PathExistsRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; +} + +message PathExistsResponse { + // Error message if any. Empty string indicates success + string error = 1; + + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool exists = 2; +} + +message MkdirRequest { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist in the host's filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; +} + +message MkdirResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message RmdirRequest { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; + + // Force remove all contents under path (if any). + bool force = 3; +} + +message RmdirResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message LinkPathRequest { + // The path where the symlink is created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + string source_path = 1; + + // Target path in the host's filesystem used for the symlink creation. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + string target_path = 2; +} + +message LinkPathResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message IsMountPointRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; +} + +message IsMountPointResponse { + // Error message if any. Empty string indicates success + string error = 1; + + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool is_mount_point = 2; +} diff --git a/csi_proxy_proto/filesystem/v1beta1/api.proto b/csi_proxy_proto/filesystem/v1beta1/api.proto new file mode 100644 index 0000000..3b4c0ab --- /dev/null +++ b/csi_proxy_proto/filesystem/v1beta1/api.proto @@ -0,0 +1,168 @@ +syntax = "proto3"; + +package v1beta1; + +service Filesystem { + // PathExists checks if the requested path exists in the host's filesystem + rpc PathExists(PathExistsRequest) returns (PathExistsResponse) {} + + // Mkdir creates a directory at the requested path in the host's filesystem + rpc Mkdir(MkdirRequest) returns (MkdirResponse) {} + + // Rmdir removes the directory at the requested path in the host's filesystem. + // This may be used for unlinking a symlink created through LinkPath + rpc Rmdir(RmdirRequest) returns (RmdirResponse) {} + + // LinkPath creates a local directory symbolic link between a source path + // and target path in the host's filesystem + rpc LinkPath(LinkPathRequest) returns (LinkPathResponse) {} + + //IsMountPoint checks if a given path is mount or not + rpc IsMountPoint(IsMountPointRequest) returns (IsMountPointResponse) {} +} + +// Context of the paths used for path prefix validation +enum PathContext { + // Indicates the kubelet-csi-plugins-path parameter of csi-proxy be used as + // the path context. This may be used while handling NodeStageVolume where + // a volume may need to be mounted at a plugin-specific path like: + // kubelet\plugins\kubernetes.io\csi\pv\\globalmount + PLUGIN = 0; + // Indicates the kubelet-pod-path parameter of csi-proxy be used as the path + // context. This may be used while handling NodePublishVolume where a staged + // volume may be need to be symlinked to a pod-specific path like: + // kubelet\pods\\volumes\kubernetes.io~csi\\mount + POD = 1; +} + +message PathExistsRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; +} + +message PathExistsResponse { + // Error message if any. Empty string indicates success + string error = 1; + + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool exists = 2; +} + +message MkdirRequest { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist in the host's filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; +} + +message MkdirResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message RmdirRequest { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Context of the path parameter. + // This is used to validate prefix for absolute paths passed + PathContext context = 2; + + // Force remove all contents under path (if any). + bool force = 3; +} + +message RmdirResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message LinkPathRequest { + // The path where the symlink is created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + string source_path = 1; + + // Target path in the host's filesystem used for the symlink creation. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + string target_path = 2; +} + +message LinkPathResponse { + // Error message if any. Empty string indicates success + string error = 1; +} + +message IsMountPointRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; +} + +message IsMountPointResponse { + // Error message if any. Empty string indicates success + string error = 1; + + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool is_mount_point = 2; +} diff --git a/csi_proxy_proto/filesystem/v1beta2/api.proto b/csi_proxy_proto/filesystem/v1beta2/api.proto new file mode 100644 index 0000000..fdaf391 --- /dev/null +++ b/csi_proxy_proto/filesystem/v1beta2/api.proto @@ -0,0 +1,136 @@ +syntax = "proto3"; + +package v1beta2; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/filesystem/v1beta2"; + +service Filesystem { + // PathExists checks if the requested path exists in the host filesystem. + rpc PathExists(PathExistsRequest) returns (PathExistsResponse) {} + + // Mkdir creates a directory at the requested path in the host filesystem. + rpc Mkdir(MkdirRequest) returns (MkdirResponse) {} + + // Rmdir removes the directory at the requested path in the host filesystem. + // This may be used for unlinking a symlink created through CreateSymlink. + rpc Rmdir(RmdirRequest) returns (RmdirResponse) {} + + // CreateSymlink creates a symbolic link called target_path that points to source_path + // in the host filesystem (target_path is the name of the symbolic link created, + // source_path is the existing path). + rpc CreateSymlink(CreateSymlinkRequest) returns (CreateSymlinkResponse) {} + + // IsSymlink checks if a given path is a symlink. + rpc IsSymlink(IsSymlinkRequest) returns (IsSymlinkResponse) {} +} + +message PathExistsRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; +} + +message PathExistsResponse { + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool exists = 1; +} + +message MkdirRequest { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist in the host's filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + string path = 1; +} + +message MkdirResponse { + // Intentionally empty. +} + +message RmdirRequest { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Force remove all contents under path (if any). + bool force = 2; +} + +message RmdirResponse { + // Intentionally empty. +} + +message CreateSymlinkRequest { + // The path of the existing directory to be linked. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + string source_path = 1; + + // Target path is the location of the new directory entry to be created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + string target_path = 2; +} + +message CreateSymlinkResponse { + // Intentionally empty. +} + +message IsSymlinkRequest { + // The path whose existence as a symlink we want to check in the host's filesystem. + string path = 1; +} + +message IsSymlinkResponse { + // Indicates whether the path in IsSymlinkRequest is a symlink. + bool is_symlink = 1; +} diff --git a/csi_proxy_proto/filesystem/v2alpha1/api.proto b/csi_proxy_proto/filesystem/v2alpha1/api.proto new file mode 100644 index 0000000..b46d736 --- /dev/null +++ b/csi_proxy_proto/filesystem/v2alpha1/api.proto @@ -0,0 +1,163 @@ +syntax = "proto3"; + +package v2alpha1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/filesystem/v2alpha1"; + +service Filesystem { + // PathExists checks if the requested path exists in the host filesystem. + rpc PathExists(PathExistsRequest) returns (PathExistsResponse) {} + + // Mkdir creates a directory at the requested path in the host filesystem. + rpc Mkdir(MkdirRequest) returns (MkdirResponse) {} + + // Rmdir removes the directory at the requested path in the host filesystem. + // This may be used for unlinking a symlink created through CreateSymlink. + rpc Rmdir(RmdirRequest) returns (RmdirResponse) {} + + // RmdirContents removes the contents of a directory in the host filesystem. + // Unlike Rmdir it won't delete the requested path, it'll only delete its contents. + rpc RmdirContents(RmdirContentsRequest) returns (RmdirContentsResponse) {} + + // CreateSymlink creates a symbolic link called target_path that points to source_path + // in the host filesystem (target_path is the name of the symbolic link created, + // source_path is the existing path). + rpc CreateSymlink(CreateSymlinkRequest) returns (CreateSymlinkResponse) {} + + // IsSymlink checks if a given path is a symlink. + rpc IsSymlink(IsSymlinkRequest) returns (IsSymlinkResponse) {} +} + +message PathExistsRequest { + // The path whose existence we want to check in the host's filesystem + string path = 1; +} + +message PathExistsResponse { + // Indicates whether the path in PathExistsRequest exists in the host's filesystem + bool exists = 1; +} + +message MkdirRequest { + // The path to create in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // Non-existent parent directories in the path will be automatically created. + // Directories will be created with Read and Write privileges of the Windows + // User account under which csi-proxy is started (typically LocalSystem). + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // The path parameter cannot already exist in the host's filesystem. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Maximum path length will be capped to 260 characters. + string path = 1; +} + +message MkdirResponse { + // Intentionally empty. +} + +message RmdirRequest { + // The path to remove in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; + + // Force remove all contents under path (if any). + bool force = 2; +} + +message RmdirResponse { + // Intentionally empty. +} + +message RmdirContentsRequest { + // The path whose contents will be removed in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // Depending on the context parameter of this function, the path prefix needs + // to match the paths specified either as kubelet-csi-plugins-path + // or as kubelet-pod-path parameters of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // Path cannot be a file of type symlink. + // Maximum path length will be capped to 260 characters. + string path = 1; +} + +message RmdirContentsResponse { + // Intentionally empty. +} + +message CreateSymlinkRequest { + // The path of the existing directory to be linked. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs needs to match the paths specified as + // kubelet-csi-plugins-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // source_path cannot already exist in the host filesystem. + // Maximum path length will be capped to 260 characters. + string source_path = 1; + + // Target path is the location of the new directory entry to be created in the host's filesystem. + // All special characters allowed by Windows in path names will be allowed + // except for restrictions noted below. For details, please check: + // https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file + // + // Restrictions: + // Only absolute path (indicated by a drive letter prefix: e.g. "C:\") is accepted. + // The path prefix needs to match the paths specified as + // kubelet-pod-path parameter of csi-proxy. + // UNC paths of the form "\\server\share\path\file" are not allowed. + // All directory separators need to be backslash character: "\". + // Characters: .. / : | ? * in the path are not allowed. + // target_path needs to exist as a directory in the host that is empty. + // target_path cannot be a symbolic link. + // Maximum path length will be capped to 260 characters. + string target_path = 2; +} + +message CreateSymlinkResponse { + // Intentionally empty. +} + +message IsSymlinkRequest { + // The path whose existence as a symlink we want to check in the host's filesystem. + string path = 1; +} + +message IsSymlinkResponse { + // Indicates whether the path in IsSymlinkRequest is a symlink. + bool is_symlink = 1; +} diff --git a/csi_proxy_proto/iscsi/v1alpha1/api.proto b/csi_proxy_proto/iscsi/v1alpha1/api.proto new file mode 100644 index 0000000..b667f8d --- /dev/null +++ b/csi_proxy_proto/iscsi/v1alpha1/api.proto @@ -0,0 +1,153 @@ +syntax = "proto3"; + +package v1alpha1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/iscsi/v1alpha1"; + +service Iscsi { + // AddTargetPortal registers an iSCSI target network address for later + // discovery. + // AddTargetPortal currently does not support selecting different NICs or + // a different iSCSI initiator (e.g a hardware initiator). This means that + // Windows will select the initiator NIC and instance on its own. + rpc AddTargetPortal(AddTargetPortalRequest) + returns (AddTargetPortalResponse) {} + + // DiscoverTargetPortal initiates discovery on an iSCSI target network address + // and returns discovered IQNs. + rpc DiscoverTargetPortal(DiscoverTargetPortalRequest) + returns (DiscoverTargetPortalResponse) {} + + // RemoveTargetPortal removes an iSCSI target network address registration. + rpc RemoveTargetPortal(RemoveTargetPortalRequest) + returns (RemoveTargetPortalResponse) {} + + // ListTargetPortal lists all currently registered iSCSI target network + // addresses. + rpc ListTargetPortals(ListTargetPortalsRequest) + returns (ListTargetPortalsResponse) {} + + // ConnectTarget connects to an iSCSI Target + rpc ConnectTarget(ConnectTargetRequest) returns (ConnectTargetResponse) {} + + // DisconnectTarget disconnects from an iSCSI Target + rpc DisconnectTarget(DisconnectTargetRequest) + returns (DisconnectTargetResponse) {} + + // GetTargetDisks returns the disk addresses that correspond to an iSCSI + // target + rpc GetTargetDisks(GetTargetDisksRequest) returns (GetTargetDisksResponse) {} +} + +// TargetPortal is an address and port pair for a specific iSCSI storage +// target. +message TargetPortal { + // iSCSI Target (server) address + string target_address = 1; + + // iSCSI Target port (default iSCSI port is 3260) + uint32 target_port = 2; +} + +message AddTargetPortalRequest { + // iSCSI Target Portal to register in the initiator + TargetPortal target_portal = 1; +} + +message AddTargetPortalResponse { + // Intentionally empty +} + +message DiscoverTargetPortalRequest { + // iSCSI Target Portal on which to initiate discovery + TargetPortal target_portal = 1; +} + +message DiscoverTargetPortalResponse { + // List of discovered IQN addresses + // follows IQN format: iqn.yyyy-mm.naming-authority:unique-name + repeated string iqns = 1; +} + +message RemoveTargetPortalRequest { + // iSCSI Target Portal + TargetPortal target_portal = 1; +} + +message RemoveTargetPortalResponse { + // Intentionally empty +} + +message ListTargetPortalsRequest { + // Intentionally empty +} + +message ListTargetPortalsResponse { + // A list of Target Portals currently registered in the initiator + repeated TargetPortal target_portals = 1; +} + +enum AuthenticationType { + // No authentication is used + NONE = 0; + + // One way CHAP authentication. The target authenticates the initiator. + ONE_WAY_CHAP = 1; + + // Mutual CHAP authentication. The target and initiator authenticate each + // other. + MUTUAL_CHAP = 2; +} + +message ConnectTargetRequest { + // Target portal to which the initiator will connect + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; + + // Connection authentication type, None by default + // + // One Way Chap uses the chap_username and chap_secret + // fields mentioned below to authenticate the initiator. + // + // Mutual Chap uses both the user/secret mentioned below + // and the Initiator Chap Secret to authenticate the target and initiator. + AuthenticationType auth_type = 3; + + // CHAP Username used to authenticate the initiator + string chap_username = 4; + + // CHAP password used to authenticate the initiator + string chap_secret = 5; +} + +message ConnectTargetResponse { + // Intentionally empty +} + +message GetTargetDisksRequest { + // Target portal whose disks will be queried + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; +} + +message GetTargetDisksResponse { + // List composed of disk ids (numbers) that are associated with the + // iSCSI target + repeated string diskIDs = 1; +} + +message DisconnectTargetRequest { + // Target portal from which initiator will disconnect + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; +} + +message DisconnectTargetResponse { + // Intentionally empty +} diff --git a/csi_proxy_proto/iscsi/v1alpha2/api.proto b/csi_proxy_proto/iscsi/v1alpha2/api.proto new file mode 100644 index 0000000..baa5bee --- /dev/null +++ b/csi_proxy_proto/iscsi/v1alpha2/api.proto @@ -0,0 +1,175 @@ +syntax = "proto3"; + +package v1alpha2; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/iscsi/v1alpha2"; + +service Iscsi { + // AddTargetPortal registers an iSCSI target network address for later + // discovery. + // AddTargetPortal currently does not support selecting different NICs or + // a different iSCSI initiator (e.g a hardware initiator). This means that + // Windows will select the initiator NIC and instance on its own. + rpc AddTargetPortal(AddTargetPortalRequest) + returns (AddTargetPortalResponse) {} + + // DiscoverTargetPortal initiates discovery on an iSCSI target network address + // and returns discovered IQNs. + rpc DiscoverTargetPortal(DiscoverTargetPortalRequest) + returns (DiscoverTargetPortalResponse) {} + + // RemoveTargetPortal removes an iSCSI target network address registration. + rpc RemoveTargetPortal(RemoveTargetPortalRequest) + returns (RemoveTargetPortalResponse) {} + + // ListTargetPortal lists all currently registered iSCSI target network + // addresses. + rpc ListTargetPortals(ListTargetPortalsRequest) + returns (ListTargetPortalsResponse) {} + + // ConnectTarget connects to an iSCSI Target + rpc ConnectTarget(ConnectTargetRequest) returns (ConnectTargetResponse) {} + + // DisconnectTarget disconnects from an iSCSI Target + rpc DisconnectTarget(DisconnectTargetRequest) + returns (DisconnectTargetResponse) {} + + // GetTargetDisks returns the disk addresses that correspond to an iSCSI + // target + rpc GetTargetDisks(GetTargetDisksRequest) returns (GetTargetDisksResponse) {} + + // SetMutualChapSecret sets the default CHAP secret that all initiators on + // this machine (node) use to authenticate the target on mutual CHAP + // authentication. + // NOTE: This method affects global node state and should only be used + // with consideration to other CSI drivers that run concurrently. + rpc SetMutualChapSecret(SetMutualChapSecretRequest) + returns (SetMutualChapSecretResponse) {} +} + +// TargetPortal is an address and port pair for a specific iSCSI storage +// target. +message TargetPortal { + // iSCSI Target (server) address + string target_address = 1; + + // iSCSI Target port (default iSCSI port is 3260) + uint32 target_port = 2; +} + +message AddTargetPortalRequest { + // iSCSI Target Portal to register in the initiator + TargetPortal target_portal = 1; +} + +message AddTargetPortalResponse { + // Intentionally empty +} + +message DiscoverTargetPortalRequest { + // iSCSI Target Portal on which to initiate discovery + TargetPortal target_portal = 1; +} + +message DiscoverTargetPortalResponse { + // List of discovered IQN addresses + // follows IQN format: iqn.yyyy-mm.naming-authority:unique-name + repeated string iqns = 1; +} + +message RemoveTargetPortalRequest { + // iSCSI Target Portal + TargetPortal target_portal = 1; +} + +message RemoveTargetPortalResponse { + // Intentionally empty +} + +message ListTargetPortalsRequest { + // Intentionally empty +} + +message ListTargetPortalsResponse { + // A list of Target Portals currently registered in the initiator + repeated TargetPortal target_portals = 1; +} + +// iSCSI logon authentication type +enum AuthenticationType { + // No authentication is used + NONE = 0; + + // One way CHAP authentication. The target authenticates the initiator. + ONE_WAY_CHAP = 1; + + // Mutual CHAP authentication. The target and initiator authenticate each + // other. + MUTUAL_CHAP = 2; +} + +message ConnectTargetRequest { + // Target portal to which the initiator will connect + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; + + // Connection authentication type, None by default + // + // One Way Chap uses the chap_username and chap_secret + // fields mentioned below to authenticate the initiator. + // + // Mutual Chap uses both the user/secret mentioned below + // and the Initiator Chap Secret (See `SetMutualChapSecret`) + // to authenticate the target and initiator. + AuthenticationType auth_type = 3; + + // CHAP Username used to authenticate the initiator + string chap_username = 4; + + // CHAP password used to authenticate the initiator + string chap_secret = 5; +} + +message ConnectTargetResponse { + // Intentionally empty +} + +message GetTargetDisksRequest { + // Target portal whose disks will be queried + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; +} + +message GetTargetDisksResponse { + // List composed of disk ids (numbers) that are associated with the + // iSCSI target + repeated string diskIDs = 1; +} + +message DisconnectTargetRequest { + // Target portal from which initiator will disconnect + TargetPortal target_portal = 1; + + // IQN of the iSCSI Target + string iqn = 2; +} + +message DisconnectTargetResponse { + // Intentionally empty +} + +message SetMutualChapSecretRequest { + // the default CHAP secret that all initiators on this machine (node) use to + // authenticate the target on mutual CHAP authentication. + // Must be at least 12 byte long for non-Ipsec connections, at least one + // byte long for Ipsec connections, and at most 16 bytes long. + string MutualChapSecret = 1; +} + +message SetMutualChapSecretResponse { + // Intentionally empty +} diff --git a/csi_proxy_proto/smb/v1/api.proto b/csi_proxy_proto/smb/v1/api.proto new file mode 100644 index 0000000..6f10635 --- /dev/null +++ b/csi_proxy_proto/smb/v1/api.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/smb/v1"; + +service Smb { + // NewSmbGlobalMapping creates an SMB mapping on the SMB client to an SMB share. + rpc NewSmbGlobalMapping(NewSmbGlobalMappingRequest) returns (NewSmbGlobalMappingResponse) {} + + // RemoveSmbGlobalMapping removes the SMB mapping to an SMB share. + rpc RemoveSmbGlobalMapping(RemoveSmbGlobalMappingRequest) returns (RemoveSmbGlobalMappingResponse) {} +} + + +message NewSmbGlobalMappingRequest { + // A remote SMB share to mount + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB remote path specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; + + // Optional local path to mount the smb on + string local_path = 2; + + // Username credential associated with the share + string username = 3; + + // Password credential associated with the share + string password = 4; +} + +message NewSmbGlobalMappingResponse { + // Intentionally empty. +} + + +message RemoveSmbGlobalMappingRequest { + // A remote SMB share mapping to remove + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB share specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; +} + +message RemoveSmbGlobalMappingResponse { + // Intentionally empty. +} diff --git a/csi_proxy_proto/smb/v1alpha1/api.proto b/csi_proxy_proto/smb/v1alpha1/api.proto new file mode 100644 index 0000000..f4d96e9 --- /dev/null +++ b/csi_proxy_proto/smb/v1alpha1/api.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +package v1alpha1; + +service Smb { + // NewSmbGlobalMapping creates an SMB mapping on the SMB client to an SMB share. + rpc NewSmbGlobalMapping(NewSmbGlobalMappingRequest) returns (NewSmbGlobalMappingResponse) {} + + // RemoveSmbGlobalMapping removes the SMB mapping to an SMB share. + rpc RemoveSmbGlobalMapping(RemoveSmbGlobalMappingRequest) returns (RemoveSmbGlobalMappingResponse) {} +} + + +message NewSmbGlobalMappingRequest { + // A remote SMB share to mount + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB remote path specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; + // Optional local path to mount the smb on + string local_path = 2; + + // Username credential associated with the share + string username = 3; + + // Password credential associated with the share + string password = 4; +} + +message NewSmbGlobalMappingResponse { + // Windows error code + // Success is represented as 0 + string error = 1; +} + + +message RemoveSmbGlobalMappingRequest { + // A remote SMB share mapping to remove + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB share specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; +} + +message RemoveSmbGlobalMappingResponse { + // Windows error code + // Success is represented as 0 + string error = 1; +} \ No newline at end of file diff --git a/csi_proxy_proto/smb/v1beta1/api.proto b/csi_proxy_proto/smb/v1beta1/api.proto new file mode 100644 index 0000000..8a2b515 --- /dev/null +++ b/csi_proxy_proto/smb/v1beta1/api.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +package v1beta1; + +service Smb { + // NewSmbGlobalMapping creates an SMB mapping on the SMB client to an SMB share. + rpc NewSmbGlobalMapping(NewSmbGlobalMappingRequest) returns (NewSmbGlobalMappingResponse) {} + + // RemoveSmbGlobalMapping removes the SMB mapping to an SMB share. + rpc RemoveSmbGlobalMapping(RemoveSmbGlobalMappingRequest) returns (RemoveSmbGlobalMappingResponse) {} +} + + +message NewSmbGlobalMappingRequest { + // A remote SMB share to mount + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB remote path specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; + // Optional local path to mount the smb on + string local_path = 2; + + // Username credential associated with the share + string username = 3; + + // Password credential associated with the share + string password = 4; +} + +message NewSmbGlobalMappingResponse { + // Windows error code + // Success is represented as 0 + string error = 1; +} + + +message RemoveSmbGlobalMappingRequest { + // A remote SMB share mapping to remove + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB share specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; +} + +message RemoveSmbGlobalMappingResponse { + // Windows error code + // Success is represented as 0 + string error = 1; +} diff --git a/csi_proxy_proto/smb/v1beta2/api.proto b/csi_proxy_proto/smb/v1beta2/api.proto new file mode 100644 index 0000000..ff28b11 --- /dev/null +++ b/csi_proxy_proto/smb/v1beta2/api.proto @@ -0,0 +1,58 @@ +syntax = "proto3"; + +package v1beta2; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/smb/v1beta2"; + +service Smb { + // NewSmbGlobalMapping creates an SMB mapping on the SMB client to an SMB share. + rpc NewSmbGlobalMapping(NewSmbGlobalMappingRequest) returns (NewSmbGlobalMappingResponse) {} + + // RemoveSmbGlobalMapping removes the SMB mapping to an SMB share. + rpc RemoveSmbGlobalMapping(RemoveSmbGlobalMappingRequest) returns (RemoveSmbGlobalMappingResponse) {} +} + + +message NewSmbGlobalMappingRequest { + // A remote SMB share to mount + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB remote path specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; + + // Optional local path to mount the smb on + string local_path = 2; + + // Username credential associated with the share + string username = 3; + + // Password credential associated with the share + string password = 4; +} + +message NewSmbGlobalMappingResponse { + // Intentionally empty. +} + + +message RemoveSmbGlobalMappingRequest { + // A remote SMB share mapping to remove + // All unicode characters allowed in SMB server name specifications are + // permitted except for restrictions below + // + // Restrictions: + // SMB share specified in the format: \\server-name\sharename, \\server.fqdn\sharename or \\a.b.c.d\sharename + // If not an IP address, share name has to be a valid DNS name. + // UNC specifications to local paths or prefix: \\?\ is not allowed. + // Characters: + [ ] " / : ; | < > , ? * = $ are not allowed. + string remote_path = 1; +} + +message RemoveSmbGlobalMappingResponse { + // Intentionally empty. +} diff --git a/csi_proxy_proto/system/v1alpha1/api.proto b/csi_proxy_proto/system/v1alpha1/api.proto new file mode 100644 index 0000000..d04923e --- /dev/null +++ b/csi_proxy_proto/system/v1alpha1/api.proto @@ -0,0 +1,93 @@ +syntax = "proto3"; + +package v1alpha1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/system/v1alpha1"; + +service System { + // GetBIOSSerialNumber returns the device's serial number + rpc GetBIOSSerialNumber(GetBIOSSerialNumberRequest) + returns (GetBIOSSerialNumberResponse) {} + + // StartService starts a Windows service + // NOTE: This method affects global node state and should only be used + // with consideration to other CSI drivers that run concurrently. + rpc StartService(StartServiceRequest) returns (StartServiceResponse) {} + + // StopService stops a Windows service + // NOTE: This method affects global node state and should only be used + // with consideration to other CSI drivers that run concurrently. + rpc StopService(StopServiceRequest) returns (StopServiceResponse) {} + + // GetService queries a Windows service state + rpc GetService(GetServiceRequest) returns (GetServiceResponse) {} +} + +message GetBIOSSerialNumberRequest { + // Intentionally empty +} + +message GetBIOSSerialNumberResponse { + // Serial number + string serial_number = 1; +} + +message StartServiceRequest { + // Service name (as listed in System\CCS\Services keys) + string name = 1; +} + +message StartServiceResponse { + // Intentionally empty +} + +message StopServiceRequest { + // Service name (as listed in System\CCS\Services keys) + string name = 1; + + // Forces stopping of services that has dependent services + bool force = 2; +} + +message StopServiceResponse { + // Intentionally empty +} + +// https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_status#members +enum ServiceStatus { + UNKNOWN = 0; + STOPPED = 1; + START_PENDING = 2; + STOP_PENDING = 3; + RUNNING = 4; + CONTINUE_PENDING = 5; + PAUSE_PENDING = 6; + PAUSED = 7; +} + +// https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nf-winsvc-changeserviceconfiga +enum StartType { + BOOT = 0; + SYSTEM = 1; + AUTOMATIC = 2; + MANUAL = 3; + DISABLED = 4; +} + +message GetServiceRequest { + // Service name (as listed in System\CCS\Services keys) + string name = 1; +} + +message GetServiceResponse { + // Service display name + string display_name = 1; + + // Service start type. + // Used to control whether a service will start on boot, and if so on which + // boot phase. + StartType start_type = 2; + + // Service status, e.g. stopped, running, paused + ServiceStatus status = 3; +} diff --git a/csi_proxy_proto/volume/v1/api.proto b/csi_proxy_proto/volume/v1/api.proto new file mode 100644 index 0000000..a065381 --- /dev/null +++ b/csi_proxy_proto/volume/v1/api.proto @@ -0,0 +1,143 @@ +syntax = "proto3"; + +package v1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/volume/v1"; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for all volumes from a + // given disk number and partition number (optional) + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + + // MountVolume mounts the volume at the requested global staging path. + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + + // UnmountVolume flushes data cache to disk and removes the global staging path. + rpc UnmountVolume(UnmountVolumeRequest) returns (UnmountVolumeResponse) {} + + // IsVolumeFormatted checks if a volume is formatted. + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + + // FormatVolume formats a volume with NTFS. + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + + // ResizeVolume performs resizing of the partition and file system for a block based volume. + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} + + // GetVolumeStats gathers total bytes and used bytes for a volume. + rpc GetVolumeStats(GetVolumeStatsRequest) returns (GetVolumeStatsResponse) {} + + // GetDiskNumberFromVolumeID gets the disk number of the disk where the volume is located. + rpc GetDiskNumberFromVolumeID(GetDiskNumberFromVolumeIDRequest) returns (GetDiskNumberFromVolumeIDResponse ) {} + + // GetVolumeIDFromTargetPath gets the volume id for a given target path. + rpc GetVolumeIDFromTargetPath(GetVolumeIDFromTargetPathRequest) returns (GetVolumeIDFromTargetPathResponse) {} + + // WriteVolumeCache write volume cache to disk. + rpc WriteVolumeCache(WriteVolumeCacheRequest) returns (WriteVolumeCacheResponse) {} +} + +message ListVolumesOnDiskRequest { + // Disk device number of the disk to query for volumes. + uint32 disk_number = 1; + // The partition number (optional), by default it uses the first partition of the disk. + uint32 partition_number = 2; +} + +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk. + repeated string volume_ids = 1; +} + +message MountVolumeRequest { + // Volume device ID of the volume to mount. + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted. + string target_path = 2; +} + +message MountVolumeResponse { + // Intentionally empty. +} + +message UnmountVolumeRequest { + // Volume device ID of the volume to dismount. + string volume_id = 1; + // Path where the volume has been mounted. + string target_path = 2; +} + +message UnmountVolumeResponse { + // Intentionally empty. +} + +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check. + string volume_id = 1; +} + +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS. + bool formatted = 1; +} + +message FormatVolumeRequest { + // Volume device ID of the volume to format. + string volume_id = 1; +} + +message FormatVolumeResponse { + // Intentionally empty. +} + +message ResizeVolumeRequest { + // Volume device ID of the volume to resize. + string volume_id = 1; + // New size in bytes of the volume. + int64 size_bytes = 2; +} + +message ResizeVolumeResponse { + // Intentionally empty. +} + +message GetVolumeStatsRequest{ + // Volume device Id of the volume to get the stats for. + string volume_id = 1; +} + +message GetVolumeStatsResponse{ + // Total bytes + int64 total_bytes = 1; + // Used bytes + int64 used_bytes = 2; +} + +message GetDiskNumberFromVolumeIDRequest { + // Volume device ID of the volume to get the disk number for. + string volume_id = 1; +} + +message GetDiskNumberFromVolumeIDResponse { + // Corresponding disk number. + uint32 disk_number = 1; +} + +message GetVolumeIDFromTargetPathRequest { + // The target path. + string target_path = 1; +} + +message GetVolumeIDFromTargetPathResponse { + // The volume device ID. + string volume_id = 1; +} + +message WriteVolumeCacheRequest { + // Volume device ID of the volume to flush the cache. + string volume_id = 1; +} + +message WriteVolumeCacheResponse { + // Intentionally empty. +} diff --git a/csi_proxy_proto/volume/v1alpha1/api.proto b/csi_proxy_proto/volume/v1alpha1/api.proto new file mode 100644 index 0000000..5037859 --- /dev/null +++ b/csi_proxy_proto/volume/v1alpha1/api.proto @@ -0,0 +1,69 @@ +syntax = "proto3"; + +package v1alpha1; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for + // all volumes on a Disk device + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + // MountVolume mounts the volume at the requested global staging path + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + // DismountVolume gracefully dismounts a volume + rpc DismountVolume(DismountVolumeRequest) returns (DismountVolumeResponse) {} + // IsVolumeFormatted checks if a volume is formatted with NTFS + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + // FormatVolume formats a volume with the provided file system + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + // ResizeVolume performs resizing of the partition and file system for a block based volume + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} +} +message ListVolumesOnDiskRequest { + // Disk device ID of the disk to query for volumes + string disk_id = 1; +} +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk + repeated string volume_ids = 1; +} +message MountVolumeRequest { + // Volume device ID of the volume to mount + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted + string path = 2; +} +message MountVolumeResponse { + // Intentionally empty +} +message DismountVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // Path where the volume has been mounted. + string path = 2; +} +message DismountVolumeResponse { + // Intentionally empty +} +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check + string volume_id = 1; +} +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS + bool formatted = 1; +} +message FormatVolumeRequest { + // Volume device ID of the volume to format + string volume_id = 1; +} +message FormatVolumeResponse { + // Intentionally empty +} +message ResizeVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // New size of the volume + int64 size = 2; +} +message ResizeVolumeResponse { + // Intentionally empty +} \ No newline at end of file diff --git a/csi_proxy_proto/volume/v1beta1/api.proto b/csi_proxy_proto/volume/v1beta1/api.proto new file mode 100644 index 0000000..07eb7b2 --- /dev/null +++ b/csi_proxy_proto/volume/v1beta1/api.proto @@ -0,0 +1,121 @@ +syntax = "proto3"; + +package v1beta1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/volume/v1beta1"; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for + // all volumes on a Disk device + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + // MountVolume mounts the volume at the requested global staging path + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + // DismountVolume gracefully dismounts a volume + rpc DismountVolume(DismountVolumeRequest) returns (DismountVolumeResponse) {} + // IsVolumeFormatted checks if a volume is formatted with NTFS + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + // FormatVolume formats a volume with the provided file system + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + // ResizeVolume performs resizing of the partition and file system for a block based volume + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} + // VolumeStats gathers DiskSize, VolumeSize and VolumeUsedSize for a volume + rpc VolumeStats(VolumeStatsRequest) returns (VolumeStatsResponse) {} + // GetVolumeDiskNumber gets the disk number of the disk where the volume is located + rpc GetVolumeDiskNumber(VolumeDiskNumberRequest) returns (VolumeDiskNumberResponse) {} + // GetVolumeIDFromMount gets the volume id for a given mount + rpc GetVolumeIDFromMount(VolumeIDFromMountRequest) returns (VolumeIDFromMountResponse) {} +} + +message ListVolumesOnDiskRequest { + // Disk device ID of the disk to query for volumes + string disk_id = 1; +} + +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk + repeated string volume_ids = 1; +} + +message MountVolumeRequest { + // Volume device ID of the volume to mount + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted + string path = 2; +} + +message MountVolumeResponse { + // Intentionally empty +} + +message DismountVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // Path where the volume has been mounted. + string path = 2; +} + +message DismountVolumeResponse { + // Intentionally empty +} + +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check + string volume_id = 1; +} + +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS + bool formatted = 1; +} + +message FormatVolumeRequest { + // Volume device ID of the volume to format + string volume_id = 1; +} + +message FormatVolumeResponse { + // Intentionally empty +} + +message ResizeVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // New size of the volume + int64 size = 2; +} + +message ResizeVolumeResponse { + // Intentionally empty +} + +message VolumeStatsRequest{ + // Volume device Id of the volume to get the stats for + string volume_id = 1; +} + +message VolumeStatsResponse{ + // Capacity of the volume + int64 volumeSize = 1; + // Used bytes + int64 volumeUsedSize = 2; +} + +message VolumeDiskNumberRequest{ + // Volume device Id of the volume to get the disk number for + string volume_id = 1; +} + +message VolumeDiskNumberResponse{ + // Corresponding disk number + int64 diskNumber = 1; +} + +message VolumeIDFromMountRequest { + // Mount + string mount = 1; +} + +message VolumeIDFromMountResponse { + // Mount + string volume_id = 1; +} diff --git a/csi_proxy_proto/volume/v1beta2/api.proto b/csi_proxy_proto/volume/v1beta2/api.proto new file mode 100644 index 0000000..c88e1f5 --- /dev/null +++ b/csi_proxy_proto/volume/v1beta2/api.proto @@ -0,0 +1,132 @@ +syntax = "proto3"; + +package v1beta2; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/volume/v1beta2"; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for + // all volumes on a Disk device + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + // MountVolume mounts the volume at the requested global staging path + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + // DismountVolume gracefully dismounts a volume + rpc DismountVolume(DismountVolumeRequest) returns (DismountVolumeResponse) {} + // IsVolumeFormatted checks if a volume is formatted with NTFS + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + // FormatVolume formats a volume with the provided file system + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + // ResizeVolume performs resizing of the partition and file system for a block based volume + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} + // VolumeStats gathers DiskSize, VolumeSize and VolumeUsedSize for a volume + rpc VolumeStats(VolumeStatsRequest) returns (VolumeStatsResponse) {} + // GetVolumeDiskNumber gets the disk number of the disk where the volume is located + rpc GetVolumeDiskNumber(VolumeDiskNumberRequest) returns (VolumeDiskNumberResponse) {} + // GetVolumeIDFromMount gets the volume id for a given mount + rpc GetVolumeIDFromMount(VolumeIDFromMountRequest) returns (VolumeIDFromMountResponse) {} + // WriteVolumeCache write volume cache to disk + rpc WriteVolumeCache(WriteVolumeCacheRequest) returns (WriteVolumeCacheResponse) {} +} + +message ListVolumesOnDiskRequest { + // Disk device ID of the disk to query for volumes + string disk_id = 1; +} + +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk + repeated string volume_ids = 1; +} + +message MountVolumeRequest { + // Volume device ID of the volume to mount + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted + string path = 2; +} + +message MountVolumeResponse { + // Intentionally empty +} + +message DismountVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // Path where the volume has been mounted. + string path = 2; +} + +message DismountVolumeResponse { + // Intentionally empty +} + +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check + string volume_id = 1; +} + +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS + bool formatted = 1; +} + +message FormatVolumeRequest { + // Volume device ID of the volume to format + string volume_id = 1; +} + +message FormatVolumeResponse { + // Intentionally empty +} + +message ResizeVolumeRequest { + // Volume device ID of the volume to dismount + string volume_id = 1; + // New size of the volume + int64 size = 2; +} + +message ResizeVolumeResponse { + // Intentionally empty +} + +message VolumeStatsRequest{ + // Volume device Id of the volume to get the stats for + string volume_id = 1; +} + +message VolumeStatsResponse{ + // Capacity of the volume + int64 volumeSize = 1; + // Used bytes + int64 volumeUsedSize = 2; +} + +message VolumeDiskNumberRequest{ + // Volume device Id of the volume to get the disk number for + string volume_id = 1; +} + +message VolumeDiskNumberResponse{ + // Corresponding disk number + int64 diskNumber = 1; +} + +message VolumeIDFromMountRequest { + // Mount + string mount = 1; +} + +message VolumeIDFromMountResponse { + // Mount + string volume_id = 1; +} + +message WriteVolumeCacheRequest { + // Volume device ID of the volume to flush the cache + string volume_id = 1; +} + +message WriteVolumeCacheResponse { + // Intentionally empty +} \ No newline at end of file diff --git a/csi_proxy_proto/volume/v1beta3/api.proto b/csi_proxy_proto/volume/v1beta3/api.proto new file mode 100644 index 0000000..2340b3c --- /dev/null +++ b/csi_proxy_proto/volume/v1beta3/api.proto @@ -0,0 +1,143 @@ +syntax = "proto3"; + +package v1beta3; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/volume/v1beta3"; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for all volumes from a + // given disk number and partition number (optional) + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + + // MountVolume mounts the volume at the requested global staging path. + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + + // UnmountVolume flushes data cache to disk and removes the global staging path. + rpc UnmountVolume(UnmountVolumeRequest) returns (UnmountVolumeResponse) {} + + // IsVolumeFormatted checks if a volume is formatted. + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + + // FormatVolume formats a volume with NTFS. + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + + // ResizeVolume performs resizing of the partition and file system for a block based volume. + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} + + // GetVolumeStats gathers total bytes and used bytes for a volume. + rpc GetVolumeStats(GetVolumeStatsRequest) returns (GetVolumeStatsResponse) {} + + // GetDiskNumberFromVolumeID gets the disk number of the disk where the volume is located. + rpc GetDiskNumberFromVolumeID(GetDiskNumberFromVolumeIDRequest) returns (GetDiskNumberFromVolumeIDResponse ) {} + + // GetVolumeIDFromTargetPath gets the volume id for a given target path. + rpc GetVolumeIDFromTargetPath(GetVolumeIDFromTargetPathRequest) returns (GetVolumeIDFromTargetPathResponse) {} + + // WriteVolumeCache write volume cache to disk. + rpc WriteVolumeCache(WriteVolumeCacheRequest) returns (WriteVolumeCacheResponse) {} +} + +message ListVolumesOnDiskRequest { + // Disk device number of the disk to query for volumes. + uint32 disk_number = 1; + // The partition number (optional), by default it uses the first partition of the disk. + uint32 partition_number = 2; +} + +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk. + repeated string volume_ids = 1; +} + +message MountVolumeRequest { + // Volume device ID of the volume to mount. + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted. + string target_path = 2; +} + +message MountVolumeResponse { + // Intentionally empty. +} + +message UnmountVolumeRequest { + // Volume device ID of the volume to dismount. + string volume_id = 1; + // Path where the volume has been mounted. + string target_path = 2; +} + +message UnmountVolumeResponse { + // Intentionally empty. +} + +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check. + string volume_id = 1; +} + +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS. + bool formatted = 1; +} + +message FormatVolumeRequest { + // Volume device ID of the volume to format. + string volume_id = 1; +} + +message FormatVolumeResponse { + // Intentionally empty. +} + +message ResizeVolumeRequest { + // Volume device ID of the volume to resize. + string volume_id = 1; + // New size in bytes of the volume. + int64 size_bytes = 2; +} + +message ResizeVolumeResponse { + // Intentionally empty. +} + +message GetVolumeStatsRequest{ + // Volume device Id of the volume to get the stats for. + string volume_id = 1; +} + +message GetVolumeStatsResponse{ + // Total bytes + int64 total_bytes = 1; + // Used bytes + int64 used_bytes = 2; +} + +message GetDiskNumberFromVolumeIDRequest { + // Volume device ID of the volume to get the disk number for. + string volume_id = 1; +} + +message GetDiskNumberFromVolumeIDResponse { + // Corresponding disk number. + uint32 disk_number = 1; +} + +message GetVolumeIDFromTargetPathRequest { + // The target path. + string target_path = 1; +} + +message GetVolumeIDFromTargetPathResponse { + // The volume device ID. + string volume_id = 1; +} + +message WriteVolumeCacheRequest { + // Volume device ID of the volume to flush the cache. + string volume_id = 1; +} + +message WriteVolumeCacheResponse { + // Intentionally empty. +} diff --git a/csi_proxy_proto/volume/v2alpha1/api.proto b/csi_proxy_proto/volume/v2alpha1/api.proto new file mode 100644 index 0000000..611d4ab --- /dev/null +++ b/csi_proxy_proto/volume/v2alpha1/api.proto @@ -0,0 +1,158 @@ +syntax = "proto3"; + +package v2alpha1; + +option go_package = "github.com/kubernetes-csi/csi-proxy/client/api/volume/v2alpha1"; + +service Volume { + // ListVolumesOnDisk returns the volume IDs (in \\.\Volume{GUID} format) for all volumes from a + // given disk number and partition number (optional) + rpc ListVolumesOnDisk(ListVolumesOnDiskRequest) returns (ListVolumesOnDiskResponse) {} + + // MountVolume mounts the volume at the requested global staging path. + rpc MountVolume(MountVolumeRequest) returns (MountVolumeResponse) {} + + // UnmountVolume flushes data cache to disk and removes the global staging path. + rpc UnmountVolume(UnmountVolumeRequest) returns (UnmountVolumeResponse) {} + + // IsVolumeFormatted checks if a volume is formatted. + rpc IsVolumeFormatted(IsVolumeFormattedRequest) returns (IsVolumeFormattedResponse) {} + + // FormatVolume formats a volume with NTFS. + rpc FormatVolume(FormatVolumeRequest) returns (FormatVolumeResponse) {} + + // ResizeVolume performs resizing of the partition and file system for a block based volume. + rpc ResizeVolume(ResizeVolumeRequest) returns (ResizeVolumeResponse) {} + + // GetVolumeStats gathers total bytes and used bytes for a volume. + rpc GetVolumeStats(GetVolumeStatsRequest) returns (GetVolumeStatsResponse) {} + + // GetDiskNumberFromVolumeID gets the disk number of the disk where the volume is located. + rpc GetDiskNumberFromVolumeID(GetDiskNumberFromVolumeIDRequest) returns (GetDiskNumberFromVolumeIDResponse ) {} + + // GetVolumeIDFromTargetPath gets the volume id for a given target path. + rpc GetVolumeIDFromTargetPath(GetVolumeIDFromTargetPathRequest) returns (GetVolumeIDFromTargetPathResponse) {} + + // GetClosestVolumeIDFromTargetPath gets the closest volume id for a given target path + // by following symlinks and moving up in the filesystem, if after moving up in the filesystem + // we get to a DriveLetter then the volume corresponding to this drive letter is returned instead. + rpc GetClosestVolumeIDFromTargetPath(GetClosestVolumeIDFromTargetPathRequest) returns (GetClosestVolumeIDFromTargetPathResponse) {} + + // WriteVolumeCache write volume cache to disk. + rpc WriteVolumeCache(WriteVolumeCacheRequest) returns (WriteVolumeCacheResponse) {} +} + +message ListVolumesOnDiskRequest { + // Disk device number of the disk to query for volumes. + uint32 disk_number = 1; + // The partition number (optional), by default it uses the first partition of the disk. + uint32 partition_number = 2; +} + +message ListVolumesOnDiskResponse { + // Volume device IDs of volumes on the specified disk. + repeated string volume_ids = 1; +} + +message MountVolumeRequest { + // Volume device ID of the volume to mount. + string volume_id = 1; + // Path in the host's file system where the volume needs to be mounted. + string target_path = 2; +} + +message MountVolumeResponse { + // Intentionally empty. +} + +message UnmountVolumeRequest { + // Volume device ID of the volume to dismount. + string volume_id = 1; + // Path where the volume has been mounted. + string target_path = 2; +} + +message UnmountVolumeResponse { + // Intentionally empty. +} + +message IsVolumeFormattedRequest { + // Volume device ID of the volume to check. + string volume_id = 1; +} + +message IsVolumeFormattedResponse { + // Is the volume formatted with NTFS. + bool formatted = 1; +} + +message FormatVolumeRequest { + // Volume device ID of the volume to format. + string volume_id = 1; +} + +message FormatVolumeResponse { + // Intentionally empty. +} + +message ResizeVolumeRequest { + // Volume device ID of the volume to resize. + string volume_id = 1; + // New size in bytes of the volume. + int64 size_bytes = 2; +} + +message ResizeVolumeResponse { + // Intentionally empty. +} + +message GetVolumeStatsRequest{ + // Volume device Id of the volume to get the stats for. + string volume_id = 1; +} + +message GetVolumeStatsResponse{ + // Total bytes + int64 total_bytes = 1; + // Used bytes + int64 used_bytes = 2; +} + +message GetDiskNumberFromVolumeIDRequest { + // Volume device ID of the volume to get the disk number for. + string volume_id = 1; +} + +message GetDiskNumberFromVolumeIDResponse { + // Corresponding disk number. + uint32 disk_number = 1; +} + +message GetVolumeIDFromTargetPathRequest { + // The target path. + string target_path = 1; +} + +message GetVolumeIDFromTargetPathResponse { + // The volume device ID. + string volume_id = 1; +} + +message GetClosestVolumeIDFromTargetPathRequest { + // The target path. + string target_path = 1; +} + +message GetClosestVolumeIDFromTargetPathResponse { + // The volume device ID. + string volume_id = 1; +} + +message WriteVolumeCacheRequest { + // Volume device ID of the volume to flush the cache. + string volume_id = 1; +} + +message WriteVolumeCacheResponse { + // Intentionally empty. +} diff --git a/examples/freenas-api-nfs.yaml b/examples/freenas-api-nfs.yaml index 68084cb..97b8a53 100644 --- a/examples/freenas-api-nfs.yaml +++ b/examples/freenas-api-nfs.yaml @@ -43,6 +43,8 @@ zfs: datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 + + # not supported yet #datasetPermissionsAcls: #- "-m everyone@:full_set:allow" #- "-m u:kube:full_set:allow" diff --git a/examples/freenas-api-smb.yaml b/examples/freenas-api-smb.yaml index 7b45f4c..a8e0a84 100644 --- a/examples/freenas-api-smb.yaml +++ b/examples/freenas-api-smb.yaml @@ -34,9 +34,10 @@ zfs: # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" - datasetProperties: - aclmode: restricted - casesensitivity: mixed + # these are managed automatically via the volume creation process when flagged as an smb volume + #datasetProperties: + # aclmode: restricted + # casesensitivity: mixed datasetParentName: tank/k8s/a/vols # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap @@ -47,8 +48,10 @@ zfs: datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 - datasetPermissionsAcls: - - "-m everyone@:full_set:allow" + + # not supported yet in api + #datasetPermissionsAcls: + #- "-m everyone@:full_set:allow" #- "-m u:kube:full_set:allow" smb: diff --git a/examples/freenas-smb.yaml b/examples/freenas-smb.yaml index 6d08b7e..8a2ed4d 100644 --- a/examples/freenas-smb.yaml +++ b/examples/freenas-smb.yaml @@ -46,7 +46,9 @@ zfs: datasetProperties: aclmode: restricted - casesensitivity: mixed + aclinherit: passthrough + acltype: nfsv4 + casesensitivity: insensitive datasetParentName: tank/k8s/a/vols # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap @@ -54,12 +56,41 @@ zfs: detachedSnapshotsDatasetParentName: tank/k8s/a/snaps datasetEnableQuotas: true datasetEnableReservation: false - datasetPermissionsMode: "0777" - datasetPermissionsUser: nobody - datasetPermissionsGroup: nobody + datasetPermissionsMode: "0770" + + # as appropriate create a dedicated user for smb connections + # and set this + datasetPermissionsUser: 65534 + datasetPermissionsGroup: 65534 + + # CORE + #datasetPermissionsAclsBinary: setfacl + + # SCALE + #datasetPermissionsAclsBinary: nfs4xdr_setfacl + + # if using a user other than guest/nobody comment the 'everyone@' acl + # and uncomment the appropriate block below datasetPermissionsAcls: - - "-m everyone@:full_set:allow" - #- "-m u:kube:full_set:allow" + - "-m everyone@:full_set:fd:allow" + + # CORE + # in CORE you cannot have multiple entries for the same principle + # or said differently, they are declarative so using -m will replace + # whatever the current value is for the principle rather than adding a + # entry in the acl list + #- "-m g:builtin_users:full_set:fd:allow" + #- "-m group@:modify_set:fd:allow" + #- "-m owner@:full_set:fd:allow" + + # SCALE + # https://www.truenas.com/community/threads/get-setfacl-on-scale-with-nfsv4-acls.95231/ + # -s replaces everything + # so we put this in specific order to mimic the defaults of SCALE when using the api + #- -s group:builtin_users:full_set:fd:allow + #- -a group:builtin_users:modify_set:fd:allow + #- -a group@:modify_set:fd:allow + #- -a owner@:full_set:fd:allow smb: shareHost: server address @@ -77,7 +108,7 @@ smb: shareAllowedHosts: [] shareDeniedHosts: [] #shareDefaultPermissions: true - shareGuestOk: true + shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true shareRecycleBin: true diff --git a/examples/node-common.yaml b/examples/node-common.yaml index 0fb61e7..1f0f3df 100644 --- a/examples/node-common.yaml +++ b/examples/node-common.yaml @@ -2,6 +2,9 @@ node: mount: + # predominantly used to facilitate testing + # mount_flags should generally be defined in storage classes, etc + mount_flags: "" # should fsck be executed before mounting the fs checkFilesystem: xfs: diff --git a/examples/private.yaml b/examples/private.yaml new file mode 100644 index 0000000..8d2d969 --- /dev/null +++ b/examples/private.yaml @@ -0,0 +1,18 @@ +# +# these SHOULD NOT be used +# they are here for documentation purposes only and are likely to: +# - be removed +# - break things +# + +_private: + csi: + volume: + derivedContext: + # driver left blank is used to auto select + driver: memory # strictly to facilitate testing + #driver: kubernetes + idHash: + strategy: crc16 + #strategy: crc32 + #strategy: md5 diff --git a/package-lock.json b/package-lock.json index d0ba2ca..cb81a02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "democratic-csi", - "version": "1.6.3", + "version": "1.7.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "democratic-csi", - "version": "1.6.3", + "version": "1.7.0", "license": "MIT", "dependencies": { "@grpc/grpc-js": "^1.5.7", @@ -70,9 +70,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.2.tgz", - "integrity": "sha512-9+89Ne1K8F9u86T+l1yIV2DS+dWHYVK61SsDZN4MFTFehOOaJ4rHxa1cW8Lwdn2/6tOx7N3+SY/vfcjztOHopA==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.5.tgz", + "integrity": "sha512-h0KSwgLiF5rmSAU6qnzK1aoD1MNqOw9HJK96N8VW3dR5FHMpq+0JNdLQFP//NcaIWVB7I7vkHl4JmU9hUw82Aw==", "dependencies": { "@grpc/proto-loader": "^0.6.4", "@types/node": ">=12.12.47" @@ -315,9 +315,9 @@ } }, "node_modules/@types/node": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", + "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" }, "node_modules/@types/request": { "version": "2.48.8", @@ -356,9 +356,9 @@ } }, "node_modules/@types/tough-cookie": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", - "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" }, "node_modules/@types/underscore": { "version": "1.11.4", @@ -465,9 +465,9 @@ } }, "node_modules/async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" }, "node_modules/async-mutex": { "version": "0.3.2", @@ -947,9 +947,9 @@ } }, "node_modules/eslint": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz", - "integrity": "sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.13.0.tgz", + "integrity": "sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.2.1", @@ -1163,9 +1163,9 @@ "dev": true }, "node_modules/fecha": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", - "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, "node_modules/file-entry-cache": { "version": "6.0.1", @@ -1757,9 +1757,9 @@ } }, "node_modules/lru-cache": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.0.tgz", - "integrity": "sha512-AmXqneQZL3KZMIgBpaPTeI6pfwh+xQ2vutMsyqOu1TBdEXFZgpG/80wuJ531w2ZN7TI0/oc8CPxzh/DKQudZqg==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", + "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==", "engines": { "node": ">=12" } @@ -2127,18 +2127,18 @@ } }, "node_modules/prompt": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.2.2.tgz", - "integrity": "sha512-XNXhNv3PUHJDcDkISpCwSJxtw9Bor4FZnlMUDW64N/KCPdxhfVlpD5+YUXI/Z8a9QWmOhs9KSiVtR8nzPS0BYA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.3.0.tgz", + "integrity": "sha512-ZkaRWtaLBZl7KKAKndKYUL8WqNT+cQHKRZnT4RYYms48jQkFw3rrBL+/N5K/KtdEveHkxs982MX2BkDKub2ZMg==", "dependencies": { "@colors/colors": "1.5.0", - "async": "~0.9.0", + "async": "3.2.3", "read": "1.0.x", "revalidator": "0.1.x", "winston": "2.x" }, "engines": { - "node": ">= 0.6.6" + "node": ">= 6.0.0" } }, "node_modules/prompt/node_modules/winston": { @@ -2440,17 +2440,28 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", - "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "dependencies": { - "lru-cache": "^7.4.0" + "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" }, "engines": { - "node": "^10.0.0 || ^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/shebang-command": { @@ -2761,9 +2772,9 @@ } }, "node_modules/uglify-js": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.3.tgz", - "integrity": "sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg==", + "version": "3.15.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.4.tgz", + "integrity": "sha512-vMOPGDuvXecPs34V74qDKk4iJ/SN4vL3Ow/23ixafENYvtrNvtbcgUeugTcUGRGsOF/5fU8/NYSL5Hyb3l1OJA==", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -2864,11 +2875,6 @@ "node": ">= 6.4.0" } }, - "node_modules/winston/node_modules/async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" - }, "node_modules/word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -2938,9 +2944,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz", - "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==", + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", + "integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -2997,9 +3003,9 @@ } }, "@grpc/grpc-js": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.2.tgz", - "integrity": "sha512-9+89Ne1K8F9u86T+l1yIV2DS+dWHYVK61SsDZN4MFTFehOOaJ4rHxa1cW8Lwdn2/6tOx7N3+SY/vfcjztOHopA==", + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.5.tgz", + "integrity": "sha512-h0KSwgLiF5rmSAU6qnzK1aoD1MNqOw9HJK96N8VW3dR5FHMpq+0JNdLQFP//NcaIWVB7I7vkHl4JmU9hUw82Aw==", "requires": { "@grpc/proto-loader": "^0.6.4", "@types/node": ">=12.12.47" @@ -3216,9 +3222,9 @@ } }, "@types/node": { - "version": "17.0.23", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" + "version": "17.0.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", + "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" }, "@types/request": { "version": "2.48.8", @@ -3257,9 +3263,9 @@ } }, "@types/tough-cookie": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.1.tgz", - "integrity": "sha512-Y0K95ThC3esLEYD6ZuqNek29lNX2EM1qxV8y2FTLUB0ff5wWrk7az+mLrnNFUnaXcgKye22+sFBRXOgpPILZNg==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", + "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" }, "@types/underscore": { "version": "1.11.4", @@ -3339,9 +3345,9 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "async": { - "version": "0.9.2", - "resolved": "https://registry.npmjs.org/async/-/async-0.9.2.tgz", - "integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0=" + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" }, "async-mutex": { "version": "0.3.2", @@ -3720,9 +3726,9 @@ "dev": true }, "eslint": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.12.0.tgz", - "integrity": "sha512-it1oBL9alZg1S8UycLm5YDMAkIhtH6FtAzuZs6YvoGVldWjbS08BkAdb/ymP9LlAyq8koANu32U7Ib/w+UNh8Q==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.13.0.tgz", + "integrity": "sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ==", "dev": true, "requires": { "@eslint/eslintrc": "^1.2.1", @@ -3884,9 +3890,9 @@ "dev": true }, "fecha": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz", - "integrity": "sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q==" + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" }, "file-entry-cache": { "version": "6.0.1", @@ -4329,9 +4335,9 @@ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, "lru-cache": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.0.tgz", - "integrity": "sha512-AmXqneQZL3KZMIgBpaPTeI6pfwh+xQ2vutMsyqOu1TBdEXFZgpG/80wuJ531w2ZN7TI0/oc8CPxzh/DKQudZqg==" + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", + "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==" }, "make-error": { "version": "1.3.6", @@ -4610,12 +4616,12 @@ "dev": true }, "prompt": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.2.2.tgz", - "integrity": "sha512-XNXhNv3PUHJDcDkISpCwSJxtw9Bor4FZnlMUDW64N/KCPdxhfVlpD5+YUXI/Z8a9QWmOhs9KSiVtR8nzPS0BYA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.3.0.tgz", + "integrity": "sha512-ZkaRWtaLBZl7KKAKndKYUL8WqNT+cQHKRZnT4RYYms48jQkFw3rrBL+/N5K/KtdEveHkxs982MX2BkDKub2ZMg==", "requires": { "@colors/colors": "1.5.0", - "async": "~0.9.0", + "async": "3.2.3", "read": "1.0.x", "revalidator": "0.1.x", "winston": "2.x" @@ -4842,11 +4848,21 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "semver": { - "version": "7.3.6", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.6.tgz", - "integrity": "sha512-HZWqcgwLsjaX1HBD31msI/rXktuIhS+lWvdE4kN9z+8IVT4Itc7vqU2WvYsyD6/sjYCt4dEKH/m1M3dwI9CC5w==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", "requires": { - "lru-cache": "^7.4.0" + "lru-cache": "^6.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + } } }, "shebang-command": { @@ -5073,9 +5089,9 @@ "dev": true }, "uglify-js": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.3.tgz", - "integrity": "sha512-6iCVm2omGJbsu3JWac+p6kUiOpg3wFO2f8lIXjfEb8RrmLjzog1wTPMmwKB7swfzzqxj9YM+sGUM++u1qN4qJg==", + "version": "3.15.4", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.4.tgz", + "integrity": "sha512-vMOPGDuvXecPs34V74qDKk4iJ/SN4vL3Ow/23ixafENYvtrNvtbcgUeugTcUGRGsOF/5fU8/NYSL5Hyb3l1OJA==", "optional": true }, "underscore": { @@ -5140,13 +5156,6 @@ "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.5.0" - }, - "dependencies": { - "async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" - } } }, "winston-transport": { @@ -5202,9 +5211,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.0.tgz", - "integrity": "sha512-WJudfrk81yWFSOkZYpAZx4Nt7V4xp7S/uJkX0CnxovMCt1wCE8LNftPpNuF9X/u9gN5nsD7ycYtRcDf2pL3UiA==", + "version": "17.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", + "integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==", "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", diff --git a/package.json b/package.json index e8e807a..8459eeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "democratic-csi", - "version": "1.6.3", + "version": "1.7.0", "description": "kubernetes csi driver framework", "main": "bin/democratic-csi", "scripts": { diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index 6db629f..abe71a0 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -3,7 +3,7 @@ const { GrpcError, grpc } = require("../../utils/grpc"); const registry = require("../../utils/registry"); const SynologyHttpClient = require("./http").SynologyHttpClient; const semver = require("semver"); -const sleep = require("../../utils/general").sleep; +const GeneralUtils = require("../../utils/general"); const __REGISTRY_NS__ = "ControllerSynologyDriver"; @@ -171,7 +171,9 @@ class ControllerSynologyDriver extends CsiBaseDriver { if ( capability.mount.fs_type && - !["nfs", "cifs"].includes(capability.mount.fs_type) + !GeneralUtils.default_supported_file_filesystems().includes( + capability.mount.fs_type + ) ) { message = `invalid fs_type ${capability.mount.fs_type}`; return false; @@ -198,7 +200,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { if (capability.access_type == "mount") { if ( capability.mount.fs_type && - !["btrfs", "ext3", "ext4", "ext4dev", "xfs"].includes( + !GeneralUtils.default_supported_block_filesystems().includes( capability.mount.fs_type ) ) { @@ -609,12 +611,12 @@ class ControllerSynologyDriver extends CsiBaseDriver { let waitTimeBetweenChecks = settleSeconds * 1000; - await sleep(waitTimeBetweenChecks); + await GeneralUtils.sleep(waitTimeBetweenChecks); lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); while (currentCheck <= settleMaxRetries && lun_uuid) { currentCheck++; - await sleep(waitTimeBetweenChecks); + await GeneralUtils.sleep(waitTimeBetweenChecks); lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); } diff --git a/src/driver/controller-zfs-local/index.js b/src/driver/controller-zfs-local/index.js index 04f1a17..3c1911b 100644 --- a/src/driver/controller-zfs-local/index.js +++ b/src/driver/controller-zfs-local/index.js @@ -1,6 +1,7 @@ const _ = require("lodash"); const { ControllerZfsBaseDriver } = require("../controller-zfs"); const { GrpcError, grpc } = require("../../utils/grpc"); +const GeneralUtils = require("../../utils/general"); const LocalCliExecClient = require("./exec").LocalCliClient; const registry = require("../../utils/registry"); const { Zetabyte } = require("../../utils/zfs"); @@ -95,7 +96,7 @@ class ControllerZfsLocalDriver extends ControllerZfsBaseDriver { case "filesystem": return ["zfs"]; case "volume": - return ["btrfs", "ext3", "ext4", "ext4dev", "xfs"]; + return GeneralUtils.default_supported_block_filesystems(); } } diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index d354e43..94f7ea7 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -1,7 +1,7 @@ const _ = require("lodash"); const { CsiBaseDriver } = require("../index"); const { GrpcError, grpc } = require("../../utils/grpc"); -const sleep = require("../../utils/general").sleep; +const GeneralUtils = require("../../utils/general"); const getLargestNumber = require("../../utils/general").getLargestNumber; const Handlebars = require("handlebars"); @@ -201,9 +201,9 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { const driverZfsResourceType = this.getDriverZfsResourceType(); switch (driverZfsResourceType) { case "filesystem": - return ["nfs", "cifs"]; + return GeneralUtils.default_supported_file_filesystems(); case "volume": - return ["btrfs", "ext3", "ext4", "ext4dev", "xfs"]; + return GeneralUtils.default_supported_block_filesystems(); } } @@ -620,6 +620,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { let snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName(); let zvolBlocksize = this.options.zfs.zvolBlocksize || "16K"; let name = call.request.name; + let volume_id = await driver.getVolumeIdFromName(name); let volume_content_source = call.request.volume_content_source; if (!datasetParentName) { @@ -710,7 +711,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { * 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; + const datasetName = datasetParentName + "/" + volume_id; // ensure volumes with the same name being requested a 2nd time but with a different size fails try { @@ -862,7 +863,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { volume_content_source_snapshot_id + "@" + VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + - name; + volume_id; } driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); @@ -909,6 +910,12 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { }); } else { try { + // remove readonly/undesired props + let cloneProperties = volumeProperties; + delete cloneProperties["aclmode"]; + delete cloneProperties["aclinherit"]; + delete cloneProperties["acltype"]; + delete cloneProperties["casesensitivity"]; response = await zb.zfs.clone(fullSnapshotName, datasetName, { properties: volumeProperties, }); @@ -971,7 +978,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { volume_content_source_volume_id + "@" + VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + - name; + volume_id; driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); @@ -1024,9 +1031,15 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { } else { // create clone // zfs origin property contains parent info, ie: pool0/k8s/test/PVC-111@clone-test + // remove readonly/undesired props + let cloneProperties = volumeProperties; + delete cloneProperties["aclmode"]; + delete cloneProperties["aclinherit"]; + delete cloneProperties["acltype"]; + delete cloneProperties["casesensitivity"]; try { response = await zb.zfs.clone(fullSnapshotName, datasetName, { - properties: volumeProperties, + properties: cloneProperties, }); } catch (err) { if (err.toString().includes("dataset does not exist")) { @@ -1128,8 +1141,13 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { // TODO: this is unsfafe approach, make it better // probably could see if ^-.*\s and split and then shell escape if (this.options.zfs.datasetPermissionsAcls) { + let aclBinary = _.get( + driver.options, + "zfs.datasetPermissionsAclsBinary", + "setfacl" + ); for (const acl of this.options.zfs.datasetPermissionsAcls) { - command = execClient.buildCommand("setfacl", [ + command = execClient.buildCommand(aclBinary, [ acl, properties.mountpoint.value, ]); @@ -1147,7 +1165,6 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { } } } - break; case "volume": // set properties @@ -1191,7 +1208,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { const res = { volume: { - volume_id: name, + volume_id, //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 capacity_bytes: this.options.zfs.datasetEnableQuotas || @@ -1315,7 +1332,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { if (current_try > max_tries) { throw err; } else { - await sleep(sleep_time); + await GeneralUtils.sleep(sleep_time); } } else { throw err; @@ -2190,7 +2207,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { }); // let things settle down - //await sleep(3000); + //await GneralUtils.sleep(3000); } else { try { await zb.zfs.snapshot(fullSnapshotName, { @@ -2198,7 +2215,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { }); // let things settle down - //await sleep(3000); + //await GeneralUtils.sleep(3000); } catch (err) { if (err.toString().includes("dataset does not exist")) { throw new GrpcError( diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index f436ff9..8af3853 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -4,9 +4,8 @@ const { CsiBaseDriver } = require("../index"); const HttpClient = require("./http").Client; const TrueNASApiClient = require("./http/api").Api; const { Zetabyte } = require("../../utils/zfs"); -const getLargestNumber = require("../../utils/general").getLargestNumber; const registry = require("../../utils/registry"); -const sleep = require("../../utils/general").sleep; +const GeneralUtils = require("../../utils/general"); const Handlebars = require("handlebars"); const uuidv4 = require("uuid").v4; @@ -1565,7 +1564,7 @@ class FreeNASApiDriver extends CsiBaseDriver { targetId, retries ); - await sleep(retryWait); + await GeneralUtils.sleep(retryWait); response = await httpClient.delete(endpoint); } @@ -1958,7 +1957,7 @@ class FreeNASApiDriver extends CsiBaseDriver { if (capability.access_type == "mount") { if ( capability.mount.fs_type && - !["btrfs", "ext3", "ext4", "ext4dev", "xfs"].includes( + !GeneralUtils.default_supported_block_filesystems().includes( capability.mount.fs_type ) ) { @@ -2066,6 +2065,7 @@ class FreeNASApiDriver extends CsiBaseDriver { let snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName(); let zvolBlocksize = this.options.zfs.zvolBlocksize || "16K"; let name = call.request.name; + let volume_id = await driver.getVolumeIdFromName(name); let volume_content_source = call.request.volume_content_source; let minimum_volume_size = await driver.getMinimumVolumeSize(); let default_required_bytes = 1073741824; @@ -2171,7 +2171,7 @@ class FreeNASApiDriver extends CsiBaseDriver { * 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; + const datasetName = datasetParentName + "/" + volume_id; // ensure volumes with the same name being requested a 2nd time but with a different size fails try { @@ -2326,7 +2326,7 @@ class FreeNASApiDriver extends CsiBaseDriver { volume_content_source_snapshot_id + "@" + VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + - name; + volume_id; } driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); @@ -2378,7 +2378,7 @@ class FreeNASApiDriver extends CsiBaseDriver { ) { job = await httpApiClient.CoreGetJobs({ id: job_id }); job = job[0]; - await sleep(3000); + await GeneralUtils.sleep(3000); } job.error = job.error || ""; @@ -2488,7 +2488,7 @@ class FreeNASApiDriver extends CsiBaseDriver { volume_content_source_volume_id + "@" + VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX + - name; + volume_id; driver.ctx.logger.debug("full snapshot name: %s", fullSnapshotName); @@ -2538,7 +2538,7 @@ class FreeNASApiDriver extends CsiBaseDriver { ) { job = await httpApiClient.CoreGetJobs({ id: job_id }); job = job[0]; - await sleep(3000); + await GeneralUtils.sleep(3000); } job.error = job.error || ""; @@ -2626,6 +2626,9 @@ class FreeNASApiDriver extends CsiBaseDriver { volsize: driverZfsResourceType == "volume" ? capacity_bytes : undefined, sparse: driverZfsResourceType == "volume" ? sparse : undefined, create_ancestors: true, + share_type: driver.getDriverShareType().includes("smb") + ? "SMB" + : "GENERIC", user_properties: httpApiClient.getPropertiesKeyValueArray( httpApiClient.getUserProperties(volumeProperties) ), @@ -2721,7 +2724,18 @@ class FreeNASApiDriver extends CsiBaseDriver { } if (setPerms) { - await httpApiClient.FilesystemSetperm(perms); + response = await httpApiClient.FilesystemSetperm(perms); + await httpApiClient.CoreWaitForJob(response, 30); + // SetPerm does not alter ownership with extended ACLs + // run this in addition just for good measure + if (perms.uid || perms.gid) { + response = await httpApiClient.FilesystemChown({ + path: perms.path, + uid: perms.uid, + gid: perms.gid, + }); + await httpApiClient.CoreWaitForJob(response, 30); + } } // set acls @@ -2777,7 +2791,7 @@ class FreeNASApiDriver extends CsiBaseDriver { const res = { volume: { - volume_id: name, + volume_id, //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 capacity_bytes: this.options.zfs.datasetEnableQuotas || @@ -3649,7 +3663,10 @@ class FreeNASApiDriver extends CsiBaseDriver { // 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); + size_bytes = GeneralUtils.getLargestNumber( + row.referenced, + row.logicalreferenced + ); } else { // get the size of the parent volume size_bytes = row.volsize; @@ -3930,7 +3947,7 @@ class FreeNASApiDriver extends CsiBaseDriver { while (!job || !["SUCCESS", "ABORTED", "FAILED"].includes(job.state)) { job = await httpApiClient.CoreGetJobs({ id: job_id }); job = job[0]; - await sleep(3000); + await GeneralUtils.sleep(3000); } job.error = job.error || ""; @@ -4045,7 +4062,7 @@ class FreeNASApiDriver extends CsiBaseDriver { // 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( + size_bytes = GeneralUtils.getLargestNumber( properties.referenced.rawvalue, properties.logicalreferenced.rawvalue // TODO: perhaps include minimum volume size here? diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index ef4b52a..9878d18 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -681,7 +681,13 @@ class Api { throw new Error(JSON.stringify(response.body)); } - async CoreWaitForJob(job_id, timeout = 0) { + /** + * + * @param {*} job_id + * @param {*} timeout in seconds + * @returns + */ + async CoreWaitForJob(job_id, timeout = 0, check_interval = 3000) { if (!job_id) { throw new Error("invalid job_id"); } @@ -692,16 +698,17 @@ class Api { let job; // wait for job to finish - while (!job || !["SUCCESS", "ABORTED", "FAILED"].includes(job.state)) { + do { + if (job) { + await sleep(check_interval); + } job = await this.CoreGetJobs({ id: job_id }); job = job[0]; - await sleep(3000); - currentTime = Date.now() / 1000; if (timeout > 0 && currentTime > startTime + timeout) { throw new Error("timeout waiting for job to complete"); } - } + } while (!["SUCCESS", "ABORTED", "FAILED"].includes(job.state)); return job; } @@ -754,7 +761,38 @@ class Api { response = await httpClient.post(endpoint, data); if (response.statusCode == 200) { - return; + return response.body; + } + + throw new Error(JSON.stringify(response.body)); + } + + /** + * + * @param {*} data + */ + async FilesystemChown(data) { + /* + { + "path": "string", + "uid": 0, + "gid": 0, + "options": { + "recursive": false, + "traverse": false + } + } + */ + + const httpClient = await this.getHttpClient(false); + let response; + let endpoint; + + endpoint = `/filesystem/chown`; + response = await httpClient.post(endpoint, data); + + if (response.statusCode == 200) { + return response.body; } throw new Error(JSON.stringify(response.body)); diff --git a/src/driver/freenas/http/index.js b/src/driver/freenas/http/index.js index d516610..9b0a02b 100644 --- a/src/driver/freenas/http/index.js +++ b/src/driver/freenas/http/index.js @@ -122,6 +122,9 @@ class Client { _.set(options, prop, "redacted"); } + delete options.httpAgent; + delete options.httpsAgent; + this.logger.debug("FREENAS HTTP REQUEST: " + stringify(options)); this.logger.debug("FREENAS HTTP ERROR: " + error); this.logger.debug("FREENAS HTTP STATUS: " + response.statusCode); diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 12c3821..0ce6d1a 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -4,6 +4,7 @@ const { GrpcError, grpc } = require("../../utils/grpc"); const registry = require("../../utils/registry"); const SshClient = require("../../utils/ssh").SshClient; const HttpClient = require("./http").Client; +const TrueNASApiClient = require("./http/api").Api; const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); const { sleep, stringify } = require("../../utils/general"); @@ -112,6 +113,13 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { ); } + async getTrueNASHttpApiClient() { + return registry.getAsync(`${__REGISTRY_NS__}:api_client`, async () => { + const httpClient = await this.getHttpClient(); + return new TrueNASApiClient(httpClient, this.ctx.cache); + }); + } + getDriverShareType() { switch (this.options.driver) { case "freenas-nfs": @@ -1716,6 +1724,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { async setFilesystemMode(path, mode) { const httpClient = await this.getHttpClient(); const apiVersion = httpClient.getApiVersion(); + const httpApiClient = await this.getTrueNASHttpApiClient(); switch (apiVersion) { case 1: @@ -1747,6 +1756,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { response = await httpClient.post(endpoint, perms); if (response.statusCode == 200) { + await httpApiClient.CoreWaitForJob(response.body, 30); return; } @@ -1764,6 +1774,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { async setFilesystemOwnership(path, user = false, group = false) { const httpClient = await this.getHttpClient(); const apiVersion = httpClient.getApiVersion(); + const httpApiClient = await this.getTrueNASHttpApiClient(); if (user === false || typeof user == "undefined" || user === null) { user = ""; @@ -1832,6 +1843,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { response = await httpClient.post(endpoint, perms); if (response.statusCode == 200) { + await httpApiClient.CoreWaitForJob(response.body, 30); return; } diff --git a/src/driver/index.js b/src/driver/index.js index 735effc..1b9d30b 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -2,6 +2,8 @@ const _ = require("lodash"); const cp = require("child_process"); const os = require("os"); const fs = require("fs"); +const CsiProxyClient = require("../utils/csi_proxy_client").CsiProxyClient; +const k8s = require("@kubernetes/client-node"); const { GrpcError, grpc } = require("../utils/grpc"); const { Mount } = require("../utils/mount"); const { OneClient } = require("../utils/oneclient"); @@ -9,11 +11,14 @@ const { Filesystem } = require("../utils/filesystem"); const { ISCSI } = require("../utils/iscsi"); const registry = require("../utils/registry"); const semver = require("semver"); -const sleep = require("../utils/general").sleep; +const GeneralUtils = require("../utils/general"); const { Zetabyte } = require("../utils/zfs"); const __REGISTRY_NS__ = "CsiBaseDriver"; +const NODE_OS_DRIVER_CSI_PROXY = "csi-proxy"; +const NODE_OS_DRIVER_POSIX = "posix"; + /** * common code shared between all drivers * this is **NOT** meant to work as a proxy @@ -161,6 +166,202 @@ class CsiBaseDriver { }); } + /** + * + * @returns CsiProxyClient + */ + getDefaultCsiProxyClientInstance() { + return registry.get(`${__REGISTRY_NS__}:default_csi_proxy_instance`, () => { + const options = {}; + options.services = _.get(this.options, "node.csiProxy.services", {}); + return new CsiProxyClient(options); + }); + } + + getDefaultKubernetsConfigInstance() { + return registry.get( + `${__REGISTRY_NS__}:default_kubernetes_config_instance`, + () => { + const kc = new k8s.KubeConfig(); + kc.loadFromDefault(); + return kc; + } + ); + } + + getCsiProxyEnabled() { + const defaultValue = process.platform == "win32"; + return _.get(this.options, "node.csiProxy.enabled", defaultValue); + } + + getNodeIsWindows() { + return process.platform == "win32"; + } + + __getNodeOsDriver() { + if (this.getNodeIsWindows() || this.getCsiProxyEnabled()) { + return NODE_OS_DRIVER_CSI_PROXY; + } + + return NODE_OS_DRIVER_POSIX; + } + + getMountFlagValue(mount_flags = [], flag = "") { + for (let i = mount_flags.length - 1; i >= 0; i--) { + const mount_flag = mount_flags[i]; + if (mount_flag.startsWith(`${flag}=`)) { + return mount_flag.split("=", 2)[1] || ""; + } + } + } + + async getDerivedVolumeContextDriver() { + const driver = this; + let d = _.get(driver.options, "_private.csi.volume.volumeContext.driver"); + if ( + !d && + (process.env.KUBERNETES_SERVICE_HOST || + process.env.KUBERNETES_SERVICE_PORT) + ) { + // test for k8s + d = "kubernetes"; + } + + if (!d) { + // test for Nomad + } + + if (!d && process.env.CSI_SANITY == 1) { + d = "memory"; + } + + return d; + } + + /** + * Used predominantly with windows due to limitations with the csi-proxy + * + * @param {*} call + * @returns + */ + async getDerivedVolumeContext(call) { + const driver = this; + const volume_id = call.request.volume_id; + const d = await driver.getDerivedVolumeContextDriver(); + driver.ctx.logger.debug(`looking up volume_context using driver: ${d}`); + let volume_context; + switch (d) { + case "memory": + driver.volume_context_cache = driver.volume_context_cache || {}; + volume_context = driver.volume_context_cache[volume_id]; + break; + case "kubernetes": + const kc = driver.getDefaultKubernetsConfigInstance(); + const k8sApi = kc.makeApiClient(k8s.CoreV1Api); + + async function findPVByDriverHandle(driver, volumeHandle) { + if (!driver || !volumeHandle) { + return; + } + + let pv; + let pvs; + let kcontinue; + do { + pvs = await k8sApi.listPersistentVolume( + undefined, + undefined, + kcontinue, + undefined, + undefined, + undefined // limit + ); + pv = pvs.body.items.find((item) => { + return ( + item.spec.csi.driver == driver && + item.spec.csi.volumeHandle == volumeHandle + ); + }); + kcontinue = pvs.body.metadata._continue; + } while (!pv && pvs.body.metadata._continue); + + return pv; + } + + const pv = await findPVByDriverHandle( + driver.ctx.args.csiName, + volume_id + ); + if (pv) { + volume_context = pv.spec.csi.volumeAttributes; + } + break; + default: + throw new Error(`unknow derived volume context driver: ${d}`); + } + + //if (!volume_context) { + // throw new Error(`failed to retrieve volume_context for ${volume_id}`); + //} + + driver.ctx.logger.debug( + "retrived derived volume_context %j", + volume_context + ); + return volume_context; + } + + /** + * Should only be used for testing purposes, generally these details should + * come from a CO or some other stateful storage mechanism + * + * @param {*} volume_id + * @param {*} volume_context + */ + async setVolumeContextCache(volume_id, volume_context) { + const driver = this; + if (process.env.CSI_SANITY == 1) { + if (!driver.volume_context_cache) { + driver.volume_context_cache = {}; + } + if (!driver.volume_context_cache[volume_id]) { + driver.ctx.logger.debug( + "setting volume_context_cache %s %j", + volume_id, + volume_context + ); + driver.volume_context_cache[volume_id] = volume_context; + } + } + } + + /** + * Translates a `name` to a `volume_id`. Generally the purpose is to shorten + * the value of `volume_id` to play nicely with scenarios that do not support + * long names (ie: smb share, etc) + * + * @param {*} name + * @returns + */ + async getVolumeIdFromName(name) { + const driver = this; + const strategy = _.get( + driver.options, + "_private.csi.volume.idHash.strategy", + "" + ); + switch (strategy.toLowerCase()) { + case "md5": + return GeneralUtils.md5(name); + case "crc32": + return GeneralUtils.crc32(name); + case "crc16": + return GeneralUtils.crc16(name); + default: + return name; + } + } + async GetPluginInfo(call) { return { name: this.ctx.args.csiName, @@ -367,7 +568,7 @@ class CsiBaseDriver { } const access_type = capability.access_type || "mount"; const volume_context = call.request.volume_context; - let fs_type; + let fs_type = _.get(capability, "mount.fs_type"); let mount_flags; let volume_mount_group; const node_attach_driver = volume_context.node_attach_driver; @@ -390,11 +591,18 @@ class CsiBaseDriver { */ if (access_type == "mount") { - fs_type = capability.mount.fs_type; mount_flags = capability.mount.mount_flags || []; + + // yaml mount_flags + if (_.get(driver.options, "node.mount.mount_flags")) { + mount_flags.push( + ..._.get(driver.options, "node.mount.mount_flags").split(",") + ); + } + // add secrets mount_flags if (normalizedSecrets.mount_flags) { - mount_flags.push(normalizedSecrets.mount_flags); + mount_flags.push(...normalizedSecrets.mount_flags.split(",")); } switch (node_attach_driver) { @@ -438,465 +646,830 @@ class CsiBaseDriver { } } - // csi spec stipulates that staging_target_path is a directory even for block mounts - result = await filesystem.pathExists(staging_target_path); - if (!result) { - await filesystem.mkdir(staging_target_path, ["-p", "-m", "0750"]); - } - - switch (node_attach_driver) { - case "nfs": - case "lustre": - device = `${volume_context.server}:${volume_context.share}`; - break; - case "smb": - device = `//${volume_context.server}/${volume_context.share}`; - - // if not present add guest - let has_username = mount_flags.some((element) => { - element = element.trim().toLowerCase(); - return element.startsWith("username="); - }); - - // prevents driver from hanging on stdin waiting for a password to be entered at the cli - if (!has_username) { - let has_guest = mount_flags.some((element) => { - element = element.trim().toLowerCase(); - return element === "guest"; - }); - - if (!has_guest) { - mount_flags.push("guest"); - } - } - break; - case "iscsi": - let portals = []; - if (volume_context.portal) { - portals.push(volume_context.portal.trim()); - } - - if (volume_context.portals) { - volume_context.portals.split(",").forEach((portal) => { - portals.push(portal.trim()); - }); - } - - // ensure full portal value - portals = portals.map((value) => { - if (!value.includes(":")) { - value += ":3260"; - } - - return value.trim(); - }); - - // ensure unique entries only - portals = [...new Set(portals)]; - - // stores actual device paths after iscsi login - let iscsiDevices = []; - - // stores configuration of targets/iqn/luns to connect to - let iscsiConnections = []; - for (let portal of portals) { - iscsiConnections.push({ - portal, - iqn: volume_context.iqn, - lun: volume_context.lun, - }); - } - - /** - * TODO: allow sending in iscsiConnection in a raw/manual format - * TODO: allow option to determine if send_targets should be invoked - * TODO: allow option to control whether nodedb entry should be created by driver - * TODO: allow option to control whether nodedb entry should be deleted by driver - */ - - for (let iscsiConnection of iscsiConnections) { - // 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.session.scan": "manual", - }; - const nodeDBKeyPrefix = "node-db."; - for (const key in normalizedSecrets) { - if (key.startsWith(nodeDBKeyPrefix)) { - nodeDB[key.substr(nodeDBKeyPrefix.length)] = - normalizedSecrets[key]; - } - } - - // create 'DB' entry - await iscsi.iscsiadm.createNodeDBEntry( - iscsiConnection.iqn, - iscsiConnection.portal, - nodeDB - ); - // login - await iscsi.iscsiadm.login( - iscsiConnection.iqn, - iscsiConnection.portal - ); - - // get associated session - let session = await iscsi.iscsiadm.getSession( - iscsiConnection.iqn, - iscsiConnection.portal - ); - - // rescan in scenarios when login previously occurred but volumes never appeared - await iscsi.iscsiadm.rescanSession(session); - - // find device name - device = `/dev/disk/by-path/ip-${iscsiConnection.portal}-iscsi-${iscsiConnection.iqn}-lun-${iscsiConnection.lun}`; - let deviceByPath = device; - - // can take some time for device to show up, loop for some period - result = await filesystem.pathExists(device); - let timer_start = Math.round(new Date().getTime() / 1000); - let timer_max = 30; - let deviceCreated = result; - while (!result) { - await sleep(2000); - result = await filesystem.pathExists(device); - - if (result) { - deviceCreated = true; - break; - } - - let current_time = Math.round(new Date().getTime() / 1000); - if (!result && current_time - timer_start > timer_max) { - driver.ctx.logger.warn( - `hit timeout waiting for device node to appear: ${device}` - ); - break; - } - } - - if (deviceCreated) { - device = await filesystem.realpath(device); - iscsiDevices.push(device); - - driver.ctx.logger.info( - `successfully logged into portal ${iscsiConnection.portal} and created device ${deviceByPath} with realpath ${device}` - ); - } - } - - // let things settle - // this will help in dm scenarios - await sleep(2000); - - // filter duplicates - iscsiDevices = iscsiDevices.filter((value, index, self) => { - return self.indexOf(value) === index; - }); - - // only throw an error if we were not able to attach to *any* devices - if (iscsiDevices.length < 1) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unable to attach any iscsi devices` - ); - } - - if (iscsiDevices.length != iscsiConnections.length) { - driver.ctx.logger.warn( - `failed to attach all iscsi devices/targets/portals` - ); - - // TODO: allow a parameter to control this behavior in some form - if (false) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unable to attach all iscsi devices` - ); - } - } - - // compare all device-mapper slaves with the newly created devices - // if any of the new devices are device-mapper slaves treat this as a - // multipath scenario - let allDeviceMapperSlaves = - await filesystem.getAllDeviceMapperSlaveDevices(); - let commonDevices = allDeviceMapperSlaves.filter((value) => - iscsiDevices.includes(value) - ); - - const useMultipath = - iscsiConnections.length > 1 || commonDevices.length > 0; - - // discover multipath device to use - if (useMultipath) { - device = await filesystem.getDeviceMapperDeviceFromSlaves( - iscsiDevices, - false - ); - - if (!device) { - throw new GrpcError( - grpc.status.UNKNOWN, - `failed to discover multipath device` - ); - } - } - break; - case "hostpath": - result = await mount.pathIsMounted(staging_target_path); - // if not mounted, mount + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + // csi spec stipulates that staging_target_path is a directory even for block mounts + result = await filesystem.pathExists(staging_target_path); if (!result) { - await mount.bindMount(volume_context.path, staging_target_path); - return {}; - } else { - return {}; + await filesystem.mkdir(staging_target_path, ["-p", "-m", "0750"]); } - break; - case "oneclient": - let oneclient = driver.getDefaultOneClientInstance(); - device = "oneclient"; - result = await mount.deviceIsMountedAtPath(device, staging_target_path); - if (result) { - return {}; - } + // get the `device` set + switch (node_attach_driver) { + case "nfs": + case "lustre": + device = `${volume_context.server}:${volume_context.share}`; + break; + case "smb": + device = `//${volume_context.server}/${volume_context.share}`; - if (volume_context.space_names) { - volume_context.space_names.split(",").forEach((space) => { - mount_flags.push("--space", space); - }); - } + // if not present add guest + let has_username = mount_flags.some((element) => { + element = element.trim().toLowerCase(); + return element.startsWith("username="); + }); - if (volume_context.space_ids) { - volume_context.space_ids.split(",").forEach((space) => { - mount_flags.push("--space-id", space); - }); - } - - if (normalizedSecrets.token) { - mount_flags.push("-t", normalizedSecrets.token); - } else { - if (volume_context.token) { - mount_flags.push("-t", volume_context.token); - } - } - - result = await oneclient.mount( - staging_target_path, - ["-H", volume_context.server].concat(mount_flags) - ); - - if (result) { - return {}; - } - - throw new GrpcError( - grpc.status.UNKNOWN, - `failed to mount oneclient: ${volume_context.server}` - ); - - break; - case "zfs-local": - // TODO: make this a geneic zb instance (to ensure works with node-manual driver) - const zb = driver.getDefaultZetabyteInstance(); - result = await zb.zfs.get(`${volume_context.zfs_asset_name}`, [ - "type", - "mountpoint", - ]); - result = result[`${volume_context.zfs_asset_name}`]; - switch (result.type.value) { - case "filesystem": - if (result.mountpoint.value != "legacy") { - // zfs set mountpoint=legacy - // zfs inherit mountpoint - await zb.zfs.set(`${volume_context.zfs_asset_name}`, { - mountpoint: "legacy", + // prevents driver from hanging on stdin waiting for a password to be entered at the cli + if (!has_username) { + let has_guest = mount_flags.some((element) => { + element = element.trim().toLowerCase(); + return element === "guest"; }); - } - device = `${volume_context.zfs_asset_name}`; - if (!fs_type) { - fs_type = "zfs"; + + if (!has_guest) { + mount_flags.push("guest"); + } } break; - case "volume": - device = `/dev/zvol/${volume_context.zfs_asset_name}`; + case "iscsi": + let portals = []; + if (volume_context.portal) { + portals.push(volume_context.portal.trim()); + } + + if (volume_context.portals) { + volume_context.portals.split(",").forEach((portal) => { + portals.push(portal.trim()); + }); + } + + // ensure full portal value + portals = portals.map((value) => { + if (!value.includes(":")) { + value += ":3260"; + } + + return value.trim(); + }); + + // ensure unique entries only + portals = [...new Set(portals)]; + + // stores actual device paths after iscsi login + let iscsiDevices = []; + + // stores configuration of targets/iqn/luns to connect to + let iscsiConnections = []; + for (let portal of portals) { + iscsiConnections.push({ + portal, + iqn: volume_context.iqn, + lun: volume_context.lun, + }); + } + + /** + * TODO: allow sending in iscsiConnection in a raw/manual format + * TODO: allow option to determine if send_targets should be invoked + * TODO: allow option to control whether nodedb entry should be created by driver + * TODO: allow option to control whether nodedb entry should be deleted by driver + */ + + for (let iscsiConnection of iscsiConnections) { + // 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.session.scan": "manual", + }; + const nodeDBKeyPrefix = "node-db."; + for (const key in normalizedSecrets) { + if (key.startsWith(nodeDBKeyPrefix)) { + nodeDB[key.substr(nodeDBKeyPrefix.length)] = + normalizedSecrets[key]; + } + } + + // create 'DB' entry + await iscsi.iscsiadm.createNodeDBEntry( + iscsiConnection.iqn, + iscsiConnection.portal, + nodeDB + ); + // login + await iscsi.iscsiadm.login( + iscsiConnection.iqn, + iscsiConnection.portal + ); + + // get associated session + let session = await iscsi.iscsiadm.getSession( + iscsiConnection.iqn, + iscsiConnection.portal + ); + + // rescan in scenarios when login previously occurred but volumes never appeared + await iscsi.iscsiadm.rescanSession(session); + + // find device name + device = `/dev/disk/by-path/ip-${iscsiConnection.portal}-iscsi-${iscsiConnection.iqn}-lun-${iscsiConnection.lun}`; + let deviceByPath = device; + + // can take some time for device to show up, loop for some period + result = await filesystem.pathExists(device); + let timer_start = Math.round(new Date().getTime() / 1000); + let timer_max = 30; + let deviceCreated = result; + while (!result) { + await GeneralUtils.sleep(2000); + result = await filesystem.pathExists(device); + + if (result) { + deviceCreated = true; + break; + } + + let current_time = Math.round(new Date().getTime() / 1000); + if (!result && current_time - timer_start > timer_max) { + driver.ctx.logger.warn( + `hit timeout waiting for device node to appear: ${device}` + ); + break; + } + } + + if (deviceCreated) { + device = await filesystem.realpath(device); + iscsiDevices.push(device); + + driver.ctx.logger.info( + `successfully logged into portal ${iscsiConnection.portal} and created device ${deviceByPath} with realpath ${device}` + ); + } + } + + // let things settle + // this will help in dm scenarios + await GeneralUtils.sleep(2000); + + // filter duplicates + iscsiDevices = iscsiDevices.filter((value, index, self) => { + return self.indexOf(value) === index; + }); + + // only throw an error if we were not able to attach to *any* devices + if (iscsiDevices.length < 1) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unable to attach any iscsi devices` + ); + } + + if (iscsiDevices.length != iscsiConnections.length) { + driver.ctx.logger.warn( + `failed to attach all iscsi devices/targets/portals` + ); + + // TODO: allow a parameter to control this behavior in some form + if (false) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unable to attach all iscsi devices` + ); + } + } + + // compare all device-mapper slaves with the newly created devices + // if any of the new devices are device-mapper slaves treat this as a + // multipath scenario + let allDeviceMapperSlaves = + await filesystem.getAllDeviceMapperSlaveDevices(); + let commonDevices = allDeviceMapperSlaves.filter((value) => + iscsiDevices.includes(value) + ); + + const useMultipath = + iscsiConnections.length > 1 || commonDevices.length > 0; + + // discover multipath device to use + if (useMultipath) { + device = await filesystem.getDeviceMapperDeviceFromSlaves( + iscsiDevices, + false + ); + + if (!device) { + throw new GrpcError( + grpc.status.UNKNOWN, + `failed to discover multipath device` + ); + } + } + + break; + case "hostpath": + result = await mount.pathIsMounted(staging_target_path); + // if not mounted, mount + if (!result) { + await mount.bindMount(volume_context.path, staging_target_path); + return {}; + } else { + return {}; + } + + break; + case "oneclient": + let oneclient = driver.getDefaultOneClientInstance(); + device = "oneclient"; + result = await mount.deviceIsMountedAtPath( + device, + staging_target_path + ); + if (result) { + return {}; + } + + if (volume_context.space_names) { + volume_context.space_names.split(",").forEach((space) => { + mount_flags.push("--space", space); + }); + } + + if (volume_context.space_ids) { + volume_context.space_ids.split(",").forEach((space) => { + mount_flags.push("--space-id", space); + }); + } + + if (normalizedSecrets.token) { + mount_flags.push("-t", normalizedSecrets.token); + } else { + if (volume_context.token) { + mount_flags.push("-t", volume_context.token); + } + } + + result = await oneclient.mount( + staging_target_path, + ["-H", volume_context.server].concat(mount_flags) + ); + + if (result) { + return {}; + } + + throw new GrpcError( + grpc.status.UNKNOWN, + `failed to mount oneclient: ${volume_context.server}` + ); + + break; + case "zfs-local": + // TODO: make this a geneic zb instance (to ensure works with node-manual driver) + const zb = driver.getDefaultZetabyteInstance(); + result = await zb.zfs.get(`${volume_context.zfs_asset_name}`, [ + "type", + "mountpoint", + ]); + result = result[`${volume_context.zfs_asset_name}`]; + switch (result.type.value) { + case "filesystem": + if (result.mountpoint.value != "legacy") { + // zfs set mountpoint=legacy + // zfs inherit mountpoint + await zb.zfs.set(`${volume_context.zfs_asset_name}`, { + mountpoint: "legacy", + }); + } + device = `${volume_context.zfs_asset_name}`; + if (!fs_type) { + fs_type = "zfs"; + } + break; + case "volume": + device = `/dev/zvol/${volume_context.zfs_asset_name}`; + break; + default: + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown zfs asset type: ${result.type.value}` + ); + } break; default: throw new GrpcError( - grpc.status.UNKNOWN, - `unknown zfs asset type: ${result.type.value}` + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` ); } - break; - default: - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unknown/unsupported node_attach_driver: ${node_attach_driver}` - ); - } - switch (access_type) { - case "mount": - let is_block = false; - switch (node_attach_driver) { - case "iscsi": - is_block = true; - break; - case "zfs-local": - is_block = device.startsWith("/dev/zvol/"); - break; - } - - if (is_block) { - // block specific logic - if (!fs_type) { - fs_type = "ext4"; - } - - if (await filesystem.isBlockDevice(device)) { - // format - result = await filesystem.deviceIsFormatted(device); - if (!result) { - let formatOptions = _.get( - driver.options.node.format, - [fs_type, "customOptions"], - [] - ); - if (!Array.isArray(formatOptions)) { - formatOptions = []; - } - await filesystem.formatDevice(device, fs_type, formatOptions); + // deal with `device` now that we have one + switch (access_type) { + case "mount": + let is_block = false; + switch (node_attach_driver) { + case "iscsi": + is_block = true; + break; + case "zfs-local": + is_block = device.startsWith("/dev/zvol/"); + break; } - let fs_info = await filesystem.getDeviceFilesystemInfo(device); - fs_type = fs_info.type; + // format device + if (is_block) { + // block specific logic + if (!fs_type) { + fs_type = "ext4"; + } - // fsck + if (await filesystem.isBlockDevice(device)) { + // format + result = await filesystem.deviceIsFormatted(device); + if (!result) { + let formatOptions = _.get( + driver.options.node.format, + [fs_type, "customOptions"], + [] + ); + if (!Array.isArray(formatOptions)) { + formatOptions = []; + } + await filesystem.formatDevice(device, fs_type, formatOptions); + } + + let fs_info = await filesystem.getDeviceFilesystemInfo(device); + fs_type = fs_info.type; + + // fsck + result = await mount.deviceIsMountedAtPath( + device, + staging_target_path + ); + if (!result) { + // https://github.com/democratic-csi/democratic-csi/issues/52#issuecomment-768463401 + let checkFilesystem = + driver.options.node.mount.checkFilesystem[fs_type] || {}; + if (checkFilesystem.enabled) { + await filesystem.checkFilesystem( + device, + fs_type, + checkFilesystem.customOptions || [], + checkFilesystem.customFilesystemOptions || [] + ); + } + } + } + } + + // set default fs_type if still unset + if (!fs_type) { + switch (node_attach_driver) { + case "nfs": + fs_type = "nfs"; + break; + case "lustre": + fs_type = "lustre"; + break; + case "smb": + fs_type = "cifs"; + break; + case "iscsi": + fs_type = "ext4"; + break; + default: + break; + } + } + + // mount `device` result = await mount.deviceIsMountedAtPath( device, staging_target_path ); if (!result) { - // https://github.com/democratic-csi/democratic-csi/issues/52#issuecomment-768463401 - let checkFilesystem = - driver.options.node.mount.checkFilesystem[fs_type] || {}; - if (checkFilesystem.enabled) { - await filesystem.checkFilesystem( - device, - fs_type, - checkFilesystem.customOptions || [], - checkFilesystem.customFilesystemOptions || [] - ); + await mount.mount( + device, + staging_target_path, + ["-t", fs_type].concat(["-o", mount_flags.join(",")]) + ); + } + + // expand fs if necessary + if (await filesystem.isBlockDevice(device)) { + // go ahead and expand fs (this covers cloned setups where expand is not explicitly invoked) + switch (fs_type) { + case "ext4": + case "ext3": + case "ext4dev": + //await filesystem.checkFilesystem(device, fs_info.type); + try { + await filesystem.expandFilesystem(device, fs_type); + } catch (err) { + // mount is clean and rw, but it will not expand until clean umount has been done + // failed to execute filesystem command: resize2fs /dev/sda, response: {"code":1,"stdout":"Couldn't find valid filesystem superblock.\n","stderr":"resize2fs 1.44.5 (15-Dec-2018)\nresize2fs: Superblock checksum does not match superblock while trying to open /dev/sda\n"} + // /dev/sda on /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-4a80757e-5e87-475d-826f-44fcc4719348/globalmount type ext4 (rw,relatime,stripe=256) + if ( + err.code == 1 && + err.stdout.includes("find valid filesystem superblock") && + err.stderr.includes("checksum does not match superblock") + ) { + driver.ctx.logger.warn( + `successful mount, unsuccessful fs resize: attempting abnormal umount/mount/resize2fs to clear things up ${staging_target_path} (${device})` + ); + + // try an unmount/mount/fsck cycle again just to clean things up + await mount.umount(staging_target_path, []); + await mount.mount( + device, + staging_target_path, + ["-t", fs_type].concat(["-o", mount_flags.join(",")]) + ); + await filesystem.expandFilesystem(device, fs_type); + } else { + throw err; + } + } + break; + case "btrfs": + case "xfs": + //await filesystem.checkFilesystem(device, fs_info.type); + await filesystem.expandFilesystem( + staging_target_path, + fs_type + ); + break; + default: + // unsupported filesystem + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `unsupported/unknown filesystem ${fs_type}` + ); } } - } - } - result = await mount.deviceIsMountedAtPath(device, staging_target_path); - if (!result) { - if (!fs_type) { - switch (node_attach_driver) { - case "nfs": - fs_type = "nfs"; - break; - case "lustre": - fs_type = "lustre"; - break; - case "smb": - fs_type = "cifs"; - break; - case "iscsi": - fs_type = "ext4"; - break; - default: - break; + break; + case "block": + //result = await mount.deviceIsMountedAtPath(device, block_path); + result = await mount.deviceIsMountedAtPath("dev", block_path); + if (!result) { + result = await filesystem.pathExists(staging_target_path); + if (!result) { + await filesystem.mkdir(staging_target_path, [ + "-p", + "-m", + "0750", + ]); + } + + result = await filesystem.pathExists(block_path); + if (!result) { + await filesystem.touch(block_path); + } + + await mount.bindMount(device, block_path, [ + "-o", + bind_mount_flags.join(","), + ]); } - } - await mount.mount( - device, - staging_target_path, - ["-t", fs_type].concat(["-o", mount_flags.join(",")]) + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported access_type: ${access_type}` + ); + } + break; + case NODE_OS_DRIVER_CSI_PROXY: + // sanity check node_attach_driver + if (!["smb", "iscsi"].includes(node_attach_driver)) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `csi-proxy does not work with node_attach_driver: ${node_attach_driver}` ); } - if (await filesystem.isBlockDevice(device)) { - // go ahead and expand fs (this covers cloned setups where expand is not explicitly invoked) - switch (fs_type) { - case "ext4": - case "ext3": - case "ext4dev": - //await filesystem.checkFilesystem(device, fs_info.type); - try { - await filesystem.expandFilesystem(device, fs_type); - } catch (err) { - // mount is clean and rw, but it will not expand until clean umount has been done - // failed to execute filesystem command: resize2fs /dev/sda, response: {"code":1,"stdout":"Couldn't find valid filesystem superblock.\n","stderr":"resize2fs 1.44.5 (15-Dec-2018)\nresize2fs: Superblock checksum does not match superblock while trying to open /dev/sda\n"} - // /dev/sda on /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-4a80757e-5e87-475d-826f-44fcc4719348/globalmount type ext4 (rw,relatime,stripe=256) - if ( - err.code == 1 && - err.stdout.includes("find valid filesystem superblock") && - err.stderr.includes("checksum does not match superblock") - ) { - driver.ctx.logger.warn( - `successful mount, unsuccessful fs resize: attempting abnormal umount/mount/resize2fs to clear things up ${staging_target_path} (${device})` - ); + // sanity check fs_type + if (fs_type && !["ntfs", "cifs"].includes(fs_type)) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `csi-proxy does not work with fs_type: ${fs_type}` + ); + } - // try an unmount/mount/fsck cycle again just to clean things up - await mount.umount(staging_target_path, []); - await mount.mount( - device, - staging_target_path, - ["-t", fs_type].concat(["-o", mount_flags.join(",")]) - ); - await filesystem.expandFilesystem(device, fs_type); - } else { - throw err; + // load up the client instance + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + + switch (node_attach_driver) { + case "smb": + /** + * smb mount creates a link at this location and if the dir already exists + * it explodes + * + * if path exists but is NOT symlink delete it + */ + result = await csiProxyClient.FilesystemPathExists( + staging_target_path + ); + if (result) { + result = await csiProxyClient.FilesystemIsSymlink( + staging_target_path + ); + if (!result) { + await csiProxyClient.executeRPC("filesystem", "Rmdir", { + path: staging_target_path, + }); + } + } + + device = `//${volume_context.server}/${volume_context.share}`; + const username = driver.getMountFlagValue(mount_flags, "username"); + const password = driver.getMountFlagValue(mount_flags, "password"); + + if (!username || !password) { + throw new Error("username and password required"); + } + + try { + await csiProxyClient.executeRPC("smb", "NewSmbGlobalMapping", { + // convert path separator for windows style path + remote_path: + filesystem.covertUnixSeparatorToWindowsSeparator(device), + local_path: staging_target_path, + username: `${volume_context.server}\\${username}`, + password, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("ResourceExists")) { + throw e; + } else { + // path should be a symlink if already present + result = await csiProxyClient.executeRPC( + "filesystem", + "IsSymlink", + { path: staging_target_path } + ); + if (!_.get(result, "is_symlink", false)) { + throw e; } } - break; - case "btrfs": - case "xfs": - //await filesystem.checkFilesystem(device, fs_info.type); - await filesystem.expandFilesystem(staging_target_path, fs_type); - break; - default: - // unsupported filesystem - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `unsupported/unknown filesystem ${fs_type}` - ); - } + } + + break; + case "iscsi": + switch (access_type) { + case "mount": + let portals = []; + if (volume_context.portal) { + portals.push(volume_context.portal.trim()); + } + + if (volume_context.portals) { + volume_context.portals.split(",").forEach((portal) => { + portals.push(portal.trim()); + }); + } + + // ensure full portal value + portals = portals.map((value) => { + if (!value.includes(":")) { + value += ":3260"; + } + + return value.trim(); + }); + + // ensure unique entries only + portals = [...new Set(portals)]; + + // stores actual device paths after iscsi login + let iscsiDevices = []; + + // stores configuration of targets/iqn/luns to connect to + let iscsiConnections = []; + for (let portal of portals) { + iscsiConnections.push({ + portal, + iqn: volume_context.iqn, + lun: volume_context.lun, + }); + } + + // no multipath support yet + // https://github.com/kubernetes-csi/csi-proxy/pull/99 + for (let iscsiConnection of iscsiConnections) { + // add target portal + let parts = iscsiConnection.portal.split(":"); + let target_address = parts[0]; + let target_port = parts[1] || "3260"; + let target_portal = { + target_address, + target_port, + }; + // this is idempotent + await csiProxyClient.executeRPC("iscsi", "AddTargetPortal", { + target_portal, + }); + + // login + try { + let auth_type = "NONE"; + let chap_username = ""; + let chap_secret = ""; + if ( + normalizedSecrets[ + "node-db.node.session.auth.authmethod" + ] == "CHAP" + ) { + // set auth_type + if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets[ + "node-db.node.session.auth.password" + ] && + normalizedSecrets[ + "node-db.node.session.auth.username_in" + ] && + normalizedSecrets[ + "node-db.node.session.auth.password_in" + ] + ) { + auth_type = "MUTUAL_CHAP"; + } else if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets["node-db.node.session.auth.password"] + ) { + auth_type = "ONE_WAY_CHAP"; + } + + // set credentials + if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets["node-db.node.session.auth.password"] + ) { + chap_username = + normalizedSecrets[ + "node-db.node.session.auth.username" + ]; + + chap_secret = + normalizedSecrets[ + "node-db.node.session.auth.password" + ]; + } + } + await csiProxyClient.executeRPC("iscsi", "ConnectTarget", { + target_portal, + iqn: iscsiConnection.iqn, + /** + * NONE + * ONE_WAY_CHAP + * MUTUAL_CHAP + */ + auth_type, + chap_username, + chap_secret, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if ( + !details.includes( + "The target has already been logged in via an iSCSI session" + ) + ) { + throw e; + } + } + + // discover? + //await csiProxyClient.executeRPC("iscsi", "DiscoverTargetPortal", { + // target_portal, + //}); + + // rescan + await csiProxyClient.executeRPC("disk", "Rescan"); + + // get device + result = await csiProxyClient.executeRPC( + "iscsi", + "GetTargetDisks", + { + target_portal, + iqn: iscsiConnection.iqn, + } + ); + + // TODO: this is a gross assumption since we currently only allow 1 lun per target + // iterate this response and find disk + //result = await csiProxyClient.executeRPC("disk", "ListDiskLocations"); + let diskIds = _.get(result, "diskIDs", []); + if (diskIds.length != 1) { + throw new Error( + `${diskIds.length} disks on the target, no way to know which is the relevant disk` + ); + } + let disk_number = diskIds[0]; + + result = await csiProxyClient.executeRPC( + "volume", + "ListVolumesOnDisk", + { disk_number } + ); + + let node_volume_id; + node_volume_id = + await csiProxyClient.getVolumeIdFromDiskNumber(disk_number); + + if (!node_volume_id) { + // this is technically idempotent call so should not hurt anything if already initialized + await csiProxyClient.executeRPC("disk", "PartitionDisk", { + disk_number, + }); + node_volume_id = + await csiProxyClient.getVolumeIdFromDiskNumber( + disk_number + ); + } + + if (!node_volume_id) { + throw new Error( + "failed to create/discover volume for disk" + ); + } + result = await csiProxyClient.executeRPC( + "volume", + "IsVolumeFormatted", + { volume_id: node_volume_id } + ); + + // format device + if (!result.formatted) { + await csiProxyClient.executeRPC("volume", "FormatVolume", { + volume_id: node_volume_id, + }); + } + + // ensure staging path present + result = await csiProxyClient.FilesystemPathExists( + staging_target_path + ); + if (!result) { + await csiProxyClient.executeRPC("filesystem", "Mkdir", { + path: staging_target_path, + }); + } + + // mount up! + try { + result = await csiProxyClient.executeRPC( + "volume", + "MountVolume", + { + volume_id: node_volume_id, + target_path: staging_target_path, + } + ); + } catch (e) { + // assume for now that if something is mounted in the location it the desired volume + let details = _.get(e, "details", ""); + if ( + !details.includes( + "The requested access path is already in use" + ) + ) { + throw e; + } + } + + // let things settle + // this will help in dm scenarios + await GeneralUtils.sleep(2000); + + // windows does not support multipath currently + // break if we make it this far + break; + } + + break; + case "block": + default: + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `access_type ${access_type} unsupported` + ); + } + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + break; } - break; - case "block": - //result = await mount.deviceIsMountedAtPath(device, block_path); - result = await mount.deviceIsMountedAtPath("dev", block_path); - if (!result) { - result = await filesystem.pathExists(staging_target_path); - if (!result) { - await filesystem.mkdir(staging_target_path, ["-p", "-m", "0750"]); - } - - result = await filesystem.pathExists(block_path); - if (!result) { - await filesystem.touch(block_path); - } - - await mount.bindMount(device, block_path, [ - "-o", - bind_mount_flags.join(","), - ]); - } break; default: throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unknown/unsupported access_type: ${access_type}` + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` ); } @@ -943,189 +1516,320 @@ class CsiBaseDriver { // TODO: use the x-* mount options to detect if we should delete target - try { - result = await mount.pathIsMounted(block_path); - } catch (err) { - /** - * on stalled fs such as nfs, even findmnt will return immediately for the base mount point - * so in the case of timeout here (base mount point and then a file/folder beneath it) we almost certainly are not a block device - * AND the fs is probably stalled - */ - if (err.timeout) { - driver.ctx.logger.warn( - `detected stale mount, attempting to force unmount: ${normalized_staging_path}` - ); - await mount.umount( - normalized_staging_path, - umount_args.concat(umount_force_extra_args) - ); - result = false; // assume we are *NOT* a block device at this point - } else { - throw err; - } - } + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + try { + result = await mount.pathIsMounted(block_path); + } catch (err) { + /** + * on stalled fs such as nfs, even findmnt will return immediately for the base mount point + * so in the case of timeout here (base mount point and then a file/folder beneath it) we almost certainly are not a block device + * AND the fs is probably stalled + */ + if (err.timeout) { + driver.ctx.logger.warn( + `detected stale mount, attempting to force unmount: ${normalized_staging_path}` + ); + await mount.umount( + normalized_staging_path, + umount_args.concat(umount_force_extra_args) + ); + result = false; // assume we are *NOT* a block device at this point + } else { + throw err; + } + } - if (result) { - is_block = true; - access_type = "block"; - block_device_info = await filesystem.getBlockDevice(block_path); - normalized_staging_path = block_path; - } else { - result = await mount.pathIsMounted(staging_target_path); - if (result) { - let device = await mount.getMountPointDevice(staging_target_path); - result = await filesystem.isBlockDevice(device); if (result) { is_block = true; - block_device_info = await filesystem.getBlockDevice(device); - } - } - } - - result = await mount.pathIsMounted(normalized_staging_path); - if (result) { - try { - result = await mount.umount(normalized_staging_path, umount_args); - } catch (err) { - if (err.timeout) { - driver.ctx.logger.warn( - `hit timeout waiting to unmount path: ${normalized_staging_path}` - ); - result = await mount.getMountDetails(normalized_staging_path); - switch (result.fstype) { - case "nfs": - case "nfs4": - driver.ctx.logger.warn( - `detected stale nfs filesystem, attempting to force unmount: ${normalized_staging_path}` - ); - result = await mount.umount( - normalized_staging_path, - umount_args.concat(umount_force_extra_args) - ); - break; - default: - throw err; - break; - } + access_type = "block"; + block_device_info = await filesystem.getBlockDevice(block_path); + normalized_staging_path = block_path; } else { - throw err; - } - } - } - - if (is_block) { - let realBlockDeviceInfos = []; - // detect if is a multipath device - is_device_mapper = await filesystem.isDeviceMapperDevice( - block_device_info.path - ); - - if (is_device_mapper) { - let realBlockDevices = await filesystem.getDeviceMapperDeviceSlaves( - block_device_info.path - ); - for (const realBlockDevice of realBlockDevices) { - realBlockDeviceInfos.push( - await filesystem.getBlockDevice(realBlockDevice) - ); - } - } else { - realBlockDeviceInfos = [block_device_info]; - } - - // TODO: this could be made async to detach all simultaneously - for (const block_device_info_i of realBlockDeviceInfos) { - if (block_device_info_i.tran == "iscsi") { - // figure out which iscsi session this belongs to and logout - // scan /dev/disk/by-path/ip-*? - // device = `/dev/disk/by-path/ip-${volume_context.portal}-iscsi-${volume_context.iqn}-lun-${volume_context.lun}`; - // parse output from `iscsiadm -m session -P 3` - let sessions = await iscsi.iscsiadm.getSessionsDetails(); - for (let i = 0; i < sessions.length; i++) { - let session = sessions[i]; - let is_attached_to_session = false; - - if ( - session.attached_scsi_devices && - session.attached_scsi_devices.host && - session.attached_scsi_devices.host.devices - ) { - is_attached_to_session = - session.attached_scsi_devices.host.devices.some((device) => { - if (device.attached_scsi_disk == block_device_info_i.name) { - return true; - } - return false; - }); + result = await mount.pathIsMounted(staging_target_path); + if (result) { + let device = await mount.getMountPointDevice(staging_target_path); + result = await filesystem.isBlockDevice(device); + if (result) { + is_block = true; + block_device_info = await filesystem.getBlockDevice(device); } + } + } - if (is_attached_to_session) { - let timer_start; - let timer_max; - - timer_start = Math.round(new Date().getTime() / 1000); - timer_max = 30; - let loggedOut = false; - while (!loggedOut) { - try { - await iscsi.iscsiadm.logout(session.target, [ - session.persistent_portal, - ]); - loggedOut = true; - } catch (err) { - await sleep(2000); - let current_time = Math.round(new Date().getTime() / 1000); - if (current_time - timer_start > timer_max) { - // not throwing error for now as future invocations would not enter code path anyhow - loggedOut = true; - //throw new GrpcError( - // grpc.status.UNKNOWN, - // `hit timeout trying to logout of iscsi target: ${session.persistent_portal}` - //); - } - } - } - - timer_start = Math.round(new Date().getTime() / 1000); - timer_max = 30; - let deletedEntry = false; - while (!deletedEntry) { - try { - await iscsi.iscsiadm.deleteNodeDBEntry( - session.target, - session.persistent_portal + result = await mount.pathIsMounted(normalized_staging_path); + if (result) { + try { + result = await mount.umount(normalized_staging_path, umount_args); + } catch (err) { + if (err.timeout) { + driver.ctx.logger.warn( + `hit timeout waiting to unmount path: ${normalized_staging_path}` + ); + result = await mount.getMountDetails(normalized_staging_path); + switch (result.fstype) { + case "nfs": + case "nfs4": + driver.ctx.logger.warn( + `detected stale nfs filesystem, attempting to force unmount: ${normalized_staging_path}` ); - deletedEntry = true; - } catch (err) { - await sleep(2000); - let current_time = Math.round(new Date().getTime() / 1000); - if (current_time - timer_start > timer_max) { - // not throwing error for now as future invocations would not enter code path anyhow - deletedEntry = true; - //throw new GrpcError( - // grpc.status.UNKNOWN, - // `hit timeout trying to delete iscsi node DB entry: ${session.target}, ${session.persistent_portal}` - //); + result = await mount.umount( + normalized_staging_path, + umount_args.concat(umount_force_extra_args) + ); + break; + default: + throw err; + } + } else { + throw err; + } + } + } + + if (is_block) { + let realBlockDeviceInfos = []; + // detect if is a multipath device + is_device_mapper = await filesystem.isDeviceMapperDevice( + block_device_info.path + ); + + if (is_device_mapper) { + let realBlockDevices = await filesystem.getDeviceMapperDeviceSlaves( + block_device_info.path + ); + for (const realBlockDevice of realBlockDevices) { + realBlockDeviceInfos.push( + await filesystem.getBlockDevice(realBlockDevice) + ); + } + } else { + realBlockDeviceInfos = [block_device_info]; + } + + // TODO: this could be made async to detach all simultaneously + for (const block_device_info_i of realBlockDeviceInfos) { + if (block_device_info_i.tran == "iscsi") { + // figure out which iscsi session this belongs to and logout + // scan /dev/disk/by-path/ip-*? + // device = `/dev/disk/by-path/ip-${volume_context.portal}-iscsi-${volume_context.iqn}-lun-${volume_context.lun}`; + // parse output from `iscsiadm -m session -P 3` + let sessions = await iscsi.iscsiadm.getSessionsDetails(); + for (let i = 0; i < sessions.length; i++) { + let session = sessions[i]; + let is_attached_to_session = false; + + if ( + session.attached_scsi_devices && + session.attached_scsi_devices.host && + session.attached_scsi_devices.host.devices + ) { + is_attached_to_session = + session.attached_scsi_devices.host.devices.some( + (device) => { + if ( + device.attached_scsi_disk == block_device_info_i.name + ) { + return true; + } + return false; + } + ); + } + + if (is_attached_to_session) { + let timer_start; + let timer_max; + + timer_start = Math.round(new Date().getTime() / 1000); + timer_max = 30; + let loggedOut = false; + while (!loggedOut) { + try { + await iscsi.iscsiadm.logout(session.target, [ + session.persistent_portal, + ]); + loggedOut = true; + } catch (err) { + await GeneralUtils.sleep(2000); + let current_time = Math.round( + new Date().getTime() / 1000 + ); + if (current_time - timer_start > timer_max) { + // not throwing error for now as future invocations would not enter code path anyhow + loggedOut = true; + //throw new GrpcError( + // grpc.status.UNKNOWN, + // `hit timeout trying to logout of iscsi target: ${session.persistent_portal}` + //); + } + } + } + + timer_start = Math.round(new Date().getTime() / 1000); + timer_max = 30; + let deletedEntry = false; + while (!deletedEntry) { + try { + await iscsi.iscsiadm.deleteNodeDBEntry( + session.target, + session.persistent_portal + ); + deletedEntry = true; + } catch (err) { + await GeneralUtils.sleep(2000); + let current_time = Math.round( + new Date().getTime() / 1000 + ); + if (current_time - timer_start > timer_max) { + // not throwing error for now as future invocations would not enter code path anyhow + deletedEntry = true; + //throw new GrpcError( + // grpc.status.UNKNOWN, + // `hit timeout trying to delete iscsi node DB entry: ${session.target}, ${session.persistent_portal}` + //); + } + } } } } } } } - } - } - if (access_type == "block") { - // remove touched file - result = await filesystem.pathExists(block_path); - if (result) { - result = await filesystem.rm(block_path); - } - } + if (access_type == "block") { + // remove touched file + result = await filesystem.pathExists(block_path); + if (result) { + result = await filesystem.rm(block_path); + } + } - result = await filesystem.pathExists(staging_target_path); - if (result) { - result = await filesystem.rmdir(staging_target_path); + result = await filesystem.pathExists(staging_target_path); + if (result) { + result = await filesystem.rmdir(staging_target_path); + } + break; + case NODE_OS_DRIVER_CSI_PROXY: + // load up the client instance + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + // for testing purposes + const volume_context = await driver.getDerivedVolumeContext(call); + if (!volume_context) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `unable to retrieve volume_context for volume: ${volume_id}` + ); + } + + const node_attach_driver = volume_context.node_attach_driver; + + async function removePath(p) { + // remove staging path + try { + await csiProxyClient.executeRPC("filesystem", "Rmdir", { + path: p, + // remove all contents under the directory + //force: false, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if ( + !details.includes("The system cannot find the file specified") + ) { + throw e; + } + } + } + + switch (node_attach_driver) { + case "smb": + try { + await csiProxyClient.executeRPC("smb", "RemoveSmbGlobalMapping", { + remote_path: `\\\\${volume_context.server}\\${volume_context.share}`, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("No MSFT_SmbGlobalMapping objects found")) { + throw e; + } + } + + break; + case "iscsi": + let target_portal = { + target_address: volume_context.portal.split(":")[0], + target_port: volume_context.portal.split(":")[1] || 3260, + }; + + let iqn = volume_context.iqn; + let node_volume_id; + + // ok to be null/undefined + node_volume_id = await csiProxyClient.getVolumeIdFromIscsiTarget( + target_portal, + iqn + ); + + if (node_volume_id) { + // write volume cache + await csiProxyClient.executeRPC("volume", "WriteVolumeCache", { + volume_id: node_volume_id, + }); + + // umount first + try { + await csiProxyClient.executeRPC("volume", "UnmountVolume", { + volume_id: node_volume_id, + target_path: staging_target_path, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("The access path is not valid")) { + throw e; + } + } + } + + try { + await csiProxyClient.executeRPC("iscsi", "DisconnectTarget", { + target_portal, + iqn, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("ObjectNotFound")) { + throw e; + } + } + + try { + await csiProxyClient.executeRPC("iscsi", "RemoveTargetPortal", { + target_portal, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("ObjectNotFound")) { + throw e; + } + } + + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + + // remove staging path + await removePath(normalized_staging_path); + break; + default: + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` + ); } return {}; @@ -1181,109 +1885,195 @@ class CsiBaseDriver { if (readonly) bind_mount_flags.push("ro"); // , "x-democratic-csi.ro" - switch (node_attach_driver) { - case "nfs": - case "smb": - case "lustre": - case "oneclient": - case "hostpath": - case "iscsi": - case "zfs-local": - // ensure appropriate directories/files - switch (access_type) { - case "mount": - // ensure directory exists - result = await filesystem.pathExists(target_path); - if (!result) { - await filesystem.mkdir(target_path, ["-p", "-m", "0750"]); + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + switch (node_attach_driver) { + case "nfs": + case "smb": + case "lustre": + case "oneclient": + case "hostpath": + case "iscsi": + case "zfs-local": + // ensure appropriate directories/files + switch (access_type) { + case "mount": + // ensure directory exists + result = await filesystem.pathExists(target_path); + if (!result) { + await filesystem.mkdir(target_path, ["-p", "-m", "0750"]); + } + + break; + case "block": + // ensure target_path directory exists as target path should be a file + let target_dir = await filesystem.dirname(target_path); + result = await filesystem.pathExists(target_dir); + if (!result) { + await filesystem.mkdir(target_dir, ["-p", "-m", "0750"]); + } + + // ensure target file exists + result = await filesystem.pathExists(target_path); + if (!result) { + await filesystem.touch(target_path); + } + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unsupported/unknown access_type ${access_type}` + ); } - break; - case "block": - // ensure target_path directory exists as target path should be a file - let target_dir = await filesystem.dirname(target_path); - result = await filesystem.pathExists(target_dir); - if (!result) { - await filesystem.mkdir(target_dir, ["-p", "-m", "0750"]); + // ensure bind mount + if (staging_target_path) { + let normalized_staging_device; + let normalized_staging_path; + + if (access_type == "block") { + normalized_staging_path = staging_target_path + "/block_device"; + } else { + normalized_staging_path = staging_target_path; + } + + // sanity check to ensure the staged path is actually mounted + result = await mount.pathIsMounted(normalized_staging_path); + if (!result) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `staging path is not mounted: ${normalized_staging_path}` + ); + } + + result = await mount.pathIsMounted(target_path); + // if not mounted, mount + if (!result) { + await mount.bindMount(normalized_staging_path, target_path, [ + "-o", + bind_mount_flags.join(","), + ]); + } else { + // if is mounted, ensure proper source + if (access_type == "block") { + normalized_staging_device = "dev"; // special syntax for single file bind mounts + } else { + normalized_staging_device = await mount.getMountPointDevice( + staging_target_path + ); + } + result = await mount.deviceIsMountedAtPath( + normalized_staging_device, + target_path + ); + if (!result) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `it appears something else is already mounted at ${target_path}` + ); + } + } + + return {}; } - // ensure target file exists - result = await filesystem.pathExists(target_path); - if (!result) { - await filesystem.touch(target_path); - } - break; + // unsupported filesystem + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `only staged configurations are valid` + ); default: throw new GrpcError( grpc.status.INVALID_ARGUMENT, - `unsupported/unknown access_type ${access_type}` + `unknown/unsupported node_attach_driver: ${node_attach_driver}` ); } + break; + case NODE_OS_DRIVER_CSI_PROXY: + switch (node_attach_driver) { + //case "nfs": + case "smb": + //case "lustre": + //case "oneclient": + //case "hostpath": + case "iscsi": + //case "zfs-local": + // ensure appropriate directories/files + switch (access_type) { + case "mount": + break; + case "block": + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unsupported/unknown access_type ${access_type}` + ); + } - // ensure bind mount - if (staging_target_path) { - let normalized_staging_device; - let normalized_staging_path; + // ensure bind mount + if (staging_target_path) { + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); - if (access_type == "block") { - normalized_staging_path = staging_target_path + "/block_device"; - } else { - normalized_staging_path = staging_target_path; - } + let normalized_staging_path; - // sanity check to ensure the staged path is actually mounted - result = await mount.pathIsMounted(normalized_staging_path); - if (!result) { + if (access_type == "block") { + normalized_staging_path = staging_target_path + "/block_device"; + } else { + normalized_staging_path = staging_target_path; + } + + // source path + result = await csiProxyClient.FilesystemPathExists( + normalized_staging_path + ); + if (!result) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `staging path is not mounted: ${normalized_staging_path}` + ); + } + + // target path + result = await csiProxyClient.FilesystemPathExists(target_path); + // already published + if (result) { + result = await csiProxyClient.FilesystemIsSymlink(target_path); + if (!result) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `target path exists but is not a symlink as it should be: ${target_path}` + ); + } + return {}; + } + + // create symlink + await csiProxyClient.executeRPC("filesystem", "CreateSymlink", { + source_path: normalized_staging_path, + target_path, + }); + + return {}; + } + + // unsupported filesystem throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `staging path is not mounted: ${normalized_staging_path}` + `only staged configurations are valid` ); - } - - result = await mount.pathIsMounted(target_path); - // if not mounted, mount - if (!result) { - await mount.bindMount(normalized_staging_path, target_path, [ - "-o", - bind_mount_flags.join(","), - ]); - } else { - // if is mounted, ensure proper source - if (access_type == "block") { - normalized_staging_device = "dev"; // special syntax for single file bind mounts - } else { - normalized_staging_device = await mount.getMountPointDevice( - staging_target_path - ); - } - result = await mount.deviceIsMountedAtPath( - normalized_staging_device, - target_path + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` ); - if (!result) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `it appears something else is already mounted at ${target_path}` - ); - } - } - - return {}; } - - // unsupported filesystem - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `only staged configurations are valid` - ); + break; default: throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unknown/unsupported node_attach_driver: ${node_attach_driver}` + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` ); } - - return {}; } async NodeUnpublishVolume(call) { @@ -1303,64 +2093,97 @@ class CsiBaseDriver { const umount_args = []; const umount_force_extra_args = ["--force", "--lazy"]; - try { - result = await mount.pathIsMounted(target_path); - } catch (err) { - // running findmnt on non-existant paths return immediately - // the only time this should timeout is on a stale fs - // so if timeout is hit we should be near certain it is indeed mounted - if (err.timeout) { - driver.ctx.logger.warn( - `detected stale mount, attempting to force unmount: ${target_path}` - ); - await mount.umount( - target_path, - umount_args.concat(umount_force_extra_args) - ); - result = false; // assume we have fully unmounted - } else { - throw err; - } - } - - if (result) { - try { - result = await mount.umount(target_path, umount_args); - } catch (err) { - if (err.timeout) { - driver.ctx.logger.warn( - `hit timeout waiting to unmount path: ${target_path}` - ); - // bind mounts do show the 'real' fs details - result = await mount.getMountDetails(target_path); - switch (result.fstype) { - case "nfs": - case "nfs4": - driver.ctx.logger.warn( - `detected stale nfs filesystem, attempting to force unmount: ${target_path}` - ); - result = await mount.umount( - target_path, - umount_args.concat(umount_force_extra_args) - ); - break; - default: - throw err; - break; + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + try { + result = await mount.pathIsMounted(target_path); + } catch (err) { + // running findmnt on non-existant paths return immediately + // the only time this should timeout is on a stale fs + // so if timeout is hit we should be near certain it is indeed mounted + if (err.timeout) { + driver.ctx.logger.warn( + `detected stale mount, attempting to force unmount: ${target_path}` + ); + await mount.umount( + target_path, + umount_args.concat(umount_force_extra_args) + ); + result = false; // assume we have fully unmounted + } else { + throw err; } - } else { - throw err; } - } - } - result = await filesystem.pathExists(target_path); - if (result) { - if (fs.lstatSync(target_path).isDirectory()) { - result = await filesystem.rmdir(target_path); - } else { - result = await filesystem.rm([target_path]); - } + if (result) { + try { + result = await mount.umount(target_path, umount_args); + } catch (err) { + if (err.timeout) { + driver.ctx.logger.warn( + `hit timeout waiting to unmount path: ${target_path}` + ); + // bind mounts do show the 'real' fs details + result = await mount.getMountDetails(target_path); + switch (result.fstype) { + case "nfs": + case "nfs4": + driver.ctx.logger.warn( + `detected stale nfs filesystem, attempting to force unmount: ${target_path}` + ); + result = await mount.umount( + target_path, + umount_args.concat(umount_force_extra_args) + ); + break; + default: + throw err; + } + } else { + throw err; + } + } + } + + result = await filesystem.pathExists(target_path); + if (result) { + if (fs.lstatSync(target_path).isDirectory()) { + result = await filesystem.rmdir(target_path); + } else { + result = await filesystem.rm([target_path]); + } + } + + break; + case NODE_OS_DRIVER_CSI_PROXY: + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + + result = await csiProxyClient.FilesystemPathExists(target_path); + if (!result) { + return {}; + } + + result = await csiProxyClient.executeRPC("filesystem", "IsSymlink", { + path: target_path, + }); + + if (!result.is_symlink) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `target path is not a symlink ${target_path}` + ); + } + + await csiProxyClient.executeRPC("filesystem", "Rmdir", { + path: target_path, + }); + + break; + default: + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` + ); } return {}; @@ -1397,60 +2220,124 @@ class CsiBaseDriver { res.volume_condition = { abnormal, message }; } - if ( - (await mount.isBindMountedBlockDevice(volume_path)) || - (await mount.isBindMountedBlockDevice(block_path)) - ) { - device_path = block_path; - access_type = "block"; - } else { - device_path = volume_path; - access_type = "mount"; - } - - switch (access_type) { - case "mount": - if (!(await mount.pathIsMounted(device_path))) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `nothing mounted at path: ${device_path}` - ); + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + if ( + (await mount.isBindMountedBlockDevice(volume_path)) || + (await mount.isBindMountedBlockDevice(block_path)) + ) { + device_path = block_path; + access_type = "block"; + } else { + device_path = volume_path; + access_type = "mount"; + } + + switch (access_type) { + case "mount": + if (!(await mount.pathIsMounted(device_path))) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `nothing mounted at path: ${device_path}` + ); + } + result = await mount.getMountDetails(device_path, [ + "avail", + "size", + "used", + ]); + + res.usage = [ + { + available: result.avail, + total: result.size, + used: result.used, + unit: "BYTES", + }, + ]; + break; + case "block": + if (!(await filesystem.pathExists(device_path))) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `nothing mounted at path: ${device_path}` + ); + } + result = await filesystem.getBlockDevice(device_path); + + res.usage = [ + { + total: result.size, + unit: "BYTES", + }, + ]; + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unsupported/unknown access_type ${access_type}` + ); } - result = await mount.getMountDetails(device_path, [ - "avail", - "size", - "used", - ]); - res.usage = [ - { - available: result.avail, - total: result.size, - used: result.used, - unit: "BYTES", - }, - ]; break; - case "block": - if (!(await filesystem.pathExists(device_path))) { + case NODE_OS_DRIVER_CSI_PROXY: + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + const volume_context = await driver.getDerivedVolumeContext(call); + if (!volume_context) { throw new GrpcError( grpc.status.NOT_FOUND, - `nothing mounted at path: ${device_path}` + `unable to retrieve volume_context for volume: ${volume_id}` ); } - result = await filesystem.getBlockDevice(device_path); - res.usage = [ - { - total: result.size, - unit: "BYTES", - }, - ]; + const node_attach_driver = volume_context.node_attach_driver; + + // ensure path is mounted + result = await csiProxyClient.FilesystemPathExists(volume_path); + if (!result) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `volume_path ${volume_path} is not currently mounted` + ); + } + + switch (node_attach_driver) { + case "smb": + res.usage = [{ total: 0, unit: "BYTES" }]; + break; + case "iscsi": + let node_volume_id = + await csiProxyClient.getVolumeIdFromIscsiTarget( + volume_context.portal, + volume_context.iqn + ); + result = await csiProxyClient.executeRPC( + "volume", + "GetVolumeStats", + { + volume_id: node_volume_id, + } + ); + res.usage = [ + { + available: result.total_bytes - result.used_bytes, + total: result.total_bytes, + used: result.used_bytes, + unit: "BYTES", + }, + ]; + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } break; default: throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `unsupported/unknown access_type ${access_type}` + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` ); } @@ -1477,6 +2364,7 @@ class CsiBaseDriver { let is_formatted; let fs_type; let is_device_mapper = false; + let result; const volume_id = call.request.volume_id; if (!volume_id) { @@ -1490,91 +2378,206 @@ class CsiBaseDriver { const capacity_range = call.request.capacity_range; const volume_capability = call.request.volume_capability; - if ( - (await mount.isBindMountedBlockDevice(volume_path)) || - (await mount.isBindMountedBlockDevice(block_path)) - ) { - access_type = "block"; - device_path = block_path; - } else { - access_type = "mount"; - device_path = volume_path; - } + switch (driver.__getNodeOsDriver()) { + case NODE_OS_DRIVER_POSIX: + if ( + (await mount.isBindMountedBlockDevice(volume_path)) || + (await mount.isBindMountedBlockDevice(block_path)) + ) { + access_type = "block"; + device_path = block_path; + } else { + access_type = "mount"; + device_path = volume_path; + } - try { - device = await mount.getMountPointDevice(device_path); - is_formatted = await filesystem.deviceIsFormatted(device); - is_block = await filesystem.isBlockDevice(device); - } catch (err) { - if (err.code == 1) { - throw new GrpcError( - grpc.status.NOT_FOUND, - `volume_path ${volume_path} is not currently mounted` - ); - } - } - - if (is_block) { - let rescan_devices = []; - // detect if is a multipath device - is_device_mapper = await filesystem.isDeviceMapperDevice(device); - if (is_device_mapper) { - // NOTE: want to make sure we scan the dm device *after* all the underlying slaves - rescan_devices = await filesystem.getDeviceMapperDeviceSlaves(device); - } - - rescan_devices.push(device); - - for (let sdevice of rescan_devices) { - // TODO: technically rescan is only relevant/available for remote drives - // such as iscsi etc, should probably limit this call as appropriate - // for now crudely checking the scenario inside the method itself - await filesystem.rescanDevice(sdevice); - } - - // let things settle - // it appears the dm devices can take a second to figure things out - if (is_device_mapper || true) { - await sleep(2000); - } - - if (is_formatted && access_type == "mount") { - fs_info = await filesystem.getDeviceFilesystemInfo(device); - fs_type = fs_info.type; - if (fs_type) { - switch (fs_type) { - case "ext4": - case "ext3": - case "ext4dev": - //await filesystem.checkFilesystem(device, fs_info.type); - await filesystem.expandFilesystem(device, fs_type); - break; - case "btrfs": - case "xfs": - let mount_info = await mount.getMountDetails(device_path); - if (["btrfs", "xfs"].includes(mount_info.fstype)) { - //await filesystem.checkFilesystem(device, fs_info.type); - await filesystem.expandFilesystem(device_path, fs_type); - } - break; - default: - // unsupported filesystem - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `unsupported/unknown filesystem ${fs_type}` - ); + try { + device = await mount.getMountPointDevice(device_path); + is_formatted = await filesystem.deviceIsFormatted(device); + is_block = await filesystem.isBlockDevice(device); + } catch (err) { + if (err.code == 1) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `volume_path ${volume_path} is not currently mounted` + ); } } - } else { - //block device unformatted - return {}; - } - } else { - // not block device - return {}; + + if (is_block) { + let rescan_devices = []; + // detect if is a multipath device + is_device_mapper = await filesystem.isDeviceMapperDevice(device); + if (is_device_mapper) { + // NOTE: want to make sure we scan the dm device *after* all the underlying slaves + rescan_devices = await filesystem.getDeviceMapperDeviceSlaves( + device + ); + } + + rescan_devices.push(device); + + for (let sdevice of rescan_devices) { + // TODO: technically rescan is only relevant/available for remote drives + // such as iscsi etc, should probably limit this call as appropriate + // for now crudely checking the scenario inside the method itself + await filesystem.rescanDevice(sdevice); + } + + // let things settle + // it appears the dm devices can take a second to figure things out + if (is_device_mapper || true) { + await GeneralUtils.sleep(2000); + } + + if (is_formatted && access_type == "mount") { + fs_info = await filesystem.getDeviceFilesystemInfo(device); + fs_type = fs_info.type; + if (fs_type) { + switch (fs_type) { + case "ext4": + case "ext3": + case "ext4dev": + //await filesystem.checkFilesystem(device, fs_info.type); + await filesystem.expandFilesystem(device, fs_type); + break; + case "btrfs": + case "xfs": + let mount_info = await mount.getMountDetails(device_path); + if (["btrfs", "xfs"].includes(mount_info.fstype)) { + //await filesystem.checkFilesystem(device, fs_info.type); + await filesystem.expandFilesystem(device_path, fs_type); + } + break; + default: + // unsupported filesystem + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `unsupported/unknown filesystem ${fs_type}` + ); + } + } + } else { + //block device unformatted + return {}; + } + } else { + // not block device + return {}; + } + + break; + case NODE_OS_DRIVER_CSI_PROXY: + const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); + const volume_context = await driver.getDerivedVolumeContext(call); + if (!volume_context) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `unable to retrieve volume_context for volume: ${volume_id}` + ); + } + + const node_attach_driver = volume_context.node_attach_driver; + + // ensure path is mounted + result = await csiProxyClient.FilesystemPathExists(volume_path); + if (!result) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `volume_path ${volume_path} is not currently mounted` + ); + } + + switch (node_attach_driver) { + case "iscsi": + const node_volume_id = + await csiProxyClient.getVolumeIdFromIscsiTarget( + volume_context.portal, + volume_context.iqn + ); + const disk_number = + await csiProxyClient.getDiskNumberFromIscsiTarget( + volume_context.portal, + volume_context.iqn + ); + + if (node_volume_id) { + const required_bytes = _.get( + call.request, + "capacity_range.required_bytes" + ); + if (required_bytes) { + await csiProxyClient.executeRPC("disk", "Rescan"); + try { + await csiProxyClient.executeRPC("volume", "ResizeVolume", { + volume_id: node_volume_id, + resize_bytes: required_bytes, + }); + } catch (e) { + let details = _.get(e, "details", ""); + // seems to be a false positive + if ( + !details.includes( + "The size of the extent is less than the minimum of 1MB" + ) + ) { + throw e; + } + + await csiProxyClient.executeRPC("disk", "GetDiskStats", { + disk_number, + }); + + result = await csiProxyClient.executeRPC( + "volume", + "GetVolumeStats", + { + volume_id: node_volume_id, + } + ); + + let diff = Math.abs(result.total_bytes - required_bytes); + let percentage_diff = parseInt((diff / required_bytes) * 100); + /** + * 15MB is used by the 1ast partition on the initialized disk + * + * 100MB + * TODO: possibly change this to a percentage instead of absolute numbers + */ + let max_delta = 104857600; + driver.ctx.logger.debug( + "resize diff %s (%s%%)", + diff, + percentage_diff + ); + if (diff > max_delta) { + throw new GrpcError( + grpc.status.OUT_OF_RANGE, + `expanded size ${result.total_bytes} is too far off (${diff}) from requested size (${required_bytes})` + ); + } + } + } + } else { + throw new GrpcError(grpc.status.NOT_FOUND, `cannot find volume`); + } + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + break; + default: + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `unkown NODE OS DRIVER: ${driver.__getNodeOsDriver()}` + ); } return {}; } } + module.exports.CsiBaseDriver = CsiBaseDriver; diff --git a/src/utils/csi_proxy_client.js b/src/utils/csi_proxy_client.js new file mode 100644 index 0000000..fb656a1 --- /dev/null +++ b/src/utils/csi_proxy_client.js @@ -0,0 +1,257 @@ +const _ = require("lodash"); +const grpc = require("./grpc").grpc; +const protoLoader = require("@grpc/proto-loader"); + +const PROTO_BASE_PATH = __dirname + "/../../csi_proxy_proto"; + +/** + * leave connection null as by default the named pipe is derrived + */ +const DEFAULT_SERVICES = { + filesystem: { version: "v1", connection: null }, + disk: { version: "v1", connection: null }, + volume: { version: "v1", connection: null }, + smb: { version: "v1", connection: null }, + system: { version: "v1alpha1", connection: null }, + iscsi: { version: "v1alpha2", connection: null }, +}; + +function capitalize(s) { + return s && s[0].toUpperCase() + s.slice(1); +} + +class CsiProxyClient { + constructor(options = {}) { + this.clients = {}; + + // initialize all clients + const services = Object.assign( + {}, + DEFAULT_SERVICES, + options.services || {} + ); + + const pipePrefix = options.pipe_prefix || "csi-proxy"; + + for (const serviceName in services) { + const service = services[serviceName]; + const serviceVersion = + service.version || DEFAULT_SERVICES[serviceName].version; + const serviceConnection = + service.connection || + `\\\\.\\\\pipe\\\\${pipePrefix}-${serviceName}-${serviceVersion}`; + + const PROTO_PATH = `/${PROTO_BASE_PATH}/${serviceName}/${serviceVersion}/api.proto`; + const packageDefinition = protoLoader.loadSync(PROTO_PATH, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs: [__dirname + "/../csi_proxy_proto"], + }); + const protoDescriptor = grpc.loadPackageDefinition(packageDefinition); + const serviceInstance = new protoDescriptor[serviceVersion][ + capitalize(serviceName) + ](serviceConnection, grpc.credentials.createInsecure()); + this.clients[serviceName] = serviceInstance; + } + } + + async executeRPC(serviceName, methodName, options = {}) { + function rescursivePathFixer(obj) { + for (const k in obj) { + if (typeof obj[k] == "object" && obj[k] !== null) { + rescursivePathFixer(obj[k]); + } else { + if (k.includes("path")) { + obj[k] = obj[k].replaceAll("/", "\\"); + } + } + } + } + + rescursivePathFixer(options); + + const cleansedOptions = JSON.parse(JSON.stringify(options)); + // This function handles arrays and objects + function recursiveCleanse(obj) { + for (const k in obj) { + if (typeof obj[k] == "object" && obj[k] !== null) { + recursiveCleanse(obj[k]); + } else { + if ( + k.includes("secret") || + k.includes("username") || + k.includes("password") + ) { + obj[k] = "redacted"; + } + } + } + } + recursiveCleanse(cleansedOptions); + + console.log( + "csi-proxy request %s/%s - data: %j", + capitalize(serviceName), + methodName, + cleansedOptions + ); + + return new Promise((resolve, reject) => { + const functionRef = this.clients[serviceName.toLowerCase()][methodName]; + if (!functionRef) { + reject( + new Error( + `missing method ${methodName} on service ${capitalize(serviceName)}` + ) + ); + return; + } + + this.clients[serviceName.toLowerCase()][methodName]( + options, + (error, data) => { + console.log( + "csi-proxy response %s/%s - error: %j, data: %j", + capitalize(serviceName), + methodName, + error, + data + ); + + if (error) { + reject(error); + } + + resolve(data); + } + ); + }); + } + + /** + * Returns a disk_number if the target has 0 or 1 disks + * + * @param {*} target_portal + * @param {*} iqn + * @returns + */ + async getDiskNumberFromIscsiTarget(target_portal, iqn) { + let result; + + if (typeof target_portal != "object") { + target_portal = { + target_address: target_portal.split(":")[0], + target_port: target_portal.split(":")[1] || 3260, + }; + } + + // get device + try { + result = await this.executeRPC("iscsi", "GetTargetDisks", { + target_portal, + iqn, + }); + } catch (e) { + let details = _.get(e, "details", ""); + if (!details.includes("ObjectNotFound")) { + throw e; + } + } + + let diskIds = _.get(result, "diskIDs", []); + if (diskIds.length > 1) { + throw new Error( + `${diskIds.length} disks on the target, no way to know which is the relevant disk` + ); + } + + return diskIds[0]; + } + + /** + * Returns a volume_id if the disk has 0 or 1 volumes + * + * @param {*} disk_number + * @returns + */ + async getVolumeIdFromDiskNumber(disk_number) { + let result; + + if (disk_number == 0 || disk_number > 0) { + result = await this.executeRPC("volume", "ListVolumesOnDisk", { + disk_number, + }); + + let volume_ids = _.get(result, "volume_ids", []); + /** + * the 1st partition is a sort of system partion and is "" + * usually around 15MB in size + */ + volume_ids = volume_ids.filter((item) => { + return Boolean(item); + }); + + if (volume_ids.length > 1) { + throw new Error( + `${volume_ids.length} volumes on the disk, no way to know which is the relevant volume` + ); + } + + // ok of null/undefined + return volume_ids[0]; + } + } + + /** + * Return a volume_id if the target and disk both have 0 or 1 entries + * + * @param {*} target_portal + * @param {*} iqn + * @returns + */ + async getVolumeIdFromIscsiTarget(target_portal, iqn) { + const disk_number = await this.getDiskNumberFromIscsiTarget(...arguments); + return await this.getVolumeIdFromDiskNumber(disk_number); + } + + async FilesystemPathExists(path) { + let result; + try { + result = await this.executeRPC("filesystem", "PathExists", { + path, + }); + + return result.exists; + } catch (e) { + let details = _.get(e, "details", ""); + if (details.includes("not an absolute Windows path")) { + return false; + } else { + throw e; + } + } + } + + async FilesystemIsSymlink(path) { + let result; + try { + result = await this.executeRPC("filesystem", "IsSymlink", { + path, + }); + + return result.is_symlink; + } catch (e) { + let details = _.get(e, "details", ""); + if (details.includes("not an absolute Windows path")) { + return false; + } else { + throw e; + } + } + } +} + +module.exports.CsiProxyClient = CsiProxyClient; diff --git a/src/utils/filesystem.js b/src/utils/filesystem.js index f2c897c..deb62ea 100644 --- a/src/utils/filesystem.js +++ b/src/utils/filesystem.js @@ -1,5 +1,6 @@ const cp = require("child_process"); const fs = require("fs"); +const path = require("path"); const DEFAULT_TIMEOUT = process.env.FILESYSTEM_DEFAULT_TIMEOUT || 30000; @@ -25,6 +26,10 @@ class Filesystem { } } + covertUnixSeparatorToWindowsSeparator(p) { + return p.replaceAll(path.posix.sep, path.win32.sep); + } + /** * Attempt to discover if device is a block device * @@ -615,12 +620,12 @@ class Filesystem { command = filesystem.options.paths.sudo; } console.log("executing filesystem command: %s %s", command, args.join(" ")); - + return new Promise((resolve, reject) => { const child = filesystem.options.executor.spawn(command, args, options); let stdout = ""; let stderr = ""; - + child.stdout.on("data", function (data) { stdout = stdout + data; }); diff --git a/src/utils/general.js b/src/utils/general.js index accc106..ed25b3c 100644 --- a/src/utils/general.js +++ b/src/utils/general.js @@ -1,4 +1,5 @@ const axios = require("axios"); +const crypto = require("crypto"); function sleep(ms) { return new Promise((resolve) => { @@ -6,6 +7,64 @@ function sleep(ms) { }); } +function md5(val) { + return crypto.createHash("md5").update(val).digest("hex"); +} + +function crc32(val) { + for (var a, o = [], c = 0; c < 256; c++) { + a = c; + for (var f = 0; f < 8; f++) a = 1 & a ? 3988292384 ^ (a >>> 1) : a >>> 1; + o[c] = a; + } + for (var n = -1, t = 0; t < val.length; t++) + n = (n >>> 8) ^ o[255 & (n ^ val.charCodeAt(t))]; + return (-1 ^ n) >>> 0; +} + +const crctab16 = new Uint16Array([ + 0x0000, 0x1189, 0x2312, 0x329b, 0x4624, 0x57ad, 0x6536, 0x74bf, 0x8c48, + 0x9dc1, 0xaf5a, 0xbed3, 0xca6c, 0xdbe5, 0xe97e, 0xf8f7, 0x1081, 0x0108, + 0x3393, 0x221a, 0x56a5, 0x472c, 0x75b7, 0x643e, 0x9cc9, 0x8d40, 0xbfdb, + 0xae52, 0xdaed, 0xcb64, 0xf9ff, 0xe876, 0x2102, 0x308b, 0x0210, 0x1399, + 0x6726, 0x76af, 0x4434, 0x55bd, 0xad4a, 0xbcc3, 0x8e58, 0x9fd1, 0xeb6e, + 0xfae7, 0xc87c, 0xd9f5, 0x3183, 0x200a, 0x1291, 0x0318, 0x77a7, 0x662e, + 0x54b5, 0x453c, 0xbdcb, 0xac42, 0x9ed9, 0x8f50, 0xfbef, 0xea66, 0xd8fd, + 0xc974, 0x4204, 0x538d, 0x6116, 0x709f, 0x0420, 0x15a9, 0x2732, 0x36bb, + 0xce4c, 0xdfc5, 0xed5e, 0xfcd7, 0x8868, 0x99e1, 0xab7a, 0xbaf3, 0x5285, + 0x430c, 0x7197, 0x601e, 0x14a1, 0x0528, 0x37b3, 0x263a, 0xdecd, 0xcf44, + 0xfddf, 0xec56, 0x98e9, 0x8960, 0xbbfb, 0xaa72, 0x6306, 0x728f, 0x4014, + 0x519d, 0x2522, 0x34ab, 0x0630, 0x17b9, 0xef4e, 0xfec7, 0xcc5c, 0xddd5, + 0xa96a, 0xb8e3, 0x8a78, 0x9bf1, 0x7387, 0x620e, 0x5095, 0x411c, 0x35a3, + 0x242a, 0x16b1, 0x0738, 0xffcf, 0xee46, 0xdcdd, 0xcd54, 0xb9eb, 0xa862, + 0x9af9, 0x8b70, 0x8408, 0x9581, 0xa71a, 0xb693, 0xc22c, 0xd3a5, 0xe13e, + 0xf0b7, 0x0840, 0x19c9, 0x2b52, 0x3adb, 0x4e64, 0x5fed, 0x6d76, 0x7cff, + 0x9489, 0x8500, 0xb79b, 0xa612, 0xd2ad, 0xc324, 0xf1bf, 0xe036, 0x18c1, + 0x0948, 0x3bd3, 0x2a5a, 0x5ee5, 0x4f6c, 0x7df7, 0x6c7e, 0xa50a, 0xb483, + 0x8618, 0x9791, 0xe32e, 0xf2a7, 0xc03c, 0xd1b5, 0x2942, 0x38cb, 0x0a50, + 0x1bd9, 0x6f66, 0x7eef, 0x4c74, 0x5dfd, 0xb58b, 0xa402, 0x9699, 0x8710, + 0xf3af, 0xe226, 0xd0bd, 0xc134, 0x39c3, 0x284a, 0x1ad1, 0x0b58, 0x7fe7, + 0x6e6e, 0x5cf5, 0x4d7c, 0xc60c, 0xd785, 0xe51e, 0xf497, 0x8028, 0x91a1, + 0xa33a, 0xb2b3, 0x4a44, 0x5bcd, 0x6956, 0x78df, 0x0c60, 0x1de9, 0x2f72, + 0x3efb, 0xd68d, 0xc704, 0xf59f, 0xe416, 0x90a9, 0x8120, 0xb3bb, 0xa232, + 0x5ac5, 0x4b4c, 0x79d7, 0x685e, 0x1ce1, 0x0d68, 0x3ff3, 0x2e7a, 0xe70e, + 0xf687, 0xc41c, 0xd595, 0xa12a, 0xb0a3, 0x8238, 0x93b1, 0x6b46, 0x7acf, + 0x4854, 0x59dd, 0x2d62, 0x3ceb, 0x0e70, 0x1ff9, 0xf78f, 0xe606, 0xd49d, + 0xc514, 0xb1ab, 0xa022, 0x92b9, 0x8330, 0x7bc7, 0x6a4e, 0x58d5, 0x495c, + 0x3de3, 0x2c6a, 0x1ef1, 0x0f78, +]); + +// calculate the 16-bit CRC of data with predetermined length. +function crc16(data) { + var res = 0x0ffff; + + for (let b of data) { + res = ((res >> 8) & 0x0ff) ^ crctab16[(res ^ b) & 0xff]; + } + + return ~res & 0x0ffff; +} + function lockKeysFromRequest(call, serviceMethodName) { switch (serviceMethodName) { // controller @@ -55,9 +114,9 @@ function getLargestNumber() { /** * transition function to replicate `request` style requests using axios - * - * @param {*} options - * @param {*} callback + * + * @param {*} options + * @param {*} callback */ function axios_request(options, callback = function () {}) { function prep_response(res) { @@ -110,8 +169,23 @@ function stringify(value) { return JSON.stringify(value, getCircularReplacer()); } +function default_supported_block_filesystems() { + return ["btrfs", "ext3", "ext4", "ext4dev", "xfs", "ntfs"]; +} + +function default_supported_file_filesystems() { + return ["nfs", "cifs"]; +} + module.exports.sleep = sleep; +module.exports.md5 = md5; +module.exports.crc32 = crc32; +module.exports.crc16 = crc16; module.exports.lockKeysFromRequest = lockKeysFromRequest; module.exports.getLargestNumber = getLargestNumber; module.exports.stringify = stringify; module.exports.axios_request = axios_request; +module.exports.default_supported_block_filesystems = + default_supported_block_filesystems; +module.exports.default_supported_file_filesystems = + default_supported_file_filesystems; From 8ac44c0a67ddf9f59476b8f04b710c364a063e3c Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sun, 17 Apr 2022 14:41:34 -0600 Subject: [PATCH 10/71] enable smb in csi, only force host mount/umount if the operator has not explicitly set a preference Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 8 ++-- ci/configs/truenas/core/12.0/core-iscsi.yaml | 7 ++++ ci/configs/truenas/core/12.0/core-nfs.yaml | 2 +- ci/configs/truenas/core/12.0/core-smb.yaml | 28 ++++++------- ci/configs/truenas/core/13.0/core-nfs.yaml | 2 +- ci/configs/truenas/core/13.0/core-smb.yaml | 40 +++++++++++-------- ci/configs/truenas/scale/22.02/scale-nfs.yaml | 2 +- ci/configs/truenas/scale/22.02/scale-smb.yaml | 36 +++++++---------- docker/mount | 12 +++--- docker/umount | 12 +++--- examples/node-common.yaml | 27 +++++++++++++ 11 files changed, 105 insertions(+), 71 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 528fb9d..bcab4e5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -74,7 +74,7 @@ jobs: - truenas/scale/22.02/scale-iscsi.yaml - truenas/scale/22.02/scale-nfs.yaml # 80 char limit - #- truenas/scale-smb.yaml + - truenas/scale/22.02/scale-smb.yaml runs-on: - self-hosted - csi-sanity-zfs-local @@ -103,10 +103,10 @@ jobs: matrix: config: # 63 char limit - #- truenas/core-iscsi.yaml + - truenas/core/12.0/core-iscsi.yaml - truenas/core/12.0/core-nfs.yaml # 80 char limit - #- truenas/core-smb.yaml + - truenas/core/12.0/core-smb.yaml runs-on: - self-hosted - csi-sanity-zfs-local @@ -137,7 +137,7 @@ jobs: - truenas/core/13.0/core-iscsi.yaml - truenas/core/13.0/core-nfs.yaml # 80 char limit - #- truenas/core-smb.yaml + - truenas/core/13.0/core-smb.yaml runs-on: - self-hosted - csi-sanity-zfs-local diff --git a/ci/configs/truenas/core/12.0/core-iscsi.yaml b/ci/configs/truenas/core/12.0/core-iscsi.yaml index 2cf1d84..7d65a5a 100644 --- a/ci/configs/truenas/core/12.0/core-iscsi.yaml +++ b/ci/configs/truenas/core/12.0/core-iscsi.yaml @@ -35,3 +35,10 @@ iscsi: targetGroupAuthGroup: # 0-100 (0 == ignore) extentAvailThreshold: 0 + +# overcome the 63 char limit for testing purposes only +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/ci/configs/truenas/core/12.0/core-nfs.yaml b/ci/configs/truenas/core/12.0/core-nfs.yaml index 04f2744..0205cb0 100644 --- a/ci/configs/truenas/core/12.0/core-nfs.yaml +++ b/ci/configs/truenas/core/12.0/core-nfs.yaml @@ -19,7 +19,7 @@ zfs: detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true + datasetEnableReservation: false datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 diff --git a/ci/configs/truenas/core/12.0/core-smb.yaml b/ci/configs/truenas/core/12.0/core-smb.yaml index 9460255..2559c5b 100644 --- a/ci/configs/truenas/core/12.0/core-smb.yaml +++ b/ci/configs/truenas/core/12.0/core-smb.yaml @@ -17,28 +17,24 @@ sshConnection: zfs: datasetProperties: # smb options - #aclmode: restricted - #casesensitivity: mixed + aclmode: restricted + aclinherit: passthrough + acltype: nfsv4 + casesensitivity: insensitive datasetParentName: tank/ci/${CI_BUILD_KEY}/v detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: 1001 + datasetPermissionsGroup: 1001 - # for smb with guest - #datasetPermissionsUser: nobody - #datasetPermissionsGroup: nobody - - #datasetPermissionsGroup: root - #datasetPermissionsAcls: - #- "-m everyone@:full_set:allow" - - #datasetPermissionsAcls: - #- "-m u:kube:full_set:allow" + datasetPermissionsAcls: + - "-m g:builtin_users:full_set:fd:allow" + - "-m group@:modify_set:fd:allow" + - "-m owner@:full_set:fd:allow" smb: shareHost: ${TRUENAS_HOST} diff --git a/ci/configs/truenas/core/13.0/core-nfs.yaml b/ci/configs/truenas/core/13.0/core-nfs.yaml index 04f2744..0205cb0 100644 --- a/ci/configs/truenas/core/13.0/core-nfs.yaml +++ b/ci/configs/truenas/core/13.0/core-nfs.yaml @@ -19,7 +19,7 @@ zfs: detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true + datasetEnableReservation: false datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 diff --git a/ci/configs/truenas/core/13.0/core-smb.yaml b/ci/configs/truenas/core/13.0/core-smb.yaml index 9460255..f5efbf3 100644 --- a/ci/configs/truenas/core/13.0/core-smb.yaml +++ b/ci/configs/truenas/core/13.0/core-smb.yaml @@ -17,28 +17,24 @@ sshConnection: zfs: datasetProperties: # smb options - #aclmode: restricted - #casesensitivity: mixed + aclmode: restricted + aclinherit: passthrough + acltype: nfsv4 + casesensitivity: insensitive datasetParentName: tank/ci/${CI_BUILD_KEY}/v detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: 1001 + datasetPermissionsGroup: 1001 - # for smb with guest - #datasetPermissionsUser: nobody - #datasetPermissionsGroup: nobody - - #datasetPermissionsGroup: root - #datasetPermissionsAcls: - #- "-m everyone@:full_set:allow" - - #datasetPermissionsAcls: - #- "-m u:kube:full_set:allow" + datasetPermissionsAcls: + - "-m g:builtin_users:full_set:fd:allow" + - "-m group@:modify_set:fd:allow" + - "-m owner@:full_set:fd:allow" smb: shareHost: ${TRUENAS_HOST} @@ -52,7 +48,7 @@ smb: shareAllowedHosts: [] shareDeniedHosts: [] #shareDefaultPermissions: true - shareGuestOk: true + shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true shareRecycleBin: true @@ -60,3 +56,13 @@ smb: shareAccessBasedEnumeration: true shareTimeMachine: false #shareStorageTask: + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" + +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/ci/configs/truenas/scale/22.02/scale-nfs.yaml b/ci/configs/truenas/scale/22.02/scale-nfs.yaml index 0e817ce..42818ae 100644 --- a/ci/configs/truenas/scale/22.02/scale-nfs.yaml +++ b/ci/configs/truenas/scale/22.02/scale-nfs.yaml @@ -13,7 +13,7 @@ zfs: detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true + datasetEnableReservation: false datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 diff --git a/ci/configs/truenas/scale/22.02/scale-smb.yaml b/ci/configs/truenas/scale/22.02/scale-smb.yaml index 74964ea..95b0b9a 100644 --- a/ci/configs/truenas/scale/22.02/scale-smb.yaml +++ b/ci/configs/truenas/scale/22.02/scale-smb.yaml @@ -9,31 +9,15 @@ httpConnection: password: ${TRUENAS_PASSWORD} zfs: - datasetProperties: - # smb options - #aclmode: restricted - #casesensitivity: mixed - datasetParentName: tank/ci/${CI_BUILD_KEY}/v detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: 1001 + datasetPermissionsGroup: 1001 - # for smb with guest - #datasetPermissionsUser: nobody - #datasetPermissionsGroup: nobody - - #datasetPermissionsGroup: root - #datasetPermissionsAcls: - #- "-m everyone@:full_set:allow" - - #datasetPermissionsAcls: - #- "-m u:kube:full_set:allow" - smb: shareHost: ${TRUENAS_HOST} #nameTemplate: "" @@ -46,7 +30,7 @@ smb: shareAllowedHosts: [] shareDeniedHosts: [] #shareDefaultPermissions: true - shareGuestOk: true + shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true shareRecycleBin: true @@ -54,3 +38,13 @@ smb: shareAccessBasedEnumeration: true shareTimeMachine: false #shareStorageTask: + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" + +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/docker/mount b/docker/mount index 0f7c17a..229e526 100755 --- a/docker/mount +++ b/docker/mount @@ -20,15 +20,17 @@ container_supported_filesystems=( while getopts "t:" opt; do case "$opt" in t) - [[ "${OPTARG,,}" == "zfs" ]] && USE_HOST_MOUNT_TOOLS=1 - [[ "${OPTARG,,}" == "lustre" ]] && USE_HOST_MOUNT_TOOLS=1 - [[ "${OPTARG,,}" == "onedata" ]] && USE_HOST_MOUNT_TOOLS=1 - #(printf '%s\0' "${container_supported_filesystems[@]}" | grep -Fqxz -- "${OPTARG}") || USE_HOST_MOUNT_TOOLS=1 + if [[ "x${USE_HOST_MOUNT_TOOLS}" == "x" ]]; then + [[ "${OPTARG,,}" == "zfs" ]] && USE_HOST_MOUNT_TOOLS=1 + [[ "${OPTARG,,}" == "lustre" ]] && USE_HOST_MOUNT_TOOLS=1 + [[ "${OPTARG,,}" == "onedata" ]] && USE_HOST_MOUNT_TOOLS=1 + #(printf '%s\0' "${container_supported_filesystems[@]}" | grep -Fqxz -- "${OPTARG}") || USE_HOST_MOUNT_TOOLS=1 + fi ;; esac done -if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]];then +if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" mount "${@:1}" else /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" mount "${@:1}" diff --git a/docker/umount b/docker/umount index 9a4b184..b38b078 100755 --- a/docker/umount +++ b/docker/umount @@ -20,15 +20,17 @@ container_supported_filesystems=( while getopts "t:" opt; do case "$opt" in t) - [[ "${OPTARG,,}" == "zfs" ]] && USE_HOST_MOUNT_TOOLS=1 - [[ "${OPTARG,,}" == "lustre" ]] && USE_HOST_MOUNT_TOOLS=1 - [[ "${OPTARG,,}" == "onedata" ]] && USE_HOST_MOUNT_TOOLS=1 - #(printf '%s\0' "${container_supported_filesystems[@]}" | grep -Fqxz -- "${OPTARG}") || USE_HOST_MOUNT_TOOLS=1 + if [[ "x${USE_HOST_MOUNT_TOOLS}" == "x" ]]; then + [[ "${OPTARG,,}" == "zfs" ]] && USE_HOST_MOUNT_TOOLS=1 + [[ "${OPTARG,,}" == "lustre" ]] && USE_HOST_MOUNT_TOOLS=1 + [[ "${OPTARG,,}" == "onedata" ]] && USE_HOST_MOUNT_TOOLS=1 + #(printf '%s\0' "${container_supported_filesystems[@]}" | grep -Fqxz -- "${OPTARG}") || USE_HOST_MOUNT_TOOLS=1 + fi ;; esac done -if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]];then +if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" umount "${@:1}" else /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" umount "${@:1}" diff --git a/examples/node-common.yaml b/examples/node-common.yaml index 1f0f3df..1cda789 100644 --- a/examples/node-common.yaml +++ b/examples/node-common.yaml @@ -30,3 +30,30 @@ node: # ... btrfs: customOptions: [] + + csiProxy: + # should be left unset in most situation, will be auto-detected + #enabled: true + + # connection attributes can be set to grpc endpoint + # ie: hostname:port, or /some/path, or \\.\pipe\foo + # connection and version will use internal defaults and should generally be left unset + services: + filesystem: + #version: v1 + #connection: + disk: + #version: v1 + #connection: + volume: + #version: v1 + #connection: + smb: + #version: v1 + #connection: + system: + #version: v1alpha1 + #connection: + iscsi: + #version: v1alpha2 + #connection: From 88860d319246bb2cce5db3396ca8f511ae81670d Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sun, 17 Apr 2022 15:31:34 -0600 Subject: [PATCH 11/71] fix config for 12 smb tests Signed-off-by: Travis Glenn Hansen --- ci/configs/truenas/core/12.0/core-smb.yaml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ci/configs/truenas/core/12.0/core-smb.yaml b/ci/configs/truenas/core/12.0/core-smb.yaml index 2559c5b..f5efbf3 100644 --- a/ci/configs/truenas/core/12.0/core-smb.yaml +++ b/ci/configs/truenas/core/12.0/core-smb.yaml @@ -48,7 +48,7 @@ smb: shareAllowedHosts: [] shareDeniedHosts: [] #shareDefaultPermissions: true - shareGuestOk: true + shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true shareRecycleBin: true @@ -56,3 +56,13 @@ smb: shareAccessBasedEnumeration: true shareTimeMachine: false #shareStorageTask: + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" + +_private: + csi: + volume: + idHash: + strategy: crc16 From 6cde0d3a700a1218e293f09eaf700a43e42a6493 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 18 Apr 2022 10:23:31 -0600 Subject: [PATCH 12/71] zfs-generic-smb driver (sharesmb=on) Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 1 + bin/democratic-csi | 7 ++- ci/configs/zfs-generic/nfs.yaml | 2 +- ci/configs/zfs-generic/smb.yaml | 40 ++++++++++++ examples/zfs-generic-smb.yaml | 57 +++++++++++++++++ src/driver/controller-zfs-generic/index.js | 73 ++++++++++++++++++++++ src/driver/factory.js | 1 + src/driver/index.js | 4 ++ 8 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 ci/configs/zfs-generic/smb.yaml create mode 100644 examples/zfs-generic-smb.yaml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index bcab4e5..c0c594c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -167,6 +167,7 @@ jobs: config: - zfs-generic/iscsi.yaml - zfs-generic/nfs.yaml + - zfs-generic/smb.yaml runs-on: - self-hosted - csi-sanity-zfs-generic diff --git a/bin/democratic-csi b/bin/democratic-csi index df94d93..0f465e2 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -1,4 +1,9 @@ -#!/usr/bin/env -S node --expose-gc ${NODE_OPTIONS_CSI_1} ${NODE_OPTIONS_CSI_2} ${NODE_OPTIONS_CSI_3} ${NODE_OPTIONS_CSI_4} ${NODE_OPTIONS_CSI_5} +#!/usr/bin/env -S node --expose-gc ${NODE_OPTIONS_CSI_1} ${NODE_OPTIONS_CSI_2} ${NODE_OPTIONS_CSI_3} ${NODE_OPTIONS_CSI_4} + +/** + * keep the shebang line length under 128 + * https://github.com/democratic-csi/democratic-csi/issues/171 + */ const yaml = require("js-yaml"); const fs = require("fs"); diff --git a/ci/configs/zfs-generic/nfs.yaml b/ci/configs/zfs-generic/nfs.yaml index a3a8b88..e451a73 100644 --- a/ci/configs/zfs-generic/nfs.yaml +++ b/ci/configs/zfs-generic/nfs.yaml @@ -11,7 +11,7 @@ zfs: detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s datasetEnableQuotas: true - datasetEnableReservation: true + datasetEnableReservation: false datasetPermissionsMode: "0777" datasetPermissionsUser: 0 datasetPermissionsGroup: 0 diff --git a/ci/configs/zfs-generic/smb.yaml b/ci/configs/zfs-generic/smb.yaml new file mode 100644 index 0000000..272a590 --- /dev/null +++ b/ci/configs/zfs-generic/smb.yaml @@ -0,0 +1,40 @@ +driver: zfs-generic-smb + +sshConnection: + host: ${SERVER_HOST} + port: 22 + username: ${SERVER_USERNAME} + password: ${SERVER_PASSWORD} + +zfs: + datasetParentName: tank/ci/${CI_BUILD_KEY}/v + detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s + + datasetProperties: + #aclmode: restricted + #aclinherit: passthrough + #acltype: nfsv4 + casesensitivity: insensitive + + datasetEnableQuotas: true + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: smbroot + datasetPermissionsGroup: smbroot + +smb: + shareHost: ${SERVER_HOST} + shareStrategy: "setDatasetProperties" + shareStrategySetDatasetProperties: + properties: + sharesmb: "on" + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" + +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/examples/zfs-generic-smb.yaml b/examples/zfs-generic-smb.yaml new file mode 100644 index 0000000..db60cf3 --- /dev/null +++ b/examples/zfs-generic-smb.yaml @@ -0,0 +1,57 @@ +driver: zfs-generic-smb +sshConnection: + host: server address + port: 22 + username: root + # use either password or key + password: "" + privateKey: | + -----BEGIN RSA PRIVATE KEY----- + ... + -----END RSA PRIVATE KEY----- + +zfs: + # can be used to override defaults if necessary + # the example below is useful for TrueNAS 12 + #cli: + # sudoEnabled: true + # paths: + # zfs: /usr/local/sbin/zfs + # zpool: /usr/local/sbin/zpool + # sudo: /usr/local/bin/sudo + # chroot: /usr/sbin/chroot + + # can be used to set arbitrary values on the dataset/zvol + # can use handlebars templates with the parameters from the storage class/CO + datasetProperties: + #aclmode: restricted + #aclinherit: passthrough + #acltype: nfsv4 + casesensitivity: insensitive + + datasetParentName: tank/k8s/test + # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap + # they may be siblings, but neither should be nested in the other + detachedSnapshotsDatasetParentName: tanks/k8s/test-snapshots + + datasetEnableQuotas: true + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: smbroot + datasetPermissionsGroup: smbroot + + #datasetPermissionsAclsBinary: nfs4_setfacl + #datasetPermissionsAcls: + #- "-m everyone@:full_set:allow" + #- -s group@:modify_set:fd:allow + #- -a owner@:full_set:fd:allow + +smb: + # https://docs.oracle.com/cd/E23824_01/html/821-1448/gayne.html + # https://www.hiroom2.com/2016/05/18/ubuntu-16-04-share-zfs-storage-via-nfs-smb/ + shareStrategy: "setDatasetProperties" + shareStrategySetDatasetProperties: + properties: + sharesmb: "on" + # share: "" + shareHost: "server address" diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 31c0fa7..756d268 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -52,6 +52,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { getDriverZfsResourceType() { switch (this.options.driver) { case "zfs-generic-nfs": + case "zfs-generic-smb": return "filesystem"; case "zfs-generic-iscsi": return "volume"; @@ -109,6 +110,48 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { }; return volume_context; + case "zfs-generic-smb": + let share; + switch (this.options.smb.shareStrategy) { + case "setDatasetProperties": + function generateShareName(dataset) { + let name = dataset; + name = name.replaceAll("/", "_"); + name = name.replaceAll("-", "_"); + return name; + } + + for (let key of ["share", "sharesmb"]) { + if ( + this.options.smb.shareStrategySetDatasetProperties.properties[ + key + ] + ) { + await zb.zfs.set(datasetName, { + [key]: + this.options.smb.shareStrategySetDatasetProperties + .properties[key], + }); + } + } + + share = generateShareName(datasetName); + break; + default: + break; + } + + properties = await zb.zfs.get(datasetName, ["mountpoint"]); + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); + + volume_context = { + node_attach_driver: "smb", + server: this.options.smb.shareHost, + share, + }; + return volume_context; + case "zfs-generic-iscsi": let basename; let iscsiName; @@ -268,6 +311,36 @@ create /backstores/block/${iscsiName} } break; + case "zfs-generic-smb": + switch (this.options.smb.shareStrategy) { + case "setDatasetProperties": + for (let key of ["share", "sharesmb"]) { + if ( + this.options.smb.shareStrategySetDatasetProperties.properties[ + key + ] + ) { + try { + await zb.zfs.inherit(datasetName, key); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + // do nothing + } else { + throw err; + } + } + } + } + await sleep(2000); // let things settle + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown shareStrategy ${this.options.nfs.shareStrategy}` + ); + } + break; + case "zfs-generic-iscsi": let basename; let iscsiName; diff --git a/src/driver/factory.js b/src/driver/factory.js index 6cb4850..02dc2ae 100644 --- a/src/driver/factory.js +++ b/src/driver/factory.js @@ -33,6 +33,7 @@ function factory(ctx, options) { case "synology-iscsi": return new ControllerSynologyDriver(ctx, options); case "zfs-generic-nfs": + case "zfs-generic-smb": case "zfs-generic-iscsi": return new ControllerZfsGenericDriver(ctx, options); case "zfs-local-dataset": diff --git a/src/driver/index.js b/src/driver/index.js index 1b9d30b..58d2f09 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -679,6 +679,10 @@ class CsiBaseDriver { if (!has_guest) { mount_flags.push("guest"); } + + if (volume_mount_group) { + mount_flags.push(`gid=${volume_mount_group}`); + } } break; case "iscsi": From 86f10090ffd3d4eb5f70a9e92daa3cf4dc3846f5 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 18 Apr 2022 11:33:25 -0600 Subject: [PATCH 13/71] better logging around smb share name generation Signed-off-by: Travis Glenn Hansen --- src/driver/controller-zfs-generic/index.js | 30 +++++++++++++++------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 756d268..06cb18a 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -61,6 +61,24 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { } } + generateSmbShareName(datasetName) { + const driver = this; + + driver.ctx.logger.verbose( + `generating smb share name for dataset: ${datasetName}` + ); + + let name = datasetName || ""; + name = name.replaceAll("/", "_"); + name = name.replaceAll("-", "_"); + + driver.ctx.logger.verbose( + `generated smb share name for dataset: ${datasetName} - ${name}` + ); + + return name; + } + /** * should create any necessary share resources * should set the SHARE_VOLUME_CONTEXT_PROPERTY_NAME propery @@ -68,6 +86,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { * @param {*} datasetName */ async createShare(call, datasetName) { + const driver = this; const zb = await this.getZetabyte(); const execClient = this.getExecClient(); @@ -114,13 +133,6 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { let share; switch (this.options.smb.shareStrategy) { case "setDatasetProperties": - function generateShareName(dataset) { - let name = dataset; - name = name.replaceAll("/", "_"); - name = name.replaceAll("-", "_"); - return name; - } - for (let key of ["share", "sharesmb"]) { if ( this.options.smb.shareStrategySetDatasetProperties.properties[ @@ -135,7 +147,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { } } - share = generateShareName(datasetName); + share = driver.generateSmbShareName(datasetName); break; default: break; @@ -336,7 +348,7 @@ create /backstores/block/${iscsiName} default: throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown shareStrategy ${this.options.nfs.shareStrategy}` + `invalid configuration: unknown shareStrategy ${this.options.smb.shareStrategy}` ); } break; From 376f8b58cc07999b5aaf9c17cbfe07941bb2dc6f Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 18 Apr 2022 12:08:08 -0600 Subject: [PATCH 14/71] debug share name issue Signed-off-by: Travis Glenn Hansen --- src/driver/controller-zfs-generic/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 06cb18a..7f1ecc7 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -65,10 +65,11 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { const driver = this; driver.ctx.logger.verbose( - `generating smb share name for dataset: ${datasetName}` + `generating smb share name for dataset: ${typeof datasetName} ${datasetName}` ); let name = datasetName || ""; + console.log(name); name = name.replaceAll("/", "_"); name = name.replaceAll("-", "_"); From b6db1c9e93c0b38e9796c5c69359eb7c8b520db1 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 18 Apr 2022 12:26:42 -0600 Subject: [PATCH 15/71] polyfill replaceAll Signed-off-by: Travis Glenn Hansen --- bin/democratic-csi | 3 +++ src/driver/controller-zfs-generic/index.js | 3 +-- src/utils/polyfills.js | 5 +++++ 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 src/utils/polyfills.js diff --git a/bin/democratic-csi b/bin/democratic-csi index 0f465e2..6a0bec9 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -5,6 +5,9 @@ * https://github.com/democratic-csi/democratic-csi/issues/171 */ + +// polyfills +require("../src/utils/polyfills"); const yaml = require("js-yaml"); const fs = require("fs"); const { grpc } = require("../src/utils/grpc"); diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 7f1ecc7..1705788 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -69,12 +69,11 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { ); let name = datasetName || ""; - console.log(name); name = name.replaceAll("/", "_"); name = name.replaceAll("-", "_"); driver.ctx.logger.verbose( - `generated smb share name for dataset: ${datasetName} - ${name}` + `generated smb share name for dataset (${datasetName}): ${name}` ); return name; diff --git a/src/utils/polyfills.js b/src/utils/polyfills.js new file mode 100644 index 0000000..0e22848 --- /dev/null +++ b/src/utils/polyfills.js @@ -0,0 +1,5 @@ +if (typeof String.prototype.replaceAll == "undefined") { + String.prototype.replaceAll = function (match, replace) { + return this.replace(new RegExp(match, "g"), () => replace); + }; +} From 75857823aafc6f40330fba7621c51fc83a822459 Mon Sep 17 00:00:00 2001 From: Kim Wittenburg Date: Wed, 20 Apr 2022 17:28:09 +0200 Subject: [PATCH 16/71] Remove validation and custom options --- docs/storage-class-parameters.md | 98 +++++----- examples/synology-iscsi.yaml | 62 ++++++- src/driver/controller-synology/http/index.js | 1 + src/driver/controller-synology/index.js | 180 ++++++------------- 4 files changed, 176 insertions(+), 165 deletions(-) diff --git a/docs/storage-class-parameters.md b/docs/storage-class-parameters.md index b9a6bd9..d71d47f 100644 --- a/docs/storage-class-parameters.md +++ b/docs/storage-class-parameters.md @@ -15,44 +15,60 @@ metadata: name: synology-iscsi parameters: fsType: ext4 - # The following options affect the LUN representing the volume - 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. Use 0 for unlimited sessions (default). - maxRecieveSegmentBytes: 262144 - maxSendSegmentBytes: 262144 + # The following options affect the LUN representing the volume. These options are passed directly to the Synology API. + # The following options are known. lunTemplate: | - # This inline yaml object will be passed to the Synology API when creating the LUN. Use this for custom options. + type: BLUN # Btrfs thin provisioning + type: BLUN_THICK # Btrfs thick provisioning + type: THIN # Ext4 thin provisioning + type: ADV # Ext4 thin provisioning with legacy advanced feature set + type: FILE # Ext4 thick provisioning + description: Some Description + + # Only for thick provisioned volumes. Known values: + # 0: Buffered Writes + # 3: Direct Write + direct_io_pattern: 0 + + # Device Attributes. See below for more info dev_attribs: - - dev_attrib: emulate_caw + - dev_attrib: emulate_tpws enable: 1 + - ... + + # The following options affect the iSCSI target. These options will be passed directly to the Synology API. + # The following options are known. targetTemplate: | - # This inline yaml object will be passed to the Synology API when creating the target. Use this for custom - # options. + has_header_checksum: false + has_data_checksum: false + + # Note that this option requires a compatible filesystem. Use 0 for unlimited sessions. max_sessions: 0 + multi_sessions: true + max_recv_seg_bytes: 262144 + max_send_seg_bytes: 262144 + + # Use this to disable authentication. To configure authentication see below + auth_type: 0 ``` -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. +#### About LUN Types +The availability of the different types of LUNs depends on the filesystem used on your Synology volume. For Btrfs volumes +you can use `BLUN` and `BLUN_THICK` volumes. For Ext4 volumes you can use `THIN`, `ADV` or `FILE` volumes. These +correspond to the options available in the UI. + +#### About `dev_attribs` +Most of the LUN options are configured via the `dev_attribs` list. This list can be specified both in the `lunTemplate` +of the global configuration and in the `lunTemplate` of the `StorageClass`. If both lists are present they will be merged +(with the `StorageClass` taking precedence). The following `dev_attribs` are known to work: + +- `emulate_tpws`: Hardware-assisted zeroing +- `emulate_caw`: Hardware-assisted locking +- `emulate_3pc`: Hardware-assisted data transfer +- `emulate_tpu`: Space Reclamation +- `emulate_fua_write`: Enable the FUA iSCSI command (DSM 7+) +- `emulate_sync_cache`: Enable the Sync Cache iSCSI command (DSM 7+) +- `can_snapshot`: Enable snapshots for this volume. Only works for thin provisioned volumes. ### Configure Snapshot Classes `synology-iscsi` can also configure different parameters on snapshot classes: @@ -63,15 +79,14 @@ kind: VolumeSnapshotClass metadata: name: synology-iscsi-snapshot parameters: - isLocked: true - # https://kb.synology.com/en-me/DSM/tutorial/What_is_file_system_consistent_snapshot - # Note that AppConsistent snapshots require a working Synology Storage Console. Otherwise both values will have - # equivalent behavior. - consistency: AppConsistent # Or CrashConsistent + # This inline yaml object will be passed to the Synology API when creating the snapshot. lunSnapshotTemplate: | - # This inline yaml object will be passed to the Synology API when creating the snapshot. Use this for custom - # options. is_locked: true + + # https://kb.synology.com/en-me/DSM/tutorial/What_is_file_system_consistent_snapshot + # Note that app consistent snapshots require a working Synology Storage Console. Otherwise both values will have + # equivalent behavior. + is_app_consistent: true ... ``` @@ -90,8 +105,9 @@ metadata: name: synology-iscsi-chap parameters: fsType: ext4 - lunType: BLUN - lunDescription: iSCSI volumes with CHAP Authentication + lunTemplate: | + type: BLUN + description: 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 @@ -112,7 +128,7 @@ stringData: password: MySecretPassword # Mutual CHAP Credentials. If these are specified mutual CHAP will be enabled. mutualUser: server - password: MyOtherPassword + mutualPassword: MyOtherPassword ``` Note that CHAP authentication will only be enabled if the secret contains a username and password. If e.g. a password is diff --git a/examples/synology-iscsi.yaml b/examples/synology-iscsi.yaml index 0fba420..cc8380f 100644 --- a/examples/synology-iscsi.yaml +++ b/examples/synology-iscsi.yaml @@ -27,5 +27,65 @@ iscsi: # full iqn limit is 223 bytes, plan accordingly namePrefix: "" nameSuffix: "" - # LUN options and CHAP authentication can be configured using StorageClasses. + + # 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 + # These options can also be configured per storage-class: # See https://github.com/democratic-csi/democratic-csi/blob/master/docs/storage-class-parameters.md + 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 diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index 9e00362..00ac3f0 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -11,6 +11,7 @@ const __REGISTRY_NS__ = "SynologyHttpClient"; SYNO_ERRORS = { 18990002: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The synology volume is out of disk space." }, + 18990318: { status: grpc.status.INVALID_ARGUMENT, message: "The requested lun type is incompatible with the Synology filesystem." }, 18990538: { status: grpc.status.ALREADY_EXISTS, message: "A LUN with this name already exists." }, 18990541: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The maximum number of LUNS has been reached." }, 18990542: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The maximum number if iSCSI target has been reached." }, diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index e1da1bf..a1e9b96 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -143,22 +143,21 @@ 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; + getObjectFromDevAttribs(list = []) { + if (!list) { + return {} } - const parsed = parseInt(value) - if (!isNaN(parsed)) { - return Boolean(parsed) - } - return "true".localeCompare(value, undefined, {sensitivity: "accent"}) === 0 + return list.reduce( + (obj, item) => Object.assign(obj, {[item.dev_attrib]: item.enable}), {} + ) + } + + getDevAttribsFromObject(obj, keepNull = false) { + return Object.entries(obj).filter( + e => keepNull || (e[1] != null) + ).map( + e => ({dev_attrib: e[0], enable: e[1]}) + ); } buildIscsiName(name) { @@ -366,6 +365,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { break; case "iscsi": let iscsiName = driver.buildIscsiName(name); + let storageClassTemplate; let data; let target; let lun_mapping; @@ -491,106 +491,47 @@ class ControllerSynologyDriver extends CsiBaseDriver { } } else { // create lun - data = Object.assign({}, driver.options.iscsi.lunTemplate, yaml.load(normalizedParameters.lunTemplate), { - name: iscsiName, - location: driver.getLocation(), - 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` - ); - } + try { + storageClassTemplate = yaml.load(normalizedParameters.lunTemplate ?? "") + const devAttribs = driver.getDevAttribsFromObject(Object.assign( + {}, + driver.getObjectFromDevAttribs(driver.options.iscsi.lunTemplate?.dev_attribs), + driver.getObjectFromDevAttribs(storageClassTemplate?.dev_attribs) + )) + data = Object.assign({}, driver.options.iscsi.lunTemplate, storageClassTemplate, { + name: iscsiName, + location: driver.getLocation(), + size: capacity_bytes, + dev_attribs: devAttribs + }); - 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.` - ); + lun_uuid = await httpClient.CreateLun(data); + } catch (err) { + if (err instanceof yaml.YAMLException) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `The lunTemplate on StorageClass is not a valid YAML document.` + ); + } else { + throw err + } } - 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); } // create target let iqn = driver.options.iscsi.baseiqn + iscsiName; - data = Object.assign({}, driver.options.iscsi.targetTemplate, yaml.load(normalizedParameters.targetTemplate), { + try { + storageClassTemplate = yaml.load(normalizedParameters.targetTemplate ?? "") + } catch (err) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `The targetTemplate on StorageClass is not a valid YAML document.` + ); + } + data = Object.assign({}, driver.options.iscsi.targetTemplate, storageClassTemplate, { 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; @@ -605,9 +546,6 @@ class ControllerSynologyDriver extends CsiBaseDriver { 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); @@ -998,27 +936,23 @@ class ControllerSynologyDriver extends CsiBaseDriver { // check for already exists let snapshot; + let snapshotClassTemplate; snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); if (!snapshot) { const normalizedParameters = driver.getNormalizedParameters(call.request.parameters); - let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, yaml.load(normalizedParameters.lunSnapshotTemplate), { + try { + snapshotClassTemplate = yaml.load(normalizedParameters.lunSnapshotTemplate ?? ""); + } catch (err) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `The snapshotTemplate on VolumeSnapshotClass is not a valid YAML document.` + ); + } + let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, snapshotClassTemplate, { 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.GetSnapshotByLunUUIDAndName(lun.uuid, name); From 7f579d9a7c060a4746e474952bd2e75d6082583d Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 20 Apr 2022 11:42:39 -0600 Subject: [PATCH 17/71] doc Signed-off-by: Travis Glenn Hansen --- README.md | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 312829e..2f1c23c 100644 --- a/README.md +++ b/README.md @@ -270,13 +270,34 @@ Issues to review: - https://jira.ixsystems.com/browse/NAS-108522 - https://jira.ixsystems.com/browse/NAS-107219 -### ZoL (zfs-generic-nfs, zfs-generic-iscsi) +### ZoL (zfs-generic-nfs, zfs-generic-iscsi, zfs-generic-smb) Ensure ssh and zfs is installed on the nfs/iscsi server and that you have installed `targetcli`. -- `sudo yum install targetcli -y` -- `sudo apt-get -y install targetcli-fb` +The driver executes many commands over an ssh connection. You may consider +disabling all the `motd` details for the ssh user as it can spike the cpu +unecessarily: + +- https://askubuntu.com/questions/318592/how-can-i-remove-the-landscape-canonical-com-greeting-from-motd +- https://linuxconfig.org/disable-dynamic-motd-and-news-on-ubuntu-20-04-focal-fossa-linux + +``` +####### iscsi +yum install targetcli -y +apt-get -y install targetcli-fb + +####### smb +apt-get install -y samba smbclient + +# create posix user +groupadd -g 1001 smbroot +useradd -u 1001 -g 1001 -M -N -s /sbin/nologin smbroot +passwd smbroot (optional) + +# create smb user and set password +smbpasswd -L -a smbroot +``` ### Synology (synology-iscsi) From 9026d5e0d643440ecfa0ad6ee1161023be436e56 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 20 Apr 2022 14:08:51 -0600 Subject: [PATCH 18/71] synology updates, dsm6 and dsm7 in ci Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 40 +++- ci/configs/synlogy/{ => dsm6}/iscsi.yaml | 0 ci/configs/synlogy/dsm7/iscsi.yaml | 77 ++++++++ examples/synology-iscsi.yaml | 3 + src/driver/controller-synology/http/index.js | 82 ++++++-- src/driver/controller-synology/index.js | 190 +++++++++++-------- src/driver/controller-zfs-generic/index.js | 2 +- 7 files changed, 291 insertions(+), 103 deletions(-) rename ci/configs/synlogy/{ => dsm6}/iscsi.yaml (100%) create mode 100644 ci/configs/synlogy/dsm7/iscsi.yaml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c0c594c..3d1b1c0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,14 +35,14 @@ jobs: path: node_modules.tar.gz retention-days: 7 - csi-sanity-synology: + csi-sanity-synology-dsm6: needs: - build-npm strategy: fail-fast: false matrix: config: - - synlogy/iscsi.yaml + - synlogy/dsm6/iscsi.yaml runs-on: - self-hosted - csi-sanity-synology @@ -57,12 +57,41 @@ jobs: ci/bin/run.sh env: TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" - SYNOLOGY_HOST: ${{ secrets.SANITY_SYNOLOGY_HOST }} - SYNOLOGY_PORT: ${{ secrets.SANITY_SYNOLOGY_PORT }} + SYNOLOGY_HOST: ${{ secrets.SANITY_SYNOLOGY_DSM6_HOST }} + SYNOLOGY_PORT: ${{ secrets.SANITY_SYNOLOGY_DSM6_PORT }} SYNOLOGY_USERNAME: ${{ secrets.SANITY_SYNOLOGY_USERNAME }} SYNOLOGY_PASSWORD: ${{ secrets.SANITY_SYNOLOGY_PASSWORD }} SYNOLOGY_VOLUME: ${{ secrets.SANITY_SYNOLOGY_VOLUME }} + csi-sanity-synology-dsm7: + needs: + - build-npm + strategy: + fail-fast: false + matrix: + config: + - synlogy/dsm7/iscsi.yaml + runs-on: + - self-hosted + - csi-sanity-synology + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + with: + name: node-modules + - name: csi-sanity + run: | + # run tests + ci/bin/run.sh + env: + TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" + SYNOLOGY_HOST: ${{ secrets.SANITY_SYNOLOGY_DSM7_HOST }} + SYNOLOGY_PORT: ${{ secrets.SANITY_SYNOLOGY_DSM7_PORT }} + SYNOLOGY_USERNAME: ${{ secrets.SANITY_SYNOLOGY_USERNAME }} + SYNOLOGY_PASSWORD: ${{ secrets.SANITY_SYNOLOGY_PASSWORD }} + SYNOLOGY_VOLUME: ${{ secrets.SANITY_SYNOLOGY_VOLUME }} + + # api-based drivers csi-sanity-truenas-scale-22_02: needs: @@ -237,7 +266,8 @@ jobs: build-docker: needs: - - csi-sanity-synology + - csi-sanity-synology-dsm6 + - csi-sanity-synology-dsm7 - csi-sanity-truenas-scale-22_02 - csi-sanity-truenas-core-12_0 - csi-sanity-truenas-core-13_0 diff --git a/ci/configs/synlogy/iscsi.yaml b/ci/configs/synlogy/dsm6/iscsi.yaml similarity index 100% rename from ci/configs/synlogy/iscsi.yaml rename to ci/configs/synlogy/dsm6/iscsi.yaml diff --git a/ci/configs/synlogy/dsm7/iscsi.yaml b/ci/configs/synlogy/dsm7/iscsi.yaml new file mode 100644 index 0000000..8b58913 --- /dev/null +++ b/ci/configs/synlogy/dsm7/iscsi.yaml @@ -0,0 +1,77 @@ +driver: synology-iscsi +httpConnection: + protocol: http + host: ${SYNOLOGY_HOST} + port: ${SYNOLOGY_PORT} + username: ${SYNOLOGY_USERNAME} + password: ${SYNOLOGY_PASSWORD} + allowInsecure: true + session: "democratic-csi-${CI_BUILD_KEY}" + serialize: true + +synology: + volume: ${SYNOLOGY_VOLUME} + +iscsi: + targetPortal: ${SYNOLOGY_HOST} + targetPortals: [] + baseiqn: "iqn.2000-01.com.synology:XpenoDsm62x." + namePrefix: "csi-${CI_BUILD_KEY}-" + nameSuffix: "-ci" + + 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 diff --git a/examples/synology-iscsi.yaml b/examples/synology-iscsi.yaml index cc8380f..e0515b2 100644 --- a/examples/synology-iscsi.yaml +++ b/examples/synology-iscsi.yaml @@ -34,6 +34,9 @@ iscsi: # These options can also be configured per storage-class: # See https://github.com/democratic-csi/democratic-csi/blob/master/docs/storage-class-parameters.md lunTemplate: + # can be static value or handlebars template + #description: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}" + # btrfs thin provisioning type: "BLUN" # tpws = Hardware-assisted zeroing diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index 00ac3f0..6697122 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -10,17 +10,46 @@ const USER_AGENT = "democratic-csi"; const __REGISTRY_NS__ = "SynologyHttpClient"; SYNO_ERRORS = { - 18990002: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The synology volume is out of disk space." }, - 18990318: { status: grpc.status.INVALID_ARGUMENT, message: "The requested lun type is incompatible with the Synology filesystem." }, - 18990538: { status: grpc.status.ALREADY_EXISTS, message: "A LUN with this name already exists." }, - 18990541: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The maximum number of LUNS has been reached." }, - 18990542: { status: grpc.status.RESOURCE_EXHAUSTED, message: "The maximum number if iSCSI target has been reached." }, - 18990744: { status: grpc.status.ALREADY_EXISTS, message: "An iSCSI target with this name already exists." }, + 400: { + status: grpc.status.UNAUTHENTICATED, + message: "Failed to authenticate to the Synology DSM", + }, + 18990002: { + status: grpc.status.RESOURCE_EXHAUSTED, + message: "The synology volume is out of disk space.", + }, + 18990318: { + status: grpc.status.INVALID_ARGUMENT, + message: + "The requested lun type is incompatible with the Synology filesystem.", + }, + 18990538: { + status: grpc.status.ALREADY_EXISTS, + message: "A LUN with this name already exists.", + }, + 18990541: { + status: grpc.status.RESOURCE_EXHAUSTED, + message: "The maximum number of LUNS has been reached.", + }, + 18990542: { + status: grpc.status.RESOURCE_EXHAUSTED, + message: "The maximum number if iSCSI target has been reached.", + }, + 18990744: { + status: grpc.status.ALREADY_EXISTS, + message: "An iSCSI target with this name already exists.", + }, 18990532: { status: grpc.status.NOT_FOUND, message: "No such snapshot." }, 18990500: { status: grpc.status.INVALID_ARGUMENT, message: "Bad LUN type" }, - 18990543: { status: grpc.status.RESOURCE_EXHAUSTED, message: "Maximum number of snapshots reached." }, - 18990635: { status: grpc.status.INVALID_ARGUMENT, message: "Invalid ioPolicy." } -} + 18990543: { + status: grpc.status.RESOURCE_EXHAUSTED, + message: "Maximum number of snapshots reached.", + }, + 18990635: { + status: grpc.status.INVALID_ARGUMENT, + message: "Invalid ioPolicy.", + }, +}; class SynologyError extends GrpcError { constructor(code, httpCode = undefined) { @@ -28,9 +57,11 @@ class SynologyError extends GrpcError { this.synoCode = code; this.httpCode = httpCode; if (code > 0) { - const error = SYNO_ERRORS[code] + const error = SYNO_ERRORS[code]; this.code = error?.status ?? grpc.status.UNKNOWN; - this.message = error?.message ?? `An unknown error occurred when executing a synology command (code = ${code}).`; + this.message = + error?.message ?? + `An unknown error occurred when executing a synology command (code = ${code}).`; } else { this.code = grpc.status.UNKNOWN; this.message = `The synology webserver returned a status code ${httpCode}`; @@ -95,6 +126,15 @@ class SynologyHttpClient { _.set(options, prop, "redacted"); } + prop = "params._sid"; + val = _.get(options, prop, false); + if (val) { + _.set(options, prop, "redacted"); + } + + delete options.httpAgent; + delete options.httpsAgent; + this.logger.debug("SYNOLOGY HTTP REQUEST: " + stringify(options)); this.logger.debug("SYNOLOGY HTTP ERROR: " + error); this.logger.debug("SYNOLOGY HTTP STATUS: " + response.statusCode); @@ -179,7 +219,7 @@ class SynologyHttpClient { } if (response.statusCode > 299 || response.statusCode < 200) { - reject(new SynologyError(null, response.statusCode)) + reject(new SynologyError(null, response.statusCode)); } if (response.body.success === false) { @@ -187,7 +227,9 @@ class SynologyHttpClient { if (response.body.error.code == 119 && sid == client.sid) { client.sid = null; } - reject(new SynologyError(response.body.error.code, response.statusCode)); + reject( + new SynologyError(response.body.error.code, response.statusCode) + ); } resolve(response); @@ -602,7 +644,12 @@ class SynologyHttpClient { ); } - async CreateClonedVolume(src_lun_uuid, dst_lun_name, dst_location, description) { + async CreateClonedVolume( + src_lun_uuid, + dst_lun_name, + dst_location, + description + ) { const create_cloned_volume = { api: "SYNO.Core.ISCSI.LUN", version: 1, @@ -619,7 +666,12 @@ class SynologyHttpClient { return await this.do_request("GET", "entry.cgi", create_cloned_volume); } - async CreateVolumeFromSnapshot(src_lun_uuid, snapshot_uuid, cloned_lun_name, description) { + async CreateVolumeFromSnapshot( + src_lun_uuid, + snapshot_uuid, + cloned_lun_name, + description + ) { const create_volume_from_snapshot = { api: "SYNO.Core.ISCSI.LUN", version: 1, diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index 825ea54..7ce5f18 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -1,11 +1,12 @@ +const _ = require("lodash"); const { CsiBaseDriver } = require("../index"); +const GeneralUtils = require("../../utils/general"); const { GrpcError, grpc } = require("../../utils/grpc"); +const Handlebars = require("handlebars"); const registry = require("../../utils/registry"); const SynologyHttpClient = require("./http").SynologyHttpClient; const semver = require("semver"); -const sleep = require("../../utils/general").sleep; const yaml = require("js-yaml"); -const GeneralUtils = require("../../utils/general"); const __REGISTRY_NS__ = "ControllerSynologyDriver"; @@ -146,19 +147,33 @@ class ControllerSynologyDriver extends CsiBaseDriver { getObjectFromDevAttribs(list = []) { if (!list) { - return {} + return {}; } return list.reduce( - (obj, item) => Object.assign(obj, {[item.dev_attrib]: item.enable}), {} - ) + (obj, item) => Object.assign(obj, { [item.dev_attrib]: item.enable }), + {} + ); } getDevAttribsFromObject(obj, keepNull = false) { - return Object.entries(obj).filter( - e => keepNull || (e[1] != null) - ).map( - e => ({dev_attrib: e[0], enable: e[1]}) - ); + return Object.entries(obj) + .filter((e) => keepNull || e[1] != null) + .map((e) => ({ dev_attrib: e[0], enable: e[1] })); + } + + parseParameterYamlData(data, fieldHint = "") { + try { + return yaml.load(data); + } catch { + if (err instanceof yaml.YAMLException) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `${fieldHint} not a valid YAML document.`.trim() + ); + } else { + throw err; + } + } } buildIscsiName(name) { @@ -183,14 +198,14 @@ class ControllerSynologyDriver extends CsiBaseDriver { * @returns {String} The location of the volume. */ getLocation() { - let location = this.options?.synology?.volume; - if (location === undefined) { - location = "volume1" + let location = _.get(this.options, "synology.volume"); + if (!location) { + location = "volume1"; } - if (!location.startsWith('/')) { - location = "/" + location + if (!location.startsWith("/")) { + location = "/" + location; } - return location + return location; } assertCapabilities(capabilities) { @@ -350,7 +365,9 @@ class ControllerSynologyDriver extends CsiBaseDriver { } let volume_context = {}; - const normalizedParameters = driver.getNormalizedParameters(call.request.parameters); + const normalizedParameters = driver.getNormalizedParameters( + call.request.parameters + ); switch (driver.getDriverShareType()) { case "nfs": // TODO: create volume here @@ -368,13 +385,53 @@ class ControllerSynologyDriver extends CsiBaseDriver { break; case "iscsi": let iscsiName = driver.buildIscsiName(name); - let storageClassTemplate; + let lunTemplate; + let targetTemplate; let data; let target; let lun_mapping; let lun_uuid; let existingLun; + lunTemplate = Object.assign( + {}, + _.get(driver.options, "iscsi.lunTemplate", {}), + driver.parseParameterYamlData( + _.get(normalizedParameters, "lunTemplate", "{}"), + "parameters.lunTemplate" + ), + driver.parseParameterYamlData( + _.get(call.request, "secrets.lunTemplate", "{}"), + "secrets.lunTemplate" + ) + ); + targetTemplate = Object.assign( + {}, + _.get(driver.options, "iscsi.targetTemplate", {}), + driver.parseParameterYamlData( + _.get(normalizedParameters, "targetTemplate", "{}"), + "parameters.targetTemplate" + ), + driver.parseParameterYamlData( + _.get(call.request, "secrets.targetTemplate", "{}"), + "secrets.targetTemplate" + ) + ); + + // render the template for description + if (lunTemplate.description) { + lunTemplate.description = Handlebars.compile(lunTemplate.description)( + { + name: call.request.name, + parameters: call.request.parameters, + csi: { + name: this.ctx.args.csiName, + version: this.ctx.args.csiVersion, + }, + } + ); + } + // ensure volumes with the same name being requested a 2nd time but with a different size fails try { let lun = await httpClient.GetLunByName(iscsiName); @@ -429,10 +486,11 @@ class ControllerSynologyDriver extends CsiBaseDriver { src_lun_uuid = await httpClient.GetLunByID(src_lun_uuid).uuid; } - let snapshot = await httpClient.GetSnapshotByLunUUIDAndSnapshotUUID( - src_lun_uuid, - snapshot_uuid - ); + let snapshot = + await httpClient.GetSnapshotByLunUUIDAndSnapshotUUID( + src_lun_uuid, + snapshot_uuid + ); if (!snapshot) { throw new GrpcError( grpc.status.NOT_FOUND, @@ -446,7 +504,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { src_lun_uuid, snapshot_uuid, iscsiName, - normalizedParameters.description + lunTemplate.description ); } break; @@ -474,7 +532,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { src_lun_uuid, iscsiName, driver.getLocation(), - normalizedParameters.description + lunTemplate.description ); } break; @@ -494,62 +552,22 @@ class ControllerSynologyDriver extends CsiBaseDriver { } } else { // create lun - try { - storageClassTemplate = yaml.load(normalizedParameters.lunTemplate ?? "") - const devAttribs = driver.getDevAttribsFromObject(Object.assign( - {}, - driver.getObjectFromDevAttribs(driver.options.iscsi.lunTemplate?.dev_attribs), - driver.getObjectFromDevAttribs(storageClassTemplate?.dev_attribs) - )) - data = Object.assign({}, driver.options.iscsi.lunTemplate, storageClassTemplate, { - name: iscsiName, - location: driver.getLocation(), - size: capacity_bytes, - dev_attribs: devAttribs - }); + data = Object.assign({}, lunTemplate, { + name: iscsiName, + location: driver.getLocation(), + size: capacity_bytes, + }); - lun_uuid = await httpClient.CreateLun(data); - } catch (err) { - if (err instanceof yaml.YAMLException) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `The lunTemplate on StorageClass is not a valid YAML document.` - ); - } else { - throw err - } - } + lun_uuid = await httpClient.CreateLun(data); } // create target let iqn = driver.options.iscsi.baseiqn + iscsiName; - try { - storageClassTemplate = yaml.load(normalizedParameters.targetTemplate ?? "") - } catch (err) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `The targetTemplate on StorageClass is not a valid YAML document.` - ); - } - data = Object.assign({}, driver.options.iscsi.targetTemplate, storageClassTemplate, { + data = Object.assign({}, targetTemplate, { name: iscsiName, iqn, }); - 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; - } - } let target_id = await httpClient.CreateTarget(data); //target = await httpClient.GetTargetByTargetID(target_id); target = await httpClient.GetTargetByIQN(iqn); @@ -924,6 +942,24 @@ class ControllerSynologyDriver extends CsiBaseDriver { ); } + const normalizedParameters = driver.getNormalizedParameters( + call.request.parameters + ); + let lunSnapshotTemplate; + + lunSnapshotTemplate = Object.assign( + {}, + _.get(driver.options, "iscsi.lunSnapshotTemplate", {}), + driver.parseParameterYamlData( + _.get(normalizedParameters, "lunSnapshotTemplate", "{}"), + "parameters.lunSnapshotTemplate" + ), + driver.parseParameterYamlData( + _.get(call.request, "secrets.lunSnapshotTemplate", "{}"), + "secrets.lunSnapshotTemplate" + ) + ); + // check for other snapshopts with the same name on other volumes and fail as appropriate // TODO: technically this should only be checking lun/snapshots relevant to this specific install of the driver // but alas an isolation/namespacing mechanism does not exist in synology @@ -939,19 +975,9 @@ class ControllerSynologyDriver extends CsiBaseDriver { // check for already exists let snapshot; - let snapshotClassTemplate; snapshot = await httpClient.GetSnapshotByLunUUIDAndName(lun.uuid, name); if (!snapshot) { - const normalizedParameters = driver.getNormalizedParameters(call.request.parameters); - try { - snapshotClassTemplate = yaml.load(normalizedParameters.lunSnapshotTemplate ?? ""); - } catch (err) { - throw new GrpcError( - grpc.status.INVALID_ARGUMENT, - `The snapshotTemplate on VolumeSnapshotClass is not a valid YAML document.` - ); - } - let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, snapshotClassTemplate, { + let data = Object.assign({}, lunSnapshotTemplate, { src_lun_uuid: lun.uuid, taken_by: "democratic-csi", description: name, //check diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 1705788..cd91b71 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -65,7 +65,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { const driver = this; driver.ctx.logger.verbose( - `generating smb share name for dataset: ${typeof datasetName} ${datasetName}` + `generating smb share name for dataset: ${datasetName}` ); let name = datasetName || ""; From e89b7cba1159c50ad9c3af8910e7ad97d6b4387d Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 20 Apr 2022 14:22:37 -0600 Subject: [PATCH 19/71] remove unsupported syntax Signed-off-by: Travis Glenn Hansen --- src/driver/controller-synology/http/index.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index 6697122..5e71b69 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -58,10 +58,11 @@ class SynologyError extends GrpcError { this.httpCode = httpCode; if (code > 0) { const error = SYNO_ERRORS[code]; - this.code = error?.status ?? grpc.status.UNKNOWN; + this.code = error && error.status ? error.status : grpc.status.UNKNOWN; this.message = - error?.message ?? - `An unknown error occurred when executing a synology command (code = ${code}).`; + error && error.message + ? error.message + : `An unknown error occurred when executing a synology command (code = ${code}).`; } else { this.code = grpc.status.UNKNOWN; this.message = `The synology webserver returned a status code ${httpCode}`; From 8a23376e307bffac2e51567cb3086efe84c3e4b7 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 20 Apr 2022 21:17:32 -0600 Subject: [PATCH 20/71] additional synology error codes, more robust redaction logic for secrets Signed-off-by: Travis Glenn Hansen --- src/driver/controller-synology/http/index.js | 70 ++++++++++++-------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index 5e71b69..dedd0f4 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -12,7 +12,12 @@ const __REGISTRY_NS__ = "SynologyHttpClient"; SYNO_ERRORS = { 400: { status: grpc.status.UNAUTHENTICATED, - message: "Failed to authenticate to the Synology DSM", + message: "Failed to authenticate to the Synology DSM.", + }, + 407: { + status: grpc.status.UNAUTHENTICATED, + message: + "IP has been blocked to the Synology DSM due to too many failed attempts.", }, 18990002: { status: grpc.status.RESOURCE_EXHAUSTED, @@ -35,6 +40,10 @@ SYNO_ERRORS = { status: grpc.status.RESOURCE_EXHAUSTED, message: "The maximum number if iSCSI target has been reached.", }, + 18990708: { + status: grpc.status.INVALID_ARGUMENT, + message: "Bad target auth info.", + }, 18990744: { status: grpc.status.ALREADY_EXISTS, message: "An iSCSI target with this name already exists.", @@ -109,38 +118,45 @@ class SynologyHttpClient { let prop; let val; - prop = "auth.username"; - val = _.get(options, prop, false); - if (val) { - _.set(options, prop, "redacted"); + const cleansedBody = JSON.parse(JSON.stringify(body)); + const cleansedOptions = JSON.parse(JSON.stringify(options)); + // This function handles arrays and objects + function recursiveCleanse(obj) { + for (const k in obj) { + if (typeof obj[k] == "object" && obj[k] !== null) { + recursiveCleanse(obj[k]); + } else { + if ( + [ + "account", + "passwd", + "username", + "password", + "_sid", + "sid", + "Authorization", + "authorization", + "user", + "mutual_user", + "mutual_password", + ].includes(k) + ) { + obj[k] = "redacted"; + } + } + } } + recursiveCleanse(cleansedBody); + recursiveCleanse(cleansedOptions); - prop = "auth.password"; - val = _.get(options, prop, false); - if (val) { - _.set(options, prop, "redacted"); - } + delete cleansedOptions.httpAgent; + delete cleansedOptions.httpsAgent; - prop = "headers.Authorization"; - val = _.get(options, prop, false); - if (val) { - _.set(options, prop, "redacted"); - } - - prop = "params._sid"; - val = _.get(options, prop, false); - if (val) { - _.set(options, prop, "redacted"); - } - - delete options.httpAgent; - delete options.httpsAgent; - - this.logger.debug("SYNOLOGY HTTP REQUEST: " + stringify(options)); + this.logger.debug("SYNOLOGY HTTP REQUEST: " + stringify(cleansedOptions)); this.logger.debug("SYNOLOGY HTTP ERROR: " + error); this.logger.debug("SYNOLOGY HTTP STATUS: " + response.statusCode); this.logger.debug("SYNOLOGY HTTP HEADERS: " + stringify(response.headers)); - this.logger.debug("SYNOLOGY HTTP BODY: " + stringify(body)); + this.logger.debug("SYNOLOGY HTTP BODY: " + stringify(cleansedBody)); } async do_request(method, path, data = {}, options = {}) { From 76de7ca45115d76e42e53fb6330b8d04c44e18ed Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 20 Apr 2022 23:14:49 -0600 Subject: [PATCH 21/71] avoid circular issues Signed-off-by: Travis Glenn Hansen --- src/driver/controller-synology/http/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index dedd0f4..9298d0c 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -118,8 +118,8 @@ class SynologyHttpClient { let prop; let val; - const cleansedBody = JSON.parse(JSON.stringify(body)); - const cleansedOptions = JSON.parse(JSON.stringify(options)); + const cleansedBody = JSON.parse(stringify(body)); + const cleansedOptions = JSON.parse(stringify(options)); // This function handles arrays and objects function recursiveCleanse(obj) { for (const k in obj) { From 68022bb4e53d992e9140b8e4a8494037b8a1b230 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 21 Apr 2022 09:00:45 -0600 Subject: [PATCH 22/71] more robust logging logic to prevent errors Signed-off-by: Travis Glenn Hansen --- src/driver/controller-synology/http/index.js | 11 ++++++----- src/driver/freenas/http/index.js | 8 ++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index 9298d0c..be9fd40 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -115,9 +115,6 @@ class SynologyHttpClient { } log_response(error, response, body, options) { - let prop; - let val; - const cleansedBody = JSON.parse(stringify(body)); const cleansedOptions = JSON.parse(stringify(options)); // This function handles arrays and objects @@ -154,8 +151,12 @@ class SynologyHttpClient { this.logger.debug("SYNOLOGY HTTP REQUEST: " + stringify(cleansedOptions)); this.logger.debug("SYNOLOGY HTTP ERROR: " + error); - this.logger.debug("SYNOLOGY HTTP STATUS: " + response.statusCode); - this.logger.debug("SYNOLOGY HTTP HEADERS: " + stringify(response.headers)); + this.logger.debug( + "SYNOLOGY HTTP STATUS: " + _.get(response, "statusCode", "") + ); + this.logger.debug( + "SYNOLOGY HTTP HEADERS: " + stringify(_.get(response, "headers", "")) + ); this.logger.debug("SYNOLOGY HTTP BODY: " + stringify(cleansedBody)); } diff --git a/src/driver/freenas/http/index.js b/src/driver/freenas/http/index.js index 9b0a02b..d800e34 100644 --- a/src/driver/freenas/http/index.js +++ b/src/driver/freenas/http/index.js @@ -127,8 +127,12 @@ class Client { this.logger.debug("FREENAS HTTP REQUEST: " + stringify(options)); this.logger.debug("FREENAS HTTP ERROR: " + error); - this.logger.debug("FREENAS HTTP STATUS: " + response.statusCode); - this.logger.debug("FREENAS HTTP HEADERS: " + stringify(response.headers)); + this.logger.debug( + "FREENAS HTTP STATUS: " + _.get(response, "statusCode", "") + ); + this.logger.debug( + "FREENAS HTTP HEADERS: " + stringify(_.get(response, "headers", "")) + ); this.logger.debug("FREENAS HTTP BODY: " + stringify(body)); } From 78d50b4836ef4b45105747f409fa5c5703688da4 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sun, 24 Apr 2022 08:38:11 -0600 Subject: [PATCH 23/71] support ntfs, more robust detection of device formatting, more robust logic for iscsi device detection Signed-off-by: Travis Glenn Hansen --- src/driver/index.js | 77 ++++++++++++++++++-- src/utils/filesystem.js | 157 ++++++++++++++++++++++++++++++++++++---- src/utils/general.js | 2 +- 3 files changed, 216 insertions(+), 20 deletions(-) diff --git a/src/driver/index.js b/src/driver/index.js index 58d2f09..0141aa2 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -550,6 +550,7 @@ class CsiBaseDriver { const iscsi = driver.getDefaultISCSIInstance(); let result; let device; + let block_device_info; const volume_id = call.request.volume_id; if (!volume_id) { @@ -978,6 +979,27 @@ class CsiBaseDriver { fs_type = "ext4"; } + if (fs_type == "ntfs") { + block_device_info = await filesystem.getBlockDevice(device); + let partition_count = + await filesystem.getBlockDevicePartitionCount(device); + if (partition_count > 0) { + device = await filesystem.getBlockDeviceLargestPartition( + device + ); + } else { + // partion/gpt + await filesystem.partitionDevice( + device, + "gpt", + "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7" + ); + device = await filesystem.getBlockDeviceLargestPartition( + device + ); + } + } + if (await filesystem.isBlockDevice(device)) { // format result = await filesystem.deviceIsFormatted(device); @@ -1043,10 +1065,39 @@ class CsiBaseDriver { staging_target_path ); if (!result) { + // expand fs if necessary + if (await filesystem.isBlockDevice(device)) { + // go ahead and expand fs (this covers cloned setups where expand is not explicitly invoked) + switch (fs_type) { + case "exfat": + case "ntfs": + case "vfat": + //await filesystem.checkFilesystem(device, fs_info.type); + await filesystem.expandFilesystem(device, fs_type); + break; + } + } + + let mount_fs_type = fs_type; + if (mount_fs_type == "ntfs") { + mount_fs_type = "ntfs3"; + } + + // handle volume_mount_group where appropriate + if (volume_mount_group) { + switch (fs_type) { + case "exfat": + case "ntfs": + case "vfat": + mount_flags.push(`gid=${volume_mount_group}`); + break; + } + } + await mount.mount( device, staging_target_path, - ["-t", fs_type].concat(["-o", mount_flags.join(",")]) + ["-t", mount_fs_type].concat(["-o", mount_flags.join(",")]) ); } @@ -1054,8 +1105,8 @@ class CsiBaseDriver { if (await filesystem.isBlockDevice(device)) { // go ahead and expand fs (this covers cloned setups where expand is not explicitly invoked) switch (fs_type) { - case "ext4": case "ext3": + case "ext4": case "ext4dev": //await filesystem.checkFilesystem(device, fs_info.type); try { @@ -1094,6 +1145,11 @@ class CsiBaseDriver { fs_type ); break; + case "exfat": + case "ntfs": + case "vfat": + // noop + break; default: // unsupported filesystem throw new GrpcError( @@ -1613,7 +1669,11 @@ class CsiBaseDriver { // TODO: this could be made async to detach all simultaneously for (const block_device_info_i of realBlockDeviceInfos) { - if (block_device_info_i.tran == "iscsi") { + if (await filesystem.deviceIsIscsi(block_device_info_i.path)) { + let parent_block_device = await filesystem.getBlockDeviceParent( + block_device_info_i.path + ); + // figure out which iscsi session this belongs to and logout // scan /dev/disk/by-path/ip-*? // device = `/dev/disk/by-path/ip-${volume_context.portal}-iscsi-${volume_context.iqn}-lun-${volume_context.lun}`; @@ -1632,7 +1692,7 @@ class CsiBaseDriver { session.attached_scsi_devices.host.devices.some( (device) => { if ( - device.attached_scsi_disk == block_device_info_i.name + device.attached_scsi_disk == parent_block_device.name ) { return true; } @@ -2439,8 +2499,8 @@ class CsiBaseDriver { fs_type = fs_info.type; if (fs_type) { switch (fs_type) { - case "ext4": case "ext3": + case "ext4": case "ext4dev": //await filesystem.checkFilesystem(device, fs_info.type); await filesystem.expandFilesystem(device, fs_type); @@ -2453,6 +2513,13 @@ class CsiBaseDriver { await filesystem.expandFilesystem(device_path, fs_type); } break; + case "exfat": + case "ntfs": + case "vfat": + // TODO: return error here, cannot be expanded while online + //await filesystem.checkFilesystem(device, fs_info.type); + //await filesystem.expandFilesystem(device, fs_type); + break; default: // unsupported filesystem throw new GrpcError( diff --git a/src/utils/filesystem.js b/src/utils/filesystem.js index deb62ea..26f28b8 100644 --- a/src/utils/filesystem.js +++ b/src/utils/filesystem.js @@ -303,7 +303,7 @@ class Filesystem { async getBlockDevice(device) { const filesystem = this; device = await filesystem.realpath(device); - let args = ["-a", "-b", "-l", "-J", "-O"]; + let args = ["-a", "-b", "-J", "-O"]; args.push(device); let result; @@ -317,30 +317,121 @@ class Filesystem { } /** - * blkid -p -o export + * + * @param {*} device + * @returns + */ + async getBlockDeviceLargestPartition(device) { + const filesystem = this; + let block_device_info = await filesystem.getBlockDevice(device); + if (block_device_info.children) { + let child; + for (const child_i of block_device_info.children) { + if (child_i.type == "part") { + if (!child) { + child = child_i; + } else { + if (child_i.size > child.size) { + child = child_i; + } + } + } + } + return `${child.path}`; + } + } + + /** + * + * @param {*} device + * @returns + */ + async getBlockDevicePartitionCount(device) { + const filesystem = this; + let count = 0; + let block_device_info = await filesystem.getBlockDevice(device); + if (block_device_info.children) { + for (const child_i of block_device_info.children) { + if (child_i.type == "part") { + count++; + } + } + } + return count; + } + + /** + * type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 = linux + * type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 = ntfs + * type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B = EFI + * + * @param {*} device + * @param {*} label + * @param {*} type + */ + async partitionDevice( + device, + label = "gpt", + type = "0FC63DAF-8483-4772-8E79-3D69D8477DE4" + ) { + const filesystem = this; + let args = [device]; + let result; + + try { + result = await filesystem.exec("sfdisk", args, { + stdin: `label: ${label}\n`, + }); + result = await filesystem.exec("sfdisk", args, { + stdin: `type=${type}\n`, + }); + } catch (err) { + throw err; + } + } + + /** * * @param {*} device */ async deviceIsFormatted(device) { const filesystem = this; - let args = ["-p", "-o", "export", device]; let result; try { - result = await filesystem.exec("blkid", args); + result = await filesystem.getBlockDevice(device); + return result.fstype ? true : false; } catch (err) { - if (err.code == 2 && err.stderr.includes("No such device or address")) { - throw err; - } - - if (err.code == 2) { - return false; - } - throw err; } + } - return true; + async deviceIsIscsi(device) { + const filesystem = this; + let result; + + do { + if (result) { + device = `/dev/${result.pkname}`; + } + result = await filesystem.getBlockDevice(device); + } while (result.pkname); + + return result && result.tran == "iscsi"; + } + + async getBlockDeviceParent(device) { + const filesystem = this; + let result; + + do { + if (result) { + device = `/dev/${result.pkname}`; + } + result = await filesystem.getBlockDevice(device); + } while (result.pkname); + + return result; } /** @@ -463,6 +554,9 @@ class Filesystem { args = args.concat(["filesystem", "resize", "max"]); args.push(device); // in this case should be a mounted path break; + case "exfat": + // https://github.com/exfatprogs/exfatprogs/issues/134 + return; case "ext4": case "ext3": case "ext4dev": @@ -470,6 +564,13 @@ class Filesystem { args = args.concat(options); args.push(device); break; + case "ntfs": + // must be unmounted + command = "ntfsresize"; + args = args.concat(options); + //args = args.concat(["-s", "max"]); + args.push(device); + break; case "xfs": command = "xfs_growfs"; args = args.concat(options); @@ -526,6 +627,14 @@ class Filesystem { args.push("-f"); args.push("-p"); break; + case "ntfs": + /** + * -b, --clear-bad-sectors Clear the bad sector list + * -d, --clear-dirty Clear the volume dirty flag + */ + command = "ntfsfix"; + args.push(device); + break; case "xfs": command = "xfs_repair"; args = args.concat(["-o", "force_geometry"]); @@ -612,6 +721,12 @@ class Filesystem { //options.timeout = DEFAULT_TIMEOUT; } + let stdin; + if (options.stdin) { + stdin = options.stdin; + delete options.stdin; + } + const filesystem = this; args = args || []; @@ -619,13 +734,27 @@ class Filesystem { args.unshift(command); command = filesystem.options.paths.sudo; } - console.log("executing filesystem command: %s %s", command, args.join(" ")); + let command_log = `${command} ${args.join(" ")}`.trim(); + if (stdin) { + command_log = `echo '${stdin}' | ${command_log}` + .trim() + .replace(/\n/, "\\n"); + } + console.log("executing filesystem command: %s", command_log); return new Promise((resolve, reject) => { const child = filesystem.options.executor.spawn(command, args, options); let stdout = ""; let stderr = ""; + child.on("spawn", function () { + if (stdin) { + child.stdin.setEncoding("utf-8"); + child.stdin.write(stdin); + child.stdin.end(); + } + }); + child.stdout.on("data", function (data) { stdout = stdout + data; }); diff --git a/src/utils/general.js b/src/utils/general.js index ed25b3c..6077802 100644 --- a/src/utils/general.js +++ b/src/utils/general.js @@ -170,7 +170,7 @@ function stringify(value) { } function default_supported_block_filesystems() { - return ["btrfs", "ext3", "ext4", "ext4dev", "xfs", "ntfs"]; + return ["btrfs", "exfat", "ext3", "ext4", "ext4dev", "ntfs", "vfat", "xfs"]; } function default_supported_file_filesystems() { From 1af37106c0cbc5c30958c10e907db9c8a39322e1 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sun, 24 Apr 2022 22:34:20 -0600 Subject: [PATCH 24/71] more robust ntfs support on linux Signed-off-by: Travis Glenn Hansen --- src/driver/index.js | 56 ++++++++++++++++++++++++--------- src/utils/filesystem.js | 70 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 109 insertions(+), 17 deletions(-) diff --git a/src/driver/index.js b/src/driver/index.js index 0141aa2..3c4281e 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -304,6 +304,13 @@ class CsiBaseDriver { // throw new Error(`failed to retrieve volume_context for ${volume_id}`); //} + if (!volume_context) { + volume_context = _.get( + driver.options, + `_private.volume_context.${volume_id}` + ); + } + driver.ctx.logger.debug( "retrived derived volume_context %j", volume_context @@ -979,21 +986,42 @@ class CsiBaseDriver { fs_type = "ext4"; } + let partition_count = + await filesystem.getBlockDevicePartitionCount(device); + if (partition_count > 0) { + // data partion MUST be the last partition on the drive + // to properly support expand/resize operations + device = await filesystem.getBlockDeviceLastPartition(device); + driver.ctx.logger.debug( + `device has partitions, mount device is: ${device}` + ); + + await filesystem.expandPartition(device); + } + if (fs_type == "ntfs") { - block_device_info = await filesystem.getBlockDevice(device); - let partition_count = - await filesystem.getBlockDevicePartitionCount(device); - if (partition_count > 0) { - device = await filesystem.getBlockDeviceLargestPartition( - device - ); - } else { - // partion/gpt - await filesystem.partitionDevice( - device, - "gpt", - "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7" - ); + if (partition_count < 1) { + // dos is what csi-proxy uses by default + let ntfs_partition_label = "dos"; + switch (ntfs_partition_label.toLowerCase()) { + case "dos": + // partion dos + await filesystem.partitionDevice(device, "dos", "07"); + break; + case "gpt": + // partion gpt + await filesystem.partitionDevice( + device, + "gpt", + "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7" + ); + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported ntfs_partition_label: ${ntfs_partition_label}` + ); + } device = await filesystem.getBlockDeviceLargestPartition( device ); diff --git a/src/utils/filesystem.js b/src/utils/filesystem.js index 26f28b8..284c713 100644 --- a/src/utils/filesystem.js +++ b/src/utils/filesystem.js @@ -341,6 +341,33 @@ class Filesystem { } } + /** + * + * @param {*} device + * @returns + */ + async getBlockDeviceLastPartition(device) { + const filesystem = this; + let block_device_info = await filesystem.getBlockDevice(device); + if (block_device_info.children) { + let child; + for (const child_i of block_device_info.children) { + if (child_i.type == "part") { + if (!child) { + child = child_i; + } else { + let minor = child["maj:min"].split(":")[1]; + let minor_i = child_i["maj:min"].split(":")[1]; + if (minor_i > minor) { + child = child_i; + } + } + } + } + return `${child.path}`; + } + } + /** * * @param {*} device @@ -360,10 +387,22 @@ class Filesystem { return count; } + async getBlockDeviceHasParitionTable(device) { + const filesystem = this; + let block_device_info = await filesystem.getBlockDevice(device); + + return block_device_info.pttype ? true : false; + } + /** - * type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 = linux - * type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 = ntfs - * type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B = EFI + * DOS + * - type=83 = Linux + * - type=07 = HPFS/NTFS/exFAT + * + * GPT + * - type=0FC63DAF-8483-4772-8E79-3D69D8477DE4 = linux + * - type=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7 = ntfs + * - type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B = EFI * * @param {*} device * @param {*} label @@ -534,6 +573,31 @@ class Filesystem { } } + async expandPartition(device) { + const filesystem = this; + const command = "growpart"; + const args = []; + + let block_device_info = await filesystem.getBlockDevice(device); + let device_fs_info = await filesystem.getDeviceFilesystemInfo(device); + let growpart_partition = device_fs_info["part_entry_number"]; + let parent_block_device = await filesystem.getBlockDeviceParent(device); + + args.push(parent_block_device.path, growpart_partition); + + try { + await filesystem.exec(command, args); + } catch (err) { + if ( + err.code == 1 && + err.stdout && + err.stdout.includes("could only be grown by") + ) { + return; + } + } + } + /** * expand a given filesystem * From 7fe916c9163d39dad6ebd2df7fc7944be2d4c30c Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 25 Apr 2022 10:45:46 -0600 Subject: [PATCH 25/71] more ntfs fixes Signed-off-by: Travis Glenn Hansen --- src/driver/index.js | 10 ++----- src/utils/filesystem.js | 62 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/driver/index.js b/src/driver/index.js index 3c4281e..8a0bd20 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -1001,8 +1001,8 @@ class CsiBaseDriver { if (fs_type == "ntfs") { if (partition_count < 1) { - // dos is what csi-proxy uses by default - let ntfs_partition_label = "dos"; + // gpt is what csi-proxy uses by default + let ntfs_partition_label = "gpt"; switch (ntfs_partition_label.toLowerCase()) { case "dos": // partion dos @@ -1010,11 +1010,7 @@ class CsiBaseDriver { break; case "gpt": // partion gpt - await filesystem.partitionDevice( - device, - "gpt", - "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7" - ); + await filesystem.partitionDeviceWindows(device); break; default: throw new GrpcError( diff --git a/src/utils/filesystem.js b/src/utils/filesystem.js index 284c713..d6fdbb9 100644 --- a/src/utils/filesystem.js +++ b/src/utils/filesystem.js @@ -429,6 +429,60 @@ class Filesystem { } } + /** + * mimic the behavior of partitioning a new data drive in windows directly + * + * https://en.wikipedia.org/wiki/Microsoft_Reserved_Partition + * + * @param {*} device + */ + async partitionDeviceWindows(device) { + const filesystem = this; + let args = [device]; + let result; + let block_device_info = await filesystem.getBlockDevice(device); + + //let sixteen_megabytes = 16777216; + //let thirtytwo_megabytes = 33554432; + //let onehundredtwentyeight_megabytes = 134217728; + + let msr_partition_size = "16M"; + let label = "gpt"; + let msr_guid = "E3C9E316-0B5C-4DB8-817D-F92DF00215AE"; + let ntfs_guid = "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7"; + + if (block_device_info.type != "disk") { + throw new Error( + `cannot partition device of type: ${block_device_info.type}` + ); + } + + /** + * On drives less than 16GB in size, the MSR is 32MB. + * On drives greater than or equal two 16GB, the MSR is 128 MB. + * It is only 128 MB for Win 7/8 ( On drives less than 16GB in size, the MSR is 32MB ) & 16 MB for win 10! + */ + let msr_partition_size_break = 17179869184; // 16GB + + // TODO: this size may be sectors so not really disk size in terms of GB + if (block_device_info.size >= msr_partition_size_break) { + // ignoring for now, appears windows 10+ use 16MB always + //msr_partition_size = "128M"; + } + + try { + result = await filesystem.exec("sfdisk", args, { + stdin: `label: ${label}\n`, + }); + // must send ALL partitions at once (newline separated), cannot send them 1 at a time + result = await filesystem.exec("sfdisk", args, { + stdin: `size=${msr_partition_size},type=${msr_guid}\ntype=${ntfs_guid}\n`, + }); + } catch (err) { + throw err; + } + } + /** * * @param {*} device @@ -631,6 +685,9 @@ class Filesystem { case "ntfs": // must be unmounted command = "ntfsresize"; + await filesystem.exec(command, ["-c", device]); + await filesystem.exec(command, ["-n", device]); + args = args.concat("-P", "-f"); args = args.concat(options); //args = args.concat(["-s", "max"]); args.push(device); @@ -651,6 +708,10 @@ class Filesystem { try { result = await filesystem.exec(command, args); + // must clear the dirty bit after resize + if (fstype.toLowerCase() == "ntfs") { + await filesystem.exec("ntfsfix", ["-d", device]); + } return result; } catch (err) { throw err; @@ -697,6 +758,7 @@ class Filesystem { * -d, --clear-dirty Clear the volume dirty flag */ command = "ntfsfix"; + args.puuh("-d"); args.push(device); break; case "xfs": From 7a5b6b58b1b625ad042aaea5ad24057ba2c229fd Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 5 May 2022 10:38:30 -0600 Subject: [PATCH 26/71] native windows support --- .gitignore | 2 + bin/democratic-csi | 1 + ci/bin/build.ps1 | 8 + ci/bin/helper.ps1 | 16 + ci/bin/launch-csi-grpc-proxy.ps1 | 15 + ci/bin/launch-csi-sanity.ps1 | 60 +++ ci/bin/launch-server.ps1 | 29 ++ ci/bin/run.ps1 | 107 +++++ ci/configs/windows/iscsi.yaml | 31 ++ ci/configs/windows/smb.yaml | 40 ++ src/driver/controller-zfs/index.js | 5 +- src/driver/index.js | 673 +++++++++++++++++++++++++- src/utils/csi_proxy_client.js | 18 +- src/utils/powershell.js | 85 ++++ src/utils/windows.js | 729 +++++++++++++++++++++++++++++ 15 files changed, 1810 insertions(+), 9 deletions(-) create mode 100644 ci/bin/build.ps1 create mode 100644 ci/bin/helper.ps1 create mode 100644 ci/bin/launch-csi-grpc-proxy.ps1 create mode 100644 ci/bin/launch-csi-sanity.ps1 create mode 100644 ci/bin/launch-server.ps1 create mode 100644 ci/bin/run.ps1 create mode 100644 ci/configs/windows/iscsi.yaml create mode 100644 ci/configs/windows/smb.yaml create mode 100644 src/utils/powershell.js create mode 100644 src/utils/windows.js diff --git a/.gitignore b/.gitignore index 26ff709..2855d28 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ +**~ node_modules dev +/ci/bin/*dev* diff --git a/bin/democratic-csi b/bin/democratic-csi index 6a0bec9..f616a8b 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -183,6 +183,7 @@ async function requestHandlerProxy(call, callback, serviceMethodName) { // for testing purposes //await GeneralUtils.sleep(10000); + //throw new Error("fake error"); // for CI/testing purposes if (["NodePublishVolume", "NodeStageVolume"].includes(serviceMethodName)) { diff --git a/ci/bin/build.ps1 b/ci/bin/build.ps1 new file mode 100644 index 0000000..e883378 --- /dev/null +++ b/ci/bin/build.ps1 @@ -0,0 +1,8 @@ +node --version +npm --version + +# install deps +npm i + +# tar node_modules to keep the number of files low to upload +tar -zcf node_modules.tar.gz node_modules diff --git a/ci/bin/helper.ps1 b/ci/bin/helper.ps1 new file mode 100644 index 0000000..6e8a357 --- /dev/null +++ b/ci/bin/helper.ps1 @@ -0,0 +1,16 @@ +#Set-StrictMode -Version Latest +#$ErrorActionPreference = "Stop" +#$PSDefaultParameterValues['*:ErrorAction'] = "Stop" +function ThrowOnNativeFailure { + if (-not $?) { + throw 'Native Failure' + } +} + +function psenvsubstr($data) { + foreach($v in Get-ChildItem env:) { + $key = '${' + $v.Name + '}' + $data = $data.Replace($key, $v.Value) + } + return $data +} \ No newline at end of file diff --git a/ci/bin/launch-csi-grpc-proxy.ps1 b/ci/bin/launch-csi-grpc-proxy.ps1 new file mode 100644 index 0000000..8dbf6b7 --- /dev/null +++ b/ci/bin/launch-csi-grpc-proxy.ps1 @@ -0,0 +1,15 @@ +if (! $PSScriptRoot) { + $PSScriptRoot = $args[0] +} + +. "${PSScriptRoot}\helper.ps1" + +Set-Location $env:PWD + +Write-Output "launching csi-grpc-proxy" + +$env:PROXY_TO = "npipe://" + $env:NPIPE_ENDPOINT +$env:BIND_TO = "unix://" + $env:CSI_ENDPOINT + +# https://stackoverflow.com/questions/2095088/error-when-calling-3rd-party-executable-from-powershell-when-using-an-ide +csi-grpc-proxy.exe 2>&1 | % { "$_" } diff --git a/ci/bin/launch-csi-sanity.ps1 b/ci/bin/launch-csi-sanity.ps1 new file mode 100644 index 0000000..d36ab5e --- /dev/null +++ b/ci/bin/launch-csi-sanity.ps1 @@ -0,0 +1,60 @@ +if (! $PSScriptRoot) { + $PSScriptRoot = $args[0] +} + +. "${PSScriptRoot}\helper.ps1" + +Set-Location $env:PWD + +$exit_code = 0 +$tmpdir = New-Item -ItemType Directory -Path ([System.IO.Path]::GetTempPath()) -Name ([System.IO.Path]::GetRandomFileName()) +$env:CSI_SANITY_TEMP_DIR = $tmpdir.FullName + +if (! $env:CSI_SANITY_FOCUS) { + $env:CSI_SANITY_FOCUS = "Node Service" +} + +if (! $env:CSI_SANITY_SKIP) { + $env:CSI_SANITY_SKIP = "" +} + +# cleanse endpoint to something csi-sanity plays nicely with +$endpoint = ${env:CSI_ENDPOINT} +$endpoint = $endpoint.replace("C:\", "/") +$endpoint = $endpoint.replace("\", "/") + +Write-Output "launching csi-sanity" +Write-Output "connecting to: ${endpoint}" +Write-Output "skip: ${env:CSI_SANITY_SKIP}" +Write-Output "focus: ${env:CSI_SANITY_FOCUS}" + +csi-sanity.exe -"csi.endpoint" "unix://${endpoint}" ` + -"ginkgo.failFast" ` + -"csi.mountdir" "${env:CSI_SANITY_TEMP_DIR}\mnt" ` + -"csi.stagingdir" "${env:CSI_SANITY_TEMP_DIR}\stage" ` + -"csi.testvolumeexpandsize" 2147483648 ` + -"csi.testvolumesize" 1073741824 ` + -"ginkgo.focus" "${env:CSI_SANITY_FOCUS}" + +# does not work the same as linux for some reason +#-"ginkgo.skip" "${env:CSI_SANITY_SKIP}" ` + +if (-not $?) { + $exit_code = $LASTEXITCODE + Write-Output "csi-sanity exit code: ${exit_code}" + $exit_code = 1 +} + +# remove tmp dir +Remove-Item -Path "$env:CSI_SANITY_TEMP_DIR" -Force -Recurse + +#Exit $exit_code +Write-Output "exiting with exit code: ${exit_code}" + +if ($exit_code -gt 0) { + throw "csi-sanity failed" +} + +# these do not work for whatever reason +#Exit $exit_code +#[System.Environment]::Exit($exit_code) diff --git a/ci/bin/launch-server.ps1 b/ci/bin/launch-server.ps1 new file mode 100644 index 0000000..3cf17a7 --- /dev/null +++ b/ci/bin/launch-server.ps1 @@ -0,0 +1,29 @@ +if (! $PSScriptRoot) { + $PSScriptRoot = $args[0] +} + +. "${PSScriptRoot}\helper.ps1" + +Set-Location $env:PWD +Write-Output "launching server" + +$env:LOG_LEVEL = "debug" +$env:CSI_VERSION = "1.5.0" +$env:CSI_NAME = "driver-test" +$env:CSI_SANITY = "1" + +if (! ${env:CONFIG_FILE}) { + $env:CONFIG_FILE = $env:TEMP + "\csi-config-" + $env:CI_BUILD_KEY + ".yaml" + if ($env:TEMPLATE_CONFIG_FILE) { + $config_data = Get-Content "${env:TEMPLATE_CONFIG_FILE}" -Raw + $config_data = psenvsubstr($config_data) + $config_data | Set-Content "${env:CONFIG_FILE}" + } +} + +node "${PSScriptRoot}\..\..\bin\democratic-csi" ` + --log-level "$env:LOG_LEVEL" ` + --driver-config-file "$env:CONFIG_FILE" ` + --csi-version "$env:CSI_VERSION" ` + --csi-name "$env:CSI_NAME" ` + --server-socket "${env:NPIPE_ENDPOINT}" 2>&1 | % { "$_" } diff --git a/ci/bin/run.ps1 b/ci/bin/run.ps1 new file mode 100644 index 0000000..24a244c --- /dev/null +++ b/ci/bin/run.ps1 @@ -0,0 +1,107 @@ +# https://stackoverflow.com/questions/2095088/error-when-calling-3rd-party-executable-from-powershell-when-using-an-ide +# +# Examples: +# +# $mypath = $MyInvocation.MyCommand.Path +# Get-ChildItem env:\ +# Get-Job | Where-Object -Property State -eq “Running” +# Get-Location (like pwd) +# if ($null -eq $env:FOO) { $env:FOO = 'bar' } + +. "${PSScriptRoot}\helper.ps1" + +function Job-Cleanup() { + Get-Job | Stop-Job + Get-Job | Remove-Job +} + +# start clean +Job-Cleanup + +# install from artifacts +if (Test-Path "node_modules.tar.gz") { + Write-Output "extracting node_modules.tar.gz" + tar -zxf node_modules.tar.gz +} + +# setup env +$env:PWD = (Get-Location).Path +$env:CI_BUILD_KEY = ([guid]::NewGuid() -Split "-")[0] +$env:CSI_ENDPOINT = $env:TEMP + "\csi-sanity-" + $env:CI_BUILD_KEY + ".sock" +$env:NPIPE_ENDPOINT = "//./pipe/csi-sanity-" + $env:CI_BUILD_KEY + "csi.sock" + +# testing values +if (Test-Path "${PSScriptRoot}\run-dev.ps1") { + . "${PSScriptRoot}\run-dev.ps1" +} + +# launch server +$server_job = Start-Job -FilePath .\ci\bin\launch-server.ps1 -InitializationScript {} -ArgumentList $PSScriptRoot + +# launch csi-grpc-proxy +$csi_grpc_proxy_job = Start-Job -FilePath .\ci\bin\launch-csi-grpc-proxy.ps1 -InitializationScript {} -ArgumentList $PSScriptRoot + +# wait for socket to appear +$iter = 0 +$max_iter = 60 +$started = 1 +while (!(Test-Path "${env:CSI_ENDPOINT}")) { + $iter++ + Write-Output "Waiting for ${env:CSI_ENDPOINT} to appear" + Start-Sleep 1 + Get-Job | Receive-Job + if ($iter -gt $max_iter) { + Write-Output "${env:CSI_ENDPOINT} failed to appear" + $started = 0 + break + } +} + +# launch csi-sanity +if ($started -eq 1) { + $csi_sanity_job = Start-Job -FilePath .\ci\bin\launch-csi-sanity.ps1 -InitializationScript {} -ArgumentList $PSScriptRoot +} + +# https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-job?view=powershell-7.2 +# -ChildJobState +while ($csi_sanity_job -and ($csi_sanity_job.State -eq "Running" -or $csi_sanity_job.State -eq "NotStarted")) { + foreach ($job in Get-Job) { + try { + $job | Receive-Job + } + catch { + if ($job.State -ne "Failed") { + throw $_ + } + } + } +} + +# spew any remaining job output to the console +foreach ($job in Get-Job) { + try { + $job | Receive-Job + } + catch {} +} + +# wait for good measure +if ($csi_sanity_job) { + Wait-Job -Job $csi_sanity_job +} + +#Get-Job | fl + +$exit_code = 0 + +if (! $csi_sanity_job) { + $exit_code = 1 +} + +if ($csi_sanity_job -and $csi_sanity_job.State -eq "Failed") { + $exit_code = 1 +} + +# cleanup after ourselves +Job-Cleanup +Exit $exit_code diff --git a/ci/configs/windows/iscsi.yaml b/ci/configs/windows/iscsi.yaml new file mode 100644 index 0000000..38dc594 --- /dev/null +++ b/ci/configs/windows/iscsi.yaml @@ -0,0 +1,31 @@ +driver: zfs-generic-iscsi + +sshConnection: + host: ${SERVER_HOST} + port: 22 + username: ${SERVER_USERNAME} + password: ${SERVER_PASSWORD} + +zfs: + datasetParentName: tank/ci/${CI_BUILD_KEY}/v + detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s + + zvolCompression: + zvolDedup: + zvolEnableReservation: false + zvolBlocksize: + +iscsi: + targetPortal: ${SERVER_HOST} + interface: "" + namePrefix: "csi-ci-${CI_BUILD_KEY}" + nameSuffix: "" + shareStrategy: "targetCli" + shareStrategyTargetCli: + basename: "iqn.2003-01.org.linux-iscsi.ubuntu-19.x8664" + tpg: + attributes: + authentication: 0 + generate_node_acls: 1 + cache_dynamic_acls: 1 + demo_mode_write_protect: 0 diff --git a/ci/configs/windows/smb.yaml b/ci/configs/windows/smb.yaml new file mode 100644 index 0000000..272a590 --- /dev/null +++ b/ci/configs/windows/smb.yaml @@ -0,0 +1,40 @@ +driver: zfs-generic-smb + +sshConnection: + host: ${SERVER_HOST} + port: 22 + username: ${SERVER_USERNAME} + password: ${SERVER_PASSWORD} + +zfs: + datasetParentName: tank/ci/${CI_BUILD_KEY}/v + detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s + + datasetProperties: + #aclmode: restricted + #aclinherit: passthrough + #acltype: nfsv4 + casesensitivity: insensitive + + datasetEnableQuotas: true + datasetEnableReservation: false + datasetPermissionsMode: "0770" + datasetPermissionsUser: smbroot + datasetPermissionsGroup: smbroot + +smb: + shareHost: ${SERVER_HOST} + shareStrategy: "setDatasetProperties" + shareStrategySetDatasetProperties: + properties: + sharesmb: "on" + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" + +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index 94f7ea7..d88d150 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -1327,7 +1327,10 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { await zb.zfs.destroy(datasetName, { recurse: true, force: true }); success = true; } catch (err) { - if (err.toString().includes("dataset is busy")) { + if ( + err.toString().includes("dataset is busy") || + err.toString().includes("target is busy") + ) { current_try++; if (current_try > max_tries) { throw err; diff --git a/src/driver/index.js b/src/driver/index.js index 8a0bd20..bccbd78 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -18,6 +18,7 @@ const __REGISTRY_NS__ = "CsiBaseDriver"; const NODE_OS_DRIVER_CSI_PROXY = "csi-proxy"; const NODE_OS_DRIVER_POSIX = "posix"; +const NODE_OS_DRIVER_WINDOWS = "windows"; /** * common code shared between all drivers @@ -199,10 +200,14 @@ class CsiBaseDriver { } __getNodeOsDriver() { - if (this.getNodeIsWindows() || this.getCsiProxyEnabled()) { - return NODE_OS_DRIVER_CSI_PROXY; + if (this.getNodeIsWindows()) { + return NODE_OS_DRIVER_WINDOWS; } + //if (this.getNodeIsWindows() || this.getCsiProxyEnabled()) { + // return NODE_OS_DRIVER_CSI_PROXY; + //} + return NODE_OS_DRIVER_POSIX; } @@ -1215,6 +1220,383 @@ class CsiBaseDriver { ); } break; + case NODE_OS_DRIVER_WINDOWS: + // sanity check node_attach_driver + if (!["smb", "iscsi"].includes(node_attach_driver)) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `csi-proxy does not work with node_attach_driver: ${node_attach_driver}` + ); + } + + // sanity check fs_type + if (fs_type && !["ntfs", "cifs"].includes(fs_type)) { + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `csi-proxy does not work with fs_type: ${fs_type}` + ); + } + + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + + switch (node_attach_driver) { + case "smb": + device = `//${volume_context.server}/${volume_context.share}`; + const username = driver.getMountFlagValue(mount_flags, "username"); + const password = driver.getMountFlagValue(mount_flags, "password"); + + if (!username || !password) { + throw new Error("username and password required"); + } + + /** + * smb mount creates a link at this location and if the dir already exists + * it explodes + * + * if path exists but is NOT symlink delete it + */ + try { + fs.statSync(staging_target_path); + result = true; + } catch (err) { + if (err.code === "ENOENT") { + result = false; + } else { + throw err; + } + } + + if (result) { + result = fs.lstatSync(staging_target_path); + if (!result.isSymbolicLink()) { + fs.rmdirSync(staging_target_path); + } else { + result = await wutils.GetItem(staging_target_path); + // UNC\172.29.0.111\tank_k8s_test_PVC_111\ + let target = _.get(result, "Target.[0]", ""); + let parts = target.split("\\"); + if ( + parts[1] != volume_context.server && + parts[2] != volume_context.share + ) { + throw new Error( + `${target} mounted already at ${staging_target_path}` + ); + } else { + // finish early, assured we have what we need + return {}; + } + } + } + + try { + result = await wutils.GetSmbGlobalMapping( + filesystem.covertUnixSeparatorToWindowsSeparator(device) + ); + if (!result) { + await wutils.NewSmbGlobalMapping( + filesystem.covertUnixSeparatorToWindowsSeparator(device), + `${volume_context.server}\\${username}`, + password + ); + } + } catch (e) { + let details = _.get(e, "stderr", ""); + if (!details.includes("0x80041001")) { + throw e; + } + } + try { + await wutils.NewSmbLink( + filesystem.covertUnixSeparatorToWindowsSeparator(device), + staging_target_path + ); + } catch (e) { + let details = _.get(e, "stderr", ""); + if (!details.includes("ResourceExists")) { + throw e; + } else { + result = fs.lstatSync(staging_target_path); + if (!result.isSymbolicLink()) { + throw new Error("staging path exists but is not symlink"); + } + } + } + break; + case "iscsi": + switch (access_type) { + case "mount": + let portals = []; + if (volume_context.portal) { + portals.push(volume_context.portal.trim()); + } + + if (volume_context.portals) { + volume_context.portals.split(",").forEach((portal) => { + portals.push(portal.trim()); + }); + } + + // ensure full portal value + portals = portals.map((value) => { + if (!value.includes(":")) { + value += ":3260"; + } + + return value.trim(); + }); + + // ensure unique entries only + portals = [...new Set(portals)]; + + // stores configuration of targets/iqn/luns to connect to + let iscsiConnections = []; + for (let portal of portals) { + iscsiConnections.push({ + portal, + iqn: volume_context.iqn, + lun: volume_context.lun, + }); + } + + let successful_logins = 0; + let multipath = iscsiConnections.length > 1; + + // no multipath support yet + // https://github.com/kubernetes-csi/csi-proxy/pull/99 + for (let iscsiConnection of iscsiConnections) { + // add target portal + let parts = iscsiConnection.portal.split(":"); + let target_address = parts[0]; + let target_port = parts[1] || "3260"; + + // this is idempotent + try { + await wutils.NewIscsiTargetPortal( + target_address, + target_port + ); + } catch (e) { + driver.ctx.logger.warn( + `failed adding target portal: ${JSON.stringify( + iscsiConnection + )}: ${e.stderr}` + ); + if (!multipath) { + throw e; + } else { + continue; + } + } + + // login + try { + let auth_type = "NONE"; + let chap_username = ""; + let chap_secret = ""; + if ( + normalizedSecrets[ + "node-db.node.session.auth.authmethod" + ] == "CHAP" + ) { + // set auth_type + if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets[ + "node-db.node.session.auth.password" + ] && + normalizedSecrets[ + "node-db.node.session.auth.username_in" + ] && + normalizedSecrets[ + "node-db.node.session.auth.password_in" + ] + ) { + auth_type = "MUTUAL_CHAP"; + } else if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets["node-db.node.session.auth.password"] + ) { + auth_type = "ONE_WAY_CHAP"; + } + + // set credentials + if ( + normalizedSecrets[ + "node-db.node.session.auth.username" + ] && + normalizedSecrets["node-db.node.session.auth.password"] + ) { + chap_username = + normalizedSecrets[ + "node-db.node.session.auth.username" + ]; + + chap_secret = + normalizedSecrets[ + "node-db.node.session.auth.password" + ]; + } + } + await wutils.ConnectIscsiTarget( + target_address, + target_port, + iscsiConnection.iqn, + auth_type, + chap_username, + chap_secret, + multipath + ); + } catch (e) { + let details = _.get(e, "stderr", ""); + if ( + !details.includes( + "The target has already been logged in via an iSCSI session" + ) + ) { + driver.ctx.logger.warn( + `failed connection to ${JSON.stringify( + iscsiConnection + )}: ${e.stderr}` + ); + if (!multipath) { + throw e; + } + } + } + + // discover? + //await csiProxyClient.executeRPC("iscsi", "DiscoverTargetPortal", { + // target_portal, + //}); + successful_logins++; + } + + if (iscsiConnections.length != successful_logins) { + driver.ctx.logger.warn( + `failed to login to all portals: total - ${iscsiConnections.length}, logins - ${successful_logins}` + ); + } + + // let things settle + // this will help in dm scenarios + await GeneralUtils.sleep(2000); + + // rescan + await wutils.UpdateHostStorageCache(); + + // get device + let disks = await wutils.GetTargetDisksByIqnLun( + volume_context.iqn, + volume_context.lun + ); + let disk; + + if (disks.length == 0) { + throw new GrpcError( + grpc.status.UNAVAILABLE, + `0 disks created by ${successful_logins} successful logins` + ); + } + + if (disks.length > 1) { + if (multipath) { + let disk_number_set = new Set(); + disks.forEach((i_disk) => { + disk_number_set.add(i_disk.DiskNumber); + }); + if (disk_number_set.length > 1) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + "using multipath but mpio is not properly configured (multiple disk numbers with same iqn/lun)" + ); + } + // find first disk that is online + disk = disks.find((i_disk) => { + return i_disk.OperationalStatus == "Online"; + }); + + if (!disk) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + "using multipath but mpio is not properly configured (failed to detect an online disk)" + ); + } + } else { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `not using multipath but discovered ${disks.length} disks (multiple disks with same iqn/lun)` + ); + } + } else { + disk = disks[0]; + } + + if (multipath && !disk.Path.startsWith("\\\\?\\mpio#")) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + "using multipath but mpio is not properly configured (discover disk is not an mpio disk)" + ); + } + + // needs to be initialized + await wutils.PartitionDisk(disk.DiskNumber); + + let partition = await wutils.GetLastPartitionByDiskNumber( + disk.DiskNumber + ); + + let volume = await wutils.GetVolumeByDiskNumberPartitionNumber( + disk.DiskNumber, + partition.PartitionNumber + ); + if (!volume) { + throw new Error("failed to create/discover volume for disk"); + } + + result = await wutils.VolumeIsFormatted(volume.UniqueId); + if (!result) { + // format device + await wutils.FormatVolume(volume.UniqueId); + } + + result = await wutils.GetItem(staging_target_path); + if (!result) { + fs.mkdirSync(staging_target_path, { + recursive: true, + mode: "755", + }); + result = await wutils.GetItem(staging_target_path); + } + + if (!volume.UniqueId.includes(result.Target[0])) { + // mount up! + await wutils.MountVolume( + volume.UniqueId, + staging_target_path + ); + } + break; + case "block": + default: + throw new GrpcError( + grpc.status.UNIMPLEMENTED, + `access_type ${access_type} unsupported` + ); + } + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + break; case NODE_OS_DRIVER_CSI_PROXY: // sanity check node_attach_driver if (!["smb", "iscsi"].includes(node_attach_driver)) { @@ -1546,9 +1928,7 @@ class CsiBaseDriver { grpc.status.INVALID_ARGUMENT, `unknown/unsupported node_attach_driver: ${node_attach_driver}` ); - break; } - break; default: throw new GrpcError( @@ -1798,6 +2178,89 @@ class CsiBaseDriver { result = await filesystem.rmdir(staging_target_path); } break; + case NODE_OS_DRIVER_WINDOWS: { + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + + async function removePath(p) { + // remove staging path + try { + fs.rmdirSync(p); + } catch (e) { + if (e.code !== "ENOENT") { + throw e; + } + } + } + + let node_attach_driver; + let win_volume_id; + + result = await wutils.GetItem(normalized_staging_path); + if (result) { + let target = _.get(result, "Target.[0]", ""); + if (target.startsWith("UNC")) { + node_attach_driver = "smb"; + } + if (target.startsWith("Volume")) { + win_volume_id = `\\\\?\\${target}`; + if (await wutils.VolumeIsIscsi(win_volume_id)) { + node_attach_driver = "iscsi"; + } + } + + if (!node_attach_driver) { + // nothing we care about + node_attach_driver = "bypass"; + } + + switch (node_attach_driver) { + case "smb": + let parts = target.split("\\"); + await wutils.RemoveSmbGlobalMapping( + `\\\\${parts[1]}\\${parts[2]}` + ); + + break; + case "iscsi": + // write volume cache + await wutils.WriteVolumeCache(win_volume_id); + + // unmount volume + await wutils.UnmountVolume( + win_volume_id, + normalized_staging_path + ); + + // find sessions associated with volume/disks + let sessions = await wutils.GetIscsiSessionsByVolumeId( + win_volume_id + ); + + // logout of sessions + for (let session of sessions) { + await wutils.DisconnectIscsiTargetByNodeAddress( + session.TargetNodeAddress + ); + } + + // delete target/target portal/etc + // do NOT do this now as removing the portal will remove all targets associated with it + break; + case "bypass": + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + } + + // remove staging path + await removePath(normalized_staging_path); + break; + } case NODE_OS_DRIVER_CSI_PROXY: // load up the client instance const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); @@ -1891,6 +2354,9 @@ class CsiBaseDriver { } } + // do NOT remove target portal etc, windows handles this quite differently than + // linux and removing the portal would remove all the targets/etc + /* try { await csiProxyClient.executeRPC("iscsi", "RemoveTargetPortal", { target_portal, @@ -1901,6 +2367,7 @@ class CsiBaseDriver { throw e; } } + */ break; default: @@ -2077,6 +2544,79 @@ class CsiBaseDriver { ); } break; + case NODE_OS_DRIVER_WINDOWS: + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + + switch (node_attach_driver) { + //case "nfs": + case "smb": + //case "lustre": + //case "oneclient": + //case "hostpath": + case "iscsi": + //case "zfs-local": + // ensure appropriate directories/files + switch (access_type) { + case "mount": + break; + case "block": + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unsupported/unknown access_type ${access_type}` + ); + } + + // ensure bind mount + if (staging_target_path) { + let normalized_staging_path; + + if (access_type == "block") { + normalized_staging_path = staging_target_path + "/block_device"; + } else { + normalized_staging_path = staging_target_path; + } + + // source path + result = await wutils.GetItem(normalized_staging_path); + if (!result) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `staging path is not mounted: ${normalized_staging_path}` + ); + } + + // target path + result = await wutils.GetItem(target_path); + // already published + if (result) { + if (_.get(result, "LinkType") != "SymbolicLink") { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `target path exists but is not a symlink as it should be: ${target_path}` + ); + } + return {}; + } + + // create symlink + fs.symlinkSync(normalized_staging_path, target_path); + return {}; + } + + // unsupported filesystem + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `only staged configurations are valid` + ); + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + break; case NODE_OS_DRIVER_CSI_PROXY: switch (node_attach_driver) { //case "nfs": @@ -2242,6 +2782,24 @@ class CsiBaseDriver { } } + break; + case NODE_OS_DRIVER_WINDOWS: + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + + result = await wutils.GetItem(target_path); + if (!result) { + return {}; + } + + if (_.get(result, "LinkType") != "SymbolicLink") { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `target path is not a symlink ${target_path}` + ); + } + + fs.rmdirSync(target_path); break; case NODE_OS_DRIVER_CSI_PROXY: const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); @@ -2368,6 +2926,61 @@ class CsiBaseDriver { } break; + case NODE_OS_DRIVER_WINDOWS: { + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + + // ensure path is mounted + result = await wutils.GetItem(volume_path); + if (!result) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `volume_path ${volume_path} is not currently mounted` + ); + } + + let node_attach_driver; + + let target = await wutils.GetRealTarget(volume_path); + if (target.startsWith("\\\\")) { + node_attach_driver = "smb"; + } + if (target.startsWith("\\\\?\\Volume")) { + if (await wutils.VolumeIsIscsi(target)) { + node_attach_driver = "iscsi"; + } + } + + if (!node_attach_driver) { + // nothing we care about + node_attach_driver = "bypass"; + } + + switch (node_attach_driver) { + case "smb": + res.usage = [{ total: 0, unit: "BYTES" }]; + break; + case "iscsi": + let node_volume = await wutils.GetVolumeByVolumeId(target); + res.usage = [ + { + available: node_volume.SizeRemaining, + total: node_volume.Size, + used: node_volume.Size - node_volume.SizeRemaining, + unit: "BYTES", + }, + ]; + break; + case "bypass": + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + break; + } case NODE_OS_DRIVER_CSI_PROXY: const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); const volume_context = await driver.getDerivedVolumeContext(call); @@ -2562,6 +3175,56 @@ class CsiBaseDriver { } break; + case NODE_OS_DRIVER_WINDOWS: { + const WindowsUtils = require("../utils/windows").Windows; + const wutils = new WindowsUtils(); + + let node_attach_driver; + + // ensure path is mounted + result = await wutils.GetItem(volume_path); + if (!result) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `volume_path ${volume_path} is not currently mounted` + ); + } + + let target = await wutils.GetRealTarget(volume_path); + if (target.startsWith("\\\\")) { + node_attach_driver = "smb"; + } + if (target.startsWith("\\\\?\\Volume")) { + if (await wutils.VolumeIsIscsi(target)) { + node_attach_driver = "iscsi"; + } + } + + if (!node_attach_driver) { + // nothing we care about + node_attach_driver = "bypass"; + } + + switch (node_attach_driver) { + case "smb": + // noop + break; + case "iscsi": + // rescan devices + await wutils.UpdateHostStorageCache(); + await wutils.ResizeVolume(target); + break; + case "bypass": + break; + default: + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `unknown/unsupported node_attach_driver: ${node_attach_driver}` + ); + } + + break; + } case NODE_OS_DRIVER_CSI_PROXY: const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); const volume_context = await driver.getDerivedVolumeContext(call); @@ -2606,7 +3269,7 @@ class CsiBaseDriver { try { await csiProxyClient.executeRPC("volume", "ResizeVolume", { volume_id: node_volume_id, - resize_bytes: required_bytes, + resize_bytes: 0, }); } catch (e) { let details = _.get(e, "details", ""); diff --git a/src/utils/csi_proxy_client.js b/src/utils/csi_proxy_client.js index fb656a1..d0587c0 100644 --- a/src/utils/csi_proxy_client.js +++ b/src/utils/csi_proxy_client.js @@ -1,8 +1,10 @@ const _ = require("lodash"); const grpc = require("./grpc").grpc; +const path = require("path"); const protoLoader = require("@grpc/proto-loader"); -const PROTO_BASE_PATH = __dirname + "/../../csi_proxy_proto"; +const PROTO_BASE_PATH = + path.dirname(path.dirname(__dirname)) + path.sep + "csi_proxy_proto"; /** * leave connection null as by default the named pipe is derrived @@ -38,10 +40,20 @@ class CsiProxyClient { const serviceVersion = service.version || DEFAULT_SERVICES[serviceName].version; const serviceConnection = + // HANGS + // Http2Session client (38) nghttp2 has 13 bytes to send directly + // Http2Session client (38) wants read? 1 + // Then pipe closes after 60 seconds-ish service.connection || - `\\\\.\\\\pipe\\\\${pipePrefix}-${serviceName}-${serviceVersion}`; + `unix:////./pipe/${pipePrefix}-${serviceName}-${serviceVersion}`; + // EACCESS + //service.connection || + //`unix:///csi/${pipePrefix}-${serviceName}-${serviceVersion}`; + //service.connection || + //`unix:///csi/csi.sock.internal`; + + const PROTO_PATH = `${PROTO_BASE_PATH}\\${serviceName}\\${serviceVersion}\\api.proto`; - const PROTO_PATH = `/${PROTO_BASE_PATH}/${serviceName}/${serviceVersion}/api.proto`; const packageDefinition = protoLoader.loadSync(PROTO_PATH, { keepCase: true, longs: String, diff --git a/src/utils/powershell.js b/src/utils/powershell.js new file mode 100644 index 0000000..08d2a56 --- /dev/null +++ b/src/utils/powershell.js @@ -0,0 +1,85 @@ +const cp = require("child_process"); + +class Powershell { + async exec(command, options = {}) { + if (!options.hasOwnProperty("timeout")) { + // TODO: cannot use this as fsck etc are too risky to kill + //options.timeout = DEFAULT_TIMEOUT; + } + + //cmd := exec.Command("powershell", "-Mta", "-NoProfile", "-Command", command) + + let stdin; + if (options.stdin) { + stdin = options.stdin; + delete options.stdin; + } + + // https://github.com/kubernetes-csi/csi-proxy/blob/master/pkg/utils/utils.go + const _command = "powershell"; + const args = [ + "-Mta", + "-NoProfile", + "-Command", + command + ]; + + let command_log = `${_command} ${args.join(" ")}`.trim(); + if (stdin) { + command_log = `echo '${stdin}' | ${command_log}` + .trim() + .replace(/\n/, "\\n"); + } + console.log("executing powershell command: %s", command_log); + + return new Promise((resolve, reject) => { + const child = cp.spawn(_command, args, options); + let stdout = ""; + let stderr = ""; + + child.on("spawn", function () { + if (stdin) { + child.stdin.setEncoding("utf-8"); + child.stdin.write(stdin); + child.stdin.end(); + } + }); + + child.stdout.on("data", function (data) { + stdout = stdout + data; + }); + + child.stderr.on("data", function (data) { + stderr = stderr + data; + }); + + child.on("close", function (code) { + const result = { code, stdout, stderr, timeout: false }; + + // timeout scenario + if (code === null) { + result.timeout = true; + reject(result); + } + + if (code) { + console.log( + "failed to execute powershell command: %s, response: %j", + command_log, + result + ); + reject(result); + } else { + try { + result.parsed = JSON.parse(result.stdout); + } catch (err) { }; + resolve(result); + } + }); + }); + } +} + + + +module.exports.Powershell = Powershell; \ No newline at end of file diff --git a/src/utils/windows.js b/src/utils/windows.js new file mode 100644 index 0000000..6f2d853 --- /dev/null +++ b/src/utils/windows.js @@ -0,0 +1,729 @@ +const { result } = require("lodash"); +const _ = require("lodash"); +const Powershell = require("./powershell").Powershell; + +/** + * https://kubernetes.io/blog/2021/08/16/windows-hostprocess-containers/ + * https://github.com/kubernetes-csi/csi-proxy/tree/master/pkg/os + * + * multipath notes: + * - http://scst.sourceforge.net/mc_s.html + * - https://github.com/kubernetes-csi/csi-proxy/pull/99 + * - https://docs.microsoft.com/en-us/azure/storsimple/storsimple-8000-configure-mpio-windows-server + * - https://support.purestorage.com/Legacy_Documentation/Setting_the_MPIO_Policy + * - https://docs.microsoft.com/en-us/powershell/module/mpio/?view=windowsserver2022-ps + * + * Get-WindowsFeature -Name 'Multipath-IO' + * Add-WindowsFeature -Name 'Multipath-IO' + * + * Enable-MSDSMAutomaticClaim -BusType "iSCSI" + * Disable-MSDSMAutomaticClaim -BusType "iSCSI" + * + * Get-MSDSMGlobalDefaultLoadBalancePolicy + * Set-MSDSMGlobalLoadBalancePolicy -Policy RR + * + * synology woes: + * - https://community.spiceworks.com/topic/2279882-synology-iscsi-will-not-disconnect-using-powershell-commands + * - https://support.hpe.com/hpesc/public/docDisplay?docId=c01880810&docLocale=en_US + * - https://askubuntu.com/questions/1159103/why-is-iscsi-trying-to-connect-on-ipv6-at-boot + */ +class Windows { + constructor() { + this.ps = new Powershell(); + } + + resultToArray(result) { + if (!result.parsed) { + result.parsed = []; + } + if (!Array.isArray(result.parsed)) { + result.parsed = [result.parsed]; + } + } + + async GetRealTarget(path) { + let item; + let target; + + do { + item = await this.GetItem(path); + path = null; + + target = _.get(item, "Target.[0]", ""); + if (target.startsWith("UNC")) { + let parts = target.split("\\", 3); + return `\\\\${parts[1]}\\${parts[2]}`; + } else if (target.startsWith("Volume")) { + return `\\\\?\\${target}`; + } else { + path = target; + } + } while (path); + } + async GetItem(localPath) { + let command; + let result; + command = 'Get-Item "$Env:localpath" | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: { + localpath: localPath, + }, + }); + return result.parsed; + } catch (err) {} + } + + async GetSmbGlobalMapping(remotePath) { + let command; + command = + "Get-SmbGlobalMapping -RemotePath $Env:smbremotepath | ConvertTo-Json"; + try { + return await this.ps.exec(command, { + env: { + smbremotepath: remotePath, + }, + }); + } catch (err) {} + } + + async NewSmbGlobalMapping(remotePath, username, password) { + let command; + command = + "$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential -RequirePrivacy $true"; + + await this.ps.exec(command, { + env: { + smbuser: username, + smbpassword: password, + smbremotepath: remotePath, + }, + }); + } + + async RemoveSmbGlobalMapping(remotePath) { + let result; + let command; + command = "Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force"; + + do { + result = await this.GetSmbGlobalMapping(remotePath); + if (result) { + await this.ps.exec(command, { + env: { + smbremotepath: remotePath, + }, + }); + } + } while (result); + } + + async NewSmbLink(remotePath, localPath) { + let command; + if (!remotePath.endsWith("\\")) { + remotePath = `${remotePath}\\`; + } + + command = + "New-Item -ItemType SymbolicLink $Env:smblocalPath -Target $Env:smbremotepath"; + await this.ps.exec(command, { + env: { + smblocalpath: localPath, + smbremotepath: remotePath, + }, + }); + } + + async NewIscsiTargetPortal(address, port) { + let command; + command = + "New-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port}"; + await this.ps.exec(command, { + env: { + iscsi_tp_address: address, + iscsi_tp_port: port, + }, + }); + } + + async RemoveIscsiTargetPortalByTargetPortalAddress(targetPortalAddress) { + let command; + command = `Remove-IscsiTargetPortal -TargetPortalAddress ${targetPortalAddress} -Confirm:$false`; + await this.ps.exec(command); + } + + async RemoveIscsiTargetPortalByTargetPortalAddressTargetPortalPort( + targetPortalAddress, + targetPortalPort + ) { + let command; + command = `Get-IscsiTargetPortal -TargetPortalAddress ${targetPortalAddress} -TargetPortalPortNumber ${targetPortalPort} | Remove-IscsiTargetPortal -Confirm:$false`; + await this.ps.exec(command); + } + + async IscsiTargetIsConnectedByPortalAddressPortalPort(address, port, iqn) { + let sessions = await this.GetIscsiSessionsByTargetNodeAddress(iqn); + for (let session of sessions) { + let connections = await this.GetIscsiConnectionsByIscsiSessionIdentifier( + session.SessionIdentifier + ); + for (let connection of connections) { + if ( + connection.TargetAddress == address && + connection.TargetPortNumber == port + ) { + return true; + } + } + } + + //process.exit(1); + + return false; + } + + /** + * -IsMultipathEnabled + * + * @param {*} address + * @param {*} port + * @param {*} iqn + * @param {*} authType + * @param {*} chapUser + * @param {*} chapSecret + */ + async ConnectIscsiTarget( + address, + port, + iqn, + authType, + chapUser, + chapSecret, + multipath = false + ) { + let is_connected = await this.IscsiTargetIsConnectedByPortalAddressPortalPort(address, port, iqn); + if (is_connected) { + return; + } + + let command; + // -IsMultipathEnabled $([System.Convert]::ToBoolean(${Env:iscsi_is_multipath})) + // -InitiatorPortalAddress + command = + "Connect-IscsiTarget -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port} -NodeAddress ${Env:iscsi_target_iqn} -AuthenticationType ${Env:iscsi_auth_type}"; + + if (chapUser) { + command += " -ChapUsername ${Env:iscsi_chap_user}"; + } + + if (chapSecret) { + command += " -ChapSecret ${Env:iscsi_chap_secret}"; + } + + if (multipath) { + command += + " -IsMultipathEnabled $([System.Convert]::ToBoolean(${Env:iscsi_is_multipath}))"; + } + + try { + await this.ps.exec(command, { + env: { + iscsi_tp_address: address, + iscsi_tp_port: port, + iscsi_target_iqn: iqn, + iscsi_auth_type: authType, + iscsi_chap_user: chapUser, + iscsi_chap_secret: chapSecret, + iscsi_is_multipath: String(multipath), + }, + }); + } catch (err) { + let details = _.get(err, "stderr", ""); + if ( + !details.includes( + "The target has already been logged in via an iSCSI session" + ) + ) { + throw err; + } + } + } + + async GetIscsiTargetsByTargetPortalAddressTargetPortalPort(address, port) { + let command; + let result; + + command = + "Get-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port} | Get-IscsiTarget | ConvertTo-Json"; + result = await this.ps.exec(command, { + env: { + iscsi_tp_address: address, + iscsi_tp_port: port, + }, + }); + this.resultToArray(result); + + return result.parsed; + } + + /** + * This disconnects *all* sessions from the target + * + * @param {*} nodeAddress + */ + async DisconnectIscsiTargetByNodeAddress(nodeAddress) { + let command; + + command = `Disconnect-IscsiTarget -NodeAddress ${nodeAddress.toLowerCase()} -Confirm:$false`; + await this.ps.exec(command); + } + + async GetIscsiConnectionsByIscsiSessionIdentifier(iscsiSessionIdentifier) { + let command; + let result; + + command = `Get-IscsiSession -SessionIdentifier ${iscsiSessionIdentifier} | Get-IscsiConnection | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetIscsiSessions() { + let command; + let result; + + command = `Get-IscsiSession | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetIscsiSessionsByDiskNumber(diskNumber) { + let command; + let result; + + command = `Get-Disk -Number ${diskNumber} | Get-IscsiSession | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetIscsiSessionsByVolumeId(volumeId) { + let sessions = []; + let disks = await this.GetDisksByVolumeId(volumeId); + for (let disk of disks) { + let i_sessions = await this.GetIscsiSessionsByDiskNumber(disk.DiskNumber); + sessions.push(...i_sessions); + } + + return sessions; + } + + async GetIscsiSessionsByTargetNodeAddress(targetNodeAddress) { + let sessions = await this.GetIscsiSessions(); + let r_sessions = []; + // Where-Object { $_.TargetNodeAddress -eq ${targetNodeAddress} } + for (let session of sessions) { + if (session.TargetNodeAddress == targetNodeAddress) { + r_sessions.push(session); + } + } + + return r_sessions; + } + + async GetIscsiSessionByIscsiConnectionIdentifier(iscsiConnectionIdentifier) { + let command; + let result; + + command = `Get-IscsiConnection -ConnectionIdentifier ${iscsiConnectionIdentifier} | Get-IscsiSession | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed; + } + + async GetIscsiTargetPortalBySessionId(sessionId) { + let command; + let result; + + command = `Get-IscsiSession -SessionIdentifier ${sessionId} | Get-IscsiTargetPortal | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed; + } + + async UpdateHostStorageCache() { + let command; + command = "Update-HostStorageCache"; + await this.ps.exec(command); + } + + async GetIscsiDisks() { + let command; + let result; + + command = "Get-iSCSISession | Get-Disk | ConvertTo-Json"; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetWin32DiskDrives() { + let command; + let result; + + command = "Get-WmiObject Win32_DiskDrive | ConvertTo-Json"; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetDiskLunByDiskNumber(diskNumber) { + let result; + result = await this.GetWin32DiskDrives(); + for (let drive of result) { + if (drive.Index == diskNumber) { + return drive.SCSILogicalUnit; + } + } + } + + async GetTargetDisks(address, port, iqn) { + let command; + let result; + + // this fails for synology for some reason + //command = + // '$ErrorActionPreference = "Stop"; $tp = Get-IscsiTargetPortal -TargetPortalAddress ${Env:iscsi_tp_address} -TargetPortalPortNumber ${Env:iscsi_tp_port}; $t = $tp | Get-IscsiTarget | Where-Object { $_.NodeAddress -eq ${Env:iscsi_target_iqn} }; $s = Get-iSCSISession -IscsiTarget $t; $s | Get-Disk | ConvertTo-Json'; + + command = + '$ErrorActionPreference = "Stop"; $s = Get-iSCSISession | Where-Object { $_.TargetNodeAddress -eq ${Env:iscsi_target_iqn} }; $s | Get-Disk | ConvertTo-Json'; + + result = await this.ps.exec(command, { + env: { + iscsi_tp_address: address, + iscsi_tp_port: port, + iscsi_target_iqn: iqn, + }, + }); + this.resultToArray(result); + + return result.parsed; + } + + async GetTargetDisksByIqn(iqn) { + let command; + let result; + + command = + '$ErrorActionPreference = "Stop"; $s = Get-iSCSISession | Where-Object { $_.TargetNodeAddress -eq ${Env:iscsi_target_iqn} }; $s | Get-Disk | ConvertTo-Json'; + + result = await this.ps.exec(command, { + env: { + iscsi_target_iqn: iqn, + }, + }); + this.resultToArray(result); + + return result.parsed; + } + + /** + * This can be multiple when mpio is not configured properly and each + * session creates a new disk + * + * @param {*} iqn + * @param {*} lun + * @returns + */ + async GetTargetDisksByIqnLun(iqn, lun) { + let result; + let dlun; + let disks = []; + + result = await this.GetTargetDisksByIqn(iqn); + for (let disk of result) { + dlun = await this.GetDiskLunByDiskNumber(disk.DiskNumber); + if (dlun == lun) { + disks.push(disk); + } + } + + return disks; + } + + async GetDiskByDiskNumber(diskNumber) { + let command; + let result; + + command = `Get-Disk -Number ${diskNumber} | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed; + } + + async GetDisks() { + let command; + let result; + + command = "Get-Disk | ConvertTo-Json"; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetPartitions() { + let command; + let result; + + command = "Get-Partition | ConvertTo-Json"; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetPartitionsByDiskNumber(diskNumber) { + let command; + let result; + + command = `Get-Disk -Number ${diskNumber} | Get-Partition | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async DiskIsInitialized(diskNumber) { + let disk = await this.GetDiskByDiskNumber(diskNumber); + + return disk.PartitionStyle != "RAW"; + } + + async InitializeDisk(diskNumber) { + let command; + + command = `Initialize-Disk -Number ${diskNumber} -PartitionStyle GPT`; + await this.ps.exec(command); + } + + async DiskHasBasicPartition(diskNumber) { + let command; + let result; + + command = `Get-Partition | Where DiskNumber -eq ${diskNumber} | Where Type -ne Reserved | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed.length > 0; + } + + async NewPartition(diskNumber) { + let command; + + command = `New-Partition -DiskNumber ${diskNumber} -UseMaximumSize`; + await this.ps.exec(command); + } + + async PartitionDisk(diskNumber) { + let is_intialized; + let has_basic_partition; + + is_intialized = await this.DiskIsInitialized(diskNumber); + if (!is_intialized) { + await this.InitializeDisk(diskNumber); + } + + has_basic_partition = await this.DiskHasBasicPartition(diskNumber); + if (!has_basic_partition) { + await this.NewPartition(diskNumber); + } + } + + async GetLastPartitionByDiskNumber(diskNumber) { + let partitions = await this.GetPartitionsByDiskNumber(diskNumber); + let p; + for (let partition of partitions) { + if (!p) { + p = partition; + } + + if (partition.PartitionNumber > p.PartitionNumber) { + p = partition; + } + } + + return p; + } + + async GetVolumesByDiskNumber(diskNumber) { + let command; + command = `Get-Disk -Number ${diskNumber} | Get-Partition | Get-Volume | ConvertTo-Json`; + result = await this.ps.exec(command); + this.resultToArray(result); + + return result.parsed; + } + + async GetVolumeByDiskNumberPartitionNumber(diskNumber, partitionNumber) { + let command; + let result; + + command = `Get-Disk -Number ${diskNumber} | Get-Partition -PartitionNumber ${partitionNumber} | Get-Volume | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed; + } + + async GetVolumeByVolumeId(volumeId) { + let command; + let result; + + command = `Get-Volume -UniqueId \"${volumeId}\" -ErrorAction Stop | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed; + } + + async GetPartitionsByVolumeId(volumeId) { + let partitions = await this.GetPartitions(); + let p = []; + for (let partition of partitions) { + let paths = _.get(partition, "AccessPaths", []); + if (paths === null) { + paths = []; + } + if (!Array.isArray(paths)) { + paths = []; + } + if (paths.includes(volumeId)) { + p.push(partition); + } + } + return p; + } + + async GetDisksByVolumeId(volumeId) { + let partitions = await this.GetPartitionsByVolumeId(volumeId); + let diskNumbers = new Set(); + for (let parition of partitions) { + diskNumbers.add(parition.DiskNumber); + } + + let disks = []; + let disk; + for (let diskNumber of diskNumbers) { + disk = await this.GetDiskByDiskNumber(diskNumber); + if (disk) { + disks.push(disk); + } + } + + return disks; + } + + async VolumeIsFormatted(volumeId) { + let volume = await this.GetVolumeByVolumeId(volumeId); + let type = volume.FileSystemType || ""; + type = type.toLowerCase().trim(); + if (!type || type == "unknown") { + return false; + } + + return true; + } + + async VolumeIsIscsi(volumeId) { + let disks = await this.GetDisksByVolumeId(volumeId); + for (let disk of disks) { + if (_.get(disk, "BusType", "").toLowerCase() == "iscsi") { + return true; + } + } + + return false; + } + + async FormatVolume(volumeId) { + let command; + command = `Get-Volume -UniqueId \"${volumeId}\" | Format-Volume -FileSystem ntfs -Confirm:$false`; + await this.ps.exec(command); + } + + async ResizeVolume(volumeId, size = 0) { + let command; + let final_size; + + if (!size) { + final_size = await this.GetVolumeMaxSize(volumeId); + } else { + final_size = size; + } + + let current_size = await this.GetVolumeSize(volumeId); + if (current_size >= final_size) { + return; + } + + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-Partition | Resize-Partition -Size ${final_size}`; + try { + await this.ps.exec(command); + } catch (err) { + let details = _.get(err, "stderr", ""); + if ( + !details.includes( + "The size of the extent is less than the minimum of 1MB" + ) + ) { + throw err; + } + } + } + + async GetVolumeMaxSize(volumeId) { + let command; + let result; + + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-partition | Get-PartitionSupportedSize | Select SizeMax | ConvertTo-Json`; + result = await this.ps.exec(command); + return result.parsed.SizeMax; + } + async GetVolumeSize(volumeId) { + let command; + let result; + + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-partition | ConvertTo-Json`; + result = await this.ps.exec(command); + + return result.parsed.Size; + } + + async MountVolume(volumeId, path) { + let command; + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-Partition | Add-PartitionAccessPath -AccessPath ${path}`; + + await this.ps.exec(command); + } + + async UnmountVolume(volumeId, path) { + let command; + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-Partition | Remove-PartitionAccessPath -AccessPath ${path}`; + + await this.ps.exec(command); + } + + async WriteVolumeCache(volumeId) { + let command; + command = `Get-Volume -UniqueId \"${volumeId}\" | Write-Volumecache`; + + await this.ps.exec(command); + } +} + +module.exports.Windows = Windows; From ef1595acb961c4f8b10810ff178633ed908cc936 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 5 May 2022 10:55:21 -0600 Subject: [PATCH 27/71] use more explicit naming ci to support windows Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 57 +++++++++++++++++++++++++------------- ci/bin/build.sh | 2 +- ci/bin/run.sh | 4 +-- 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3d1b1c0..8a862e4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,10 +17,12 @@ jobs: with: access_token: ${{ github.token }} - build-npm: + build-npm-linux-amd64: name: build-npm runs-on: - self-hosted + - Linux + - X64 steps: - uses: actions/checkout@v2 - shell: bash @@ -30,14 +32,14 @@ jobs: - name: upload build uses: actions/upload-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 #path: node_modules/ - path: node_modules.tar.gz + path: node_modules-linux-amd64.tar.gz retention-days: 7 csi-sanity-synology-dsm6: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -45,12 +47,14 @@ jobs: - synlogy/dsm6/iscsi.yaml runs-on: - self-hosted + - Linux + - X64 - csi-sanity-synology steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -65,7 +69,7 @@ jobs: csi-sanity-synology-dsm7: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -73,12 +77,14 @@ jobs: - synlogy/dsm7/iscsi.yaml runs-on: - self-hosted + - Linux + - X64 - csi-sanity-synology steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -95,7 +101,7 @@ jobs: # api-based drivers csi-sanity-truenas-scale-22_02: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -106,13 +112,15 @@ jobs: - truenas/scale/22.02/scale-smb.yaml runs-on: - self-hosted + - Linux + - X64 - csi-sanity-zfs-local #- csi-sanity-truenas-scale steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -126,7 +134,7 @@ jobs: # ssh-based drivers csi-sanity-truenas-core-12_0: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -138,13 +146,15 @@ jobs: - truenas/core/12.0/core-smb.yaml runs-on: - self-hosted + - Linux + - X64 - csi-sanity-zfs-local #- csi-sanity-truenas-core steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -158,7 +168,7 @@ jobs: # ssh-based drivers csi-sanity-truenas-core-13_0: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -169,13 +179,15 @@ jobs: - truenas/core/13.0/core-smb.yaml runs-on: - self-hosted + - Linux + - X64 - csi-sanity-zfs-local #- csi-sanity-truenas-core steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -189,7 +201,7 @@ jobs: # ssh-based drivers csi-sanity-zfs-generic: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -199,12 +211,14 @@ jobs: - zfs-generic/smb.yaml runs-on: - self-hosted + - Linux + - X64 - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -218,7 +232,7 @@ jobs: # zfs-local drivers csi-sanity-zfs-local: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -227,12 +241,14 @@ jobs: - zfs-local/dataset.yaml runs-on: - self-hosted + - Linux + - X64 - csi-sanity-zfs-local steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests @@ -243,7 +259,7 @@ jobs: # local-hostpath driver csi-sanity-local-hostpath: needs: - - build-npm + - build-npm-linux-amd64 strategy: fail-fast: false matrix: @@ -251,11 +267,14 @@ jobs: - local-hostpath/basic.yaml runs-on: - self-hosted + - Linux + - X64 + - csi-sanity-zfs-local steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules + name: node-modules-linux-amd64 - name: csi-sanity run: | # run tests diff --git a/ci/bin/build.sh b/ci/bin/build.sh index a2d08d3..0e8afbf 100755 --- a/ci/bin/build.sh +++ b/ci/bin/build.sh @@ -12,4 +12,4 @@ npm --version npm i # tar node_modules to keep the number of files low to upload -tar -zcf node_modules.tar.gz node_modules +tar -zcf node_modules-linux-amd64.tar.gz node_modules diff --git a/ci/bin/run.sh b/ci/bin/run.sh index 92b18d6..b16a92a 100755 --- a/ci/bin/run.sh +++ b/ci/bin/run.sh @@ -15,8 +15,8 @@ export PATH="/usr/local/lib/nodejs/bin:${PATH}" # install deps #npm i # install from artifacts -if [[ -f "node_modules.tar.gz" ]];then - tar -zxf node_modules.tar.gz +if [[ -f "node_modules-linux-amd64.tar.gz" ]];then + tar -zxf node_modules-linux-amd64.tar.gz fi # generate key for paths etc From 99c11bccae5ae1460bc4e56c3a295aadad3fa1b5 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 5 May 2022 11:45:09 -0600 Subject: [PATCH 28/71] add windows node tests to ci Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 51 +++++++++++++++++++++++++++++++++++--- ci/bin/build.ps1 | 2 +- ci/bin/run.ps1 | 6 ++--- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8a862e4..d00bdd8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,6 @@ jobs: access_token: ${{ github.token }} build-npm-linux-amd64: - name: build-npm runs-on: - self-hosted - Linux @@ -33,10 +32,27 @@ jobs: uses: actions/upload-artifact@v2 with: name: node-modules-linux-amd64 - #path: node_modules/ path: node_modules-linux-amd64.tar.gz retention-days: 7 + build-npm-windows-amd64: + runs-on: + - self-hosted + - Windows + - X64 + steps: + - uses: actions/checkout@v2 + - shell: pwsh + name: npm install + run: | + ci\bin\build.ps1 + - name: upload build + uses: actions/upload-artifact@v2 + with: + name: node-modules-windows-amd64 + path: node_modules-windows-amd64.tar.gz + retention-days: 7 + csi-sanity-synology-dsm6: needs: - build-npm-linux-amd64 @@ -283,7 +299,35 @@ jobs: TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" CSI_SANITY_SKIP: "should fail when requesting to create a snapshot with already existing name and different source volume ID|should fail when requesting to create a volume with already existing name and different capacity" - build-docker: + csi-sanity-windows-node: + needs: + - build-npm-windows-amd64 + strategy: + fail-fast: false + matrix: + config: + - windows\iscsi.yaml + - windows\smb.yaml + runs-on: + - self-hosted + - Windows + - X64 + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + with: + name: node-modules-windows-amd64 + - name: csi-sanity + run: | + # run tests + ci\bin\run.ps1 + env: + TEMPLATE_CONFIG_FILE: ".\\ci\\configs\\${{ matrix.config }}" + SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_HOST }} + SERVER_USERNAME: ${{ secrets.SANITY_ZFS_GENERIC_USERNAME }} + SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }} + + build-docker-linux: needs: - csi-sanity-synology-dsm6 - csi-sanity-synology-dsm7 @@ -293,6 +337,7 @@ jobs: - csi-sanity-zfs-generic - csi-sanity-zfs-local - csi-sanity-local-hostpath + - csi-sanity-windows-node runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 diff --git a/ci/bin/build.ps1 b/ci/bin/build.ps1 index e883378..7f3f8ad 100644 --- a/ci/bin/build.ps1 +++ b/ci/bin/build.ps1 @@ -5,4 +5,4 @@ npm --version npm i # tar node_modules to keep the number of files low to upload -tar -zcf node_modules.tar.gz node_modules +tar -zcf node_modules-windows-amd64.tar.gz node_modules diff --git a/ci/bin/run.ps1 b/ci/bin/run.ps1 index 24a244c..98beeaa 100644 --- a/ci/bin/run.ps1 +++ b/ci/bin/run.ps1 @@ -19,9 +19,9 @@ function Job-Cleanup() { Job-Cleanup # install from artifacts -if (Test-Path "node_modules.tar.gz") { - Write-Output "extracting node_modules.tar.gz" - tar -zxf node_modules.tar.gz +if (Test-Path "node_modules-windows-amd64.tar.gz") { + Write-Output "extracting node_modules-windows-amd64.tar.gz" + tar -zxf node_modules-windows-amd64.tar.gz } # setup env From 52f6bd0f0a044af3f2aec71621bb428ee4ddeb95 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 5 May 2022 13:36:12 -0600 Subject: [PATCH 29/71] more verbose windows ci info for debugging Signed-off-by: Travis Glenn Hansen --- ci/bin/run.ps1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ci/bin/run.ps1 b/ci/bin/run.ps1 index 98beeaa..fd6e165 100644 --- a/ci/bin/run.ps1 +++ b/ci/bin/run.ps1 @@ -10,6 +10,13 @@ . "${PSScriptRoot}\helper.ps1" +Write-Output "current user" +whoami +Write-Output "current working directory" +(Get-Location).Path +Write-Output "current PATH" +$Env:PATH + function Job-Cleanup() { Get-Job | Stop-Job Get-Job | Remove-Job From 301af17421ce5bf4d4f19acc00e65c87b699332b Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 5 May 2022 16:00:39 -0600 Subject: [PATCH 30/71] add nfs-client and smb-client to test matrix Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 29 +++++++++++++++++++++++++++++ ci/bin/build.ps1 | 11 +++++++++++ ci/configs/client/nfs.yaml | 10 ++++++++++ ci/configs/client/smb.yaml | 10 ++++++++++ 4 files changed, 60 insertions(+) create mode 100644 ci/configs/client/nfs.yaml create mode 100644 ci/configs/client/smb.yaml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d00bdd8..c6285eb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -245,6 +245,35 @@ jobs: SERVER_USERNAME: ${{ secrets.SANITY_ZFS_GENERIC_USERNAME }} SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }} + # client drivers + csi-sanity-client: + needs: + - build-npm-linux-amd64 + strategy: + fail-fast: false + matrix: + config: + - client/nfs.yaml + - client/smb.yaml + runs-on: + - self-hosted + - Linux + - X64 + - csi-sanity-zfs-generic + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + with: + name: node-modules-linux-amd64 + - name: csi-sanity + run: | + # run tests + ci/bin/run.sh + env: + TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" + SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_HOST }} + SHARE_NAME: tank_client_smb + # zfs-local drivers csi-sanity-zfs-local: needs: diff --git a/ci/bin/build.ps1 b/ci/bin/build.ps1 index 7f3f8ad..5b7deb9 100644 --- a/ci/bin/build.ps1 +++ b/ci/bin/build.ps1 @@ -1,8 +1,19 @@ +Write-Output "current user" +whoami +Write-Output "current working directory" +(Get-Location).Path +Write-Output "current PATH" +$Env:PATH + +Write-Output "node version" node --version +Write-Output "npm version" npm --version # install deps +Write-Output "running npm i" npm i +Write-Output "creating tar.gz" # tar node_modules to keep the number of files low to upload tar -zcf node_modules-windows-amd64.tar.gz node_modules diff --git a/ci/configs/client/nfs.yaml b/ci/configs/client/nfs.yaml new file mode 100644 index 0000000..a3a6f5e --- /dev/null +++ b/ci/configs/client/nfs.yaml @@ -0,0 +1,10 @@ +driver: nfs-client +instance_id: +nfs: + shareHost: ${SERVER_HOST} + shareBasePath: "/mnt/tank/client/nfs/${CI_BUILD_KEY}" + # shareHost:shareBasePath should be mounted at this location in the controller container + controllerBasePath: "/mnt/client/nfs/${CI_BUILD_KEY}" + dirPermissionsMode: "0777" + dirPermissionsUser: root + dirPermissionsGroup: wheel diff --git a/ci/configs/client/smb.yaml b/ci/configs/client/smb.yaml new file mode 100644 index 0000000..8ce57c0 --- /dev/null +++ b/ci/configs/client/smb.yaml @@ -0,0 +1,10 @@ +driver: smb-client +instance_id: +smb: + shareHost: ${SERVER_HOST} + shareBasePath: "${SHARE_NAME}/${CI_BUILD_KEY}" + # shareHost:shareBasePath should be mounted at this location in the controller container + controllerBasePath: "/mnt/client/smb/${CI_BUILD_KEY}" + dirPermissionsMode: "0777" + dirPermissionsUser: root + dirPermissionsGroup: wheel From e960cf4b6d90908b229c435d10e2542e22e2d4eb Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 5 May 2022 16:42:06 -0600 Subject: [PATCH 31/71] use id instead of names for user/group in ci configs Signed-off-by: Travis Glenn Hansen --- ci/configs/client/nfs.yaml | 4 ++-- ci/configs/client/smb.yaml | 8 ++++++-- ci/configs/zfs-generic/nfs.yaml | 3 ++- examples/local-hostpath.yaml | 4 ++-- examples/zfs-generic-nfs.yaml | 5 +++-- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/ci/configs/client/nfs.yaml b/ci/configs/client/nfs.yaml index a3a6f5e..b277805 100644 --- a/ci/configs/client/nfs.yaml +++ b/ci/configs/client/nfs.yaml @@ -6,5 +6,5 @@ nfs: # shareHost:shareBasePath should be mounted at this location in the controller container controllerBasePath: "/mnt/client/nfs/${CI_BUILD_KEY}" dirPermissionsMode: "0777" - dirPermissionsUser: root - dirPermissionsGroup: wheel + dirPermissionsUser: 0 + dirPermissionsGroup: 0 diff --git a/ci/configs/client/smb.yaml b/ci/configs/client/smb.yaml index 8ce57c0..485a782 100644 --- a/ci/configs/client/smb.yaml +++ b/ci/configs/client/smb.yaml @@ -6,5 +6,9 @@ smb: # shareHost:shareBasePath should be mounted at this location in the controller container controllerBasePath: "/mnt/client/smb/${CI_BUILD_KEY}" dirPermissionsMode: "0777" - dirPermissionsUser: root - dirPermissionsGroup: wheel + dirPermissionsUser: 0 + dirPermissionsGroup: 0 + +node: + mount: + mount_flags: "username=smbroot,password=smbroot" diff --git a/ci/configs/zfs-generic/nfs.yaml b/ci/configs/zfs-generic/nfs.yaml index e451a73..46acf25 100644 --- a/ci/configs/zfs-generic/nfs.yaml +++ b/ci/configs/zfs-generic/nfs.yaml @@ -21,4 +21,5 @@ nfs: shareStrategy: "setDatasetProperties" shareStrategySetDatasetProperties: properties: - sharenfs: "on" + #sharenfs: "on" + sharenfs: "rw,no_subtree_check,no_root_squash" diff --git a/examples/local-hostpath.yaml b/examples/local-hostpath.yaml index 7470f95..4c4a9ed 100644 --- a/examples/local-hostpath.yaml +++ b/examples/local-hostpath.yaml @@ -6,5 +6,5 @@ local-hostpath: shareBasePath: "/var/lib/csi-local-hostpath" controllerBasePath: "/var/lib/csi-local-hostpath" dirPermissionsMode: "0777" - dirPermissionsUser: root - dirPermissionsGroup: root + dirPermissionsUser: 0 + dirPermissionsGroup: 0 diff --git a/examples/zfs-generic-nfs.yaml b/examples/zfs-generic-nfs.yaml index 54cc8d9..e068c29 100644 --- a/examples/zfs-generic-nfs.yaml +++ b/examples/zfs-generic-nfs.yaml @@ -36,8 +36,8 @@ zfs: datasetEnableQuotas: true datasetEnableReservation: false datasetPermissionsMode: "0777" - datasetPermissionsUser: root - datasetPermissionsGroup: root + datasetPermissionsUser: 0 + datasetPermissionsGroup: 0 #datasetPermissionsAcls: #- "-m everyone@:full_set:allow" #- "-m u:kube:full_set:allow" @@ -48,6 +48,7 @@ nfs: shareStrategy: "setDatasetProperties" shareStrategySetDatasetProperties: properties: + #sharenfs: "rw,no_subtree_check,no_root_squash" sharenfs: "on" # share: "" shareHost: "server address" From 7d25f82d9dfbe4e990bd416b89b4724dab19b19f Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 5 May 2022 17:19:16 -0600 Subject: [PATCH 32/71] ensure windows-ized paths for node operations on windows Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 1 + src/driver/index.js | 67 ++++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c6285eb..9eb2439 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -273,6 +273,7 @@ jobs: TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_HOST }} SHARE_NAME: tank_client_smb + CSI_SANITY_SKIP: "should fail when requesting to create a snapshot with already existing name and different source volume ID|should fail when requesting to create a volume with already existing name and different capacity" # zfs-local drivers csi-sanity-zfs-local: diff --git a/src/driver/index.js b/src/driver/index.js index bccbd78..2bcacf2 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -1242,6 +1242,10 @@ class CsiBaseDriver { switch (node_attach_driver) { case "smb": + let win_staging_target_path = + filesystem.covertUnixSeparatorToWindowsSeparator( + staging_target_path + ); device = `//${volume_context.server}/${volume_context.share}`; const username = driver.getMountFlagValue(mount_flags, "username"); const password = driver.getMountFlagValue(mount_flags, "password"); @@ -1257,7 +1261,7 @@ class CsiBaseDriver { * if path exists but is NOT symlink delete it */ try { - fs.statSync(staging_target_path); + fs.statSync(win_staging_target_path); result = true; } catch (err) { if (err.code === "ENOENT") { @@ -1268,11 +1272,11 @@ class CsiBaseDriver { } if (result) { - result = fs.lstatSync(staging_target_path); + result = fs.lstatSync(win_staging_target_path); if (!result.isSymbolicLink()) { - fs.rmdirSync(staging_target_path); + fs.rmdirSync(win_staging_target_path); } else { - result = await wutils.GetItem(staging_target_path); + result = await wutils.GetItem(win_staging_target_path); // UNC\172.29.0.111\tank_k8s_test_PVC_111\ let target = _.get(result, "Target.[0]", ""); let parts = target.split("\\"); @@ -1281,7 +1285,7 @@ class CsiBaseDriver { parts[2] != volume_context.share ) { throw new Error( - `${target} mounted already at ${staging_target_path}` + `${target} mounted already at ${win_staging_target_path}` ); } else { // finish early, assured we have what we need @@ -1310,14 +1314,14 @@ class CsiBaseDriver { try { await wutils.NewSmbLink( filesystem.covertUnixSeparatorToWindowsSeparator(device), - staging_target_path + win_staging_target_path ); } catch (e) { let details = _.get(e, "stderr", ""); if (!details.includes("ResourceExists")) { throw e; } else { - result = fs.lstatSync(staging_target_path); + result = fs.lstatSync(win_staging_target_path); if (!result.isSymbolicLink()) { throw new Error("staging path exists but is not symlink"); } @@ -1565,20 +1569,20 @@ class CsiBaseDriver { await wutils.FormatVolume(volume.UniqueId); } - result = await wutils.GetItem(staging_target_path); + result = await wutils.GetItem(win_staging_target_path); if (!result) { - fs.mkdirSync(staging_target_path, { + fs.mkdirSync(win_staging_target_path, { recursive: true, mode: "755", }); - result = await wutils.GetItem(staging_target_path); + result = await wutils.GetItem(win_staging_target_path); } if (!volume.UniqueId.includes(result.Target[0])) { // mount up! await wutils.MountVolume( volume.UniqueId, - staging_target_path + win_staging_target_path ); } break; @@ -2182,6 +2186,11 @@ class CsiBaseDriver { const WindowsUtils = require("../utils/windows").Windows; const wutils = new WindowsUtils(); + let win_normalized_staging_path = + filesystem.covertUnixSeparatorToWindowsSeparator( + normalized_staging_path + ); + async function removePath(p) { // remove staging path try { @@ -2196,7 +2205,7 @@ class CsiBaseDriver { let node_attach_driver; let win_volume_id; - result = await wutils.GetItem(normalized_staging_path); + result = await wutils.GetItem(win_normalized_staging_path); if (result) { let target = _.get(result, "Target.[0]", ""); if (target.startsWith("UNC")) { @@ -2229,7 +2238,7 @@ class CsiBaseDriver { // unmount volume await wutils.UnmountVolume( win_volume_id, - normalized_staging_path + win_normalized_staging_path ); // find sessions associated with volume/disks @@ -2258,7 +2267,7 @@ class CsiBaseDriver { } // remove staging path - await removePath(normalized_staging_path); + await removePath(win_normalized_staging_path); break; } case NODE_OS_DRIVER_CSI_PROXY: @@ -2578,6 +2587,11 @@ class CsiBaseDriver { normalized_staging_path = staging_target_path; } + normalized_staging_path = + filesystem.covertUnixSeparatorToWindowsSeparator( + normalized_staging_path + ); + // source path result = await wutils.GetItem(normalized_staging_path); if (!result) { @@ -2786,8 +2800,10 @@ class CsiBaseDriver { case NODE_OS_DRIVER_WINDOWS: const WindowsUtils = require("../utils/windows").Windows; const wutils = new WindowsUtils(); + let win_target_path = + filesystem.covertUnixSeparatorToWindowsSeparator(target_path); - result = await wutils.GetItem(target_path); + result = await wutils.GetItem(win_target_path); if (!result) { return {}; } @@ -2795,11 +2811,11 @@ class CsiBaseDriver { if (_.get(result, "LinkType") != "SymbolicLink") { throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `target path is not a symlink ${target_path}` + `target path is not a symlink ${win_target_path}` ); } - fs.rmdirSync(target_path); + fs.rmdirSync(win_target_path); break; case NODE_OS_DRIVER_CSI_PROXY: const csiProxyClient = driver.getDefaultCsiProxyClientInstance(); @@ -2929,19 +2945,20 @@ class CsiBaseDriver { case NODE_OS_DRIVER_WINDOWS: { const WindowsUtils = require("../utils/windows").Windows; const wutils = new WindowsUtils(); - + let win_volume_path = + filesystem.covertUnixSeparatorToWindowsSeparator(volume_path); // ensure path is mounted - result = await wutils.GetItem(volume_path); + result = await wutils.GetItem(win_volume_path); if (!result) { throw new GrpcError( grpc.status.NOT_FOUND, - `volume_path ${volume_path} is not currently mounted` + `volume_path ${win_volume_path} is not currently mounted` ); } let node_attach_driver; - let target = await wutils.GetRealTarget(volume_path); + let target = await wutils.GetRealTarget(win_volume_path); if (target.startsWith("\\\\")) { node_attach_driver = "smb"; } @@ -3180,17 +3197,19 @@ class CsiBaseDriver { const wutils = new WindowsUtils(); let node_attach_driver; + let win_volume_path = + filesystem.covertUnixSeparatorToWindowsSeparator(volume_path); // ensure path is mounted - result = await wutils.GetItem(volume_path); + result = await wutils.GetItem(win_volume_path); if (!result) { throw new GrpcError( grpc.status.NOT_FOUND, - `volume_path ${volume_path} is not currently mounted` + `volume_path ${win_volume_path} is not currently mounted` ); } - let target = await wutils.GetRealTarget(volume_path); + let target = await wutils.GetRealTarget(win_volume_path); if (target.startsWith("\\\\")) { node_attach_driver = "smb"; } From d071b879b0ba201b3d1a54991ec63026dc5d55a0 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 5 May 2022 20:55:41 -0600 Subject: [PATCH 33/71] proper scope for variable Signed-off-by: Travis Glenn Hansen --- src/driver/index.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/driver/index.js b/src/driver/index.js index 2bcacf2..0323644 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -1239,13 +1239,11 @@ class CsiBaseDriver { const WindowsUtils = require("../utils/windows").Windows; const wutils = new WindowsUtils(); + let win_staging_target_path = + filesystem.covertUnixSeparatorToWindowsSeparator(staging_target_path); switch (node_attach_driver) { case "smb": - let win_staging_target_path = - filesystem.covertUnixSeparatorToWindowsSeparator( - staging_target_path - ); device = `//${volume_context.server}/${volume_context.share}`; const username = driver.getMountFlagValue(mount_flags, "username"); const password = driver.getMountFlagValue(mount_flags, "password"); From 8014fed9c0b4e8e58ad5d23ba390bd7c54c61a07 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 6 May 2022 09:21:55 -0600 Subject: [PATCH 34/71] make controller-client more portable (support for windows) Signed-off-by: Travis Glenn Hansen --- ci/configs/synlogy/dsm6/iscsi.yaml | 4 +- ci/configs/synlogy/dsm7/iscsi.yaml | 4 +- ci/configs/truenas/core/12.0/core-iscsi.yaml | 2 +- ci/configs/truenas/core/12.0/core-smb.yaml | 4 +- ci/configs/truenas/core/13.0/core-iscsi.yaml | 2 +- ci/configs/truenas/core/13.0/core-smb.yaml | 4 +- .../truenas/scale/22.02/scale-iscsi.yaml | 2 +- ci/configs/truenas/scale/22.02/scale-smb.yaml | 4 +- ci/configs/zfs-generic/iscsi.yaml | 2 +- package-lock.json | 336 +++++++++++------- package.json | 3 +- src/driver/controller-client-common/index.js | 127 +++++-- 12 files changed, 320 insertions(+), 174 deletions(-) diff --git a/ci/configs/synlogy/dsm6/iscsi.yaml b/ci/configs/synlogy/dsm6/iscsi.yaml index 8b58913..7c0ca87 100644 --- a/ci/configs/synlogy/dsm6/iscsi.yaml +++ b/ci/configs/synlogy/dsm6/iscsi.yaml @@ -16,8 +16,8 @@ iscsi: targetPortal: ${SYNOLOGY_HOST} targetPortals: [] baseiqn: "iqn.2000-01.com.synology:XpenoDsm62x." - namePrefix: "csi-${CI_BUILD_KEY}-" - nameSuffix: "-ci" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" + nameSuffix: "" lunTemplate: # btrfs thin provisioning diff --git a/ci/configs/synlogy/dsm7/iscsi.yaml b/ci/configs/synlogy/dsm7/iscsi.yaml index 8b58913..7c0ca87 100644 --- a/ci/configs/synlogy/dsm7/iscsi.yaml +++ b/ci/configs/synlogy/dsm7/iscsi.yaml @@ -16,8 +16,8 @@ iscsi: targetPortal: ${SYNOLOGY_HOST} targetPortals: [] baseiqn: "iqn.2000-01.com.synology:XpenoDsm62x." - namePrefix: "csi-${CI_BUILD_KEY}-" - nameSuffix: "-ci" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" + nameSuffix: "" lunTemplate: # btrfs thin provisioning diff --git a/ci/configs/truenas/core/12.0/core-iscsi.yaml b/ci/configs/truenas/core/12.0/core-iscsi.yaml index 7d65a5a..aa32a78 100644 --- a/ci/configs/truenas/core/12.0/core-iscsi.yaml +++ b/ci/configs/truenas/core/12.0/core-iscsi.yaml @@ -26,7 +26,7 @@ zfs: iscsi: targetPortal: ${TRUENAS_HOST} interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" targetGroups: - targetGroupPortalGroup: 1 diff --git a/ci/configs/truenas/core/12.0/core-smb.yaml b/ci/configs/truenas/core/12.0/core-smb.yaml index f5efbf3..fe6856e 100644 --- a/ci/configs/truenas/core/12.0/core-smb.yaml +++ b/ci/configs/truenas/core/12.0/core-smb.yaml @@ -39,7 +39,7 @@ zfs: smb: shareHost: ${TRUENAS_HOST} #nameTemplate: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" shareAuxiliaryConfigurationTemplate: | #guest ok = yes @@ -51,7 +51,7 @@ smb: shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true - shareRecycleBin: true + shareRecycleBin: false shareBrowsable: false shareAccessBasedEnumeration: true shareTimeMachine: false diff --git a/ci/configs/truenas/core/13.0/core-iscsi.yaml b/ci/configs/truenas/core/13.0/core-iscsi.yaml index 2cf1d84..3a8619b 100644 --- a/ci/configs/truenas/core/13.0/core-iscsi.yaml +++ b/ci/configs/truenas/core/13.0/core-iscsi.yaml @@ -26,7 +26,7 @@ zfs: iscsi: targetPortal: ${TRUENAS_HOST} interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" targetGroups: - targetGroupPortalGroup: 1 diff --git a/ci/configs/truenas/core/13.0/core-smb.yaml b/ci/configs/truenas/core/13.0/core-smb.yaml index f5efbf3..fe6856e 100644 --- a/ci/configs/truenas/core/13.0/core-smb.yaml +++ b/ci/configs/truenas/core/13.0/core-smb.yaml @@ -39,7 +39,7 @@ zfs: smb: shareHost: ${TRUENAS_HOST} #nameTemplate: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" shareAuxiliaryConfigurationTemplate: | #guest ok = yes @@ -51,7 +51,7 @@ smb: shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true - shareRecycleBin: true + shareRecycleBin: false shareBrowsable: false shareAccessBasedEnumeration: true shareTimeMachine: false diff --git a/ci/configs/truenas/scale/22.02/scale-iscsi.yaml b/ci/configs/truenas/scale/22.02/scale-iscsi.yaml index 05d0359..b6b6f43 100644 --- a/ci/configs/truenas/scale/22.02/scale-iscsi.yaml +++ b/ci/configs/truenas/scale/22.02/scale-iscsi.yaml @@ -20,7 +20,7 @@ zfs: iscsi: targetPortal: ${TRUENAS_HOST} interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" targetGroups: - targetGroupPortalGroup: 1 diff --git a/ci/configs/truenas/scale/22.02/scale-smb.yaml b/ci/configs/truenas/scale/22.02/scale-smb.yaml index 95b0b9a..2a8861e 100644 --- a/ci/configs/truenas/scale/22.02/scale-smb.yaml +++ b/ci/configs/truenas/scale/22.02/scale-smb.yaml @@ -21,7 +21,7 @@ zfs: smb: shareHost: ${TRUENAS_HOST} #nameTemplate: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" shareAuxiliaryConfigurationTemplate: | #guest ok = yes @@ -33,7 +33,7 @@ smb: shareGuestOk: false #shareGuestOnly: true #shareShowHiddenFiles: true - shareRecycleBin: true + shareRecycleBin: false shareBrowsable: false shareAccessBasedEnumeration: true shareTimeMachine: false diff --git a/ci/configs/zfs-generic/iscsi.yaml b/ci/configs/zfs-generic/iscsi.yaml index 38dc594..e5bc3e2 100644 --- a/ci/configs/zfs-generic/iscsi.yaml +++ b/ci/configs/zfs-generic/iscsi.yaml @@ -18,7 +18,7 @@ zfs: iscsi: targetPortal: ${SERVER_HOST} interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}" + namePrefix: "csi-ci-${CI_BUILD_KEY}-" nameSuffix: "" shareStrategy: "targetCli" shareStrategyTargetCli: diff --git a/package-lock.json b/package-lock.json index cb81a02..d252a35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,8 +13,9 @@ "@grpc/proto-loader": "^0.6.0", "@kubernetes/client-node": "^0.16.3", "async-mutex": "^0.3.1", - "axios": "^0.26.1", + "axios": "^0.27.2", "bunyan": "^1.8.15", + "fs-extra": "^10.1.0", "handlebars": "^4.7.7", "js-yaml": "^4.0.0", "lodash": "^4.17.21", @@ -50,9 +51,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", + "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -70,9 +71,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.5.tgz", - "integrity": "sha512-h0KSwgLiF5rmSAU6qnzK1aoD1MNqOw9HJK96N8VW3dR5FHMpq+0JNdLQFP//NcaIWVB7I7vkHl4JmU9hUw82Aw==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.7.tgz", + "integrity": "sha512-eBM03pu9hd3VqDQG+kHahiG1x80RGkkqqRb1Pchcwqej/KkAH95gAvKs6laqaHCycYaPK+TKuNQnOz9UXYA8qw==", "dependencies": { "@grpc/proto-loader": "^0.6.4", "@types/node": ">=12.12.47" @@ -82,9 +83,9 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", - "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", + "version": "0.6.12", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.12.tgz", + "integrity": "sha512-filTVbETFnxb9CyRX98zN18ilChTuf/C5scZ2xyaOTp0EHGq0/ufX8rjqXUcSb1Gpv7eZq4M2jDvbh9BogKnrg==", "dependencies": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", @@ -302,9 +303,9 @@ } }, "node_modules/@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" }, "node_modules/@types/minipass": { "version": "3.1.2", @@ -315,9 +316,9 @@ } }, "node_modules/@types/node": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", - "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" + "version": "17.0.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz", + "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==" }, "node_modules/@types/request": { "version": "2.48.8", @@ -374,9 +375,9 @@ } }, "node_modules/acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -478,9 +479,9 @@ } }, "node_modules/async-mutex/node_modules/tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" }, "node_modules/asynckit": { "version": "0.4.0", @@ -501,11 +502,25 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", "dependencies": { - "follow-redirects": "^1.14.8" + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, "node_modules/balanced-match": { @@ -693,9 +708,9 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/color-string": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", - "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -743,9 +758,9 @@ } }, "node_modules/compress-brotli": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.6.tgz", - "integrity": "sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.8.tgz", + "integrity": "sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==", "dependencies": { "@types/json-buffer": "~3.0.0", "json-buffer": "~3.0.1" @@ -947,12 +962,12 @@ } }, "node_modules/eslint": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.13.0.tgz", - "integrity": "sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", + "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.2.1", + "@eslint/eslintrc": "^1.2.2", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -1204,9 +1219,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz", + "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==", "funding": [ { "type": "individual", @@ -1243,6 +1258,19 @@ "node": ">= 0.12" } }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -1367,6 +1395,11 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, "node_modules/handlebars": { "version": "4.7.7", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", @@ -1537,9 +1570,9 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "node_modules/is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", "dependencies": { "has": "^1.0.3" }, @@ -1666,6 +1699,17 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, "node_modules/jsonpath-plus": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz", @@ -1757,9 +1801,9 @@ } }, "node_modules/lru-cache": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", - "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.9.0.tgz", + "integrity": "sha512-lkcNMUKqdJk96TuIXUidxaPuEg5sJo/+ZyVE2BDFnuZGzwXem7d8582eG8vbu4todLfT14snP6iHriCHXXi5Rw==", "engines": { "node": ">=12" } @@ -1861,9 +1905,9 @@ } }, "node_modules/moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "optional": true, "engines": { "node": "*" @@ -2142,11 +2186,11 @@ } }, "node_modules/prompt/node_modules/winston": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", - "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", + "integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", "dependencies": { - "async": "~1.0.0", + "async": "^3.2.3", "colors": "1.0.x", "cycle": "1.0.x", "eyes": "0.1.x", @@ -2157,11 +2201,6 @@ "node": ">= 0.10.0" } }, - "node_modules/prompt/node_modules/winston/node_modules/async": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", - "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" - }, "node_modules/protobufjs": { "version": "6.11.2", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", @@ -2521,9 +2560,9 @@ } }, "node_modules/ssh2": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.9.0.tgz", - "integrity": "sha512-rhhIZT0eMPvCBSOG8CpqZZ7gre2vgXaIqmb3Jb83t88rjsxIsFzDanqBJM9Ns8BmP1835A5IbQ199io4EUZwOA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.10.0.tgz", + "integrity": "sha512-OnKAAmf4j8wCRrXXZv3Tp5lCZkLJZtgZbn45ELiShCg27djDQ3XFGvIzuGsIsf4hdHslP+VdhA9BhUQdTdfd9w==", "hasInstallScript": true, "dependencies": { "asn1": "^0.2.4", @@ -2784,9 +2823,17 @@ } }, "node_modules/underscore": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", - "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==" + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.3.tgz", + "integrity": "sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==" + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } }, "node_modules/uri-js": { "version": "4.4.1", @@ -2986,9 +3033,9 @@ } }, "@eslint/eslintrc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz", - "integrity": "sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", + "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", "dev": true, "requires": { "ajv": "^6.12.4", @@ -3003,18 +3050,18 @@ } }, "@grpc/grpc-js": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.5.tgz", - "integrity": "sha512-h0KSwgLiF5rmSAU6qnzK1aoD1MNqOw9HJK96N8VW3dR5FHMpq+0JNdLQFP//NcaIWVB7I7vkHl4JmU9hUw82Aw==", + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.7.tgz", + "integrity": "sha512-eBM03pu9hd3VqDQG+kHahiG1x80RGkkqqRb1Pchcwqej/KkAH95gAvKs6laqaHCycYaPK+TKuNQnOz9UXYA8qw==", "requires": { "@grpc/proto-loader": "^0.6.4", "@types/node": ">=12.12.47" } }, "@grpc/proto-loader": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.9.tgz", - "integrity": "sha512-UlcCS8VbsU9d3XTXGiEVFonN7hXk+oMXZtoHHG2oSA1/GcDP1q6OUgs20PzHDGizzyi8ufGSUDlk3O2NyY7leg==", + "version": "0.6.12", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.12.tgz", + "integrity": "sha512-filTVbETFnxb9CyRX98zN18ilChTuf/C5scZ2xyaOTp0EHGq0/ufX8rjqXUcSb1Gpv7eZq4M2jDvbh9BogKnrg==", "requires": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", @@ -3209,9 +3256,9 @@ } }, "@types/long": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.1.tgz", - "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" }, "@types/minipass": { "version": "3.1.2", @@ -3222,9 +3269,9 @@ } }, "@types/node": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.24.tgz", - "integrity": "sha512-aveCYRQbgTH9Pssp1voEP7HiuWlD2jW2BO56w+bVrJn04i61yh6mRfoKO6hEYQD9vF+W8Chkwc6j1M36uPkx4g==" + "version": "17.0.31", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz", + "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==" }, "@types/request": { "version": "2.48.8", @@ -3281,9 +3328,9 @@ } }, "acorn": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz", - "integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==", + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", "dev": true }, "acorn-jsx": { @@ -3358,9 +3405,9 @@ }, "dependencies": { "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" } } }, @@ -3380,11 +3427,24 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", "requires": { - "follow-redirects": "^1.14.8" + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } } }, "balanced-match": { @@ -3547,9 +3607,9 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "color-string": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", - "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", "requires": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -3578,9 +3638,9 @@ } }, "compress-brotli": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.6.tgz", - "integrity": "sha512-au99/GqZtUtiCBliqLFbWlhnCxn+XSYjwZ77q6mKN4La4qOXDoLVPZ50iXr0WmAyMxl8yqoq3Yq4OeQNPPkyeQ==", + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/compress-brotli/-/compress-brotli-1.3.8.tgz", + "integrity": "sha512-lVcQsjhxhIXsuupfy9fmZUFtAIdBmXA7EGY6GBdgZ++qkM9zG4YFT8iU7FoBxzryNDMOpD1HIFHUSX4D87oqhQ==", "requires": { "@types/json-buffer": "~3.0.0", "json-buffer": "~3.0.1" @@ -3726,12 +3786,12 @@ "dev": true }, "eslint": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.13.0.tgz", - "integrity": "sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", + "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.1", + "@eslint/eslintrc": "^1.2.2", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -3925,9 +3985,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "follow-redirects": { - "version": "1.14.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", - "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz", + "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==" }, "forever-agent": { "version": "0.6.1", @@ -3944,6 +4004,16 @@ "mime-types": "^2.1.12" } }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -4035,6 +4105,11 @@ "responselike": "^2.0.0" } }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + }, "handlebars": { "version": "4.7.7", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", @@ -4156,9 +4231,9 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, "is-core-module": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz", - "integrity": "sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.9.0.tgz", + "integrity": "sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A==", "requires": { "has": "^1.0.3" } @@ -4256,6 +4331,15 @@ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, "jsonpath-plus": { "version": "0.19.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-0.19.0.tgz", @@ -4335,9 +4419,9 @@ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, "lru-cache": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.8.1.tgz", - "integrity": "sha512-E1v547OCgJvbvevfjgK9sNKIVXO96NnsTsFPBlg4ZxjhsJSODoH9lk8Bm0OxvHNm6Vm5Yqkl/1fErDxhYL8Skg==" + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.9.0.tgz", + "integrity": "sha512-lkcNMUKqdJk96TuIXUidxaPuEg5sJo/+ZyVE2BDFnuZGzwXem7d8582eG8vbu4todLfT14snP6iHriCHXXi5Rw==" }, "make-error": { "version": "1.3.6", @@ -4412,9 +4496,9 @@ } }, "moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "optional": true }, "ms": { @@ -4628,23 +4712,16 @@ }, "dependencies": { "winston": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.5.tgz", - "integrity": "sha512-TWoamHt5yYvsMarGlGEQE59SbJHqGsZV8/lwC+iCcGeAe0vUaOh+Lv6SYM17ouzC/a/LB1/hz/7sxFBtlu1l4A==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.6.tgz", + "integrity": "sha512-J5Zu4p0tojLde8mIOyDSsmLmcP8I3Z6wtwpTDHx1+hGcdhxcJaAmG4CFtagkb+NiN1M9Ek4b42pzMWqfc9jm8w==", "requires": { - "async": "~1.0.0", + "async": "^3.2.3", "colors": "1.0.x", "cycle": "1.0.x", "eyes": "0.1.x", "isstream": "0.1.x", "stack-trace": "0.0.x" - }, - "dependencies": { - "async": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async/-/async-1.0.0.tgz", - "integrity": "sha1-+PwEyjoTeErenhZBr5hXjPvWR6k=" - } } } } @@ -4907,9 +4984,9 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" }, "ssh2": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.9.0.tgz", - "integrity": "sha512-rhhIZT0eMPvCBSOG8CpqZZ7gre2vgXaIqmb3Jb83t88rjsxIsFzDanqBJM9Ns8BmP1835A5IbQ199io4EUZwOA==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.10.0.tgz", + "integrity": "sha512-OnKAAmf4j8wCRrXXZv3Tp5lCZkLJZtgZbn45ELiShCg27djDQ3XFGvIzuGsIsf4hdHslP+VdhA9BhUQdTdfd9w==", "requires": { "asn1": "^0.2.4", "bcrypt-pbkdf": "^1.0.2", @@ -5095,9 +5172,14 @@ "optional": true }, "underscore": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.2.tgz", - "integrity": "sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==" + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.3.tgz", + "integrity": "sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==" + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" }, "uri-js": { "version": "4.4.1", diff --git a/package.json b/package.json index 8459eeb..4d16cb0 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,9 @@ "@grpc/proto-loader": "^0.6.0", "@kubernetes/client-node": "^0.16.3", "async-mutex": "^0.3.1", - "axios": "^0.26.1", + "axios": "^0.27.2", "bunyan": "^1.8.15", + "fs-extra": "^10.1.0", "handlebars": "^4.7.7", "js-yaml": "^4.0.0", "lodash": "^4.17.21", diff --git a/src/driver/controller-client-common/index.js b/src/driver/controller-client-common/index.js index 1297b8f..cfe3df9 100644 --- a/src/driver/controller-client-common/index.js +++ b/src/driver/controller-client-common/index.js @@ -3,6 +3,8 @@ const { CsiBaseDriver } = require("../index"); const { GrpcError, grpc } = require("../../utils/grpc"); const cp = require("child_process"); const fs = require("fs"); +const fse = require("fs-extra"); +const path = require("path"); const semver = require("semver"); /** @@ -230,9 +232,14 @@ class ControllerClientCommonDriver extends CsiBaseDriver { } async getDirectoryUsage(path) { - let result = await this.exec("du", ["-s", "--block-size=1", path]); - let size = result.stdout.split("\t", 1)[0]; - return size; + if (this.getNodeIsWindows()) { + this.ctx.logger.warn("du not implemented on windows"); + return 0; + } else { + let result = await this.exec("du", ["-s", "--block-size=1", path]); + let size = result.stdout.split("\t", 1)[0]; + return size; + } } exec(command, args, options = {}) { @@ -297,20 +304,39 @@ class ControllerClientCommonDriver extends CsiBaseDriver { } async cloneDir(source_path, target_path) { - await this.exec("mkdir", ["-p", target_path]); + if (this.getNodeIsWindows()) { + fse.copySync( + this.stripTrailingSlash(source_path), + this.stripTrailingSlash(target_path), + { + overwrite: true, + dereference: true, + preserveTimestamps: true, + //errorOnExist: true, + } + ); + } else { + await this.createDir(target_path); - /** - * trailing / is important - * rsync -a /mnt/storage/s/foo/ /mnt/storage/v/PVC-111/ - */ - await this.exec("rsync", [ - "-a", - this.stripTrailingSlash(source_path) + "/", - this.stripTrailingSlash(target_path) + "/", - ]); + /** + * trailing / is important + * rsync -a /mnt/storage/s/foo/ /mnt/storage/v/PVC-111/ + */ + await this.exec("rsync", [ + "-a", + this.stripTrailingSlash(source_path) + "/", + this.stripTrailingSlash(target_path) + "/", + ]); + } } async getAvailableSpaceAtPath(path) { + // https://www.npmjs.com/package/diskusage + // https://www.npmjs.com/package/check-disk-space + if (this.getNodeIsWindows()) { + this.ctx.logger.warn("df not implemented on windows"); + return 0; + } //df --block-size=1 --output=avail /mnt/storage/ // Avail //1481334328 @@ -325,11 +351,14 @@ class ControllerClientCommonDriver extends CsiBaseDriver { } async createDir(path) { - await this.exec("mkdir", ["-p", path]); + fs.mkdirSync(path, { + recursive: true, + mode: "755", + }); } async deleteDir(path) { - await this.exec("rm", ["-rf", path]); + fs.rmSync(path, { recursive: true, force: true }); return; @@ -346,7 +375,40 @@ class ControllerClientCommonDriver extends CsiBaseDriver { } async directoryExists(path) { - return fs.existsSync(path); + let r; + r = fs.existsSync(path); + if (!r) { + return r; + } + + if (!fs.statSync(path).isDirectory()) { + throw new Error(`path [${path}] exists but is not a directory`); + } + + return true; + } + + /** + * Have to be careful with the logic here as the controller could be running + * on win32 for *-client vs local-hostpath + * + * @param {*} path + * @returns + */ + async normalizePath(path) { + if (process.platform == "win32") { + return await this.noramlizePathWin32(path); + } else { + return await this.normalizePathPosix(path); + } + } + + async normalizePathPosix(p) { + return p.replaceAll(path.win32.sep, path.posix.sep); + } + + async noramlizePathWin32(p) { + return p.replaceAll(path.posix.sep, path.win32.sep); } /** @@ -441,7 +503,7 @@ class ControllerClientCommonDriver extends CsiBaseDriver { //let volume_content_source_volume_id; // create target dir - response = await driver.exec("mkdir", ["-p", volume_path]); + await driver.createDir(target_path); // create dataset if (volume_content_source) { @@ -476,7 +538,7 @@ class ControllerClientCommonDriver extends CsiBaseDriver { } driver.ctx.logger.debug("controller source path: %s", source_path); - response = await driver.cloneDir(source_path, volume_path); + await driver.cloneDir(source_path, volume_path); } // set mode @@ -486,10 +548,7 @@ class ControllerClientCommonDriver extends CsiBaseDriver { this.options[config_key].dirPermissionsMode, volume_path ); - response = await driver.exec("chmod", [ - this.options[config_key].dirPermissionsMode, - volume_path, - ]); + fs.chmodSync(volume_path, this.options[config_key].dirPermissionsMode); } // set ownership @@ -503,16 +562,20 @@ class ControllerClientCommonDriver extends CsiBaseDriver { this.options[config_key].dirPermissionsGroup, volume_path ); - response = await driver.exec("chown", [ - (this.options[config_key].dirPermissionsUser - ? this.options[config_key].dirPermissionsUser - : "") + - ":" + - (this.options[config_key].dirPermissionsGroup - ? this.options[config_key].dirPermissionsGroup - : ""), - volume_path, - ]); + if (this.getNodeIsWindows()) { + driver.ctx.logger.warn("chown not implemented on windows"); + } else { + await driver.exec("chown", [ + (this.options[config_key].dirPermissionsUser + ? this.options[config_key].dirPermissionsUser + : "") + + ":" + + (this.options[config_key].dirPermissionsGroup + ? this.options[config_key].dirPermissionsGroup + : ""), + volume_path, + ]); + } } let volume_context = driver.getVolumeContext(name); From c6f9fd9a6e516568c8f2ec818b1a06b05e71951a Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 6 May 2022 11:53:36 -0600 Subject: [PATCH 35/71] windows hostpath --- .github/workflows/main.yml | 24 ++++++---- ci/bin/launch-csi-sanity.ps1 | 33 +++++++++----- src/driver/controller-client-common/index.js | 2 +- src/driver/index.js | 47 +++++++++++++++++--- 4 files changed, 80 insertions(+), 26 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9eb2439..2f71dca 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -309,24 +309,31 @@ jobs: strategy: fail-fast: false matrix: - config: - - local-hostpath/basic.yaml + os: [Linux, Windows] + include: + - os: Linux + template: "./ci/configs/local-hostpath/basic.yaml" + run: | + # run tests + ci/bin/run.sh + - os: Windows + template: ".\\ci\\configs\\local-hostpath/basic.yaml" + run: | + # run tests + ci\bin\run.ps1 runs-on: - self-hosted - - Linux + - ${{ matrix.os }} - X64 - - csi-sanity-zfs-local steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: name: node-modules-linux-amd64 - name: csi-sanity - run: | - # run tests - ci/bin/run.sh + run: ${{ matrix.run }} env: - TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" + TEMPLATE_CONFIG_FILE: "${{ matrix.template }}" CSI_SANITY_SKIP: "should fail when requesting to create a snapshot with already existing name and different source volume ID|should fail when requesting to create a volume with already existing name and different capacity" csi-sanity-windows-node: @@ -356,6 +363,7 @@ jobs: SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_HOST }} SERVER_USERNAME: ${{ secrets.SANITY_ZFS_GENERIC_USERNAME }} SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }} + CSI_SANITY_FOCUS: "Node Service" build-docker-linux: needs: diff --git a/ci/bin/launch-csi-sanity.ps1 b/ci/bin/launch-csi-sanity.ps1 index d36ab5e..ba99971 100644 --- a/ci/bin/launch-csi-sanity.ps1 +++ b/ci/bin/launch-csi-sanity.ps1 @@ -10,39 +10,48 @@ $exit_code = 0 $tmpdir = New-Item -ItemType Directory -Path ([System.IO.Path]::GetTempPath()) -Name ([System.IO.Path]::GetRandomFileName()) $env:CSI_SANITY_TEMP_DIR = $tmpdir.FullName -if (! $env:CSI_SANITY_FOCUS) { - $env:CSI_SANITY_FOCUS = "Node Service" -} - -if (! $env:CSI_SANITY_SKIP) { - $env:CSI_SANITY_SKIP = "" -} - # cleanse endpoint to something csi-sanity plays nicely with $endpoint = ${env:CSI_ENDPOINT} $endpoint = $endpoint.replace("C:\", "/") $endpoint = $endpoint.replace("\", "/") +if (! $env:CSI_SANITY_FAILFAST) { + $env:CSI_SANITY_FAILFAST = "false" +} + +$failfast = "" + +if ($env:CSI_SANITY_FAILFAST -eq "true") { + $failfast = "-ginkgo.failFast" +} + Write-Output "launching csi-sanity" Write-Output "connecting to: ${endpoint}" +Write-Output "failfast: ${env:CSI_SANITY_FAILFAST}" Write-Output "skip: ${env:CSI_SANITY_SKIP}" Write-Output "focus: ${env:CSI_SANITY_FOCUS}" +$skip = '"' + ${env:CSI_SANITY_SKIP} + '"' +$focus = '"' + ${env:CSI_SANITY_FOCUS} + '"' + csi-sanity.exe -"csi.endpoint" "unix://${endpoint}" ` - -"ginkgo.failFast" ` + $failfast ` -"csi.mountdir" "${env:CSI_SANITY_TEMP_DIR}\mnt" ` -"csi.stagingdir" "${env:CSI_SANITY_TEMP_DIR}\stage" ` -"csi.testvolumeexpandsize" 2147483648 ` -"csi.testvolumesize" 1073741824 ` - -"ginkgo.focus" "${env:CSI_SANITY_FOCUS}" + -"ginkgo.skip" $skip ` + -"ginkgo.focus" $focus # does not work the same as linux for some reason -#-"ginkgo.skip" "${env:CSI_SANITY_SKIP}" ` +# -"ginkgo.skip" "'" + ${env:CSI_SANITY_SKIP} + "'" ` if (-not $?) { $exit_code = $LASTEXITCODE Write-Output "csi-sanity exit code: ${exit_code}" - $exit_code = 1 + if ($exit_code -gt 0) { + $exit_code = 1 + } } # remove tmp dir diff --git a/src/driver/controller-client-common/index.js b/src/driver/controller-client-common/index.js index cfe3df9..a211888 100644 --- a/src/driver/controller-client-common/index.js +++ b/src/driver/controller-client-common/index.js @@ -503,7 +503,7 @@ class ControllerClientCommonDriver extends CsiBaseDriver { //let volume_content_source_volume_id; // create target dir - await driver.createDir(target_path); + await driver.createDir(volume_path); // create dataset if (volume_content_source) { diff --git a/src/driver/index.js b/src/driver/index.js index 0323644..8a68fc8 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -1222,10 +1222,10 @@ class CsiBaseDriver { break; case NODE_OS_DRIVER_WINDOWS: // sanity check node_attach_driver - if (!["smb", "iscsi"].includes(node_attach_driver)) { + if (!["smb", "iscsi", "hostpath"].includes(node_attach_driver)) { throw new GrpcError( grpc.status.UNIMPLEMENTED, - `csi-proxy does not work with node_attach_driver: ${node_attach_driver}` + `windows does not work with node_attach_driver: ${node_attach_driver}` ); } @@ -1233,7 +1233,7 @@ class CsiBaseDriver { if (fs_type && !["ntfs", "cifs"].includes(fs_type)) { throw new GrpcError( grpc.status.UNIMPLEMENTED, - `csi-proxy does not work with fs_type: ${fs_type}` + `windows does not work with fs_type: ${fs_type}` ); } @@ -1592,6 +1592,39 @@ class CsiBaseDriver { ); } break; + case "hostpath": + try { + fs.statSync(win_staging_target_path); + result = true; + } catch (err) { + if (err.code === "ENOENT") { + result = false; + } else { + throw err; + } + } + + // if exists already delete if folder, return if symlink + if (result) { + result = fs.lstatSync(win_staging_target_path); + // remove pre-created dir by CO + if (!result.isSymbolicLink()) { + fs.rmdirSync(win_staging_target_path); + } else { + // assume symlink points to the correct location + return {}; + } + } + + // create symlink + fs.symlinkSync( + filesystem.covertUnixSeparatorToWindowsSeparator( + volume_context.path + ), + win_staging_target_path + ); + return {}; + break; default: throw new GrpcError( grpc.status.INVALID_ARGUMENT, @@ -2254,6 +2287,9 @@ class CsiBaseDriver { // delete target/target portal/etc // do NOT do this now as removing the portal will remove all targets associated with it break; + case "hostpath": + // allow below code to remove symlink + break; case "bypass": break; default: @@ -2560,7 +2596,7 @@ class CsiBaseDriver { case "smb": //case "lustre": //case "oneclient": - //case "hostpath": + case "hostpath": case "iscsi": //case "zfs-local": // ensure appropriate directories/files @@ -2956,7 +2992,7 @@ class CsiBaseDriver { let node_attach_driver; - let target = await wutils.GetRealTarget(win_volume_path); + let target = await wutils.GetRealTarget(win_volume_path) || ""; if (target.startsWith("\\\\")) { node_attach_driver = "smb"; } @@ -2987,6 +3023,7 @@ class CsiBaseDriver { ]; break; case "bypass": + res.usage = [{ total: 0, unit: "BYTES" }]; break; default: throw new GrpcError( From e202c63420379abeb3cdb0b99167ebd045a3544a Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 6 May 2022 12:22:39 -0600 Subject: [PATCH 36/71] better needs, log running node version at startup Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 1 + bin/democratic-csi | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2f71dca..94bdf89 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -306,6 +306,7 @@ jobs: csi-sanity-local-hostpath: needs: - build-npm-linux-amd64 + - build-npm-windows-amd64 strategy: fail-fast: false matrix: diff --git a/bin/democratic-csi b/bin/democratic-csi index f616a8b..254ebb2 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -5,7 +5,6 @@ * https://github.com/democratic-csi/democratic-csi/issues/171 */ - // polyfills require("../src/utils/polyfills"); const yaml = require("js-yaml"); @@ -373,9 +372,10 @@ if (args.serverSocket) { } logger.info( - "starting csi server - name: %s, version: %s, driver: %s, mode: %s, csi version: %s, address: %s, socket: %s", - args.csiName, + "starting csi server - node version: %s, package version: %s, csi-name: %s, csi-driver: %s, csi-mode: %s, csi-version: %s, address: %s, socket: %s", + process.version, args.version, + args.csiName, options.driver, args.csiMode.join(","), args.csiVersion, From 7ea288a00884ce1899569875c69a2b97c62ce06a Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 6 May 2022 12:37:43 -0600 Subject: [PATCH 37/71] ci/debug Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 4 +++- ci/bin/launch-server.sh | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 94bdf89..020cb61 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -313,11 +313,13 @@ jobs: os: [Linux, Windows] include: - os: Linux + npmartifact: node-modules-linux-amd64 template: "./ci/configs/local-hostpath/basic.yaml" run: | # run tests ci/bin/run.sh - os: Windows + npmartifact: node-modules-windows-amd64 template: ".\\ci\\configs\\local-hostpath/basic.yaml" run: | # run tests @@ -330,7 +332,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 with: - name: node-modules-linux-amd64 + name: ${{ matrix.npmartifact }} - name: csi-sanity run: ${{ matrix.run }} env: diff --git a/ci/bin/launch-server.sh b/ci/bin/launch-server.sh index 93274a5..3c9e1bc 100755 --- a/ci/bin/launch-server.sh +++ b/ci/bin/launch-server.sh @@ -3,6 +3,8 @@ set -e set -x +echo "current PATH: ${PATH}" + : ${CI_BUILD_KEY:="local"} : ${TEMPLATE_CONFIG_FILE:=${1}} : ${CSI_MODE:=""} From 83556d02fd04c672033124b5c6b2c7c468602769 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 6 May 2022 12:51:45 -0600 Subject: [PATCH 38/71] force proper path for ci server Signed-off-by: Travis Glenn Hansen --- ci/bin/launch-server.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ci/bin/launch-server.sh b/ci/bin/launch-server.sh index 3c9e1bc..5d3b387 100755 --- a/ci/bin/launch-server.sh +++ b/ci/bin/launch-server.sh @@ -3,7 +3,8 @@ set -e set -x -echo "current PATH: ${PATH}" +export PATH="/usr/local/lib/nodejs/bin:${PATH}" +echo "current launch-server PATH: ${PATH}" : ${CI_BUILD_KEY:="local"} : ${TEMPLATE_CONFIG_FILE:=${1}} From 2d3851282a0ed494a89168a55540cf69609c3af4 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 6 May 2022 19:23:19 -0600 Subject: [PATCH 39/71] minor fixes Signed-off-by: Travis Glenn Hansen --- bin/democratic-csi | 3 ++- ci/bin/run.sh | 2 +- src/driver/freenas/api.js | 2 +- src/driver/index.js | 35 +++++++---------------------------- src/utils/filesystem.js | 21 +++++++++++++-------- src/utils/windows.js | 8 +++++++- 6 files changed, 31 insertions(+), 40 deletions(-) diff --git a/bin/democratic-csi b/bin/democratic-csi index 254ebb2..7d2e0ad 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -372,9 +372,10 @@ if (args.serverSocket) { } logger.info( - "starting csi server - node version: %s, package version: %s, csi-name: %s, csi-driver: %s, csi-mode: %s, csi-version: %s, address: %s, socket: %s", + "starting csi server - node version: %s, package version: %s, config file: %s, csi-name: %s, csi-driver: %s, csi-mode: %s, csi-version: %s, address: %s, socket: %s", process.version, args.version, + fs.realpathSync(args.driverConfigFile), args.csiName, options.driver, args.csiMode.join(","), diff --git a/ci/bin/run.sh b/ci/bin/run.sh index b16a92a..1d4eaf8 100755 --- a/ci/bin/run.sh +++ b/ci/bin/run.sh @@ -15,7 +15,7 @@ export PATH="/usr/local/lib/nodejs/bin:${PATH}" # install deps #npm i # install from artifacts -if [[ -f "node_modules-linux-amd64.tar.gz" ]];then +if [[ -f "node_modules-linux-amd64.tar.gz" && ! -d "node_modules" ]];then tar -zxf node_modules-linux-amd64.tar.gz fi diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 8af3853..241ea7e 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -1476,7 +1476,7 @@ class FreeNASApiDriver extends CsiBaseDriver { break; case "iscsi": // Delete target - // NOTE: deletting a target inherently deletes associated targetgroup(s) and targettoextent(s) + // NOTE: deleting a target inherently deletes associated targetgroup(s) and targettoextent(s) // Delete extent try { diff --git a/src/driver/index.js b/src/driver/index.js index 8a68fc8..23b30fd 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -1258,16 +1258,7 @@ class CsiBaseDriver { * * if path exists but is NOT symlink delete it */ - try { - fs.statSync(win_staging_target_path); - result = true; - } catch (err) { - if (err.code === "ENOENT") { - result = false; - } else { - throw err; - } - } + result = await filesystem.pathExists(win_staging_target_path); if (result) { result = fs.lstatSync(win_staging_target_path); @@ -1593,22 +1584,10 @@ class CsiBaseDriver { } break; case "hostpath": - try { - fs.statSync(win_staging_target_path); - result = true; - } catch (err) { - if (err.code === "ENOENT") { - result = false; - } else { - throw err; - } - } - // if exists already delete if folder, return if symlink - if (result) { - result = fs.lstatSync(win_staging_target_path); + if (await filesystem.pathExists(win_staging_target_path)) { // remove pre-created dir by CO - if (!result.isSymbolicLink()) { + if (!(await filesystem.isSymbolicLink(win_staging_target_path))) { fs.rmdirSync(win_staging_target_path); } else { // assume symlink points to the correct location @@ -2627,7 +2606,7 @@ class CsiBaseDriver { ); // source path - result = await wutils.GetItem(normalized_staging_path); + result = await filesystem.pathExists(normalized_staging_path); if (!result) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, @@ -2982,7 +2961,7 @@ class CsiBaseDriver { let win_volume_path = filesystem.covertUnixSeparatorToWindowsSeparator(volume_path); // ensure path is mounted - result = await wutils.GetItem(win_volume_path); + result = await filesystem.pathExists(win_volume_path); if (!result) { throw new GrpcError( grpc.status.NOT_FOUND, @@ -2992,7 +2971,7 @@ class CsiBaseDriver { let node_attach_driver; - let target = await wutils.GetRealTarget(win_volume_path) || ""; + let target = (await wutils.GetRealTarget(win_volume_path)) || ""; if (target.startsWith("\\\\")) { node_attach_driver = "smb"; } @@ -3236,7 +3215,7 @@ class CsiBaseDriver { filesystem.covertUnixSeparatorToWindowsSeparator(volume_path); // ensure path is mounted - result = await wutils.GetItem(win_volume_path); + result = filesystem.pathExists(win_volume_path); if (!result) { throw new GrpcError( grpc.status.NOT_FOUND, diff --git a/src/utils/filesystem.js b/src/utils/filesystem.js index d6fdbb9..de4bce7 100644 --- a/src/utils/filesystem.js +++ b/src/utils/filesystem.js @@ -228,8 +228,12 @@ class Filesystem { } } + async isSymboliclink(path) { + return fs.lstatSync(path).isSymbolicLink(); + } + /** - * create symlink + * remove file * * @param {*} device */ @@ -829,16 +833,17 @@ class Filesystem { * @param {*} path */ async pathExists(path) { - const filesystem = this; - let args = []; - args.push(path); - + let result = false; try { - await filesystem.exec("stat", args); + fs.statSync(path); + result = true; } catch (err) { - return false; + if (err.code !== "ENOENT") { + throw err; + } } - return true; + + return result; } exec(command, args, options = {}) { diff --git a/src/utils/windows.js b/src/utils/windows.js index 6f2d853..7b591c3 100644 --- a/src/utils/windows.js +++ b/src/utils/windows.js @@ -60,6 +60,7 @@ class Windows { } } while (path); } + async GetItem(localPath) { let command; let result; @@ -201,7 +202,12 @@ class Windows { chapSecret, multipath = false ) { - let is_connected = await this.IscsiTargetIsConnectedByPortalAddressPortalPort(address, port, iqn); + let is_connected = + await this.IscsiTargetIsConnectedByPortalAddressPortalPort( + address, + port, + iqn + ); if (is_connected) { return; } From 910e9d8fcaee164cd0b8db769cd5a35fc0e738ba Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 6 May 2022 19:24:41 -0600 Subject: [PATCH 40/71] do not log csi-grpc-proxy output --- ci/bin/run.ps1 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ci/bin/run.ps1 b/ci/bin/run.ps1 index fd6e165..b9201b9 100644 --- a/ci/bin/run.ps1 +++ b/ci/bin/run.ps1 @@ -26,7 +26,7 @@ function Job-Cleanup() { Job-Cleanup # install from artifacts -if (Test-Path "node_modules-windows-amd64.tar.gz") { +if ((Test-Path "node_modules-windows-amd64.tar.gz") -and !(Test-Path "node_modules")) { Write-Output "extracting node_modules-windows-amd64.tar.gz" tar -zxf node_modules-windows-amd64.tar.gz } @@ -71,8 +71,13 @@ if ($started -eq 1) { # https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/get-job?view=powershell-7.2 # -ChildJobState +$iter = 0 while ($csi_sanity_job -and ($csi_sanity_job.State -eq "Running" -or $csi_sanity_job.State -eq "NotStarted")) { + $iter++ foreach ($job in Get-Job) { + if ($job -eq $csi_grpc_proxy_job -and $iter -gt 20) { + continue + } try { $job | Receive-Job } @@ -86,6 +91,9 @@ while ($csi_sanity_job -and ($csi_sanity_job.State -eq "Running" -or $csi_sanity # spew any remaining job output to the console foreach ($job in Get-Job) { + if ($job -eq $csi_grpc_proxy_job) { + continue + } try { $job | Receive-Job } From 3d6c26a2511940c2da102007707b420ccd1b5f30 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 6 May 2022 20:37:04 -0600 Subject: [PATCH 41/71] more minor fixes Signed-off-by: Travis Glenn Hansen --- src/driver/controller-zfs/index.js | 4 ++-- src/driver/index.js | 12 ++++++------ src/utils/filesystem.js | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index d88d150..6adfa90 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -1318,8 +1318,8 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { // NOTE: -R will recursively delete items + dependent filesets // delete dataset try { - let max_tries = 5; - let sleep_time = 3000; + let max_tries = 12; + let sleep_time = 5000; let current_try = 1; let success = false; while (!success && current_try <= max_tries) { diff --git a/src/driver/index.js b/src/driver/index.js index 23b30fd..8225ecf 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -1261,8 +1261,7 @@ class CsiBaseDriver { result = await filesystem.pathExists(win_staging_target_path); if (result) { - result = fs.lstatSync(win_staging_target_path); - if (!result.isSymbolicLink()) { + if (!(await filesystem.isSymbolicLink(win_staging_target_path))) { fs.rmdirSync(win_staging_target_path); } else { result = await wutils.GetItem(win_staging_target_path); @@ -1310,8 +1309,9 @@ class CsiBaseDriver { if (!details.includes("ResourceExists")) { throw e; } else { - result = fs.lstatSync(win_staging_target_path); - if (!result.isSymbolicLink()) { + if ( + !(await filesystem.isSymbolicLink(win_staging_target_path)) + ) { throw new Error("staging path exists but is not symlink"); } } @@ -3215,7 +3215,7 @@ class CsiBaseDriver { filesystem.covertUnixSeparatorToWindowsSeparator(volume_path); // ensure path is mounted - result = filesystem.pathExists(win_volume_path); + result = await filesystem.pathExists(win_volume_path); if (!result) { throw new GrpcError( grpc.status.NOT_FOUND, @@ -3223,7 +3223,7 @@ class CsiBaseDriver { ); } - let target = await wutils.GetRealTarget(win_volume_path); + let target = (await wutils.GetRealTarget(win_volume_path)) || ""; if (target.startsWith("\\\\")) { node_attach_driver = "smb"; } diff --git a/src/utils/filesystem.js b/src/utils/filesystem.js index de4bce7..e4f90c2 100644 --- a/src/utils/filesystem.js +++ b/src/utils/filesystem.js @@ -228,7 +228,7 @@ class Filesystem { } } - async isSymboliclink(path) { + async isSymbolicLink(path) { return fs.lstatSync(path).isSymbolicLink(); } From c05abda434627f7e1479fde1c82c56acfd87a891 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 6 May 2022 22:02:16 -0600 Subject: [PATCH 42/71] enable debug tracing Signed-off-by: Travis Glenn Hansen --- ci/bin/run.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ci/bin/run.ps1 b/ci/bin/run.ps1 index b9201b9..ca4e5ef 100644 --- a/ci/bin/run.ps1 +++ b/ci/bin/run.ps1 @@ -10,6 +10,8 @@ . "${PSScriptRoot}\helper.ps1" +Set-PSDebug -Trace 2 + Write-Output "current user" whoami Write-Output "current working directory" From e4b1e51a90534f8af23458a75b1c1cad9dd3ff6f Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 6 May 2022 22:17:43 -0600 Subject: [PATCH 43/71] more robust error logging Signed-off-by: Travis Glenn Hansen --- ci/bin/run.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ci/bin/run.ps1 b/ci/bin/run.ps1 index ca4e5ef..31adc78 100644 --- a/ci/bin/run.ps1 +++ b/ci/bin/run.ps1 @@ -77,7 +77,7 @@ $iter = 0 while ($csi_sanity_job -and ($csi_sanity_job.State -eq "Running" -or $csi_sanity_job.State -eq "NotStarted")) { $iter++ foreach ($job in Get-Job) { - if ($job -eq $csi_grpc_proxy_job -and $iter -gt 20) { + if (($job -eq $csi_grpc_proxy_job) -and ($iter -gt 20)) { continue } try { @@ -85,6 +85,9 @@ while ($csi_sanity_job -and ($csi_sanity_job.State -eq "Running" -or $csi_sanity } catch { if ($job.State -ne "Failed") { + Write-Output "failure receiving job data" + $job | ConvertTo-Json | Write-Output + Write-Output $_ throw $_ } } From b7194f0a96d60e4cb650ea90cc9c9e471d01d713 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 7 May 2022 00:15:43 -0600 Subject: [PATCH 44/71] swallow failures to receive job data Signed-off-by: Travis Glenn Hansen --- ci/bin/run.ps1 | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/ci/bin/run.ps1 b/ci/bin/run.ps1 index 31adc78..3e7be00 100644 --- a/ci/bin/run.ps1 +++ b/ci/bin/run.ps1 @@ -10,7 +10,7 @@ . "${PSScriptRoot}\helper.ps1" -Set-PSDebug -Trace 2 +#Set-PSDebug -Trace 2 Write-Output "current user" whoami @@ -85,10 +85,9 @@ while ($csi_sanity_job -and ($csi_sanity_job.State -eq "Running" -or $csi_sanity } catch { if ($job.State -ne "Failed") { - Write-Output "failure receiving job data" - $job | ConvertTo-Json | Write-Output - Write-Output $_ - throw $_ + Write-Output "failure receiving job data: " + $_ + $job | fl + #throw $_ } } } From 2032c67be002f00638d5af1693d247c97c1aa6dd Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 7 May 2022 08:59:57 -0600 Subject: [PATCH 45/71] implement generic retry function, use it to make tests more robust Signed-off-by: Travis Glenn Hansen --- ci/bin/run.ps1 | 13 ++++-- src/driver/controller-zfs-generic/index.js | 43 ++++++++++++++++--- src/driver/controller-zfs/index.js | 36 +++++++--------- src/driver/freenas/api.js | 44 ++++++++++++++++++- src/driver/freenas/ssh.js | 50 +++++++++++++++++++--- src/driver/index.js | 23 ++++++---- src/utils/general.js | 41 ++++++++++++++++++ 7 files changed, 203 insertions(+), 47 deletions(-) diff --git a/ci/bin/run.ps1 b/ci/bin/run.ps1 index 3e7be00..e3d30aa 100644 --- a/ci/bin/run.ps1 +++ b/ci/bin/run.ps1 @@ -58,7 +58,9 @@ while (!(Test-Path "${env:CSI_ENDPOINT}")) { $iter++ Write-Output "Waiting for ${env:CSI_ENDPOINT} to appear" Start-Sleep 1 - Get-Job | Receive-Job + try { + Get-Job | Receive-Job + } catch {} if ($iter -gt $max_iter) { Write-Output "${env:CSI_ENDPOINT} failed to appear" $started = 0 @@ -80,13 +82,18 @@ while ($csi_sanity_job -and ($csi_sanity_job.State -eq "Running" -or $csi_sanity if (($job -eq $csi_grpc_proxy_job) -and ($iter -gt 20)) { continue } + if (!$job.HasMoreData) { + continue + } try { $job | Receive-Job } catch { if ($job.State -ne "Failed") { - Write-Output "failure receiving job data: " + $_ - $job | fl + Write-Output "failure receiving job data: ${_}" + # just swallow the errors as it seems there are various reasons errors + # may show up (perhaps no data currently, etc) + #$job | fl #throw $_ } } diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index cd91b71..4d522b8 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -1,9 +1,9 @@ const _ = require("lodash"); const { ControllerZfsBaseDriver } = require("../controller-zfs"); const { GrpcError, grpc } = require("../../utils/grpc"); +const GeneralUtils = require("../../utils/general"); const registry = require("../../utils/registry"); const SshClient = require("../../utils/ssh").SshClient; -const sleep = require("../../utils/general").sleep; const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); const Handlebars = require("handlebars"); @@ -231,8 +231,12 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { } } - response = await this.targetCliCommand( - ` + await GeneralUtils.retry( + 3, + 2000, + async () => { + await this.targetCliCommand( + ` # create target cd /iscsi create ${basename}:${iscsiName} @@ -250,6 +254,16 @@ create ${iscsiName} /dev/${extentDiskName} cd /iscsi/${basename}:${iscsiName}/tpg1/luns create /backstores/block/${iscsiName} ` + ); + }, + { + retryCondition: (err) => { + if (err.stdout && err.stdout.includes("Ran out of input")) { + return true; + } + return false; + }, + } ); break; default: @@ -313,7 +327,7 @@ create /backstores/block/${iscsiName} } } } - await sleep(2000); // let things settle + await GeneralUtils.sleep(2000); // let things settle break; default: throw new GrpcError( @@ -343,7 +357,7 @@ create /backstores/block/${iscsiName} } } } - await sleep(2000); // let things settle + await GeneralUtils.sleep(2000); // let things settle break; default: throw new GrpcError( @@ -392,8 +406,12 @@ create /backstores/block/${iscsiName} switch (this.options.iscsi.shareStrategy) { case "targetCli": basename = this.options.iscsi.shareStrategyTargetCli.basename; - response = await this.targetCliCommand( - ` + await GeneralUtils.retry( + 3, + 2000, + async () => { + await this.targetCliCommand( + ` # delete target cd /iscsi delete ${basename}:${iscsiName} @@ -402,7 +420,18 @@ delete ${basename}:${iscsiName} cd /backstores/block delete ${iscsiName} ` + ); + }, + { + retryCondition: (err) => { + if (err.stdout && err.stdout.includes("Ran out of input")) { + return true; + } + return false; + }, + } ); + break; default: break; diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index 6adfa90..ccc5b01 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -1318,30 +1318,24 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { // NOTE: -R will recursively delete items + dependent filesets // delete dataset try { - let max_tries = 12; - let sleep_time = 5000; - let current_try = 1; - let success = false; - while (!success && current_try <= max_tries) { - try { + await GeneralUtils.retry( + 12, + 5000, + async () => { await zb.zfs.destroy(datasetName, { recurse: true, force: true }); - success = true; - } catch (err) { - if ( - err.toString().includes("dataset is busy") || - err.toString().includes("target is busy") - ) { - current_try++; - if (current_try > max_tries) { - throw err; - } else { - await GeneralUtils.sleep(sleep_time); + }, + { + retryCondition: (err) => { + if ( + err.toString().includes("dataset is busy") || + err.toString().includes("target is busy") + ) { + return true; } - } else { - throw err; - } + return false; + }, } - } + ); } catch (err) { if (err.toString().includes("filesystem has dependent clones")) { throw new GrpcError( diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 241ea7e..8788d6a 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -261,7 +261,27 @@ class FreeNASApiDriver extends CsiBaseDriver { break; } - response = await httpClient.post("/sharing/nfs", share); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.post("/sharing/nfs", share); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); /** * v1 = 201 @@ -482,7 +502,27 @@ class FreeNASApiDriver extends CsiBaseDriver { break; } - response = await httpClient.post(endpoint, share); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.post(endpoint, share); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); /** * v1 = 201 diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 0ce6d1a..249f05d 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -6,7 +6,7 @@ const SshClient = require("../../utils/ssh").SshClient; const HttpClient = require("./http").Client; const TrueNASApiClient = require("./http/api").Api; const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); -const { sleep, stringify } = require("../../utils/general"); +const GeneralUtils = require("../../utils/general"); const Handlebars = require("handlebars"); @@ -308,7 +308,27 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { break; } - response = await httpClient.post("/sharing/nfs", share); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.post("/sharing/nfs", share); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); /** * v1 = 201 @@ -529,7 +549,27 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { break; } - response = await httpClient.post(endpoint, share); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.post(endpoint, share); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); /** * v1 = 201 @@ -1614,7 +1654,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { targetId, retries ); - await sleep(retryWait); + await GeneralUtils.sleep(retryWait); response = await httpClient.delete(endpoint); } @@ -2134,7 +2174,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { // likely bad creds/url throw new GrpcError( grpc.status.UNKNOWN, - `FreeNAS error getting system version info: ${stringify({ + `FreeNAS error getting system version info: ${GeneralUtils.stringify({ errors: versionErrors, responses: versionResponses, })}` diff --git a/src/driver/index.js b/src/driver/index.js index 8225ecf..2f82745 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -759,16 +759,21 @@ class CsiBaseDriver { } // create 'DB' entry - await iscsi.iscsiadm.createNodeDBEntry( - iscsiConnection.iqn, - iscsiConnection.portal, - nodeDB - ); + await GeneralUtils.retry(5, 2000, async () => { + await iscsi.iscsiadm.createNodeDBEntry( + iscsiConnection.iqn, + iscsiConnection.portal, + nodeDB + ); + }); + // login - await iscsi.iscsiadm.login( - iscsiConnection.iqn, - iscsiConnection.portal - ); + await GeneralUtils.retry(15, 2000, async () => { + await iscsi.iscsiadm.login( + iscsiConnection.iqn, + iscsiConnection.portal + ); + }); // get associated session let session = await iscsi.iscsiadm.getSession( diff --git a/src/utils/general.js b/src/utils/general.js index 6077802..928d613 100644 --- a/src/utils/general.js +++ b/src/utils/general.js @@ -1,3 +1,4 @@ +const _ = require("lodash"); const axios = require("axios"); const crypto = require("crypto"); @@ -177,6 +178,45 @@ function default_supported_file_filesystems() { return ["nfs", "cifs"]; } +async function retry(retries, retriesDelay, code, options = {}) { + let current_try = 0; + let maxwait = _.get(options, "maxwait"); + let logerrors = _.get(options, "logerrors", false); + let retryCondition = options.retryCondition; + do { + current_try++; + try { + return await code(); + } catch (err) { + if (current_try >= retries) { + throw err; + } + if (retryCondition) { + let retry = retryCondition(err); + if (!retry) { + console.log(`retry - failed condition, not trying again`); + throw err; + } + } + if (logerrors === true) { + console.log(`retry - err:`, err); + } + } + let sleep_time = retriesDelay; + if (_.get(options, "exponential", false) === true) { + sleep_time = retriesDelay * current_try; + } + + if (maxwait) { + if (sleep_time > maxwait) { + sleep_time = maxwait; + } + } + console.log(`retry - waiting ${sleep_time}ms before trying again`); + await sleep(sleep_time); + } while (true); +} + module.exports.sleep = sleep; module.exports.md5 = md5; module.exports.crc32 = crc32; @@ -189,3 +229,4 @@ module.exports.default_supported_block_filesystems = default_supported_block_filesystems; module.exports.default_supported_file_filesystems = default_supported_file_filesystems; +module.exports.retry = retry; From 466845cbd91ef8f72c5c9d7f09f3aaed338d1651 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 7 May 2022 18:10:08 -0600 Subject: [PATCH 46/71] more complete usage of retry logic Signed-off-by: Travis Glenn Hansen --- src/driver/freenas/api.js | 44 ++++++++++++++++++++++++++++++-- src/driver/freenas/http/index.js | 6 +++++ src/driver/freenas/ssh.js | 44 ++++++++++++++++++++++++++++++-- src/utils/general.js | 24 ++++++++++++++++- 4 files changed, 113 insertions(+), 5 deletions(-) diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 8788d6a..e082250 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -1402,7 +1402,27 @@ class FreeNASApiDriver extends CsiBaseDriver { }); if (deleteAsset) { - response = await httpClient.delete(endpoint); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.delete(endpoint); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); // returns a 500 if does not exist // v1 = 204 @@ -1483,7 +1503,27 @@ class FreeNASApiDriver extends CsiBaseDriver { }); if (deleteAsset) { - response = await httpClient.delete(endpoint); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.delete(endpoint); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); // returns a 500 if does not exist // v1 = 204 diff --git a/src/driver/freenas/http/index.js b/src/driver/freenas/http/index.js index d800e34..4d2009c 100644 --- a/src/driver/freenas/http/index.js +++ b/src/driver/freenas/http/index.js @@ -86,6 +86,12 @@ class Client { httpAgent: this.getHttpAgent(), httpsAgent: this.getHttpsAgent(), timeout: 60 * 1000, + validateStatus: function (status) { + if (status >= 500) { + return false; + } + return true; + }, }; if (client.options.apiKey) { diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 249f05d..3ca8069 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -1450,7 +1450,27 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { }); if (deleteAsset) { - response = await httpClient.delete(endpoint); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.delete(endpoint); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); // returns a 500 if does not exist // v1 = 204 @@ -1532,7 +1552,27 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { }); if (deleteAsset) { - response = await httpClient.delete(endpoint); + response = await GeneralUtils.retry( + 3, + 1000, + async () => { + return await httpClient.delete(endpoint); + }, + { + retryCondition: (err) => { + if (err.code == "ECONNRESET") { + return true; + } + if (err.code == "ECONNABORTED") { + return true; + } + if (err.response && err.response.statusCode == 504) { + return true; + } + return false; + }, + } + ); // returns a 500 if does not exist // v1 = 204 diff --git a/src/utils/general.js b/src/utils/general.js index 928d613..b08acae 100644 --- a/src/utils/general.js +++ b/src/utils/general.js @@ -140,7 +140,16 @@ function axios_request(options, callback = function () {}) { // The request was made and the server responded with a status code // that falls out of the range of 2xx let res = prep_response(err.response); - callback(null, res, res.body); + let senderr = false; + if ( + options.validateStatus && + typeof options.validateStatus == "function" + ) { + if (!options.validateStatus(res.statusCode)) { + senderr = true; + } + } + callback(senderr ? err : null, res, res.body); } else if (err.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of @@ -183,9 +192,12 @@ async function retry(retries, retriesDelay, code, options = {}) { let maxwait = _.get(options, "maxwait"); let logerrors = _.get(options, "logerrors", false); let retryCondition = options.retryCondition; + let executeStartTime; + do { current_try++; try { + executeStartTime = Date.now(); return await code(); } catch (err) { if (current_try >= retries) { @@ -202,6 +214,16 @@ async function retry(retries, retriesDelay, code, options = {}) { console.log(`retry - err:`, err); } } + // handle minExecutionTime + if (options.minExecutionTime > 0) { + let minDelayTime = + options.minExecutionTime - (Date.now() - executeStartTime); + if (minDelayTime > 0) { + await sleep(minDelayTime); + } + } + + // handle delay let sleep_time = retriesDelay; if (_.get(options, "exponential", false) === true) { sleep_time = retriesDelay * current_try; From b9e4f20863ccf61d8824737f04721d6859ab8680 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 7 May 2022 19:44:35 -0600 Subject: [PATCH 47/71] more robust unpublish and unstage logic Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 26 +++++------ src/driver/index.js | 89 +++++++++++++++++++++++++++++++++++--- src/utils/general.js | 20 +++++---- 3 files changed, 107 insertions(+), 28 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 020cb61..f72131b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -113,7 +113,6 @@ jobs: SYNOLOGY_PASSWORD: ${{ secrets.SANITY_SYNOLOGY_PASSWORD }} SYNOLOGY_VOLUME: ${{ secrets.SANITY_SYNOLOGY_VOLUME }} - # api-based drivers csi-sanity-truenas-scale-22_02: needs: @@ -312,18 +311,18 @@ jobs: matrix: os: [Linux, Windows] include: - - os: Linux - npmartifact: node-modules-linux-amd64 - template: "./ci/configs/local-hostpath/basic.yaml" - run: | - # run tests - ci/bin/run.sh - - os: Windows - npmartifact: node-modules-windows-amd64 - template: ".\\ci\\configs\\local-hostpath/basic.yaml" - run: | - # run tests - ci\bin\run.ps1 + - os: Linux + npmartifact: node-modules-linux-amd64 + template: "./ci/configs/local-hostpath/basic.yaml" + run: | + # run tests + ci/bin/run.sh + - os: Windows + npmartifact: node-modules-windows-amd64 + template: ".\\ci\\configs\\local-hostpath/basic.yaml" + run: | + # run tests + ci\bin\run.ps1 runs-on: - self-hosted - ${{ matrix.os }} @@ -376,6 +375,7 @@ jobs: - csi-sanity-truenas-core-12_0 - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic + - csi-sanity-client - csi-sanity-zfs-local - csi-sanity-local-hostpath - csi-sanity-windows-node diff --git a/src/driver/index.js b/src/driver/index.js index 2f82745..605d37d 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -2043,7 +2043,21 @@ class CsiBaseDriver { result = await mount.pathIsMounted(normalized_staging_path); if (result) { try { - result = await mount.umount(normalized_staging_path, umount_args); + result = await GeneralUtils.retry( + 10, + 0, + async () => { + return await mount.umount(normalized_staging_path, umount_args); + }, + { + minExecutionTime: 1000, + retryCondition: (err) => { + if (_.get(err, "stderr", "").includes("busy")) { + return true; + } + }, + } + ); } catch (err) { if (err.timeout) { driver.ctx.logger.warn( @@ -2188,13 +2202,41 @@ class CsiBaseDriver { // remove touched file result = await filesystem.pathExists(block_path); if (result) { - result = await filesystem.rm(block_path); + result = await GeneralUtils.retry( + 10, + 0, + async () => { + return await filesystem.rm(block_path); + }, + { + minExecutionTime: 1000, + retryCondition: (err) => { + if (_.get(err, "stderr", "").includes("busy")) { + return true; + } + }, + } + ); } } result = await filesystem.pathExists(staging_target_path); if (result) { - result = await filesystem.rmdir(staging_target_path); + result = await GeneralUtils.retry( + 10, + 0, + async () => { + return await filesystem.rmdir(staging_target_path); + }, + { + minExecutionTime: 1000, + retryCondition: (err) => { + if (_.get(err, "stderr", "").includes("busy")) { + return true; + } + }, + } + ); } break; case NODE_OS_DRIVER_WINDOWS: { @@ -2777,7 +2819,21 @@ class CsiBaseDriver { if (result) { try { - result = await mount.umount(target_path, umount_args); + result = await GeneralUtils.retry( + 10, + 0, + async () => { + return await mount.umount(target_path, umount_args); + }, + { + minExecutionTime: 1000, + retryCondition: (err) => { + if (_.get(err, "stderr", "").includes("busy")) { + return true; + } + }, + } + ); } catch (err) { if (err.timeout) { driver.ctx.logger.warn( @@ -2808,9 +2864,30 @@ class CsiBaseDriver { result = await filesystem.pathExists(target_path); if (result) { if (fs.lstatSync(target_path).isDirectory()) { - result = await filesystem.rmdir(target_path); + result = await GeneralUtils.retry( + 10, + 0, + async () => { + return await filesystem.rmdir(target_path); + }, + { + minExecutionTime: 1000, + retryCondition: (err) => { + if (_.get(err, "stderr", "").includes("busy")) { + return true; + } + }, + } + ); } else { - result = await filesystem.rm([target_path]); + result = await GeneralUtils.retry( + 10, + 0, + async () => { + return await filesystem.rm([target_path]); + }, + { minExecutionTime: 1000 } + ); } } diff --git a/src/utils/general.js b/src/utils/general.js index b08acae..746f2ef 100644 --- a/src/utils/general.js +++ b/src/utils/general.js @@ -145,9 +145,7 @@ function axios_request(options, callback = function () {}) { options.validateStatus && typeof options.validateStatus == "function" ) { - if (!options.validateStatus(res.statusCode)) { - senderr = true; - } + senderr = true; } callback(senderr ? err : null, res, res.body); } else if (err.request) { @@ -214,12 +212,14 @@ async function retry(retries, retriesDelay, code, options = {}) { console.log(`retry - err:`, err); } } + // handle minExecutionTime if (options.minExecutionTime > 0) { - let minDelayTime = - options.minExecutionTime - (Date.now() - executeStartTime); - if (minDelayTime > 0) { - await sleep(minDelayTime); + let executionElapsedTIme = Date.now() - executeStartTime; + let minExecutionDelayTime = + options.minExecutionTime - executionElapsedTIme; + if (minExecutionDelayTime > 0) { + await sleep(minExecutionDelayTime); } } @@ -234,8 +234,10 @@ async function retry(retries, retriesDelay, code, options = {}) { sleep_time = maxwait; } } - console.log(`retry - waiting ${sleep_time}ms before trying again`); - await sleep(sleep_time); + if (sleep_time > 0) { + console.log(`retry - waiting ${sleep_time}ms before trying again`); + await sleep(sleep_time); + } } while (true); } From 888556aa5eee3fd1caa1df2debe9911cde7bff77 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 7 May 2022 21:20:24 -0600 Subject: [PATCH 48/71] more robust smb delete for freenas Signed-off-by: Travis Glenn Hansen --- src/driver/controller-zfs-generic/index.js | 2 +- src/driver/freenas/api.js | 5 ++++- src/driver/freenas/ssh.js | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 4d522b8..6a6c9c7 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -520,7 +520,7 @@ delete ${iscsiName} options ); if (response.code != 0) { - throw new Error(JSON.stringify(response)); + throw new Error(response); } driver.ctx.logger.verbose( "TargetCLI response: " + JSON.stringify(response) diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index e082250..9281b2d 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -1528,7 +1528,10 @@ class FreeNASApiDriver extends CsiBaseDriver { // returns a 500 if does not exist // v1 = 204 // v2 = 200 - if (![200, 204].includes(response.statusCode)) { + if ( + ![200, 204].includes(response.statusCode) && + !JSON.stringify(response.body).includes("does not exist") + ) { throw new GrpcError( grpc.status.UNKNOWN, `received error deleting smb share - share: ${shareId} code: ${ diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 3ca8069..3debb76 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -1577,7 +1577,10 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { // returns a 500 if does not exist // v1 = 204 // v2 = 200 - if (![200, 204].includes(response.statusCode)) { + if ( + ![200, 204].includes(response.statusCode) && + !JSON.stringify(response.body).includes("does not exist") + ) { throw new GrpcError( grpc.status.UNKNOWN, `received error deleting smb share - share: ${shareId} code: ${ From 9a17db5f2107ece3c7319d264e70abbc25ad26fb Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 7 May 2022 22:55:39 -0600 Subject: [PATCH 49/71] further effort to make test runs more robust Signed-off-by: Travis Glenn Hansen --- src/driver/controller-zfs-generic/index.js | 6 +++--- src/driver/index.js | 8 ++++---- src/utils/filesystem.js | 17 ++++++++++++++++- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 6a6c9c7..c3aa985 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -519,12 +519,12 @@ delete ${iscsiName} execClient.buildCommand(command, args), options ); - if (response.code != 0) { - throw new Error(response); - } driver.ctx.logger.verbose( "TargetCLI response: " + JSON.stringify(response) ); + if (response.code != 0) { + throw response; + } return response; } } diff --git a/src/driver/index.js b/src/driver/index.js index 605d37d..6d0a87b 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -2203,7 +2203,7 @@ class CsiBaseDriver { result = await filesystem.pathExists(block_path); if (result) { result = await GeneralUtils.retry( - 10, + 30, 0, async () => { return await filesystem.rm(block_path); @@ -2223,7 +2223,7 @@ class CsiBaseDriver { result = await filesystem.pathExists(staging_target_path); if (result) { result = await GeneralUtils.retry( - 10, + 30, 0, async () => { return await filesystem.rmdir(staging_target_path); @@ -2865,7 +2865,7 @@ class CsiBaseDriver { if (result) { if (fs.lstatSync(target_path).isDirectory()) { result = await GeneralUtils.retry( - 10, + 30, 0, async () => { return await filesystem.rmdir(target_path); @@ -2881,7 +2881,7 @@ class CsiBaseDriver { ); } else { result = await GeneralUtils.retry( - 10, + 30, 0, async () => { return await filesystem.rm([target_path]); diff --git a/src/utils/filesystem.js b/src/utils/filesystem.js index e4f90c2..a5cfd63 100644 --- a/src/utils/filesystem.js +++ b/src/utils/filesystem.js @@ -1,5 +1,6 @@ const cp = require("child_process"); const fs = require("fs"); +const GeneralUtils = require("./general"); const path = require("path"); const DEFAULT_TIMEOUT = process.env.FILESYSTEM_DEFAULT_TIMEOUT || 30000; @@ -835,7 +836,21 @@ class Filesystem { async pathExists(path) { let result = false; try { - fs.statSync(path); + await GeneralUtils.retry( + 10, + 200, + () => { + fs.statSync(path); + }, + { + retryCondition: (err) => { + if (err.code == "UNKNOWN") { + return true; + } + return false; + }, + } + ); result = true; } catch (err) { if (err.code !== "ENOENT") { From 7e2b8374287f8ecaa133ca21018efed7e2463c88 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 7 May 2022 23:24:19 -0600 Subject: [PATCH 50/71] improve windows performance with native calls vs powershell calls Signed-off-by: Travis Glenn Hansen --- src/driver/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/driver/index.js b/src/driver/index.js index 6d0a87b..de383d0 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -2662,10 +2662,10 @@ class CsiBaseDriver { } // target path - result = await wutils.GetItem(target_path); + result = await filesystem.pathExists(target_path); // already published if (result) { - if (_.get(result, "LinkType") != "SymbolicLink") { + if (!(await filesystem.isSymbolicLink(target_path))) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `target path exists but is not a symlink as it should be: ${target_path}` @@ -2898,12 +2898,12 @@ class CsiBaseDriver { let win_target_path = filesystem.covertUnixSeparatorToWindowsSeparator(target_path); - result = await wutils.GetItem(win_target_path); + result = await filesystem.pathExists(win_target_path); if (!result) { return {}; } - if (_.get(result, "LinkType") != "SymbolicLink") { + if (!(await filesystem.isSymbolicLink(win_target_path))) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, `target path is not a symlink ${win_target_path}` From 2ab3e5f3c585fd9b7c425097df64486305a683df Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sun, 8 May 2022 10:04:31 -0600 Subject: [PATCH 51/71] use non-self-hosted runners for npm build Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f72131b..5d54ca6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,12 +18,12 @@ jobs: access_token: ${{ github.token }} build-npm-linux-amd64: - runs-on: - - self-hosted - - Linux - - X64 + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 + - uses: actions/setup-node@v3 + with: + node-version: 16 - shell: bash name: npm install run: | @@ -36,12 +36,12 @@ jobs: retention-days: 7 build-npm-windows-amd64: - runs-on: - - self-hosted - - Windows - - X64 + runs-on: windows-2022 steps: - uses: actions/checkout@v2 + - uses: actions/setup-node@v3 + with: + node-version: 16 - shell: pwsh name: npm install run: | From 46b9b6ca121e842e79460856447a72e5ac92bfc1 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 9 May 2022 00:31:18 -0600 Subject: [PATCH 52/71] reuse ssh connection for all commands to greatly improve performance Signed-off-by: Travis Glenn Hansen --- src/utils/ssh.js | 163 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 155 insertions(+), 8 deletions(-) diff --git a/src/utils/ssh.js b/src/utils/ssh.js index d008090..e39041f 100644 --- a/src/utils/ssh.js +++ b/src/utils/ssh.js @@ -1,4 +1,6 @@ -var Client = require("ssh2").Client; +const Client = require("ssh2").Client; +const { E_CANCELED, Mutex } = require("async-mutex"); +const GeneralUtils = require("./general"); class SshClient { constructor(options = {}) { @@ -8,7 +10,47 @@ class SshClient { this.logger = this.options.logger; } else { this.logger = console; + console.silly = console.debug; } + + if (!this.options.connection.hasOwnProperty("keepaliveInterval")) { + this.options.connection.keepaliveInterval = 10000; + } + + if (this.options.connection.debug == true) { + this.options.connection.debug = function (msg) { + this.debug(msg); + }; + } + + this.conn_mutex = new Mutex(); + this.conn_state; + this.conn_err; + this.ready_event_count = 0; + this.error_event_count = 0; + + this.conn = new Client(); + // invoked before close + this.conn.on("end", () => { + this.conn_state = "ended"; + this.debug("Client :: end"); + }); + // invoked after end + this.conn.on("close", () => { + this.conn_state = "closed"; + this.debug("Client :: close"); + }); + this.conn.on("error", (err) => { + this.conn_state = "error"; + this.conn_err = err; + this.error_event_count++; + this.debug("Client :: error"); + }); + this.conn.on("ready", () => { + this.conn_state = "ready"; + this.ready_event_count++; + this.debug("Client :: ready"); + }); } /** @@ -27,17 +69,119 @@ class SshClient { this.logger.silly(...arguments); } + async _connect() { + const start_ready_event_count = this.ready_event_count; + const start_error_event_count = this.error_event_count; + try { + await this.conn_mutex.runExclusive(async () => { + this.conn.connect(this.options.connection); + do { + if (start_error_event_count != this.error_event_count) { + throw this.conn_err; + } + + if (start_ready_event_count != this.ready_event_count) { + break; + } + + await GeneralUtils.sleep(100); + } while (true); + }); + } catch (err) { + if (err === E_CANCELED) { + return; + } + throw err; + } + } + + async connect() { + if (this.conn_state == "ready") { + return; + } + + return this._connect(); + } + async exec(command, options = {}, stream_proxy = null) { + // default is to reuse + if (process.env.SSH_REUSE_CONNECTION == "0") { + return this._nexec(...arguments); + } else { + return this._rexec(...arguments); + } + } + + async _rexec(command, options = {}, stream_proxy = null) { + const client = this; + const conn = this.conn; + + return new Promise(async (resolve, reject) => { + do { + try { + await this.connect(); + conn.exec(command, options, function (err, stream) { + if (err) { + reject(err); + return; + } + let stderr; + let stdout; + + if (stream_proxy) { + stream_proxy.on("kill", (signal) => { + stream.destroy(); + }); + } + + stream + .on("close", function (code, signal) { + client.debug( + "Stream :: close :: code: " + code + ", signal: " + signal + ); + if (stream_proxy) { + stream_proxy.emit("close", ...arguments); + } + resolve({ stderr, stdout, code, signal }); + //conn.end(); + }) + .on("data", function (data) { + client.debug("STDOUT: " + data); + if (stream_proxy) { + stream_proxy.stdout.emit("data", ...arguments); + } + if (stdout == undefined) { + stdout = ""; + } + stdout = stdout.concat(data); + }) + .stderr.on("data", function (data) { + client.debug("STDERR: " + data); + if (stream_proxy) { + stream_proxy.stderr.emit("data", ...arguments); + } + if (stderr == undefined) { + stderr = ""; + } + stderr = stderr.concat(data); + }); + }); + break; + } catch (err) { + if (err.message && !err.message.includes("Not connected")) { + throw err; + } + } + await GeneralUtils.sleep(1000); + } while (true); + }); + } + + async _nexec(command, options = {}, stream_proxy = null) { const client = this; return new Promise((resolve, reject) => { var conn = new Client(); - if (client.options.connection.debug == true) { - client.options.connection.debug = function (msg) { - client.debug(msg); - }; - } - conn .on("error", function (err) { client.debug("Client :: error"); @@ -50,7 +194,10 @@ class SshClient { // TERM: "", //}; conn.exec(command, options, function (err, stream) { - if (err) reject(err); + if (err) { + reject(err); + return; + } let stderr; let stdout; stream From b81a47d126dd974933a5a93a0924e8f8e0ce57bf Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 9 May 2022 15:22:22 -0600 Subject: [PATCH 53/71] build windows images Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 39 ++++++++++++++++++++ Dockerfile | 4 +-- Dockerfile.Windows | 73 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 2 deletions(-) create mode 100644 Dockerfile.Windows diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5d54ca6..90158a1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -403,3 +403,42 @@ jobs: GHCR_PASSWORD: ${{ secrets.GHCR_PASSWORD }} DOCKER_CLI_EXPERIMENTAL: enabled DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le + + build-docker-windows: + needs: + - csi-sanity-synology-dsm6 + - csi-sanity-synology-dsm7 + - csi-sanity-truenas-scale-22_02 + - csi-sanity-truenas-core-12_0 + - csi-sanity-truenas-core-13_0 + - csi-sanity-zfs-generic + - csi-sanity-client + - csi-sanity-zfs-local + - csi-sanity-local-hostpath + - csi-sanity-windows-node + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [windows-2019, windows-2022] + include: + - os: windows-2019 + base_tag: ltsc2019 + file: Dockerfile.Windows + - os: windows-2022 + base_tag: ltsc2022 + file: Dockerfile.Windows + steps: + - uses: actions/checkout@v2 + - name: docker build + shell: bash + run: | + docker info + docker build --pull -f ${{ matrix.file }} --build-arg BASE_TAG=${{ matrix.base_tag }} -t windows:${{ matrix.base_tag }} . + docker inspect windows:${{ matrix.base_tag }} + docker save windows:${{ matrix.base_tag }} -o windows-${{ matrix.base_tag }}.tar + - name: upload image tar + uses: actions/upload-artifact@v2 + with: + name: windows-${{ matrix.base_tag }}.tar + path: windows-${{ matrix.base_tag }}.tar + retention-days: 7 diff --git a/Dockerfile b/Dockerfile index 82349b4..a1bba72 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y locales && rm -rf /var/lib/apt/lists/* && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 ENV LANG=en_US.utf8 -ENV NODE_VERSION=v16.14.2 +ENV NODE_VERSION=v16.15.0 ENV NODE_ENV=production # install build deps @@ -73,7 +73,7 @@ COPY --from=build /usr/local/lib/nodejs/bin/node /usr/local/bin/node # netbase is required by rpcbind/rpcinfo to work properly # /etc/{services,rpc} are required RUN apt-get update && \ - apt-get install -y netbase socat e2fsprogs xfsprogs btrfs-progs fatresize dosfstools nfs-common cifs-utils sudo rsync && \ + apt-get install -y netbase socat e2fsprogs exfatprogs xfsprogs btrfs-progs fatresize dosfstools ntfs-3g nfs-common cifs-utils fdisk gdisk cloud-guest-utils sudo rsync && \ rm -rf /var/lib/apt/lists/* # controller requirements diff --git a/Dockerfile.Windows b/Dockerfile.Windows new file mode 100644 index 0000000..038b283 --- /dev/null +++ b/Dockerfile.Windows @@ -0,0 +1,73 @@ +# docker build --build-arg BASE_TAG=ltsc2019 -t foobar -f Dockerfile.Windows . +# docker run --rm -ti --entrypoint powershell foobar + + +# mcr.microsoft.com/windows/servercore:ltsc2019 +# mcr.microsoft.com/windows/nanoserver:1809 + +ARG BASE_TAG + +FROM mcr.microsoft.com/windows/servercore:${BASE_TAG} as build + +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +#ENV GPG_VERSION 4.0.2 +ENV GPG_VERSION 2.3.4 + +RUN Invoke-WebRequest $('https://files.gpg4win.org/gpg4win-vanilla-{0}.exe' -f $env:GPG_VERSION) -OutFile 'gpg4win.exe' -UseBasicParsing ; \ + Start-Process .\gpg4win.exe -ArgumentList '/S' -NoNewWindow -Wait + +# https://github.com/nodejs/node#release-keys +RUN @( \ + '4ED778F539E3634C779C87C6D7062848A1AB005C', \ + '141F07595B7B3FFE74309A937405533BE57C7D57', \ + '94AE36675C464D64BAFA68DD7434390BDBE9B9C5', \ + '74F12602B6F1C4E913FAA37AD3A89613643B6201', \ + '71DCFD284A79C3B38668286BC97EC7A07EDE3FC1', \ + '61FC681DFB92A079F1685E77973F295594EC4689', \ + '8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600', \ + 'C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8', \ + 'C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C', \ + 'DD8F2338BAE7501E3DD5AC78C273792F7D83545D', \ + 'A48C2BEE680E841632CD4E44F07496B3EB3C1762', \ + '108F52B48DB57BB0CC439B2997B01419BD92F80A', \ + 'B9E2F5981AA6E0CD28160D9FF13993A75599653C' \ + ) | foreach { \ + gpg --keyserver hkps://keys.openpgp.org --recv-keys $_ ; \ + } + +ENV NODE_VERSION 16.15.0 + +RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/SHASUMS256.txt.asc' -f $env:NODE_VERSION) -OutFile 'SHASUMS256.txt.asc' -UseBasicParsing ; +#RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/SHASUMS256.txt.asc' -f $env:NODE_VERSION) -OutFile 'SHASUMS256.txt.asc' -UseBasicParsing ; \ +# gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc +#gpg --verify SHASUMS256.txt.sig SHASUMS256.txt + +RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; \ + $sum = $(cat SHASUMS256.txt.asc | sls $(' node-v{0}-win-x64.zip' -f $env:NODE_VERSION)) -Split ' ' ; \ + if ((Get-FileHash node.zip -Algorithm sha256).Hash -ne $sum[0]) { Write-Error 'SHA256 mismatch' } ; \ + Expand-Archive node.zip -DestinationPath C:\ ; \ + Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs' + +#RUN setx /M PATH "%PATH%;C:\nodejs" +RUN setx /M PATH $(${Env:PATH} + \";C:\nodejs\") + +RUN node --version; npm --version; + +RUN mkdir /app +WORKDIR /app + +COPY package*.json ./ +RUN npm install --only=production; ls / +COPY . . + +FROM mcr.microsoft.com/windows/servercore:${BASE_TAG} + +LABEL org.opencontainers.image.source https://github.com/democratic-csi/democratic-csi + +COPY --from=build /nodejs/node.exe /Windows/system32 +COPY --from=build /app /app + +WORKDIR /app + +ENTRYPOINT [ "node.exe", "--expose-gc", "bin/democratic-csi" ] From 81a42d789f9b2c8cdf5fd93e40d740b8a2ae3699 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 10 May 2022 22:03:02 -0600 Subject: [PATCH 54/71] include missing PATH for iscsiadm chroot, update PATH for other docker tools, build/push windows images Signed-off-by: Travis Glenn Hansen --- .github/bin/docker-release-windows.sh | 44 ++++++++++++++++ .github/bin/docker-release.sh | 16 +----- .github/workflows/main.yml | 75 +++++++++++++++++++++++---- docker/iscsiadm | 2 +- docker/mount | 4 +- docker/multipath | 2 +- docker/oneclient | 2 +- docker/umount | 4 +- docker/zfs | 2 +- docker/zpool | 2 +- src/driver/index.js | 8 +++ 11 files changed, 128 insertions(+), 33 deletions(-) create mode 100755 .github/bin/docker-release-windows.sh diff --git a/.github/bin/docker-release-windows.sh b/.github/bin/docker-release-windows.sh new file mode 100755 index 0000000..21e2e13 --- /dev/null +++ b/.github/bin/docker-release-windows.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +set -e + +echo "$DOCKER_PASSWORD" | docker login docker.io -u "$DOCKER_USERNAME" --password-stdin +echo "$GHCR_PASSWORD" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin + +export DOCKER_ORG="democraticcsi" +export DOCKER_PROJECT="democratic-csi" +export DOCKER_REPO="docker.io/${DOCKER_ORG}/${DOCKER_PROJECT}" + +export GHCR_ORG="democratic-csi" +export GHCR_PROJECT="democratic-csi" +export GHCR_REPO="ghcr.io/${GHCR_ORG}/${GHCR_PROJECT}" + +export MANIFEST_NAME="democratic-csi-combined:${IMAGE_TAG}" + +if [[ -n "${IMAGE_TAG}" ]]; then + # create local manifest to work with + buildah manifest rm "${MANIFEST_NAME}" || true + buildah manifest create "${MANIFEST_NAME}" + + # all all the existing linux data to the manifest + buildah manifest add "${MANIFEST_NAME}" --all "${DOCKER_REPO}:${IMAGE_TAG}" + buildah manifest inspect "${MANIFEST_NAME}" + + # import pre-built images + buildah pull docker-archive:democratic-csi-windows-ltsc2019.tar + buildah pull docker-archive:democratic-csi-windows-ltsc2022.tar + + # add pre-built images to manifest + buildah manifest add "${MANIFEST_NAME}" democratic-csi-windows:${GITHUB_RUN_ID}-ltsc2019 + buildah manifest add "${MANIFEST_NAME}" democratic-csi-windows:${GITHUB_RUN_ID}-ltsc2022 + buildah manifest inspect "${MANIFEST_NAME}" + + # push manifest + buildah manifest push --all "${MANIFEST_NAME}" docker://${DOCKER_REPO}:${IMAGE_TAG} + buildah manifest push --all "${MANIFEST_NAME}" docker://${GHCR_REPO}:${IMAGE_TAG} + + # cleanup + buildah manifest rm "${MANIFEST_NAME}" || true +else + : +fi \ No newline at end of file diff --git a/.github/bin/docker-release.sh b/.github/bin/docker-release.sh index f9686c1..be27592 100755 --- a/.github/bin/docker-release.sh +++ b/.github/bin/docker-release.sh @@ -11,20 +11,8 @@ export GHCR_ORG="democratic-csi" export GHCR_PROJECT="democratic-csi" export GHCR_REPO="ghcr.io/${GHCR_ORG}/${GHCR_PROJECT}" -if [[ $GITHUB_REF == refs/tags/* ]]; then - export GIT_TAG=${GITHUB_REF#refs/tags/} -else - export GIT_BRANCH=${GITHUB_REF#refs/heads/} -fi - -if [[ -n "${GIT_TAG}" ]]; then - docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${GIT_TAG} -t ${GHCR_REPO}:${GIT_TAG} . -elif [[ -n "${GIT_BRANCH}" ]]; then - if [[ "${GIT_BRANCH}" == "master" ]]; then - docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:latest -t ${GHCR_REPO}:latest . - else - docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${GIT_BRANCH} -t ${GHCR_REPO}:${GIT_BRANCH} . - fi +if [[ -n "${IMAGE_TAG}" ]]; then + docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${IMAGE_TAG} -t ${GHCR_REPO}:${IMAGE_TAG} . else : fi diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 90158a1..503f3e3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -367,8 +367,33 @@ jobs: SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }} CSI_SANITY_FOCUS: "Node Service" + determine-image-tag: + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.tag.outputs.tag }} + steps: + - id: tag + run: | + if [[ $GITHUB_REF == refs/tags/* ]]; then + export GIT_TAG=${GITHUB_REF#refs/tags/} + else + export GIT_BRANCH=${GITHUB_REF#refs/heads/} + fi + if [[ -n "${GIT_TAG}" ]]; then + echo "::set-output name=tag::${GIT_TAG}" + elif [[ -n "${GIT_BRANCH}" ]]; then + if [[ "${GIT_BRANCH}" == "master" ]]; then + echo "::set-output name=tag::latest" + else + echo "::set-output name=tag::${GIT_BRANCH}" + fi + else + : + fi + build-docker-linux: needs: + - determine-image-tag - csi-sanity-synology-dsm6 - csi-sanity-synology-dsm7 - csi-sanity-truenas-scale-22_02 @@ -403,6 +428,7 @@ jobs: GHCR_PASSWORD: ${{ secrets.GHCR_PASSWORD }} DOCKER_CLI_EXPERIMENTAL: enabled DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le + IMAGE_TAG: ${{needs.determine-image-tag.outputs.tag}} build-docker-windows: needs: @@ -421,24 +447,53 @@ jobs: matrix: os: [windows-2019, windows-2022] include: - - os: windows-2019 - base_tag: ltsc2019 - file: Dockerfile.Windows - - os: windows-2022 - base_tag: ltsc2022 - file: Dockerfile.Windows + - os: windows-2019 + base_tag: ltsc2019 + file: Dockerfile.Windows + - os: windows-2022 + base_tag: ltsc2022 + file: Dockerfile.Windows steps: - uses: actions/checkout@v2 - name: docker build shell: bash run: | docker info - docker build --pull -f ${{ matrix.file }} --build-arg BASE_TAG=${{ matrix.base_tag }} -t windows:${{ matrix.base_tag }} . + docker build --pull -f ${{ matrix.file }} --build-arg BASE_TAG=${{ matrix.base_tag }} -t democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.base_tag }} . docker inspect windows:${{ matrix.base_tag }} - docker save windows:${{ matrix.base_tag }} -o windows-${{ matrix.base_tag }}.tar + docker save democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.base_tag }} -o democratic-csi-windows-${{ matrix.base_tag }}.tar - name: upload image tar uses: actions/upload-artifact@v2 with: - name: windows-${{ matrix.base_tag }}.tar - path: windows-${{ matrix.base_tag }}.tar + name: democratic-csi-windows-${{ matrix.base_tag }}.tar + path: democratic-csi-windows-${{ matrix.base_tag }}.tar retention-days: 7 + + push-docker-windows: + needs: + - build-docker-linux + - build-docker-windows + - determine-image-tag + runs-on: + - self-hosted + - buildah + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v3 + with: + name: democratic-csi-windows-ltsc2019.tar + - uses: actions/download-artifact@v3 + with: + name: democratic-csi-windows-ltsc2022.tar + - name: push windows images with buildah + run: | + #.github/bin/install_latest_buildah.sh + buildah version + .github/bin/docker-release-windows.sh + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }} + GHCR_PASSWORD: ${{ secrets.GHCR_PASSWORD }} + DOCKER_CLI_EXPERIMENTAL: enabled + IMAGE_TAG: ${{needs.determine-image-tag.outputs.tag}} diff --git a/docker/iscsiadm b/docker/iscsiadm index cce2b01..56623d7 100755 --- a/docker/iscsiadm +++ b/docker/iscsiadm @@ -2,4 +2,4 @@ # https://engineering.docker.com/2019/07/road-to-containing-iscsi/ -chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin" iscsiadm "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" iscsiadm "${@:1}" diff --git a/docker/mount b/docker/mount index 229e526..ac3281b 100755 --- a/docker/mount +++ b/docker/mount @@ -31,7 +31,7 @@ while getopts "t:" opt; do done if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then - chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" mount "${@:1}" + chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" mount "${@:1}" else - /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" mount "${@:1}" + /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" mount "${@:1}" fi diff --git a/docker/multipath b/docker/multipath index 0a95bc8..3d1d6ee 100755 --- a/docker/multipath +++ b/docker/multipath @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/sbin:/usr/bin" multipath "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" multipath "${@:1}" diff --git a/docker/oneclient b/docker/oneclient index 7b5ef21..0815dee 100755 --- a/docker/oneclient +++ b/docker/oneclient @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" oneclient "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" oneclient "${@:1}" diff --git a/docker/umount b/docker/umount index b38b078..fe08061 100755 --- a/docker/umount +++ b/docker/umount @@ -31,7 +31,7 @@ while getopts "t:" opt; do done if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then - chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" umount "${@:1}" + chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" umount "${@:1}" else - /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" umount "${@:1}" + /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" umount "${@:1}" fi diff --git a/docker/zfs b/docker/zfs index bc49b65..4a1f79e 100755 --- a/docker/zfs +++ b/docker/zfs @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" zfs "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" zfs "${@:1}" diff --git a/docker/zpool b/docker/zpool index c68eb0a..07241b2 100755 --- a/docker/zpool +++ b/docker/zpool @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/sbin:/bin:/usr/bin:/usr/sbin" zpool "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" zpool "${@:1}" diff --git a/src/driver/index.js b/src/driver/index.js index de383d0..84d5cc7 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -1128,6 +1128,14 @@ class CsiBaseDriver { } } + switch (fs_type) { + case "xfs": + // https://github.com/democratic-csi/democratic-csi/issues/191 + // to avoid issues with cloned volumes + mount_flags.push(`nouuid`); + break; + } + await mount.mount( device, staging_target_path, From 8021570b714dc8b2af3219f4c7023a02c0ec7b59 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 11 May 2022 03:16:24 -0600 Subject: [PATCH 55/71] ci fixes Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 503f3e3..f5f47bc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -33,7 +33,7 @@ jobs: with: name: node-modules-linux-amd64 path: node_modules-linux-amd64.tar.gz - retention-days: 7 + retention-days: 1 build-npm-windows-amd64: runs-on: windows-2022 @@ -51,7 +51,7 @@ jobs: with: name: node-modules-windows-amd64 path: node_modules-windows-amd64.tar.gz - retention-days: 7 + retention-days: 1 csi-sanity-synology-dsm6: needs: @@ -258,7 +258,7 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-zfs-generic + - csi-sanity-client steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -327,6 +327,7 @@ jobs: - self-hosted - ${{ matrix.os }} - X64 + - csi-sanity-local-hostpath steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -460,14 +461,14 @@ jobs: run: | docker info docker build --pull -f ${{ matrix.file }} --build-arg BASE_TAG=${{ matrix.base_tag }} -t democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.base_tag }} . - docker inspect windows:${{ matrix.base_tag }} + docker inspect democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.base_tag }} docker save democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.base_tag }} -o democratic-csi-windows-${{ matrix.base_tag }}.tar - name: upload image tar uses: actions/upload-artifact@v2 with: name: democratic-csi-windows-${{ matrix.base_tag }}.tar path: democratic-csi-windows-${{ matrix.base_tag }}.tar - retention-days: 7 + retention-days: 1 push-docker-windows: needs: From f2ca4ed41fd73e1a582cf9c690648d49934bec3f Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 11 May 2022 15:53:32 -0600 Subject: [PATCH 56/71] use nanoserver for windows image Signed-off-by: Travis Glenn Hansen --- .dockerignore | 18 +++++++++--------- .github/workflows/main.yml | 25 ++++++++++++------------- Dockerfile.Windows | 34 +++++++++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 27 deletions(-) diff --git a/.dockerignore b/.dockerignore index 5a21efb..b483184 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,9 @@ -chart -dev -examples -contrib -node_modules -Dockerfile* -TODO.md -.git -/ci +** + +!/bin +!/csi_proto +!/csi_proxy_proto +!/docker +!/LICENSE +!/package*.json +!/src diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f5f47bc..b43dc9f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -129,8 +129,7 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-zfs-local - #- csi-sanity-truenas-scale + - csi-sanity-truenas steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -163,8 +162,7 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-zfs-local - #- csi-sanity-truenas-core + - csi-sanity-truenas steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -196,8 +194,7 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-zfs-local - #- csi-sanity-truenas-core + - csi-sanity-truenas steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -449,10 +446,12 @@ jobs: os: [windows-2019, windows-2022] include: - os: windows-2019 - base_tag: ltsc2019 + core_base_tag: ltsc2019 + nano_base_tag: "1809" file: Dockerfile.Windows - os: windows-2022 - base_tag: ltsc2022 + core_base_tag: ltsc2022 + nano_base_tag: ltsc2022 file: Dockerfile.Windows steps: - uses: actions/checkout@v2 @@ -460,14 +459,14 @@ jobs: shell: bash run: | docker info - docker build --pull -f ${{ matrix.file }} --build-arg BASE_TAG=${{ matrix.base_tag }} -t democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.base_tag }} . - docker inspect democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.base_tag }} - docker save democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.base_tag }} -o democratic-csi-windows-${{ matrix.base_tag }}.tar + docker build --pull -f ${{ matrix.file }} --build-arg NANO_BASE_TAG=${{ matrix.nano_base_tag }} --build-arg CORE_BASE_TAG=${{ matrix.core_base_tag }} -t democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} . + docker inspect democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} + docker save democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} -o democratic-csi-windows-${{ matrix.core_base_tag }}.tar - name: upload image tar uses: actions/upload-artifact@v2 with: - name: democratic-csi-windows-${{ matrix.base_tag }}.tar - path: democratic-csi-windows-${{ matrix.base_tag }}.tar + name: democratic-csi-windows-${{ matrix.core_base_tag }}.tar + path: democratic-csi-windows-${{ matrix.core_base_tag }}.tar retention-days: 1 push-docker-windows: diff --git a/Dockerfile.Windows b/Dockerfile.Windows index 038b283..1f26eca 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -1,13 +1,34 @@ -# docker build --build-arg BASE_TAG=ltsc2019 -t foobar -f Dockerfile.Windows . +# +# https://github.com/kubernetes/kubernetes/blob/master/test/images/windows/powershell-helper/Dockerfile_windows +# https://github.com/kubernetes/kubernetes/blob/master/test/images/busybox/Dockerfile_windows +# https://github.com/kubernetes/kubernetes/tree/master/test/images#windows-test-images-considerations +# https://stefanscherer.github.io/find-dependencies-in-windows-containers/ +# +# docker build --build-arg NANO_BASE_TAG=1809 --build-arg CORE_BASE_TAG=ltsc2019 -t foobar -f Dockerfile.Windows . # docker run --rm -ti --entrypoint powershell foobar - +# docker run --rm foobar +# docker save foobar -o foobar.tar +# buildah pull docker-archive:foobar.tar # mcr.microsoft.com/windows/servercore:ltsc2019 # mcr.microsoft.com/windows/nanoserver:1809 -ARG BASE_TAG +ARG NANO_BASE_TAG +ARG CORE_BASE_TAG -FROM mcr.microsoft.com/windows/servercore:${BASE_TAG} as build +FROM mcr.microsoft.com/windows/servercore:${CORE_BASE_TAG} as powershell + +# install powershell +ENV PS_VERSION=6.2.7 +ADD https://github.com/PowerShell/PowerShell/releases/download/v$PS_VERSION/PowerShell-$PS_VERSION-win-x64.zip /PowerShell/powershell.zip + +RUN cd C:\PowerShell &\ + tar.exe -xf powershell.zip &\ + del powershell.zip &\ + mklink powershell.exe pwsh.exe + + +FROM mcr.microsoft.com/windows/servercore:${CORE_BASE_TAG} as build SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] @@ -61,10 +82,13 @@ COPY package*.json ./ RUN npm install --only=production; ls / COPY . . -FROM mcr.microsoft.com/windows/servercore:${BASE_TAG} +FROM mcr.microsoft.com/windows/nanoserver:${NANO_BASE_TAG} LABEL org.opencontainers.image.source https://github.com/democratic-csi/democratic-csi +# if additional dlls are required can copy like this +#COPY --from=build /Windows/System32/nltest.exe /Windows/System32/nltest.exe + COPY --from=build /nodejs/node.exe /Windows/system32 COPY --from=build /app /app From 47d18084bab93d8920d704d4968fe2fc76467e0c Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 11 May 2022 17:03:08 -0600 Subject: [PATCH 57/71] make container work with host-process semantics Signed-off-by: Travis Glenn Hansen --- Dockerfile.Windows | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile.Windows b/Dockerfile.Windows index 1f26eca..bb818cb 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -89,9 +89,11 @@ LABEL org.opencontainers.image.source https://github.com/democratic-csi/democrat # if additional dlls are required can copy like this #COPY --from=build /Windows/System32/nltest.exe /Windows/System32/nltest.exe -COPY --from=build /nodejs/node.exe /Windows/system32 COPY --from=build /app /app - WORKDIR /app +# this works for both host-process and non-host-process container semantics +COPY --from=build /nodejs/node.exe . + + ENTRYPOINT [ "node.exe", "--expose-gc", "bin/democratic-csi" ] From fc581fa6d05ef295d665e1743aa12b2dee65b018 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 12 May 2022 11:15:16 -0600 Subject: [PATCH 58/71] minor fixes Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 9 ++++++--- Dockerfile.Windows | 5 ++--- src/utils/mount.js | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b43dc9f..2370c89 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -129,7 +129,8 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-truenas + #- csi-sanity-truenas + - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -162,7 +163,8 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-truenas + #- csi-sanity-truenas + - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -194,7 +196,8 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-truenas + #- csi-sanity-truenas + - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 diff --git a/Dockerfile.Windows b/Dockerfile.Windows index bb818cb..7af6c2c 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -93,7 +93,6 @@ COPY --from=build /app /app WORKDIR /app # this works for both host-process and non-host-process container semantics -COPY --from=build /nodejs/node.exe . +COPY --from=build /nodejs/node.exe ./bin - -ENTRYPOINT [ "node.exe", "--expose-gc", "bin/democratic-csi" ] +ENTRYPOINT [ "bin/node.exe", "--expose-gc", "bin/democratic-csi" ] diff --git a/src/utils/mount.js b/src/utils/mount.js index eade177..0448557 100644 --- a/src/utils/mount.js +++ b/src/utils/mount.js @@ -298,7 +298,7 @@ class Mount { return false; } const mount_info = await mount.getMountDetails(path); - const is_block = filesystem.isBlockDevice(path); + const is_block = await filesystem.isBlockDevice(path); if (mount_info.fstype == "devtmpfs" && is_block) { return true; } From 908685f07311fd4286e0d42001317aadfa1e4b27 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 12 May 2022 14:42:27 -0600 Subject: [PATCH 59/71] better windows smb logic --- src/driver/index.js | 14 ++++++++---- src/utils/windows.js | 51 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/src/driver/index.js b/src/driver/index.js index 84d5cc7..a1db307 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -1300,6 +1300,7 @@ class CsiBaseDriver { filesystem.covertUnixSeparatorToWindowsSeparator(device) ); if (!result) { + // check for mount option cache=none and set -UseWriteThrough $true await wutils.NewSmbGlobalMapping( filesystem.covertUnixSeparatorToWindowsSeparator(device), `${volume_context.server}\\${username}`, @@ -2290,11 +2291,16 @@ class CsiBaseDriver { switch (node_attach_driver) { case "smb": + // remove symlink *before* disconnecting + await removePath(win_normalized_staging_path); let parts = target.split("\\"); - await wutils.RemoveSmbGlobalMapping( - `\\\\${parts[1]}\\${parts[2]}` - ); - + // only remove global mapping if we certain there may not be other + // consumers of the mapping/share (ie: smb-client scenarios, etc) + if (!parts[3]) { + await wutils.RemoveSmbGlobalMapping( + `\\\\${parts[1]}\\${parts[2]}` + ); + } break; case "iscsi": // write volume cache diff --git a/src/utils/windows.js b/src/utils/windows.js index 7b591c3..90a530d 100644 --- a/src/utils/windows.js +++ b/src/utils/windows.js @@ -41,6 +41,20 @@ class Windows { } } + uncPathToShare(path) { + // UNC\\[\\] + if (path.startsWith("UNC")) { + path = path.replace("UNC", "\\"); + } + + if (!path.startsWith("\\\\")) { + path = `\\\\${path}`; + } + + let parts = path.split("\\"); + return `\\\\${parts[2]}\\${parts[3]}`; + } + async GetRealTarget(path) { let item; let target; @@ -77,6 +91,9 @@ class Windows { async GetSmbGlobalMapping(remotePath) { let command; + // cannot have trailing slash nor a path + // must be \\\ + remotePath = this.uncPathToShare(remotePath); command = "Get-SmbGlobalMapping -RemotePath $Env:smbremotepath | ConvertTo-Json"; try { @@ -88,23 +105,41 @@ class Windows { } catch (err) {} } + /** + * Global in this context is allowed access by all users + * + * @param {*} remotePath + * @param {*} username + * @param {*} password + */ async NewSmbGlobalMapping(remotePath, username, password) { + let result; let command; + // -UseWriteThrough $true + // cannot have trailing slash nor a path + // must be \\\ + remotePath = this.uncPathToShare(remotePath); command = "$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential -RequirePrivacy $true"; - await this.ps.exec(command, { - env: { - smbuser: username, - smbpassword: password, - smbremotepath: remotePath, - }, - }); + result = await this.GetSmbGlobalMapping(remotePath); + if (!result) { + await this.ps.exec(command, { + env: { + smbuser: username, + smbpassword: password, + smbremotepath: remotePath, + }, + }); + } } async RemoveSmbGlobalMapping(remotePath) { let result; let command; + // cannot have trailing slash nor a path + // must be \\\ + remotePath = this.uncPathToShare(remotePath); command = "Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force"; do { @@ -121,6 +156,8 @@ class Windows { async NewSmbLink(remotePath, localPath) { let command; + // trailing slash required + // may include subdirectories on the share if desired if (!remotePath.endsWith("\\")) { remotePath = `${remotePath}\\`; } From d511bf4e43bea8763eddb880b53033717269370d Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 12 May 2022 14:53:10 -0600 Subject: [PATCH 60/71] additional labels on images, test smb-client on windows Signed-off-by: Travis Glenn Hansen --- .github/bin/docker-release.sh | 5 ++++- .github/workflows/main.yml | 39 +++++++++++++++++++++++++++++++++-- Dockerfile | 2 ++ Dockerfile.Windows | 2 ++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/.github/bin/docker-release.sh b/.github/bin/docker-release.sh index be27592..bac8448 100755 --- a/.github/bin/docker-release.sh +++ b/.github/bin/docker-release.sh @@ -12,7 +12,10 @@ export GHCR_PROJECT="democratic-csi" export GHCR_REPO="ghcr.io/${GHCR_ORG}/${GHCR_PROJECT}" if [[ -n "${IMAGE_TAG}" ]]; then - docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${IMAGE_TAG} -t ${GHCR_REPO}:${IMAGE_TAG} . + docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${IMAGE_TAG} -t ${GHCR_REPO}:${IMAGE_TAG} \ + --label "org.opencontainers.image.created=$(date -u --iso-8601=seconds)" \ + --label "org.opencontainers.image.revision=${CI_COMMIT_SHA}" \ + . else : fi diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2370c89..2e36cf4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -274,6 +274,36 @@ jobs: SHARE_NAME: tank_client_smb CSI_SANITY_SKIP: "should fail when requesting to create a snapshot with already existing name and different source volume ID|should fail when requesting to create a volume with already existing name and different capacity" + csi-sanity-client-windows: + needs: + - build-npm-windows-amd64 + strategy: + fail-fast: false + matrix: + config: + - client\smb.yaml + runs-on: + - self-hosted + - Windows + - X64 + - csi-sanity-client + steps: + - uses: actions/checkout@v2 + - uses: actions/download-artifact@v2 + with: + name: node-modules-windows-amd64 + - name: csi-sanity + run: | + # run tests + ci\bin\run.ps1 + env: + TEMPLATE_CONFIG_FILE: ".\\ci\\configs\\${{ matrix.config }}" + SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_HOST }} + SHARE_NAME: tank_client_smb + CSI_SANITY_SKIP: "should fail when requesting to create a snapshot with already existing name and different source volume ID|should fail when requesting to create a volume with already existing name and different capacity" + + + # zfs-local drivers csi-sanity-zfs-local: needs: @@ -319,7 +349,7 @@ jobs: ci/bin/run.sh - os: Windows npmartifact: node-modules-windows-amd64 - template: ".\\ci\\configs\\local-hostpath/basic.yaml" + template: ".\\ci\\configs\\local-hostpath\\basic.yaml" run: | # run tests ci\bin\run.ps1 @@ -402,6 +432,7 @@ jobs: - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic - csi-sanity-client + - csi-sanity-client-windows - csi-sanity-zfs-local - csi-sanity-local-hostpath - csi-sanity-windows-node @@ -440,6 +471,7 @@ jobs: - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic - csi-sanity-client + - csi-sanity-client-windows - csi-sanity-zfs-local - csi-sanity-local-hostpath - csi-sanity-windows-node @@ -462,7 +494,10 @@ jobs: shell: bash run: | docker info - docker build --pull -f ${{ matrix.file }} --build-arg NANO_BASE_TAG=${{ matrix.nano_base_tag }} --build-arg CORE_BASE_TAG=${{ matrix.core_base_tag }} -t democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} . + docker build --pull -f ${{ matrix.file }} --build-arg NANO_BASE_TAG=${{ matrix.nano_base_tag }} --build-arg CORE_BASE_TAG=${{ matrix.core_base_tag }} -t democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} \ + --label "org.opencontainers.image.created=$(date -u --iso-8601=seconds)" \ + --label "org.opencontainers.image.revision=${CI_COMMIT_SHA}" \ + . docker inspect democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} docker save democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} -o democratic-csi-windows-${{ matrix.core_base_tag }}.tar - name: upload image tar diff --git a/Dockerfile b/Dockerfile index a1bba72..661fa9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,6 +43,8 @@ RUN rm -rf docker FROM debian:11-slim LABEL org.opencontainers.image.source https://github.com/democratic-csi/democratic-csi +LABEL org.opencontainers.image.url https://github.com/democratic-csi/democratic-csi +LABEL org.opencontainers.image.licenses MIT ENV DEBIAN_FRONTEND=noninteractive diff --git a/Dockerfile.Windows b/Dockerfile.Windows index 7af6c2c..847225d 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -85,6 +85,8 @@ COPY . . FROM mcr.microsoft.com/windows/nanoserver:${NANO_BASE_TAG} LABEL org.opencontainers.image.source https://github.com/democratic-csi/democratic-csi +LABEL org.opencontainers.image.url https://github.com/democratic-csi/democratic-csi +LABEL org.opencontainers.image.licenses MIT # if additional dlls are required can copy like this #COPY --from=build /Windows/System32/nltest.exe /Windows/System32/nltest.exe From a7d5b53fd3a6b6f7d0f6c6c3ad918b1de63ae5db Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 21 May 2022 07:28:47 -0600 Subject: [PATCH 61/71] better support for windows host-process containers Signed-off-by: Travis Glenn Hansen --- .github/bin/docker-release.sh | 2 +- .github/workflows/main.yml | 2 +- bin/democratic-csi | 23 +++++++++++++++++++++++ src/utils/windows.js | 1 + 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/.github/bin/docker-release.sh b/.github/bin/docker-release.sh index bac8448..0d31255 100755 --- a/.github/bin/docker-release.sh +++ b/.github/bin/docker-release.sh @@ -14,7 +14,7 @@ export GHCR_REPO="ghcr.io/${GHCR_ORG}/${GHCR_PROJECT}" if [[ -n "${IMAGE_TAG}" ]]; then docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${IMAGE_TAG} -t ${GHCR_REPO}:${IMAGE_TAG} \ --label "org.opencontainers.image.created=$(date -u --iso-8601=seconds)" \ - --label "org.opencontainers.image.revision=${CI_COMMIT_SHA}" \ + --label "org.opencontainers.image.revision=${GITHUB_SHA}" \ . else : diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2e36cf4..6664cb9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -496,7 +496,7 @@ jobs: docker info docker build --pull -f ${{ matrix.file }} --build-arg NANO_BASE_TAG=${{ matrix.nano_base_tag }} --build-arg CORE_BASE_TAG=${{ matrix.core_base_tag }} -t democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} \ --label "org.opencontainers.image.created=$(date -u --iso-8601=seconds)" \ - --label "org.opencontainers.image.revision=${CI_COMMIT_SHA}" \ + --label "org.opencontainers.image.revision=${GITHUB_SHA}" \ . docker inspect democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} docker save democratic-csi-windows:${GITHUB_RUN_ID}-${{ matrix.core_base_tag }} -o democratic-csi-windows-${{ matrix.core_base_tag }}.tar diff --git a/bin/democratic-csi b/bin/democratic-csi index 7d2e0ad..e45f7eb 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -31,6 +31,29 @@ const args = require("yargs") return true; } catch (e) {} + // CONTAINER_SANDBOX_MOUNT_POINT C:\C\0eac9a8da76f6d7119c5d9f86c8b3106d67dbbf01dbeb22fdc0192476b7e31cb\ + if (process.env.CONTAINER_SANDBOX_MOUNTPOINT) { + try { + options = JSON.parse( + fs.readFileSync( + `${process.env.CONTAINER_SANDBOX_MOUNTPOINT}${path}`, + "utf-8" + ) + ); + return true; + } catch (e) {} + + try { + options = yaml.load( + fs.readFileSync( + `${process.env.CONTAINER_SANDBOX_MOUNTPOINT}${path}`, + "utf8" + ) + ); + return true; + } catch (e) {} + } + throw new Error("failed parsing config file: " + path); }, }) diff --git a/src/utils/windows.js b/src/utils/windows.js index 90a530d..d2a969e 100644 --- a/src/utils/windows.js +++ b/src/utils/windows.js @@ -317,6 +317,7 @@ class Windows { async DisconnectIscsiTargetByNodeAddress(nodeAddress) { let command; + // https://github.com/PowerShell/PowerShell/issues/17306 command = `Disconnect-IscsiTarget -NodeAddress ${nodeAddress.toLowerCase()} -Confirm:$false`; await this.ps.exec(command); } From 82961ac36fb3eef0f9074a176d1dbc4f04d19350 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 21 May 2022 10:10:20 -0600 Subject: [PATCH 62/71] better host-process support Signed-off-by: Travis Glenn Hansen --- bin/democratic-csi | 34 ++++++++++------------------------ src/utils/general.js | 5 +++++ 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/bin/democratic-csi b/bin/democratic-csi index e45f7eb..4082952 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -10,7 +10,7 @@ require("../src/utils/polyfills"); const yaml = require("js-yaml"); const fs = require("fs"); const { grpc } = require("../src/utils/grpc"); -const { stringify } = require("../src/utils/general"); +const { stringify, stripWindowsDriveLetter } = require("../src/utils/general"); let options; const args = require("yargs") @@ -21,6 +21,15 @@ const args = require("yargs") describe: "provide a path to driver config file", config: true, configParser: (path) => { + // normalize path for host-process containers + // CONTAINER_SANDBOX_MOUNT_POINT C:\C\0eac9a8da76f6d7119c5d9f86c8b3106d67dbbf01dbeb22fdc0192476b7e31cb\ + // path is injected as C:\config\driver-config-file.yaml + if (process.env.CONTAINER_SANDBOX_MOUNT_POINT) { + path = `${ + process.env.CONTAINER_SANDBOX_MOUNT_POINT + }${stripWindowsDriveLetter(path)}`; + } + try { options = JSON.parse(fs.readFileSync(path, "utf-8")); return true; @@ -31,29 +40,6 @@ const args = require("yargs") return true; } catch (e) {} - // CONTAINER_SANDBOX_MOUNT_POINT C:\C\0eac9a8da76f6d7119c5d9f86c8b3106d67dbbf01dbeb22fdc0192476b7e31cb\ - if (process.env.CONTAINER_SANDBOX_MOUNTPOINT) { - try { - options = JSON.parse( - fs.readFileSync( - `${process.env.CONTAINER_SANDBOX_MOUNTPOINT}${path}`, - "utf-8" - ) - ); - return true; - } catch (e) {} - - try { - options = yaml.load( - fs.readFileSync( - `${process.env.CONTAINER_SANDBOX_MOUNTPOINT}${path}`, - "utf8" - ) - ); - return true; - } catch (e) {} - } - throw new Error("failed parsing config file: " + path); }, }) diff --git a/src/utils/general.js b/src/utils/general.js index 746f2ef..17abb49 100644 --- a/src/utils/general.js +++ b/src/utils/general.js @@ -113,6 +113,10 @@ function getLargestNumber() { return number; } +function stripWindowsDriveLetter(path) { + return path.replace(/^[a-zA-Z]:/, ""); +} + /** * transition function to replicate `request` style requests using axios * @@ -248,6 +252,7 @@ module.exports.crc16 = crc16; module.exports.lockKeysFromRequest = lockKeysFromRequest; module.exports.getLargestNumber = getLargestNumber; module.exports.stringify = stringify; +module.exports.stripWindowsDriveLetter = stripWindowsDriveLetter; module.exports.axios_request = axios_request; module.exports.default_supported_block_filesystems = default_supported_block_filesystems; From 323271067f711b502d2307cd222dfa9e9d7beb9a Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 21 May 2022 16:47:21 -0600 Subject: [PATCH 63/71] proper pathing for config file logging Signed-off-by: Travis Glenn Hansen --- bin/democratic-csi | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/democratic-csi b/bin/democratic-csi index 4082952..5c4a54b 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -12,6 +12,7 @@ const fs = require("fs"); const { grpc } = require("../src/utils/grpc"); const { stringify, stripWindowsDriveLetter } = require("../src/utils/general"); +let driverConfigFile; let options; const args = require("yargs") .env("DEMOCRATIC_CSI") @@ -32,11 +33,13 @@ const args = require("yargs") try { options = JSON.parse(fs.readFileSync(path, "utf-8")); + driverConfigFile = fs.realpathSync(path); return true; } catch (e) {} try { options = yaml.load(fs.readFileSync(path, "utf8")); + driverConfigFile = fs.realpathSync(path); return true; } catch (e) {} @@ -384,7 +387,7 @@ logger.info( "starting csi server - node version: %s, package version: %s, config file: %s, csi-name: %s, csi-driver: %s, csi-mode: %s, csi-version: %s, address: %s, socket: %s", process.version, args.version, - fs.realpathSync(args.driverConfigFile), + driverConfigFile, args.csiName, options.driver, args.csiMode.join(","), From 7fd385d2d07dece8b8785d33ec5b78a7bd368111 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 21 May 2022 21:26:10 -0600 Subject: [PATCH 64/71] windows craziness Signed-off-by: Travis Glenn Hansen --- bin/democratic-csi | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/bin/democratic-csi b/bin/democratic-csi index 5c4a54b..52e0a34 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -31,19 +31,20 @@ const args = require("yargs") }${stripWindowsDriveLetter(path)}`; } - try { - options = JSON.parse(fs.readFileSync(path, "utf-8")); - driverConfigFile = fs.realpathSync(path); - return true; - } catch (e) {} - try { options = yaml.load(fs.readFileSync(path, "utf8")); - driverConfigFile = fs.realpathSync(path); - return true; - } catch (e) {} + try { + driverConfigFile = fs.realpathSync(path); + } catch (e) { + console.log("failed finding config file realpath: " + e.toString()); + driverConfigFile = path; + } - throw new Error("failed parsing config file: " + path); + return true; + } catch (e) { + console.log("failed parsing config file: " + path); + throw e; + } }, }) .demandOption(["driver-config-file"], "driver-config-file is required") From a81246664b185d0c55af287c287fd071dd1536de Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 25 May 2022 17:08:18 -0600 Subject: [PATCH 65/71] fix unstage issues Signed-off-by: Travis Glenn Hansen --- src/utils/general.js | 5 +++++ src/utils/windows.js | 12 +++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/utils/general.js b/src/utils/general.js index 17abb49..79654ac 100644 --- a/src/utils/general.js +++ b/src/utils/general.js @@ -117,6 +117,10 @@ function stripWindowsDriveLetter(path) { return path.replace(/^[a-zA-Z]:/, ""); } +function hasWindowsDriveLetter(path) { + return /^[a-zA-Z]:/i.test(path); +} + /** * transition function to replicate `request` style requests using axios * @@ -253,6 +257,7 @@ module.exports.lockKeysFromRequest = lockKeysFromRequest; module.exports.getLargestNumber = getLargestNumber; module.exports.stringify = stringify; module.exports.stripWindowsDriveLetter = stripWindowsDriveLetter; +module.exports.hasWindowsDriveLetter = hasWindowsDriveLetter; module.exports.axios_request = axios_request; module.exports.default_supported_block_filesystems = default_supported_block_filesystems; diff --git a/src/utils/windows.js b/src/utils/windows.js index d2a969e..c02ceb3 100644 --- a/src/utils/windows.js +++ b/src/utils/windows.js @@ -1,5 +1,5 @@ -const { result } = require("lodash"); const _ = require("lodash"); +const GeneralUtils = require("./general"); const Powershell = require("./powershell").Powershell; /** @@ -757,6 +757,16 @@ class Windows { async UnmountVolume(volumeId, path) { let command; + + // this errors if it does not have a drive letter + if (!GeneralUtils.hasWindowsDriveLetter(path)) { + let item = await this.GetItem(path); + if (!item) { + return; + } + path = item.FullName; + } + command = `Get-Volume -UniqueId \"${volumeId}\" | Get-Partition | Remove-PartitionAccessPath -AccessPath ${path}`; await this.ps.exec(command); From a0d664abfa5bd6e3300e4e1f8361c9fb4f35e321 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 25 May 2022 20:59:03 -0600 Subject: [PATCH 66/71] do not push to ghcr with buildx Signed-off-by: Travis Glenn Hansen --- .github/bin/docker-release.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/bin/docker-release.sh b/.github/bin/docker-release.sh index 0d31255..f5fe0d4 100755 --- a/.github/bin/docker-release.sh +++ b/.github/bin/docker-release.sh @@ -12,7 +12,8 @@ export GHCR_PROJECT="democratic-csi" export GHCR_REPO="ghcr.io/${GHCR_ORG}/${GHCR_PROJECT}" if [[ -n "${IMAGE_TAG}" ]]; then - docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${IMAGE_TAG} -t ${GHCR_REPO}:${IMAGE_TAG} \ + # -t ${GHCR_REPO}:${IMAGE_TAG} + docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${IMAGE_TAG} \ --label "org.opencontainers.image.created=$(date -u --iso-8601=seconds)" \ --label "org.opencontainers.image.revision=${GITHUB_SHA}" \ . From f626a93f578a258132831521086f1ccbc01e4345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Su=C5=A1nik?= Date: Thu, 26 May 2022 18:00:34 +0200 Subject: [PATCH 67/71] Apply sudo only to targetcli bin, not entire command --- src/driver/controller-zfs-generic/index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index c3aa985..991a922 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -476,19 +476,19 @@ delete ${iscsiName} let command = "sh"; let args = ["-c"]; - let taregetCliCommand = []; - taregetCliCommand.push(`echo "${data}"`.trim()); - taregetCliCommand.push("|"); - taregetCliCommand.push("targetcli"); + let targetCliArgs = ["targetcli"]; if ( _.get(this.options, "iscsi.shareStrategyTargetCli.sudoEnabled", false) ) { - command = "sudo"; - args.unshift("sh"); + targetCliArgs.unshift("sudo"); } - args.push("'" + taregetCliCommand.join(" ") + "'"); + let targetCliCommand = []; + targetCliCommand.push(`echo "${data}"`.trim()); + targetCliCommand.push("|"); + targetCliCommand.push(targetCliArgs.join(" ")); + args.push("'" + targetCliCommand.join(" ") + "'"); let logCommandTmp = command + " " + args.join(" "); let logCommand = ""; From bd08538a872862f4a951af757c5e117d5f57779d Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sun, 5 Jun 2022 20:45:05 -0600 Subject: [PATCH 68/71] ensure SCALE for freenas-api-* Probes, better sudo usage, prep v1.7.0 Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 12 +- CHANGELOG.md | 27 +++ README.md | 42 +++- package-lock.json | 444 ++++++++++++++++++------------------- src/driver/freenas/api.js | 9 + src/utils/zfs.js | 26 ++- 6 files changed, 326 insertions(+), 234 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6664cb9..f143fc0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -129,8 +129,8 @@ jobs: - self-hosted - Linux - X64 - #- csi-sanity-truenas - - csi-sanity-zfs-generic + - csi-sanity-truenas + #- csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -163,8 +163,8 @@ jobs: - self-hosted - Linux - X64 - #- csi-sanity-truenas - - csi-sanity-zfs-generic + - csi-sanity-truenas + #- csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -196,8 +196,8 @@ jobs: - self-hosted - Linux - X64 - #- csi-sanity-truenas - - csi-sanity-zfs-generic + - csi-sanity-truenas + #- csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f5444d..3b59824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,30 @@ +# v1.7.0 + +Released 2022-06-05 + +The windows release. + +- windows smb, iscsi, and local-hostpath support +- ntfs, exfat, vfat fs support +- zfs-generic-smb driver +- synology improvements + - DSM7 support + - synology enhancements to allow templates to be configured at various + 'levels' +- testing improvements + - support (for testing) generating volume_id from name + - test all the smb variants + - test all nfs/smb client drivers +- misc fixes + - wait for chown/chmod jobs to complete (freenas) + - general improvement to smb behavior throughout + - better logging + - better sudo logic throughout +- more robust logic for connecting to iscsi devices with partition tables +- massive performance improvement for ssh-based drivers (reusing existing + connection instead of new connection per-command) +- dep bumps + # v1.6.3 Released 2022-04-08 diff --git a/README.md b/README.md index 49d7d64..ae5d8db 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,16 @@ Predominantly 3 things are needed: You should install/configure the requirements for both nfs and iscsi. +### cifs + +``` +RHEL / CentOS +sudo yum install -y cifs-utils + +Ubuntu / Debian +sudo apt-get install -y cifs-utils +``` + ### nfs ``` @@ -176,6 +186,35 @@ volume is/was provisioned. The nature of this `driver` also prevents the enforcement of quotas. In short the requested volume size is generally ignored. +### windows + +Support for Windows was introduced in `v1.7.0`. Currently support is limited +to kubernetes nodes capabale of running `HostProcess` containers. Support was +tested against `Windows Server 2019` using `rke2-v1.24`. Currently any of the +`-smb` and `-iscsi` drivers will work. Support for `ntfs` was added to the +linux nodes as well (using the `ntfs3` driver) so volumes created can be +utilized by nodes with either operating system (in the case of `cifs` by both +simultaneously). + +Due to current limits in the kubernetes tooling it is not possible to use the +`local-hostpath` driver but support is implemented in this project and will +work as soon as kubernetes support is available. + +``` +# ensure all updates are installed + +# enable the container feature +Enable-WindowsOptionalFeature -Online -FeatureName Containers –All + +# create symbolic link due to current limitations in the driver-registrar container +New-Item -ItemType SymbolicLink -Path "C:\registration\" -Target "C:\var\lib\kubelet\plugins_registry\" + +# install a HostProcess compatible kubernetes +``` + +- https://kubernetes.io/blog/2021/08/16/windows-hostprocess-containers/ +- https://kubernetes.io/docs/tasks/configure-pod-container/create-hostprocess-pod/ + ## Server Prep Server preparation depends slightly on which `driver` you are using. @@ -201,6 +240,7 @@ Ensure the following services are configurged and running: - ensure `zsh`, `bash`, or `sh` is set as the root shell, `csh` gives false errors due to quoting - nfs - iscsi + - (fixed in 12.0-U2+) when using the FreeNAS API concurrently the `/etc/ctl.conf` file on the server can become invalid, some sample scripts are provided in the `contrib` directory to clean things up ie: copy the @@ -216,7 +256,7 @@ Ensure the following services are configurged and running: - `curl --header "Accept: application/json" --user root: 'http(s):///api/v2.0/iscsi/initiator'` - `curl --header "Accept: application/json" --user root: 'http(s):///api/v2.0/iscsi/auth'` - The maximum number of volumes is limited to 255 by default on FreeBSD (physical devices such as disks and CD-ROM drives count against this value). - Be sure to properly adjust both [tunables](https://www.freebsd.org/cgi/man.cgi?query=ctl&sektion=4#end) `kern.cam.ctl.max_ports` and `kern.cam.ctl.max_luns` to avoid running out of resources when dynamically provisioning iSCSI volumes on FreeNAS or TrueNAS Core. + Be sure to properly adjust both [tunables](https://www.freebsd.org/cgi/man.cgi?query=ctl&sektion=4#end) `kern.cam.ctl.max_ports` and `kern.cam.ctl.max_luns` to avoid running out of resources when dynamically provisioning iSCSI volumes on FreeNAS or TrueNAS Core. - smb diff --git a/package-lock.json b/package-lock.json index d252a35..03d205e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,19 +51,19 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", - "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.1", - "globals": "^13.9.0", + "espree": "^9.3.2", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, "engines": { @@ -190,7 +190,7 @@ "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", @@ -205,12 +205,12 @@ "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -219,27 +219,27 @@ "node_modules/@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@sindresorhus/is": { "version": "4.6.0", @@ -316,9 +316,9 @@ } }, "node_modules/@types/node": { - "version": "17.0.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz", - "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==" + "version": "17.0.40", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.40.tgz", + "integrity": "sha512-UXdBxNGqTMtm7hCwh9HtncFVLrXoqA3oJW30j6XWp5BH/wu3mVeaxo7cq5benFdBw34HB3XDT2TRPI7rXZ+mDg==" }, "node_modules/@types/request": { "version": "2.48.8", @@ -460,7 +460,7 @@ "node_modules/assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "engines": { "node": ">=0.8" } @@ -486,12 +486,12 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", "engines": { "node": "*" } @@ -531,7 +531,7 @@ "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "dependencies": { "tweetnacl": "^0.14.3" } @@ -574,7 +574,7 @@ "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=", + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", "engines": { "node": ">=0.10.0" } @@ -630,7 +630,7 @@ "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, "node_modules/chalk": { "version": "4.1.2", @@ -677,7 +677,7 @@ "node_modules/clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "dependencies": { "mimic-response": "^1.0.0" } @@ -727,12 +727,12 @@ "node_modules/color/node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" }, "node_modules/colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=", + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", "engines": { "node": ">=0.1.90" } @@ -772,12 +772,12 @@ "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/cpu-features": { "version": "0.0.4", @@ -809,7 +809,7 @@ "node_modules/cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", + "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", "engines": { "node": ">=0.4.0" } @@ -817,7 +817,7 @@ "node_modules/dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "dependencies": { "assert-plus": "^1.0.0" }, @@ -884,7 +884,7 @@ "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "engines": { "node": ">=0.4.0" } @@ -917,7 +917,7 @@ "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -962,12 +962,12 @@ } }, "node_modules/eslint": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", - "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", "dev": true, "dependencies": { - "@eslint/eslintrc": "^1.2.2", + "@eslint/eslintrc": "^1.3.0", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -978,14 +978,14 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", + "espree": "^9.3.2", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", - "globals": "^13.6.0", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -994,7 +994,7 @@ "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", @@ -1063,13 +1063,13 @@ } }, "node_modules/espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", "dev": true, "dependencies": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1148,7 +1148,7 @@ "node_modules/extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "engines": [ "node >=0.6.0" ] @@ -1156,7 +1156,7 @@ "node_modules/eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", "engines": { "node": "> 0.1.90" } @@ -1174,7 +1174,7 @@ "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "node_modules/fecha": { @@ -1219,9 +1219,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz", - "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", "funding": [ { "type": "individual", @@ -1240,7 +1240,7 @@ "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", "engines": { "node": "*" } @@ -1285,7 +1285,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/function-bind": { "version": "1.1.1", @@ -1295,7 +1295,7 @@ "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, "node_modules/get-caller-file": { @@ -1320,20 +1320,20 @@ "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "dependencies": { "assert-plus": "^1.0.0" } }, "node_modules/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, @@ -1357,9 +1357,9 @@ } }, "node_modules/globals": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", - "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -1372,9 +1372,9 @@ } }, "node_modules/got": { - "version": "11.8.3", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz", - "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==", + "version": "11.8.5", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", + "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==", "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -1423,7 +1423,7 @@ "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", "engines": { "node": ">=4" } @@ -1469,7 +1469,7 @@ "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -1528,7 +1528,7 @@ "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "engines": { "node": ">=0.8.19" @@ -1545,7 +1545,7 @@ "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1583,7 +1583,7 @@ "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, "engines": { "node": ">=0.10.0" @@ -1623,12 +1623,12 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isomorphic-ws": { "version": "4.0.1", @@ -1641,7 +1641,7 @@ "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "node_modules/jose": { "version": "2.0.5", @@ -1671,7 +1671,7 @@ "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "node_modules/json-buffer": { "version": "3.0.1", @@ -1691,13 +1691,13 @@ "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "node_modules/jsonfile": { "version": "6.1.0", @@ -1733,11 +1733,11 @@ } }, "node_modules/keyv": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.2.2.tgz", - "integrity": "sha512-uYS0vKTlBIjNCAUqrjlxmruxOEiZxZIHXyp32sdcGmP+ukFrmWUnE//RcPXJH3Vxrni1H2gsQbjHE0bH7MtMQQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.3.0.tgz", + "integrity": "sha512-C30Un9+63J0CsR7Wka5quXKqYZsT6dcRQ2aOwGcSc3RiQ4HGWpTAHlCA+puNfw2jA/s11EsxA1nCXgZRuRKMQQ==", "dependencies": { - "compress-brotli": "^1.3.6", + "compress-brotli": "^1.3.8", "json-buffer": "3.0.1" } }, @@ -1767,7 +1767,7 @@ "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -1801,9 +1801,9 @@ } }, "node_modules/lru-cache": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.9.0.tgz", - "integrity": "sha512-lkcNMUKqdJk96TuIXUidxaPuEg5sJo/+ZyVE2BDFnuZGzwXem7d8582eG8vbu4todLfT14snP6iHriCHXXi5Rw==", + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", + "integrity": "sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==", "engines": { "node": ">=12" } @@ -1926,7 +1926,7 @@ "node_modules/mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", "optional": true, "dependencies": { "mkdirp": "~0.5.1", @@ -1940,7 +1940,7 @@ "node_modules/mv/node_modules/glob": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", "optional": true, "dependencies": { "inflight": "^1.0.4", @@ -1966,21 +1966,21 @@ } }, "node_modules/nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", "optional": true }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "node_modules/ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", "optional": true, "bin": { "ncp": "bin/ncp" @@ -2040,7 +2040,7 @@ "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dependencies": { "wrappy": "1" } @@ -2138,7 +2138,7 @@ "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "engines": { "node": ">=0.10.0" } @@ -2159,7 +2159,7 @@ "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -2202,9 +2202,9 @@ } }, "node_modules/protobufjs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", + "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -2422,9 +2422,9 @@ } }, "node_modules/rfc4648": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.1.tgz", - "integrity": "sha512-60e/YWs2/D3MV1ErdjhJHcmlgnyLUiG4X/14dgsfm9/zmCWLN16xI6YqJYSCd/OANM7bUNzJqPY5B8/02S9Ibw==" + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.2.tgz", + "integrity": "sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==" }, "node_modules/rimraf": { "version": "3.0.2", @@ -2811,9 +2811,9 @@ } }, "node_modules/uglify-js": { - "version": "3.15.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.4.tgz", - "integrity": "sha512-vMOPGDuvXecPs34V74qDKk4iJ/SN4vL3Ow/23ixafENYvtrNvtbcgUeugTcUGRGsOF/5fU8/NYSL5Hyb3l1OJA==", + "version": "3.15.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.5.tgz", + "integrity": "sha512-hNM5q5GbBRB5xB+PMqVRcgYe4c8jbyZ1pzZhS6jbq54/4F2gFK869ZheiE5A8/t+W5jtTNpWef/5Q9zk639FNQ==", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -2823,9 +2823,9 @@ } }, "node_modules/underscore": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.3.tgz", - "integrity": "sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==" + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", + "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==" }, "node_modules/universalify": { "version": "2.0.0", @@ -2958,9 +2958,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", + "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", "engines": { "node": ">=8.3.0" }, @@ -2991,9 +2991,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { - "version": "17.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", - "integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==", + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -3033,19 +3033,19 @@ } }, "@eslint/eslintrc": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.2.tgz", - "integrity": "sha512-lTVWHs7O2hjBFZunXTZYnYqtB9GakA1lnxIf+gKq2nY5gxkkNi/lQvveW6t8gFdOHTg6nG50Xs95PrLqVpcaLg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.3.1", - "globals": "^13.9.0", + "espree": "^9.3.2", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, @@ -3152,7 +3152,7 @@ "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" }, "@protobufjs/base64": { "version": "1.1.2", @@ -3167,12 +3167,12 @@ "@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" }, "@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", "requires": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -3181,27 +3181,27 @@ "@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" }, "@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" }, "@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" }, "@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" }, "@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "@sindresorhus/is": { "version": "4.6.0", @@ -3269,9 +3269,9 @@ } }, "@types/node": { - "version": "17.0.31", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.31.tgz", - "integrity": "sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==" + "version": "17.0.40", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.40.tgz", + "integrity": "sha512-UXdBxNGqTMtm7hCwh9HtncFVLrXoqA3oJW30j6XWp5BH/wu3mVeaxo7cq5benFdBw34HB3XDT2TRPI7rXZ+mDg==" }, "@types/request": { "version": "2.48.8", @@ -3389,7 +3389,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" }, "async": { "version": "3.2.3", @@ -3414,12 +3414,12 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" }, "aws4": { "version": "1.11.0", @@ -3455,7 +3455,7 @@ "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "requires": { "tweetnacl": "^0.14.3" } @@ -3489,7 +3489,7 @@ "byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==" }, "cacheable-lookup": { "version": "5.0.4", @@ -3529,7 +3529,7 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, "chalk": { "version": "4.1.2", @@ -3564,7 +3564,7 @@ "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", "requires": { "mimic-response": "^1.0.0" } @@ -3589,7 +3589,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" } } }, @@ -3618,7 +3618,7 @@ "colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", - "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" + "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==" }, "colorspace": { "version": "1.1.4", @@ -3649,12 +3649,12 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "cpu-features": { "version": "0.0.4", @@ -3679,12 +3679,12 @@ "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=" + "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==" }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "requires": { "assert-plus": "^1.0.0" } @@ -3727,7 +3727,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "doctrine": { "version": "3.0.0", @@ -3750,7 +3750,7 @@ "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -3786,12 +3786,12 @@ "dev": true }, "eslint": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.14.0.tgz", - "integrity": "sha512-3/CE4aJX7LNEiE3i6FeodHmI/38GZtWCsAtsymScmzYapx8q1nVVb+eLcLSzATmCPXw5pT4TqVs1E0OmxAd9tw==", + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.17.0.tgz", + "integrity": "sha512-gq0m0BTJfci60Fz4nczYxNAlED+sMcihltndR8t9t1evnU/azx53x3t2UHXC/uRjcbvRw/XctpaNygSTcQD+Iw==", "dev": true, "requires": { - "@eslint/eslintrc": "^1.2.2", + "@eslint/eslintrc": "^1.3.0", "@humanwhocodes/config-array": "^0.9.2", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -3802,14 +3802,14 @@ "eslint-scope": "^7.1.1", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.3.0", - "espree": "^9.3.1", + "espree": "^9.3.2", "esquery": "^1.4.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", - "globals": "^13.6.0", + "globals": "^13.15.0", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", @@ -3818,7 +3818,7 @@ "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.1", "regexpp": "^3.2.0", @@ -3862,13 +3862,13 @@ "dev": true }, "espree": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz", - "integrity": "sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", "dev": true, "requires": { - "acorn": "^8.7.0", - "acorn-jsx": "^5.3.1", + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.3.0" } }, @@ -3926,12 +3926,12 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" }, "eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" + "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==" }, "fast-deep-equal": { "version": "3.1.3", @@ -3946,7 +3946,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, "fecha": { @@ -3985,14 +3985,14 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "follow-redirects": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.0.tgz", - "integrity": "sha512-aExlJShTV4qOUOL7yF1U5tvLCB0xQuudbf6toyYA0E/acBNw71mvjFTnLaRp50aQaYocMR0a/RMMBIHeZnGyjQ==" + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" }, "form-data": { "version": "2.5.1", @@ -4025,7 +4025,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "function-bind": { "version": "1.1.1", @@ -4035,7 +4035,7 @@ "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, "get-caller-file": { @@ -4051,20 +4051,20 @@ "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "requires": { "assert-plus": "^1.0.0" } }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } @@ -4079,18 +4079,18 @@ } }, "globals": { - "version": "13.13.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.13.0.tgz", - "integrity": "sha512-EQ7Q18AJlPwp3vUDL4mKA0KXrXyNIQyWon6T6XQiBQF0XHvRsiCSrWmmeATpUzdJN2HhWZU6Pdl0a9zdep5p6A==", + "version": "13.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.15.0.tgz", + "integrity": "sha512-bpzcOlgDhMG070Av0Vy5Owklpv1I6+j96GhUI7Rh7IzDCKLzboflLrrfqMu8NquDbiR4EOQk7XzJwqVJxicxog==", "dev": true, "requires": { "type-fest": "^0.20.2" } }, "got": { - "version": "11.8.3", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.3.tgz", - "integrity": "sha512-7gtQ5KiPh1RtGS9/Jbv1ofDpBFuq42gyfEib+ejaRBJuj/3tQFeR5+gw57e4ipaU8c/rCjvX6fkQz2lyDlGAOg==", + "version": "11.8.5", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.5.tgz", + "integrity": "sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==", "requires": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", @@ -4125,7 +4125,7 @@ "har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" }, "har-validator": { "version": "5.1.5", @@ -4158,7 +4158,7 @@ "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "requires": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -4198,7 +4198,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, "indent-string": { @@ -4209,7 +4209,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "requires": { "once": "^1.3.0", "wrappy": "1" @@ -4241,7 +4241,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, "is-fullwidth-code-point": { @@ -4266,12 +4266,12 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "isomorphic-ws": { "version": "4.0.1", @@ -4282,7 +4282,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "jose": { "version": "2.0.5", @@ -4303,7 +4303,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "json-buffer": { "version": "3.0.1", @@ -4323,13 +4323,13 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "jsonfile": { "version": "6.1.0", @@ -4357,11 +4357,11 @@ } }, "keyv": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.2.2.tgz", - "integrity": "sha512-uYS0vKTlBIjNCAUqrjlxmruxOEiZxZIHXyp32sdcGmP+ukFrmWUnE//RcPXJH3Vxrni1H2gsQbjHE0bH7MtMQQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.3.0.tgz", + "integrity": "sha512-C30Un9+63J0CsR7Wka5quXKqYZsT6dcRQ2aOwGcSc3RiQ4HGWpTAHlCA+puNfw2jA/s11EsxA1nCXgZRuRKMQQ==", "requires": { - "compress-brotli": "^1.3.6", + "compress-brotli": "^1.3.8", "json-buffer": "3.0.1" } }, @@ -4388,7 +4388,7 @@ "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=" + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" }, "lodash.merge": { "version": "4.6.2", @@ -4419,9 +4419,9 @@ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, "lru-cache": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.9.0.tgz", - "integrity": "sha512-lkcNMUKqdJk96TuIXUidxaPuEg5sJo/+ZyVE2BDFnuZGzwXem7d8582eG8vbu4todLfT14snP6iHriCHXXi5Rw==" + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.10.1.tgz", + "integrity": "sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==" }, "make-error": { "version": "1.3.6", @@ -4514,7 +4514,7 @@ "mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", + "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", "optional": true, "requires": { "mkdirp": "~0.5.1", @@ -4525,7 +4525,7 @@ "glob": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", "optional": true, "requires": { "inflight": "^1.0.4", @@ -4547,21 +4547,21 @@ } }, "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.16.0.tgz", + "integrity": "sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==", "optional": true }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", + "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", "optional": true }, "neo-async": { @@ -4600,7 +4600,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "requires": { "wrappy": "1" } @@ -4676,7 +4676,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "path-key": { "version": "3.1.1", @@ -4691,7 +4691,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "prelude-ls": { "version": "1.2.1", @@ -4727,9 +4727,9 @@ } }, "protobufjs": { - "version": "6.11.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.2.tgz", - "integrity": "sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw==", + "version": "6.11.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", + "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", "requires": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -4891,9 +4891,9 @@ "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=" }, "rfc4648": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.1.tgz", - "integrity": "sha512-60e/YWs2/D3MV1ErdjhJHcmlgnyLUiG4X/14dgsfm9/zmCWLN16xI6YqJYSCd/OANM7bUNzJqPY5B8/02S9Ibw==" + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.2.tgz", + "integrity": "sha512-tLOizhR6YGovrEBLatX1sdcuhoSCXddw3mqNVAcKxGJ+J0hFeJ+SjeWCv5UPA/WU3YzWPPuCVYgXBKZUPGpKtg==" }, "rimraf": { "version": "3.0.2", @@ -5166,15 +5166,15 @@ "dev": true }, "uglify-js": { - "version": "3.15.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.4.tgz", - "integrity": "sha512-vMOPGDuvXecPs34V74qDKk4iJ/SN4vL3Ow/23ixafENYvtrNvtbcgUeugTcUGRGsOF/5fU8/NYSL5Hyb3l1OJA==", + "version": "3.15.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.5.tgz", + "integrity": "sha512-hNM5q5GbBRB5xB+PMqVRcgYe4c8jbyZ1pzZhS6jbq54/4F2gFK869ZheiE5A8/t+W5jtTNpWef/5Q9zk639FNQ==", "optional": true }, "underscore": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.3.tgz", - "integrity": "sha512-QvjkYpiD+dJJraRA8+dGAU4i7aBbb2s0S3jA45TFOvg2VgqvdCDd/3N6CqA8gluk1W91GLoXg5enMUx560QzuA==" + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.4.tgz", + "integrity": "sha512-BQFnUDuAQ4Yf/cYY5LNrK9NCJFKriaRbD9uR1fTeXnBeoa97W0i41qkZfGO9pSo8I5KzjAcSY2XYtdf0oKd7KQ==" }, "universalify": { "version": "2.0.0", @@ -5277,9 +5277,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.7.tgz", - "integrity": "sha512-KMvVuFzpKBuiIXW3E4u3mySRO2/mCHSyZDJQM5NQ9Q9KHWHWh0NHgfbRMLLrceUK5qAL4ytALJbpRMjixFZh8A==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", + "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", "requires": {} }, "y18n": { @@ -5293,9 +5293,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "17.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.4.1.tgz", - "integrity": "sha512-WSZD9jgobAg3ZKuCQZSa3g9QOJeCCqLoLAykiWgmXnDo9EPnn4RPf5qVTtzgOx66o6/oqhcA5tHtJXpG8pMt3g==", + "version": "17.5.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", + "integrity": "sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==", "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 9281b2d..3dd86aa 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -2107,6 +2107,7 @@ class FreeNASApiDriver extends CsiBaseDriver { */ async Probe(call) { const driver = this; + const httpApiClient = await driver.getTrueNASHttpApiClient(); if (driver.ctx.args.csiMode.includes("controller")) { let datasetParentName = this.getVolumeParentDatasetName() + "/"; @@ -2121,6 +2122,14 @@ class FreeNASApiDriver extends CsiBaseDriver { `datasetParentName and detachedSnapshotsDatasetParentName must not overlap` ); } + + if (!(await httpApiClient.getIsScale())) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `driver is only availalbe with TrueNAS SCALE` + ); + } + return { ready: { value: true } }; } else { return { ready: { value: true } }; diff --git a/src/utils/zfs.js b/src/utils/zfs.js index f682eb8..a26db89 100644 --- a/src/utils/zfs.js +++ b/src/utils/zfs.js @@ -1147,14 +1147,23 @@ class Zetabyte { if (arguments.length < 4) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { + // specially handle sudo here to avoid the need for using sudo on the whole script + // but rather limit sudo access to only the zfs command + let use_sudo = zb.options.sudo; let args = ["-c"]; let command = []; + if (use_sudo) { + command = command.concat(zb.options.paths.sudo); + } command = command.concat(["zfs", "send"]); command = command.concat(send_options); command.push(source); command.push("|"); + if (use_sudo) { + command = command.concat(zb.options.paths.sudo); + } command = command.concat(["zfs", "receive"]); command = command.concat(receive_options); command.push(target); @@ -1164,7 +1173,7 @@ class Zetabyte { zb.exec( "/bin/sh", args, - { timeout: zb.options.timeout }, + { timeout: zb.options.timeout, sudo: false }, function (error, stdout, stderr) { if (error) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); @@ -1550,7 +1559,12 @@ class Zetabyte { command = zb.options.paths.chroot; } - if (zb.options.sudo) { + let use_sudo = zb.options.sudo; + if (options && options.hasOwnProperty("sudo")) { + use_sudo = options.sudo; + } + + if (use_sudo) { args = args || []; args.unshift(command); command = zb.options.paths.sudo; @@ -1558,11 +1572,13 @@ class Zetabyte { if (zb.options.log_commands) { if (typeof zb.options.logger.verbose != "function") { - zb.options.logger.verbose = function() { + zb.options.logger.verbose = function () { console.debug(...arguments); - } + }; } - zb.options.logger.verbose(`executing zfs command: ${command} ${args.join(" ")}`); + zb.options.logger.verbose( + `executing zfs command: ${command} ${args.join(" ")}` + ); } const child = zb.options.executor.spawn(command, args, options); From f0a22331993369181991e090faa216bf30443be8 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 6 Jun 2022 11:17:10 -0600 Subject: [PATCH 69/71] prep for 1.7.0 Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 8 ++++---- CHANGELOG.md | 9 ++++++--- Dockerfile | 2 +- Dockerfile.Windows | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f143fc0..fafd564 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -163,8 +163,8 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-truenas - #- csi-sanity-zfs-generic + #- csi-sanity-truenas + - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -196,8 +196,8 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-truenas - #- csi-sanity-zfs-generic + #- csi-sanity-truenas + - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b59824..206de41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,12 @@ # v1.7.0 -Released 2022-06-05 +Released 2022-06-06 The windows release. -- windows smb, iscsi, and local-hostpath support +- windows smb, iscsi, and local-hostpath support (requires chart `v0.13.0+`) - ntfs, exfat, vfat fs support -- zfs-generic-smb driver +- `zfs-generic-smb` driver - synology improvements - DSM7 support - synology enhancements to allow templates to be configured at various @@ -20,10 +20,13 @@ The windows release. - general improvement to smb behavior throughout - better logging - better sudo logic throughout + - minor fixes throughout - more robust logic for connecting to iscsi devices with partition tables - massive performance improvement for ssh-based drivers (reusing existing connection instead of new connection per-command) - dep bumps +- trimmed container images +- windows container images for 2019 and 2022 # v1.6.3 diff --git a/Dockerfile b/Dockerfile index 661fa9f..f5abf73 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,7 +12,7 @@ RUN apt-get update && apt-get install -y locales && rm -rf /var/lib/apt/lists/* && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 ENV LANG=en_US.utf8 -ENV NODE_VERSION=v16.15.0 +ENV NODE_VERSION=v16.15.1 ENV NODE_ENV=production # install build deps diff --git a/Dockerfile.Windows b/Dockerfile.Windows index 847225d..2d7436c 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -57,7 +57,7 @@ RUN @( \ gpg --keyserver hkps://keys.openpgp.org --recv-keys $_ ; \ } -ENV NODE_VERSION 16.15.0 +ENV NODE_VERSION 16.15.1 RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/SHASUMS256.txt.asc' -f $env:NODE_VERSION) -OutFile 'SHASUMS256.txt.asc' -UseBasicParsing ; #RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/SHASUMS256.txt.asc' -f $env:NODE_VERSION) -OutFile 'SHASUMS256.txt.asc' -UseBasicParsing ; \ From c1069d57646de1053d51d8f5da088fe499adf64d Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 6 Jun 2022 14:43:47 -0600 Subject: [PATCH 70/71] use ix ci nodes Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fafd564..f143fc0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -163,8 +163,8 @@ jobs: - self-hosted - Linux - X64 - #- csi-sanity-truenas - - csi-sanity-zfs-generic + - csi-sanity-truenas + #- csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -196,8 +196,8 @@ jobs: - self-hosted - Linux - X64 - #- csi-sanity-truenas - - csi-sanity-zfs-generic + - csi-sanity-truenas + #- csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 From c83275bb988c71470494870f0717c558dfe0d0a7 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 8 Jun 2022 12:27:57 -0600 Subject: [PATCH 71/71] v1.7.0 Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 8 ++--- CHANGELOG.md | 2 +- package-lock.json | 60 +++++++++++++++++++------------------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f143fc0..fafd564 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -163,8 +163,8 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-truenas - #- csi-sanity-zfs-generic + #- csi-sanity-truenas + - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 @@ -196,8 +196,8 @@ jobs: - self-hosted - Linux - X64 - - csi-sanity-truenas - #- csi-sanity-zfs-generic + #- csi-sanity-truenas + - csi-sanity-zfs-generic steps: - uses: actions/checkout@v2 - uses: actions/download-artifact@v2 diff --git a/CHANGELOG.md b/CHANGELOG.md index 206de41..a0d3b85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # v1.7.0 -Released 2022-06-06 +Released 2022-06-08 The windows release. diff --git a/package-lock.json b/package-lock.json index 03d205e..a6d0980 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,14 +83,14 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.12.tgz", - "integrity": "sha512-filTVbETFnxb9CyRX98zN18ilChTuf/C5scZ2xyaOTp0EHGq0/ufX8rjqXUcSb1Gpv7eZq4M2jDvbh9BogKnrg==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", "dependencies": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", "long": "^4.0.0", - "protobufjs": "^6.10.0", + "protobufjs": "^6.11.3", "yargs": "^16.2.0" }, "bin": { @@ -316,9 +316,9 @@ } }, "node_modules/@types/node": { - "version": "17.0.40", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.40.tgz", - "integrity": "sha512-UXdBxNGqTMtm7hCwh9HtncFVLrXoqA3oJW30j6XWp5BH/wu3mVeaxo7cq5benFdBw34HB3XDT2TRPI7rXZ+mDg==" + "version": "17.0.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz", + "integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==" }, "node_modules/@types/request": { "version": "2.48.8", @@ -1956,7 +1956,7 @@ "node_modules/mv/node_modules/rimraf": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", "optional": true, "dependencies": { "glob": "^6.0.1" @@ -2270,7 +2270,7 @@ "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "dependencies": { "mute-stream": "~0.0.4" }, @@ -2294,7 +2294,7 @@ "node_modules/rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "dependencies": { "resolve": "^1.1.6" }, @@ -2370,7 +2370,7 @@ "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "engines": { "node": ">=0.10.0" } @@ -2416,7 +2416,7 @@ "node_modules/revalidator": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", - "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=", + "integrity": "sha512-xcBILK2pA9oh4SiinPEZfhP8HfrB/ha+a2fTMyl7Om2WjlDVrOQy99N2MXXlUHqGJz4qEu2duXxHJjDWuK/0xg==", "engines": { "node": ">= 0.4.0" } @@ -2811,9 +2811,9 @@ } }, "node_modules/uglify-js": { - "version": "3.15.5", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.5.tgz", - "integrity": "sha512-hNM5q5GbBRB5xB+PMqVRcgYe4c8jbyZ1pzZhS6jbq54/4F2gFK869ZheiE5A8/t+W5jtTNpWef/5Q9zk639FNQ==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.0.tgz", + "integrity": "sha512-FEikl6bR30n0T3amyBh3LoiBdqHRy/f4H80+My34HOesOKyHfOsxAPAxOoqC0JUnC1amnO0IwkYC3sko51caSw==", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -3059,14 +3059,14 @@ } }, "@grpc/proto-loader": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.12.tgz", - "integrity": "sha512-filTVbETFnxb9CyRX98zN18ilChTuf/C5scZ2xyaOTp0EHGq0/ufX8rjqXUcSb1Gpv7eZq4M2jDvbh9BogKnrg==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", + "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", "requires": { "@types/long": "^4.0.1", "lodash.camelcase": "^4.3.0", "long": "^4.0.0", - "protobufjs": "^6.10.0", + "protobufjs": "^6.11.3", "yargs": "^16.2.0" }, "dependencies": { @@ -3269,9 +3269,9 @@ } }, "@types/node": { - "version": "17.0.40", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.40.tgz", - "integrity": "sha512-UXdBxNGqTMtm7hCwh9HtncFVLrXoqA3oJW30j6XWp5BH/wu3mVeaxo7cq5benFdBw34HB3XDT2TRPI7rXZ+mDg==" + "version": "17.0.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz", + "integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==" }, "@types/request": { "version": "2.48.8", @@ -4538,7 +4538,7 @@ "rimraf": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", + "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", "optional": true, "requires": { "glob": "^6.0.1" @@ -4778,7 +4778,7 @@ "read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", "requires": { "mute-stream": "~0.0.4" } @@ -4796,7 +4796,7 @@ "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "requires": { "resolve": "^1.1.6" } @@ -4854,7 +4854,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" }, "resolve": { "version": "1.22.0", @@ -4888,7 +4888,7 @@ "revalidator": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", - "integrity": "sha1-/s5hv6DBtSoga9axgZgYS91SOjs=" + "integrity": "sha512-xcBILK2pA9oh4SiinPEZfhP8HfrB/ha+a2fTMyl7Om2WjlDVrOQy99N2MXXlUHqGJz4qEu2duXxHJjDWuK/0xg==" }, "rfc4648": { "version": "1.5.2", @@ -5166,9 +5166,9 @@ "dev": true }, "uglify-js": { - "version": "3.15.5", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.15.5.tgz", - "integrity": "sha512-hNM5q5GbBRB5xB+PMqVRcgYe4c8jbyZ1pzZhS6jbq54/4F2gFK869ZheiE5A8/t+W5jtTNpWef/5Q9zk639FNQ==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.16.0.tgz", + "integrity": "sha512-FEikl6bR30n0T3amyBh3LoiBdqHRy/f4H80+My34HOesOKyHfOsxAPAxOoqC0JUnC1amnO0IwkYC3sko51caSw==", "optional": true }, "underscore": {