From b52e84778ed3ec1cf3a198433efcaebbe0048c85 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 21 Jan 2025 00:18:46 -0700 Subject: [PATCH 01/55] remove old TN SCALE versions, update ci to use 24.10 Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 14 +++--- .../truenas/scale/22.02/scale-iscsi.yaml | 31 ------------ ci/configs/truenas/scale/22.02/scale-smb.yaml | 50 ------------------- .../truenas/scale/22.12/scale-iscsi.yaml | 38 -------------- ci/configs/truenas/scale/22.12/scale-nfs.yaml | 29 ----------- ci/configs/truenas/scale/23.10/scale-nfs.yaml | 29 ----------- ci/configs/truenas/scale/23.10/scale-smb.yaml | 50 ------------------- .../truenas/scale/24.04/scale-iscsi.yaml | 38 -------------- ci/configs/truenas/scale/24.04/scale-nfs.yaml | 29 ----------- ci/configs/truenas/scale/24.04/scale-smb.yaml | 50 ------------------- .../scale/{23.10 => 24.10}/scale-iscsi.yaml | 2 +- .../scale/{22.02 => 24.10}/scale-nfs.yaml | 0 .../scale/{22.12 => 24.10}/scale-smb.yaml | 10 ++-- 13 files changed, 13 insertions(+), 357 deletions(-) delete mode 100644 ci/configs/truenas/scale/22.02/scale-iscsi.yaml delete mode 100644 ci/configs/truenas/scale/22.02/scale-smb.yaml delete mode 100644 ci/configs/truenas/scale/22.12/scale-iscsi.yaml delete mode 100644 ci/configs/truenas/scale/22.12/scale-nfs.yaml delete mode 100644 ci/configs/truenas/scale/23.10/scale-nfs.yaml delete mode 100644 ci/configs/truenas/scale/23.10/scale-smb.yaml delete mode 100644 ci/configs/truenas/scale/24.04/scale-iscsi.yaml delete mode 100644 ci/configs/truenas/scale/24.04/scale-nfs.yaml delete mode 100644 ci/configs/truenas/scale/24.04/scale-smb.yaml rename ci/configs/truenas/scale/{23.10 => 24.10}/scale-iscsi.yaml (98%) rename ci/configs/truenas/scale/{22.02 => 24.10}/scale-nfs.yaml (100%) rename ci/configs/truenas/scale/{22.12 => 24.10}/scale-smb.yaml (89%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c5d4f0c..47b6c73 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -115,7 +115,7 @@ jobs: SYNOLOGY_PASSWORD: ${{ secrets.SANITY_SYNOLOGY_PASSWORD }} SYNOLOGY_VOLUME: ${{ secrets.SANITY_SYNOLOGY_VOLUME }} - csi-sanity-truenas-scale-24_04: + csi-sanity-truenas-scale-24_10: needs: - build-npm-linux-amd64 strategy: @@ -123,10 +123,10 @@ jobs: max-parallel: 1 matrix: config: - - truenas/scale/24.04/scale-iscsi.yaml - - truenas/scale/24.04/scale-nfs.yaml + - truenas/scale/24.10/scale-iscsi.yaml + - truenas/scale/24.10/scale-nfs.yaml # 80 char limit - - truenas/scale/24.04/scale-smb.yaml + - truenas/scale/24.10/scale-smb.yaml runs-on: - self-hosted - Linux @@ -144,7 +144,7 @@ jobs: ci/bin/run.sh env: TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" - TRUENAS_HOST: ${{ secrets.SANITY_TRUENAS_SCALE_24_04_HOST }} + TRUENAS_HOST: ${{ secrets.SANITY_TRUENAS_SCALE_HOST }} TRUENAS_USERNAME: ${{ secrets.SANITY_TRUENAS_USERNAME }} TRUENAS_PASSWORD: ${{ secrets.SANITY_TRUENAS_PASSWORD }} @@ -435,7 +435,7 @@ jobs: - determine-image-tag - csi-sanity-synology-dsm6 - csi-sanity-synology-dsm7 - - csi-sanity-truenas-scale-24_04 + - csi-sanity-truenas-scale-24_10 - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic - csi-sanity-objectivefs @@ -475,7 +475,7 @@ jobs: needs: - csi-sanity-synology-dsm6 - csi-sanity-synology-dsm7 - - csi-sanity-truenas-scale-24_04 + - csi-sanity-truenas-scale-24_10 - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic - csi-sanity-objectivefs diff --git a/ci/configs/truenas/scale/22.02/scale-iscsi.yaml b/ci/configs/truenas/scale/22.02/scale-iscsi.yaml deleted file mode 100644 index b6b6f43..0000000 --- a/ci/configs/truenas/scale/22.02/scale-iscsi.yaml +++ /dev/null @@ -1,31 +0,0 @@ -driver: freenas-api-iscsi - -httpConnection: - protocol: http - host: ${TRUENAS_HOST} - port: 80 - #apiKey: - username: ${TRUENAS_USERNAME} - password: ${TRUENAS_PASSWORD} - -zfs: - datasetParentName: tank/ci/${CI_BUILD_KEY}/v - detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s - - zvolCompression: - zvolDedup: - zvolEnableReservation: false - zvolBlocksize: - -iscsi: - targetPortal: ${TRUENAS_HOST} - interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}-" - nameSuffix: "" - targetGroups: - - targetGroupPortalGroup: 1 - targetGroupInitiatorGroup: 1 - targetGroupAuthType: None - targetGroupAuthGroup: - # 0-100 (0 == ignore) - extentAvailThreshold: 0 diff --git a/ci/configs/truenas/scale/22.02/scale-smb.yaml b/ci/configs/truenas/scale/22.02/scale-smb.yaml deleted file mode 100644 index 2a8861e..0000000 --- a/ci/configs/truenas/scale/22.02/scale-smb.yaml +++ /dev/null @@ -1,50 +0,0 @@ -driver: freenas-api-smb - -httpConnection: - protocol: http - host: ${TRUENAS_HOST} - port: 80 - #apiKey: - username: ${TRUENAS_USERNAME} - password: ${TRUENAS_PASSWORD} - -zfs: - datasetParentName: tank/ci/${CI_BUILD_KEY}/v - detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s - - datasetEnableQuotas: true - datasetEnableReservation: false - datasetPermissionsMode: "0770" - datasetPermissionsUser: 1001 - datasetPermissionsGroup: 1001 - -smb: - shareHost: ${TRUENAS_HOST} - #nameTemplate: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}-" - nameSuffix: "" - shareAuxiliaryConfigurationTemplate: | - #guest ok = yes - #guest only = yes - shareHome: false - shareAllowedHosts: [] - shareDeniedHosts: [] - #shareDefaultPermissions: true - shareGuestOk: false - #shareGuestOnly: true - #shareShowHiddenFiles: true - shareRecycleBin: false - shareBrowsable: false - 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.12/scale-iscsi.yaml b/ci/configs/truenas/scale/22.12/scale-iscsi.yaml deleted file mode 100644 index a2f7d04..0000000 --- a/ci/configs/truenas/scale/22.12/scale-iscsi.yaml +++ /dev/null @@ -1,38 +0,0 @@ -driver: freenas-api-iscsi - -httpConnection: - protocol: http - host: ${TRUENAS_HOST} - port: 80 - #apiKey: - username: ${TRUENAS_USERNAME} - password: ${TRUENAS_PASSWORD} - -zfs: - datasetParentName: tank/ci/${CI_BUILD_KEY}/v - detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s - - zvolCompression: - zvolDedup: - zvolEnableReservation: false - zvolBlocksize: - -iscsi: - targetPortal: ${TRUENAS_HOST} - interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}-" - nameSuffix: "" - targetGroups: - - targetGroupPortalGroup: 1 - targetGroupInitiatorGroup: 1 - targetGroupAuthType: None - targetGroupAuthGroup: - # 0-100 (0 == ignore) - extentAvailThreshold: 0 - -# https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 -_private: - csi: - volume: - idHash: - strategy: crc16 diff --git a/ci/configs/truenas/scale/22.12/scale-nfs.yaml b/ci/configs/truenas/scale/22.12/scale-nfs.yaml deleted file mode 100644 index 42818ae..0000000 --- a/ci/configs/truenas/scale/22.12/scale-nfs.yaml +++ /dev/null @@ -1,29 +0,0 @@ -driver: freenas-api-nfs - -httpConnection: - protocol: http - host: ${TRUENAS_HOST} - port: 80 - #apiKey: - username: ${TRUENAS_USERNAME} - password: ${TRUENAS_PASSWORD} - -zfs: - datasetParentName: tank/ci/${CI_BUILD_KEY}/v - detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s - - datasetEnableQuotas: true - datasetEnableReservation: false - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 - -nfs: - shareHost: ${TRUENAS_HOST} - shareAlldirs: false - shareAllowedHosts: [] - shareAllowedNetworks: [] - shareMaprootUser: root - shareMaprootGroup: root - shareMapallUser: "" - shareMapallGroup: "" diff --git a/ci/configs/truenas/scale/23.10/scale-nfs.yaml b/ci/configs/truenas/scale/23.10/scale-nfs.yaml deleted file mode 100644 index 42818ae..0000000 --- a/ci/configs/truenas/scale/23.10/scale-nfs.yaml +++ /dev/null @@ -1,29 +0,0 @@ -driver: freenas-api-nfs - -httpConnection: - protocol: http - host: ${TRUENAS_HOST} - port: 80 - #apiKey: - username: ${TRUENAS_USERNAME} - password: ${TRUENAS_PASSWORD} - -zfs: - datasetParentName: tank/ci/${CI_BUILD_KEY}/v - detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s - - datasetEnableQuotas: true - datasetEnableReservation: false - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 - -nfs: - shareHost: ${TRUENAS_HOST} - shareAlldirs: false - shareAllowedHosts: [] - shareAllowedNetworks: [] - shareMaprootUser: root - shareMaprootGroup: root - shareMapallUser: "" - shareMapallGroup: "" diff --git a/ci/configs/truenas/scale/23.10/scale-smb.yaml b/ci/configs/truenas/scale/23.10/scale-smb.yaml deleted file mode 100644 index 2a8861e..0000000 --- a/ci/configs/truenas/scale/23.10/scale-smb.yaml +++ /dev/null @@ -1,50 +0,0 @@ -driver: freenas-api-smb - -httpConnection: - protocol: http - host: ${TRUENAS_HOST} - port: 80 - #apiKey: - username: ${TRUENAS_USERNAME} - password: ${TRUENAS_PASSWORD} - -zfs: - datasetParentName: tank/ci/${CI_BUILD_KEY}/v - detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s - - datasetEnableQuotas: true - datasetEnableReservation: false - datasetPermissionsMode: "0770" - datasetPermissionsUser: 1001 - datasetPermissionsGroup: 1001 - -smb: - shareHost: ${TRUENAS_HOST} - #nameTemplate: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}-" - nameSuffix: "" - shareAuxiliaryConfigurationTemplate: | - #guest ok = yes - #guest only = yes - shareHome: false - shareAllowedHosts: [] - shareDeniedHosts: [] - #shareDefaultPermissions: true - shareGuestOk: false - #shareGuestOnly: true - #shareShowHiddenFiles: true - shareRecycleBin: false - shareBrowsable: false - 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/24.04/scale-iscsi.yaml b/ci/configs/truenas/scale/24.04/scale-iscsi.yaml deleted file mode 100644 index a2f7d04..0000000 --- a/ci/configs/truenas/scale/24.04/scale-iscsi.yaml +++ /dev/null @@ -1,38 +0,0 @@ -driver: freenas-api-iscsi - -httpConnection: - protocol: http - host: ${TRUENAS_HOST} - port: 80 - #apiKey: - username: ${TRUENAS_USERNAME} - password: ${TRUENAS_PASSWORD} - -zfs: - datasetParentName: tank/ci/${CI_BUILD_KEY}/v - detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s - - zvolCompression: - zvolDedup: - zvolEnableReservation: false - zvolBlocksize: - -iscsi: - targetPortal: ${TRUENAS_HOST} - interface: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}-" - nameSuffix: "" - targetGroups: - - targetGroupPortalGroup: 1 - targetGroupInitiatorGroup: 1 - targetGroupAuthType: None - targetGroupAuthGroup: - # 0-100 (0 == ignore) - extentAvailThreshold: 0 - -# https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 -_private: - csi: - volume: - idHash: - strategy: crc16 diff --git a/ci/configs/truenas/scale/24.04/scale-nfs.yaml b/ci/configs/truenas/scale/24.04/scale-nfs.yaml deleted file mode 100644 index 42818ae..0000000 --- a/ci/configs/truenas/scale/24.04/scale-nfs.yaml +++ /dev/null @@ -1,29 +0,0 @@ -driver: freenas-api-nfs - -httpConnection: - protocol: http - host: ${TRUENAS_HOST} - port: 80 - #apiKey: - username: ${TRUENAS_USERNAME} - password: ${TRUENAS_PASSWORD} - -zfs: - datasetParentName: tank/ci/${CI_BUILD_KEY}/v - detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s - - datasetEnableQuotas: true - datasetEnableReservation: false - datasetPermissionsMode: "0777" - datasetPermissionsUser: 0 - datasetPermissionsGroup: 0 - -nfs: - shareHost: ${TRUENAS_HOST} - shareAlldirs: false - shareAllowedHosts: [] - shareAllowedNetworks: [] - shareMaprootUser: root - shareMaprootGroup: root - shareMapallUser: "" - shareMapallGroup: "" diff --git a/ci/configs/truenas/scale/24.04/scale-smb.yaml b/ci/configs/truenas/scale/24.04/scale-smb.yaml deleted file mode 100644 index 2a8861e..0000000 --- a/ci/configs/truenas/scale/24.04/scale-smb.yaml +++ /dev/null @@ -1,50 +0,0 @@ -driver: freenas-api-smb - -httpConnection: - protocol: http - host: ${TRUENAS_HOST} - port: 80 - #apiKey: - username: ${TRUENAS_USERNAME} - password: ${TRUENAS_PASSWORD} - -zfs: - datasetParentName: tank/ci/${CI_BUILD_KEY}/v - detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s - - datasetEnableQuotas: true - datasetEnableReservation: false - datasetPermissionsMode: "0770" - datasetPermissionsUser: 1001 - datasetPermissionsGroup: 1001 - -smb: - shareHost: ${TRUENAS_HOST} - #nameTemplate: "" - namePrefix: "csi-ci-${CI_BUILD_KEY}-" - nameSuffix: "" - shareAuxiliaryConfigurationTemplate: | - #guest ok = yes - #guest only = yes - shareHome: false - shareAllowedHosts: [] - shareDeniedHosts: [] - #shareDefaultPermissions: true - shareGuestOk: false - #shareGuestOnly: true - #shareShowHiddenFiles: true - shareRecycleBin: false - shareBrowsable: false - 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/23.10/scale-iscsi.yaml b/ci/configs/truenas/scale/24.10/scale-iscsi.yaml similarity index 98% rename from ci/configs/truenas/scale/23.10/scale-iscsi.yaml rename to ci/configs/truenas/scale/24.10/scale-iscsi.yaml index a2f7d04..316cda2 100644 --- a/ci/configs/truenas/scale/23.10/scale-iscsi.yaml +++ b/ci/configs/truenas/scale/24.10/scale-iscsi.yaml @@ -4,7 +4,7 @@ httpConnection: protocol: http host: ${TRUENAS_HOST} port: 80 - #apiKey: + #apiKey: username: ${TRUENAS_USERNAME} password: ${TRUENAS_PASSWORD} diff --git a/ci/configs/truenas/scale/22.02/scale-nfs.yaml b/ci/configs/truenas/scale/24.10/scale-nfs.yaml similarity index 100% rename from ci/configs/truenas/scale/22.02/scale-nfs.yaml rename to ci/configs/truenas/scale/24.10/scale-nfs.yaml diff --git a/ci/configs/truenas/scale/22.12/scale-smb.yaml b/ci/configs/truenas/scale/24.10/scale-smb.yaml similarity index 89% rename from ci/configs/truenas/scale/22.12/scale-smb.yaml rename to ci/configs/truenas/scale/24.10/scale-smb.yaml index 2a8861e..a9bc10a 100644 --- a/ci/configs/truenas/scale/22.12/scale-smb.yaml +++ b/ci/configs/truenas/scale/24.10/scale-smb.yaml @@ -4,7 +4,7 @@ httpConnection: protocol: http host: ${TRUENAS_HOST} port: 80 - #apiKey: + #apiKey: username: ${TRUENAS_USERNAME} password: ${TRUENAS_PASSWORD} @@ -14,10 +14,10 @@ zfs: datasetEnableQuotas: true datasetEnableReservation: false - datasetPermissionsMode: "0770" - datasetPermissionsUser: 1001 - datasetPermissionsGroup: 1001 - + #datasetPermissionsMode: "0770" + #datasetPermissionsUser: 1001 + #datasetPermissionsGroup: 1001 + smb: shareHost: ${TRUENAS_HOST} #nameTemplate: "" From 0ac7e331523b889b11188a3ea06d5e03979c26be Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 21 Jan 2025 00:20:33 -0700 Subject: [PATCH 02/55] better support for nixos Signed-off-by: Travis Glenn Hansen --- docker/iscsiadm | 2 +- docker/mount | 4 ++-- docker/multipath | 2 +- docker/oneclient | 2 +- docker/umount | 4 ++-- docker/zfs | 2 +- docker/zpool | 2 +- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docker/iscsiadm b/docker/iscsiadm index 8d44262..551a9f5 100755 --- a/docker/iscsiadm +++ b/docker/iscsiadm @@ -8,7 +8,7 @@ echoerr() { printf "%s\n" "$*" >&2; } case ${ISCSIADM_HOST_STRATEGY} in chroot) # https://engineering.docker.com/2019/07/road-to-containing-iscsi/ - chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" ${ISCSIADM_HOST_PATH} "${@:1}" + chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" ${ISCSIADM_HOST_PATH} "${@:1}" ;; nsenter) diff --git a/docker/mount b/docker/mount index fcfd59d..c0603e6 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="/usr/sbin:/usr/bin:/sbin:/bin" mount "${@:1}" + chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" mount "${@:1}" else - /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" mount "${@:1}" + /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" mount "${@:1}" fi diff --git a/docker/multipath b/docker/multipath index 3d1d6ee..ac097d4 100755 --- a/docker/multipath +++ b/docker/multipath @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" multipath "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" multipath "${@:1}" diff --git a/docker/oneclient b/docker/oneclient index 0815dee..373cefd 100755 --- a/docker/oneclient +++ b/docker/oneclient @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" oneclient "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" oneclient "${@:1}" diff --git a/docker/umount b/docker/umount index 48dd314..1bcd0f2 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="/usr/sbin:/usr/bin:/sbin:/bin" umount "${@:1}" + chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" umount "${@:1}" else - /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" umount "${@:1}" + /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" umount "${@:1}" fi diff --git a/docker/zfs b/docker/zfs index 4a1f79e..aceb182 100755 --- a/docker/zfs +++ b/docker/zfs @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" zfs "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" zfs "${@:1}" diff --git a/docker/zpool b/docker/zpool index 07241b2..a4696aa 100755 --- a/docker/zpool +++ b/docker/zpool @@ -1,3 +1,3 @@ #!/bin/bash -chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin" zpool "${@:1}" +chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" zpool "${@:1}" From 2605e5e0f4d8649d201606edd00e6fd22074c00f Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 21 Jan 2025 00:20:55 -0700 Subject: [PATCH 03/55] drop support for very old versions of TN, force version 2 of the api Signed-off-by: Travis Glenn Hansen --- src/driver/freenas/http/api.js | 63 +------------------------------- src/driver/freenas/http/index.js | 6 +-- src/utils/mount.js | 12 +++++- 3 files changed, 13 insertions(+), 68 deletions(-) diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index b9f0057..7bfcda9 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -110,54 +110,15 @@ class Api { } async getApiVersion() { - const systemVersion = await this.getSystemVersion(); - - if (systemVersion.v2) { - if ((await this.getSystemVersionMajorMinor()) == 11.2) { - return 1; - } - return 2; - } - - if (systemVersion.v1) { - return 1; - } - return 2; } async getIsFreeNAS() { - const systemVersion = await this.getSystemVersion(); - let version; - - if (systemVersion.v2) { - version = systemVersion.v2; - } else { - version = systemVersion.v1.fullversion; - } - - if (version.toLowerCase().includes("freenas")) { - return true; - } - return false; } async getIsTrueNAS() { - const systemVersion = await this.getSystemVersion(); - let version; - - if (systemVersion.v2) { - version = systemVersion.v2; - } else { - version = systemVersion.v1.fullversion; - } - - if (version.toLowerCase().includes("truenas")) { - return true; - } - - return false; + return true; } async getIsScale() { @@ -261,28 +222,6 @@ class Api { versionErrors.v2 = e.toString(); } - httpClient.setApiVersion(1); - /** - * {"fullversion": "FreeNAS-9.3-STABLE-201503200528", "name": "FreeNAS", "version": "9.3"} - * {"fullversion": "FreeNAS-11.2-U5 (c129415c52)", "name": "FreeNAS", "version": ""} - */ - try { - response = await httpClient.get(endpoint, null, { timeout: 5 * 1000 }); - versionResponses.v1 = response; - if (response.statusCode == 200 && IsJsonString(response.body)) { - versionInfo.v1 = response.body; - await this.setVersionInfoCache(versionInfo); - - // reset apiVersion - httpClient.setApiVersion(startApiVersion); - - return versionInfo; - } - } catch (e) { - // if more info is needed use e.stack - versionErrors.v1 = e.toString(); - } - // throw error if cannot get v1 or v2 data // likely bad creds/url throw new Error( diff --git a/src/driver/freenas/http/index.js b/src/driver/freenas/http/index.js index 54f4a34..cafd6ff 100644 --- a/src/driver/freenas/http/index.js +++ b/src/driver/freenas/http/index.js @@ -9,11 +9,7 @@ class Client { constructor(options = {}) { this.options = JSON.parse(JSON.stringify(options)); this.logger = console; - - // default to v1.0 for now - if (!this.options.apiVersion) { - this.options.apiVersion = 2; - } + this.options.apiVersion = 2; } getHttpAgent() { diff --git a/src/utils/mount.js b/src/utils/mount.js index 0448557..e1884f0 100644 --- a/src/utils/mount.js +++ b/src/utils/mount.js @@ -10,7 +10,17 @@ const FINDMNT_COMMON_OPTIONS = [ "--nofsroot", // prevents unwanted behavior with cifs volumes ]; -const DEFAULT_TIMEOUT = process.env.MOUNT_DEFAULT_TIMEOUT || 30000; +let DEFAULT_TIMEOUT = 30 * 1000; + +if (process.env.MOUNT_DEFAULT_TIMEOUT) { + if (/^\d+$/.test(process.env.MOUNT_DEFAULT_TIMEOUT)) { + DEFAULT_TIMEOUT = parseInt(process.env.MOUNT_DEFAULT_TIMEOUT); + } else { + console.log( + "invalid MOUNT_DEFAULT_TIMEOUT set: " + process.env.MOUNT_DEFAULT_TIMEOUT + ); + } +} class Mount { constructor(options = {}) { From b8d0129ae4250425defcd9d188452765c85df513 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 21 Jan 2025 01:27:58 -0700 Subject: [PATCH 04/55] update ci config Signed-off-by: Travis Glenn Hansen --- ci/configs/truenas/scale/24.10/scale-iscsi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/configs/truenas/scale/24.10/scale-iscsi.yaml b/ci/configs/truenas/scale/24.10/scale-iscsi.yaml index 316cda2..6e164c3 100644 --- a/ci/configs/truenas/scale/24.10/scale-iscsi.yaml +++ b/ci/configs/truenas/scale/24.10/scale-iscsi.yaml @@ -24,7 +24,7 @@ iscsi: nameSuffix: "" targetGroups: - targetGroupPortalGroup: 1 - targetGroupInitiatorGroup: 1 + targetGroupInitiatorGroup: 3 targetGroupAuthType: None targetGroupAuthGroup: # 0-100 (0 == ignore) From b13184b02a5a038485d9e2c8f39ea5186adaa4bb Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 21 Jan 2025 02:22:26 -0700 Subject: [PATCH 05/55] include badly spelled error message in logic Signed-off-by: Travis Glenn Hansen --- src/utils/nvmeof.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/utils/nvmeof.js b/src/utils/nvmeof.js index dc76571..e2f47e1 100644 --- a/src/utils/nvmeof.js +++ b/src/utils/nvmeof.js @@ -29,9 +29,9 @@ class NVMEoF { nvmeof.logger = nvmeof.options.logger; } else { nvmeof.logger = console; - console.verbose = function() { + console.verbose = function () { console.log(...arguments); - } + }; } } @@ -112,7 +112,7 @@ class NVMEoF { if (!arg.startsWith("-")) { arg = `--${arg}`; } - + transport_args.push(arg, value); } } @@ -122,9 +122,11 @@ class NVMEoF { try { await nvmeof.exec(nvmeof.options.paths.nvme, args); } catch (err) { + // already connnected - is mispelled in older versions so we include both if ( err.stderr && (err.stderr.includes("already connected") || + err.stderr.includes("already connnected") || err.stderr.includes("Operation already in progress")) ) { // idempotent @@ -279,9 +281,7 @@ class NVMEoF { async pathExists(path) { const nvmeof = this; try { - await nvmeof.exec("stat", [ - path, - ]); + await nvmeof.exec("stat", [path]); return true; } catch (err) { return false; @@ -302,7 +302,7 @@ class NVMEoF { } throw err; } - + return result.stdout.trim() == "Y"; } From de242c2d7f1ed13a6416e97889fe27be9b0f0591 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 21 Jan 2025 18:32:30 -0700 Subject: [PATCH 06/55] better talos and nixos support with wrapper scripts Signed-off-by: Travis Glenn Hansen --- docker/iscsiadm | 23 ++++++++++++++++++++--- docker/mount | 16 +++++++++++++--- docker/multipath | 14 ++++++++++++-- docker/oneclient | 14 ++++++++++++-- docker/umount | 16 +++++++++++++--- docker/zfs | 14 ++++++++++++-- docker/zpool | 14 ++++++++++++-- 7 files changed, 94 insertions(+), 17 deletions(-) diff --git a/docker/iscsiadm b/docker/iscsiadm index 551a9f5..1769006 100755 --- a/docker/iscsiadm +++ b/docker/iscsiadm @@ -1,14 +1,30 @@ -#!/bin/bash +#!/usr/bin/env bash : "${ISCSIADM_HOST_STRATEGY:=chroot}" : "${ISCSIADM_HOST_PATH:=iscsiadm}" echoerr() { printf "%s\n" "$*" >&2; } +P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" + case ${ISCSIADM_HOST_STRATEGY} in chroot) - # https://engineering.docker.com/2019/07/road-to-containing-iscsi/ - chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" ${ISCSIADM_HOST_PATH} "${@:1}" + # https://www.docker.com/blog/road-to-containing-iscsi/ + + if [[ "${ISCSIADM_HOST_PATH}" =~ ^\/ && -f "/host${ISCSIADM_HOST_PATH}" ]]; then + chroot /host "${ISCSIADM_HOST_PATH}" "${@:1}" + exit $? + fi + + for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do + if [[ -f "/host${p}/iscsiadm" ]]; then + chroot /host "${p}/iscsiadm" "${@:1}" + exit $? + fi + done + + chroot /host /usr/bin/env -i PATH="${P}" ${ISCSIADM_HOST_PATH} "${@:1}" + exit $? ;; nsenter) @@ -19,6 +35,7 @@ case ${ISCSIADM_HOST_STRATEGY} in exit 1 fi nsenter --mount="/proc/${iscsid_pid}/ns/mnt" --net="/proc/${iscsid_pid}/ns/net" -- ${ISCSIADM_HOST_PATH} "${@:1}" + exit $? ;; *) diff --git a/docker/mount b/docker/mount index c0603e6..90400cf 100755 --- a/docker/mount +++ b/docker/mount @@ -1,4 +1,6 @@ -#!/bin/bash +#!/usr/bin/env bash + +P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" container_supported_filesystems=( "ext2" @@ -31,7 +33,15 @@ while getopts "t:" opt; do done if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then - chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" mount "${@:1}" + for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do + if [[ -f "/host${p}/mount" ]]; then + chroot /host "${p}/mount" "${@:1}" + exit $? + fi + done + chroot /host /usr/bin/env -i PATH="${P}" mount "${@:1}" + exit $? else - /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" mount "${@:1}" + /usr/bin/env -i PATH="${P}" mount "${@:1}" + exit $? fi diff --git a/docker/multipath b/docker/multipath index ac097d4..7b204d1 100755 --- a/docker/multipath +++ b/docker/multipath @@ -1,3 +1,13 @@ -#!/bin/bash +#!/usr/bin/env bash -chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" multipath "${@:1}" +P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" + +for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do + if [[ -f "/host${p}/multipath" ]]; then + chroot /host "${p}/multipath" "${@:1}" + exit $? + fi +done + +chroot /host /usr/bin/env -i PATH="${P}" multipath "${@:1}" +echo $? diff --git a/docker/oneclient b/docker/oneclient index 373cefd..467ac4d 100755 --- a/docker/oneclient +++ b/docker/oneclient @@ -1,3 +1,13 @@ -#!/bin/bash +#!/usr/bin/env bash -chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" oneclient "${@:1}" +P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" + +for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do + if [[ -f "/host${p}/oneclient" ]]; then + chroot /host "${p}/oneclient" "${@:1}" + exit $? + fi +done + +chroot /host /usr/bin/env -i PATH="${P}" oneclient "${@:1}" +exit $? diff --git a/docker/umount b/docker/umount index 1bcd0f2..70507de 100755 --- a/docker/umount +++ b/docker/umount @@ -1,4 +1,6 @@ -#!/bin/bash +#!/usr/bin/env bash + +P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" container_supported_filesystems=( "ext2" @@ -31,7 +33,15 @@ while getopts "t:" opt; do done if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then - chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" umount "${@:1}" + for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do + if [[ -f "/host${p}/umount" ]]; then + chroot /host "${p}/umount" "${@:1}" + exit $? + fi + done + chroot /host /usr/bin/env -i PATH="${P}" umount "${@:1}" + exit $? else - /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" umount "${@:1}" + /usr/bin/env -i PATH="${P}" umount "${@:1}" + exit $? fi diff --git a/docker/zfs b/docker/zfs index aceb182..2efd259 100755 --- a/docker/zfs +++ b/docker/zfs @@ -1,3 +1,13 @@ -#!/bin/bash +#!/usr/bin/env bash -chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" zfs "${@:1}" +P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" + +for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do + if [[ -f "/host${p}/zfs" ]]; then + chroot /host "${p}/zfs" "${@:1}" + exit $? + fi +done + +chroot /host /usr/bin/env -i PATH="${P}" zfs "${@:1}" +exit $? diff --git a/docker/zpool b/docker/zpool index a4696aa..ec0d5b4 100755 --- a/docker/zpool +++ b/docker/zpool @@ -1,3 +1,13 @@ -#!/bin/bash +#!/usr/bin/env bash -chroot /host /usr/bin/env -i PATH="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" zpool "${@:1}" +P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" + +for p in $(echo $P | cut -d ":" -f 1- --output-delimiter=" "); do + if [[ -f "/host${p}/zpool" ]]; then + chroot /host "${p}/zpool" "${@:1}" + exit $? + fi +done + +chroot /host /usr/bin/env -i PATH="${P}" zpool "${@:1}" +exit $? From 93e0446fa39a50df5ddc47cd15c03fd721128589 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 22 Jan 2025 21:29:51 -0700 Subject: [PATCH 07/55] fix recursive mount operations Signed-off-by: Travis Glenn Hansen --- docker/mount | 3 +- docker/umount | 3 +- package-lock.json | 774 ++++++++++++++++++++++++++++++--------------- package.json | 2 + src/utils/mount.js | 2 +- 5 files changed, 533 insertions(+), 251 deletions(-) diff --git a/docker/mount b/docker/mount index 90400cf..dfdcee1 100755 --- a/docker/mount +++ b/docker/mount @@ -1,6 +1,7 @@ #!/usr/bin/env bash P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" +PL="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" container_supported_filesystems=( "ext2" @@ -42,6 +43,6 @@ if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then chroot /host /usr/bin/env -i PATH="${P}" mount "${@:1}" exit $? else - /usr/bin/env -i PATH="${P}" mount "${@:1}" + /usr/bin/env -i PATH="${PL}" mount "${@:1}" exit $? fi diff --git a/docker/umount b/docker/umount index 70507de..fc1922a 100755 --- a/docker/umount +++ b/docker/umount @@ -1,6 +1,7 @@ #!/usr/bin/env bash P="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" +PL="/usr/sbin:/usr/bin:/sbin:/bin:/run/current-system/sw/bin" container_supported_filesystems=( "ext2" @@ -42,6 +43,6 @@ if [[ ${USE_HOST_MOUNT_TOOLS} -eq 1 ]]; then chroot /host /usr/bin/env -i PATH="${P}" umount "${@:1}" exit $? else - /usr/bin/env -i PATH="${P}" umount "${@:1}" + /usr/bin/env -i PATH="${PL}" umount "${@:1}" exit $? fi diff --git a/package-lock.json b/package-lock.json index ea1ef0d..3908a56 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,30 +22,24 @@ "lodash": "^4.17.21", "lru-cache": "^7.4.0", "prompt": "^1.2.2", + "reconnecting-websocket": "^4.4.0", "semver": "^7.3.4", "ssh2": "^1.1.0", "uri-js": "^4.4.1", "uuid": "^9.0.0", "winston": "^3.6.0", + "ws": "^8.18.0", "yargs": "^17.0.1" }, "devDependencies": { "eslint": "^8.10.0" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "license": "MIT", "engines": { "node": ">=0.1.90" } @@ -54,6 +48,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", @@ -61,25 +56,30 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", "dev": true, + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -89,6 +89,7 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -108,20 +109,22 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, + "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@grpc/grpc-js": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.3.tgz", - "integrity": "sha512-qiO9MNgYnwbvZ8MK0YLWbnGrNX3zTcj6/Ef7UHu5ZofER3e2nF3Y35GaPo9qNJJ/UJQKa4KL+z/F4Q8Q+uCdUQ==", + "version": "1.12.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz", + "integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==", + "license": "Apache-2.0", "dependencies": { - "@grpc/proto-loader": "^0.7.10", + "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" }, "engines": { @@ -129,13 +132,14 @@ } }, "node_modules/@grpc/proto-loader": { - "version": "0.7.10", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", - "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "license": "Apache-2.0", "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", - "protobufjs": "^7.2.4", + "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { @@ -146,12 +150,14 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -164,6 +170,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=12.22" }, @@ -173,15 +180,18 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/js-sdsl" @@ -191,6 +201,7 @@ "version": "0.18.1", "resolved": "https://registry.npmjs.org/@kubernetes/client-node/-/client-node-0.18.1.tgz", "integrity": "sha512-F3JiK9iZnbh81O/da1tD0h8fQMi/MDttWc/JydyUVnjPEom55wVfnpl4zQ/sWD4uKB8FlxYRPiLwV2ZXB+xPKw==", + "license": "Apache-2.0", "dependencies": { "@types/js-yaml": "^4.0.1", "@types/node": "^18.11.17", @@ -218,6 +229,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -231,6 +243,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -240,6 +253,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -251,27 +265,32 @@ "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/base64": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/codegen": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/eventemitter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/fetch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" @@ -280,42 +299,50 @@ "node_modules/@protobufjs/float": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/inquire": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/path": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/pool": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" }, "node_modules/@protobufjs/utf8": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "license": "BSD-3-Clause" }, "node_modules/@types/caseless": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", - "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==" + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "license": "MIT" }, "node_modules/@types/js-yaml": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", - "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==" + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.26", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.26.tgz", - "integrity": "sha512-+wiMJsIwLOYCvUqSdKTrfkS8mpTp+MPINe6+Np4TAGFWWRWiBQ5kSq9nZGCSPkzx9mvT+uEukzpX4MOSCydcvw==", + "version": "18.19.71", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.71.tgz", + "integrity": "sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==", + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } @@ -324,6 +351,7 @@ "version": "2.48.12", "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "license": "MIT", "dependencies": { "@types/caseless": "*", "@types/node": "*", @@ -334,32 +362,37 @@ "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "license": "MIT" }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", - "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.10", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", - "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", + "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "dev": true, + "license": "ISC" }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -372,6 +405,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -380,6 +414,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -395,6 +430,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { "node": ">=8" } @@ -403,6 +439,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -416,12 +453,14 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "license": "MIT", "dependencies": { "safer-buffer": "~2.1.0" } @@ -430,6 +469,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "license": "MIT", "engines": { "node": ">=0.8" } @@ -437,12 +477,14 @@ "node_modules/async": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" + "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", + "license": "MIT" }, "node_modules/async-mutex": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", "integrity": "sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==", + "license": "MIT", "dependencies": { "tslib": "^2.4.0" } @@ -450,25 +492,29 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "license": "Apache-2.0", "engines": { "node": "*" } }, "node_modules/aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "license": "MIT" }, "node_modules/axios": { - "version": "1.6.8", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", - "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -476,9 +522,10 @@ } }, "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==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -492,12 +539,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true + "devOptional": true, + "license": "MIT" }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "license": "BSD-3-Clause", "dependencies": { "tweetnacl": "^0.14.3" } @@ -507,6 +556,7 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "devOptional": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -528,6 +578,7 @@ "engines": [ "node >=0.10.0" ], + "license": "MIT", "bin": { "bunyan": "bin/bunyan" }, @@ -542,6 +593,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -551,6 +603,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -558,13 +611,15 @@ "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "license": "Apache-2.0" }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -580,6 +635,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "license": "ISC", "engines": { "node": ">=10" } @@ -588,6 +644,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -601,6 +658,7 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" @@ -610,6 +668,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -620,12 +679,14 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/color-string": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" @@ -635,6 +696,7 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", "dependencies": { "color-name": "1.1.3" } @@ -642,12 +704,14 @@ "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": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" }, "node_modules/colors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz", "integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==", + "license": "MIT", "engines": { "node": ">=0.1.90" } @@ -656,6 +720,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" @@ -665,6 +730,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -676,22 +742,24 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true + "devOptional": true, + "license": "MIT" }, "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": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "license": "MIT" }, "node_modules/cpu-features": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz", - "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", "hasInstallScript": true, "optional": true, "dependencies": { "buildcheck": "~0.0.6", - "nan": "^2.17.0" + "nan": "^2.19.0" }, "engines": { "node": ">=10.0.0" @@ -701,6 +769,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/crc/-/crc-4.3.2.tgz", "integrity": "sha512-uGDHf4KLLh2zsHa8D8hIQ1H/HtFQhyHrc0uhHBcoKGol/Xnb+MPYfUMw7cvON6ze/GUESTudKayDcJC5HnJv1A==", + "license": "MIT", "engines": { "node": ">=12" }, @@ -714,10 +783,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -739,6 +809,7 @@ "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" }, @@ -747,12 +818,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -767,12 +839,14 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -782,6 +856,7 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, + "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -794,6 +869,7 @@ "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", "integrity": "sha512-b7Z7cNtHPhH9EJhNNbbeqTcXB8LGFFZhq1PGgEvpeHlzd36bhbdTWoE/Ba/YguqpBSlAPKnARWhVlhunCMwfxg==", "hasInstallScript": true, + "license": "BSD-2-Clause", "optional": true, "dependencies": { "nan": "^2.14.0" @@ -806,6 +882,7 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "license": "MIT", "dependencies": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -814,17 +891,20 @@ "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", - "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -834,6 +914,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -842,16 +923,18 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -901,6 +984,7 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -917,6 +1001,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -929,6 +1014,7 @@ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -942,10 +1028,11 @@ } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "estraverse": "^5.1.0" }, @@ -958,6 +1045,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -970,6 +1058,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -979,6 +1068,7 @@ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } @@ -986,7 +1076,8 @@ "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" }, "node_modules/extsprintf": { "version": "1.3.0", @@ -994,7 +1085,8 @@ "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "engines": [ "node >=0.6.0" - ] + ], + "license": "MIT" }, "node_modules/eyes": { "version": "0.1.8", @@ -1007,24 +1099,28 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -1032,13 +1128,15 @@ "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, + "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, @@ -1051,6 +1149,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -1067,6 +1166,7 @@ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -1077,26 +1177,29 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true, + "license": "ISC" }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", - "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -1110,27 +1213,31 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "license": "Apache-2.0", "engines": { "node": "*" } }, "node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", + "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "mime-types": "^2.1.12", + "safe-buffer": "^5.2.1" }, "engines": { "node": ">= 0.12" } }, "node_modules/fs-extra": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.0.tgz", + "integrity": "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew==", + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1144,6 +1251,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "license": "ISC", "dependencies": { "minipass": "^3.0.0" }, @@ -1155,6 +1263,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -1166,12 +1275,14 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -1180,6 +1291,7 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0" } @@ -1188,7 +1300,9 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1209,6 +1323,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.3" }, @@ -1221,6 +1336,7 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -1234,18 +1350,21 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -1266,6 +1385,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "license": "ISC", "engines": { "node": ">=4" } @@ -1275,6 +1395,7 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "deprecated": "this library is no longer supported", + "license": "MIT", "dependencies": { "ajv": "^6.12.3", "har-schema": "^2.0.0" @@ -1288,6 +1409,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1296,6 +1418,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "jsprim": "^1.2.2", @@ -1307,10 +1430,11 @@ } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -1320,6 +1444,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -1336,6 +1461,7 @@ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.19" } @@ -1344,7 +1470,9 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "devOptional": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1353,18 +1481,21 @@ "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1373,6 +1504,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", "engines": { "node": ">=8" } @@ -1382,6 +1514,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -1394,6 +1527,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1402,6 +1536,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", "engines": { "node": ">=8" }, @@ -1412,18 +1547,21 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/isomorphic-ws": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "license": "MIT", "peerDependencies": { "ws": "*" } @@ -1431,12 +1569,14 @@ "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "license": "MIT" }, "node_modules/jose": { - "version": "4.15.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", - "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "license": "MIT", "optional": true, "funding": { "url": "https://github.com/sponsors/panva" @@ -1446,6 +1586,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -1456,39 +1597,46 @@ "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "license": "MIT" }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" }, "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": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "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": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "license": "ISC" }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -1500,6 +1648,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-7.2.0.tgz", "integrity": "sha512-zBfiUPM5nD0YZSBT/o/fbCUlCcepMIdP0CJZxM1+KgA4f2T206f6VAg9e7mX35+KlMaIc5qXW34f3BnwJ3w+RA==", + "license": "MIT", "engines": { "node": ">=12.0.0" } @@ -1508,6 +1657,7 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "license": "MIT", "dependencies": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", @@ -1523,6 +1673,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -1530,13 +1681,15 @@ "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", - "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -1550,6 +1703,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -1563,23 +1717,27 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/logform": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", - "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", @@ -1596,19 +1754,22 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", "engines": { "node": ">=0.1.90" } }, "node_modules/long": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", - "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", + "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "license": "Apache-2.0" }, "node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", "engines": { "node": ">=12" } @@ -1617,6 +1778,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -1625,6 +1787,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -1637,6 +1800,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "devOptional": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1648,6 +1812,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -1656,6 +1821,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", "engines": { "node": ">=8" } @@ -1664,6 +1830,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "license": "MIT", "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" @@ -1676,6 +1843,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -1687,6 +1855,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", "optional": true, "dependencies": { "minimist": "^1.2.6" @@ -1699,25 +1868,29 @@ "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", "optional": true, "engines": { "node": "*" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", - "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "license": "ISC" }, "node_modules/mv": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", "integrity": "sha512-at/ZndSy3xEGJ8i0ygALh8ru9qy7gWW1cmkaqBN29JmMlIvM//MEO9y1sk/avxuwnPcfhkejkLsuPxH81BrkSg==", + "license": "MIT", "optional": true, "dependencies": { "mkdirp": "~0.5.1", @@ -1732,6 +1905,8 @@ "version": "6.0.4", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", "integrity": "sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", "optional": true, "dependencies": { "inflight": "^1.0.4", @@ -1748,6 +1923,8 @@ "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", "integrity": "sha512-J5xnxTyqaiw06JjMftq7L9ouA448dw/E7dKghkP9WpKNuwmARNNg+Gk8/u5ryb9N/Yo2+z3MCwuqFK/+qPOPfQ==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "license": "ISC", "optional": true, "dependencies": { "glob": "^6.0.1" @@ -1757,21 +1934,24 @@ } }, "node_modules/nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT", "optional": true }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", "integrity": "sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==", + "license": "MIT", "optional": true, "bin": { "ncp": "bin/ncp" @@ -1780,12 +1960,14 @@ "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "license": "Apache-2.0", "engines": { "node": "*" } @@ -1794,6 +1976,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz", "integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==", + "license": "MIT", "optional": true, "engines": { "node": ">= 6" @@ -1803,6 +1986,7 @@ "version": "5.0.3", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "license": "MIT", "optional": true, "engines": { "node": "^10.13.0 || >=12.0.0" @@ -1813,6 +1997,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "devOptional": true, + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -1821,17 +2006,19 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", "dependencies": { "fn.name": "1.x.x" } }, "node_modules/openid-client": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", - "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz", + "integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==", + "license": "MIT", "optional": true, "dependencies": { - "jose": "^4.15.5", + "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -1844,6 +2031,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", "optional": true, "dependencies": { "yallist": "^4.0.0" @@ -1853,17 +2041,18 @@ } }, "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, + "license": "MIT", "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, "engines": { "node": ">= 0.8.0" @@ -1874,6 +2063,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -1889,6 +2079,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -1904,6 +2095,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -1916,6 +2108,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1925,6 +2118,7 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "devOptional": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1934,6 +2128,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1941,13 +2136,15 @@ "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT" }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.8.0" } @@ -1956,6 +2153,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.3.0.tgz", "integrity": "sha512-ZkaRWtaLBZl7KKAKndKYUL8WqNT+cQHKRZnT4RYYms48jQkFw3rrBL+/N5K/KtdEveHkxs982MX2BkDKub2ZMg==", + "license": "MIT", "dependencies": { "@colors/colors": "1.5.0", "async": "3.2.3", @@ -1971,6 +2169,7 @@ "version": "2.4.7", "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.7.tgz", "integrity": "sha512-vLB4BqzCKDnnZH9PHGoS2ycawueX4HLqENXQitvFHczhgW2vFpSOn31LZtVr1KU8YTw7DS4tM+cqyovxo8taVg==", + "license": "MIT", "dependencies": { "async": "^2.6.4", "colors": "1.0.x", @@ -1987,15 +2186,17 @@ "version": "2.6.4", "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "license": "MIT", "dependencies": { "lodash": "^4.17.14" } }, "node_modules/protobufjs": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", - "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", "hasInstallScript": true, + "license": "BSD-3-Clause", "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", @@ -2017,17 +2218,26 @@ "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" }, "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", "engines": { "node": ">=6" } @@ -2036,6 +2246,7 @@ "version": "6.5.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.6" } @@ -2058,12 +2269,14 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "license": "ISC", "dependencies": { "mute-stream": "~0.0.4" }, @@ -2075,6 +2288,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -2084,11 +2298,18 @@ "node": ">= 6" } }, + "node_modules/reconnecting-websocket": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz", + "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==", + "license": "MIT" + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "license": "Apache-2.0", "dependencies": { "aws-sign2": "~0.7.0", "aws4": "^1.8.0", @@ -2119,6 +2340,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.6", @@ -2133,6 +2355,7 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "license": "MIT", "bin": { "uuid": "bin/uuid" } @@ -2141,6 +2364,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2150,6 +2374,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -2159,6 +2384,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -2168,20 +2394,24 @@ "version": "0.1.8", "resolved": "https://registry.npmjs.org/revalidator/-/revalidator-0.1.8.tgz", "integrity": "sha512-xcBILK2pA9oh4SiinPEZfhP8HfrB/ha+a2fTMyl7Om2WjlDVrOQy99N2MXXlUHqGJz4qEu2duXxHJjDWuK/0xg==", + "license": "Apache 2.0", "engines": { "node": ">= 0.4.0" } }, "node_modules/rfc4648": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.3.tgz", - "integrity": "sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==" + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/rfc4648/-/rfc4648-1.5.4.tgz", + "integrity": "sha512-rRg/6Lb+IGfJqO05HZkN50UtY7K/JhxJag1kP23+zyMfrvoB0B7RWv06MbOzoc79RgCdNTiUaNsTT1AJZ7Z+cg==", + "license": "MIT" }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -2211,6 +2441,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -2232,18 +2463,21 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safe-json-stringify": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.2.0.tgz", "integrity": "sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==", + "license": "MIT", "optional": true }, "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", "engines": { "node": ">=10" } @@ -2251,15 +2485,14 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -2267,22 +2500,12 @@ "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": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2295,6 +2518,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2303,6 +2527,7 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" } @@ -2311,14 +2536,15 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/ssh2": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.15.0.tgz", - "integrity": "sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.16.0.tgz", + "integrity": "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==", "hasInstallScript": true, "dependencies": { "asn1": "^0.2.6", @@ -2328,14 +2554,15 @@ "node": ">=10.16.0" }, "optionalDependencies": { - "cpu-features": "~0.0.9", - "nan": "^2.18.0" + "cpu-features": "~0.0.10", + "nan": "^2.20.0" } }, "node_modules/sshpk": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "license": "MIT", "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", @@ -2360,14 +2587,16 @@ "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", "engines": { "node": "*" } }, "node_modules/stream-buffers": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.2.tgz", - "integrity": "sha512-DQi1h8VEBA/lURbSwFtEHnSTb9s2/pwLEaFuNhXwy1Dx3Sa0lOuYT2yNUr4/j2fs8oCAMANtrZ5OrPZtyVs3MQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-3.0.3.tgz", + "integrity": "sha512-pqMqwQCso0PBJt2PQmDO0cFj0lyqmiwOMiMSkVtRokl7e+ZTRYgDHKnuZNbqjiJXgsg4nuqtD/zxuo9KqTp0Yw==", + "license": "Unlicense", "engines": { "node": ">= 0.10.0" } @@ -2376,6 +2605,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } @@ -2384,6 +2614,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -2397,6 +2628,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2409,6 +2641,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -2421,6 +2654,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2432,6 +2666,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "license": "ISC", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -2448,6 +2683,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" }, @@ -2458,18 +2694,21 @@ "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "license": "MIT", "engines": { "node": ">=14.14" } @@ -2478,6 +2717,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-3.0.3.tgz", "integrity": "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==", + "license": "MIT", "dependencies": { "tmp": "^0.2.0" } @@ -2486,6 +2726,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "license": "BSD-3-Clause", "dependencies": { "psl": "^1.1.28", "punycode": "^2.1.1" @@ -2498,19 +2739,22 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", "engines": { "node": ">= 14.0.0" } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" }, @@ -2521,13 +2765,15 @@ "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "license": "Unlicense" }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, + "license": "MIT", "dependencies": { "prelude-ls": "^1.2.1" }, @@ -2540,6 +2786,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -2548,9 +2795,10 @@ } }, "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -2560,19 +2808,22 @@ } }, "node_modules/underscore": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", - "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "license": "MIT" }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -2581,6 +2832,7 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } @@ -2588,7 +2840,8 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, "node_modules/uuid": { "version": "9.0.1", @@ -2598,6 +2851,7 @@ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } @@ -2609,6 +2863,7 @@ "engines": [ "node >=0.6.0" ], + "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -2620,6 +2875,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -2631,33 +2887,35 @@ } }, "node_modules/winston": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.13.0.tgz", - "integrity": "sha512-rwidmA1w3SE4j0E5MuIufFhyJPBDG7Nu71RkZor1p2+qHvJSZ9GYDA81AyleQcZbh/+V6HjeBdfnTZJm9rSeQQ==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.4.0", + "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.7.0" + "winston-transport": "^4.9.0" }, "engines": { "node": ">= 12.0.0" } }, "node_modules/winston-transport": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.7.0.tgz", - "integrity": "sha512-ajBj65K5I7denzer2IYW6+2bNIVqLGDHqDw3Ow8Ohh+vdW+rv4MZ6eiDvHoKhfJFZ2auyN8byXieDDJ96ViONg==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", "dependencies": { - "logform": "^2.3.2", - "readable-stream": "^3.6.0", + "logform": "^2.7.0", + "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" }, "engines": { @@ -2668,19 +2926,32 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", "engines": { "node": ">=0.1.90" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2697,12 +2968,14 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "devOptional": true + "devOptional": true, + "license": "ISC" }, "node_modules/ws": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", - "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -2723,6 +2996,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", "engines": { "node": ">=10" } @@ -2730,12 +3004,14 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -2753,6 +3029,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", "engines": { "node": ">=12" } @@ -2762,6 +3039,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 099fd4e..4bb7516 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,13 @@ "lodash": "^4.17.21", "lru-cache": "^7.4.0", "prompt": "^1.2.2", + "reconnecting-websocket": "^4.4.0", "semver": "^7.3.4", "ssh2": "^1.1.0", "uri-js": "^4.4.1", "uuid": "^9.0.0", "winston": "^3.6.0", + "ws": "^8.18.0", "yargs": "^17.0.1" }, "devDependencies": { diff --git a/src/utils/mount.js b/src/utils/mount.js index e1884f0..f6cd787 100644 --- a/src/utils/mount.js +++ b/src/utils/mount.js @@ -397,7 +397,7 @@ class Mount { exec(command, args, options = {}) { if (!options.hasOwnProperty("timeout")) { - options.timeout = DEFAULT_TIMEOUT; + options.timeout = parseInt(DEFAULT_TIMEOUT) || 30 * 1000; } const mount = this; From 746c489641965f1095e39ffe31a68265b354ab6c Mon Sep 17 00:00:00 2001 From: Danil Uzlov <36223296+d-uzlov@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:15:19 +0000 Subject: [PATCH 08/55] fix this.ctx.args.driver in switches --- src/driver/controller-synology/index.js | 4 ++-- src/driver/controller-zfs-generic/index.js | 2 +- src/driver/controller-zfs-local/index.js | 2 +- src/driver/freenas/api.js | 4 ++-- src/driver/freenas/ssh.js | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index 031ff66..29f5563 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -128,7 +128,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { case "synology-iscsi": return "volume"; default: - throw new Error("unknown driver: " + this.ctx.args.driver); + throw new Error("unknown driver: " + this.options.driver); } } @@ -141,7 +141,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { case "synology-iscsi": return "iscsi"; default: - throw new Error("unknown driver: " + this.ctx.args.driver); + throw new Error("unknown driver: " + this.options.driver); } } diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index d3080ae..56c1963 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -71,7 +71,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { case "zfs-generic-nvmeof": return "volume"; default: - throw new Error("unknown driver: " + this.ctx.args.driver); + throw new Error("unknown driver: " + this.options.driver); } } diff --git a/src/driver/controller-zfs-local/index.js b/src/driver/controller-zfs-local/index.js index 646e52c..90b02ac 100644 --- a/src/driver/controller-zfs-local/index.js +++ b/src/driver/controller-zfs-local/index.js @@ -87,7 +87,7 @@ class ControllerZfsLocalDriver extends ControllerZfsBaseDriver { case "zfs-local-zvol": return "volume"; default: - throw new Error("unknown driver: " + this.ctx.args.driver); + throw new Error("unknown driver: " + this.options.driver); } } diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 600b167..2a9615a 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -1975,7 +1975,7 @@ class FreeNASApiDriver extends CsiBaseDriver { case "truenas-api-iscsi": return "volume"; default: - throw new Error("unknown driver: " + this.ctx.args.driver); + throw new Error("unknown driver: " + this.options.driver); } } @@ -1991,7 +1991,7 @@ class FreeNASApiDriver extends CsiBaseDriver { case "truenas-api-iscsi": return "iscsi"; default: - throw new Error("unknown driver: " + this.ctx.args.driver); + throw new Error("unknown driver: " + this.options.driver); } } diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index b3a1a31..3d422fa 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -105,7 +105,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { case "truenas-iscsi": return "volume"; default: - throw new Error("unknown driver: " + this.ctx.args.driver); + throw new Error("unknown driver: " + this.options.driver); } } @@ -161,7 +161,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { case "truenas-iscsi": return "iscsi"; default: - throw new Error("unknown driver: " + this.ctx.args.driver); + throw new Error("unknown driver: " + this.options.driver); } } From a1fdb7f4052468ecf7db45610957d0cbe1363200 Mon Sep 17 00:00:00 2001 From: solidDoWant Date: Tue, 18 Feb 2025 19:41:19 +0000 Subject: [PATCH 09/55] Add the ability to set ZFS snapshot properties (functionally identical to `datasetProperties`) Signed-off-by: solidDoWant --- examples/freenas-api-iscsi.yaml | 2 ++ examples/freenas-api-nfs.yaml | 2 ++ examples/freenas-api-smb.yaml | 2 ++ examples/freenas-iscsi.yaml | 2 ++ examples/freenas-nfs.yaml | 2 ++ examples/freenas-smb.yaml | 2 ++ examples/zfs-generic-iscsi.yaml | 2 ++ examples/zfs-generic-nfs.yaml | 2 ++ examples/zfs-generic-nvmeof.yaml | 2 ++ examples/zfs-generic-smb.yaml | 2 ++ examples/zfs-local-dataset.yaml | 2 ++ examples/zfs-local-zvol.yaml | 2 ++ src/driver/controller-zfs/index.js | 13 +++++++++++++ src/driver/freenas/api.js | 13 +++++++++++++ 14 files changed, 50 insertions(+) diff --git a/examples/freenas-api-iscsi.yaml b/examples/freenas-api-iscsi.yaml index 3b2d922..22d87ee 100644 --- a/examples/freenas-api-iscsi.yaml +++ b/examples/freenas-api-iscsi.yaml @@ -33,6 +33,8 @@ zfs: # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" + # snapshotProperties: + # "org.freenas:key": "value" # total volume name (zvol//) length cannot exceed 63 chars # https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab diff --git a/examples/freenas-api-nfs.yaml b/examples/freenas-api-nfs.yaml index 1ec960e..d0a2206 100644 --- a/examples/freenas-api-nfs.yaml +++ b/examples/freenas-api-nfs.yaml @@ -33,6 +33,8 @@ zfs: # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" + # snapshotProperties: + # "org.freenas:key": "value" datasetParentName: tank/k8s/a/vols # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap diff --git a/examples/freenas-api-smb.yaml b/examples/freenas-api-smb.yaml index 9d13cef..ea057d7 100644 --- a/examples/freenas-api-smb.yaml +++ b/examples/freenas-api-smb.yaml @@ -33,6 +33,8 @@ zfs: # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" + # snapshotProperties: + # "org.freenas:key": "value" # these are managed automatically via the volume creation process when flagged as an smb volume #datasetProperties: diff --git a/examples/freenas-iscsi.yaml b/examples/freenas-iscsi.yaml index 6a20b6b..980c051 100644 --- a/examples/freenas-iscsi.yaml +++ b/examples/freenas-iscsi.yaml @@ -43,6 +43,8 @@ zfs: # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" + # snapshotProperties: + # "org.freenas:key": "value" # total volume name (zvol//) length cannot exceed 63 chars # https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab diff --git a/examples/freenas-nfs.yaml b/examples/freenas-nfs.yaml index 3ed9ec4..9934d52 100644 --- a/examples/freenas-nfs.yaml +++ b/examples/freenas-nfs.yaml @@ -43,6 +43,8 @@ zfs: # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" + # snapshotProperties: + # "org.freenas:key": "value" datasetParentName: tank/k8s/a/vols # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap diff --git a/examples/freenas-smb.yaml b/examples/freenas-smb.yaml index 8124e17..fa490bf 100644 --- a/examples/freenas-smb.yaml +++ b/examples/freenas-smb.yaml @@ -43,6 +43,8 @@ zfs: # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" + # snapshotProperties: + # "org.freenas:key": "value" datasetProperties: aclmode: restricted diff --git a/examples/zfs-generic-iscsi.yaml b/examples/zfs-generic-iscsi.yaml index af5df37..8e38e42 100644 --- a/examples/zfs-generic-iscsi.yaml +++ b/examples/zfs-generic-iscsi.yaml @@ -27,6 +27,8 @@ zfs: # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" + # snapshotProperties: + # "org.freenas:key": "value" datasetParentName: tank/k8s/test # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap diff --git a/examples/zfs-generic-nfs.yaml b/examples/zfs-generic-nfs.yaml index 7b6a2d2..620775f 100644 --- a/examples/zfs-generic-nfs.yaml +++ b/examples/zfs-generic-nfs.yaml @@ -27,6 +27,8 @@ zfs: # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" + # snapshotProperties: + # "org.freenas:key": "value" datasetParentName: tank/k8s/test # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap diff --git a/examples/zfs-generic-nvmeof.yaml b/examples/zfs-generic-nvmeof.yaml index b56b3ae..d236665 100644 --- a/examples/zfs-generic-nvmeof.yaml +++ b/examples/zfs-generic-nvmeof.yaml @@ -27,6 +27,8 @@ zfs: # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" # "org.freenas:test": "{{ parameters.foo }}" # "org.freenas:test2": "some value" + # snapshotProperties: + # "org.freenas:key": "value" datasetParentName: tank/k8s/test # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap diff --git a/examples/zfs-generic-smb.yaml b/examples/zfs-generic-smb.yaml index cbc8f8f..361b860 100644 --- a/examples/zfs-generic-smb.yaml +++ b/examples/zfs-generic-smb.yaml @@ -28,6 +28,8 @@ zfs: #aclinherit: passthrough #acltype: nfsv4 casesensitivity: insensitive + # snapshotProperties: + # "org.freenas:key": "value" datasetParentName: tank/k8s/test # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap diff --git a/examples/zfs-local-dataset.yaml b/examples/zfs-local-dataset.yaml index fd91346..273614d 100644 --- a/examples/zfs-local-dataset.yaml +++ b/examples/zfs-local-dataset.yaml @@ -6,6 +6,8 @@ zfs: datasetProperties: # key: value + snapshotProperties: + # "org.freenas:key": "value" datasetEnableQuotas: true datasetEnableReservation: false diff --git a/examples/zfs-local-zvol.yaml b/examples/zfs-local-zvol.yaml index e08da1d..4e7485a 100644 --- a/examples/zfs-local-zvol.yaml +++ b/examples/zfs-local-zvol.yaml @@ -6,6 +6,8 @@ zfs: datasetProperties: # key: value + snapshotProperties: + # "org.freenas:key": "value" zvolCompression: zvolDedup: diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index d665979..2865df4 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -2106,6 +2106,19 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { ); } + // user-supplied properties + // put early to prevent stupid (user-supplied values overwriting system values) + if (driver.options.zfs.snapshotProperties) { + for (let property in driver.options.zfs.snapshotProperties) { + let value = driver.options.zfs.snapshotProperties[property]; + const template = Handlebars.compile(value); + + snapshotProperties[property] = template({ + parameters: call.request.parameters, + }); + } + } + const volumeDatasetName = volumeParentDatasetName + "/" + source_volume_id; const datasetName = datasetParentName + "/" + source_volume_id; snapshotProperties[SNAPSHOT_CSI_NAME_PROPERTY_NAME] = name; diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 600b167..c944f21 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -4000,6 +4000,19 @@ class FreeNASApiDriver extends CsiBaseDriver { ); } + // user-supplied properties + // put early to prevent stupid (user-supplied values overwriting system values) + if (driver.options.zfs.snapshotProperties) { + for (let property in driver.options.zfs.snapshotProperties) { + let value = driver.options.zfs.snapshotProperties[property]; + const template = Handlebars.compile(value); + + snapshotProperties[property] = template({ + parameters: call.request.parameters, + }); + } + } + const datasetName = datasetParentName + "/" + source_volume_id; snapshotProperties[SNAPSHOT_CSI_NAME_PROPERTY_NAME] = name; snapshotProperties[SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME] = From de80f4b25ee4e6317cd1f76f17cbe91c4c8926e2 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 22 Mar 2025 14:54:48 -0600 Subject: [PATCH 10/55] detect SCALE 25 properly Signed-off-by: Travis Glenn Hansen --- src/driver/freenas/http/api.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index 7bfcda9..8000743 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -123,8 +123,13 @@ class Api { async getIsScale() { const systemVersion = await this.getSystemVersion(); + const major = await this.getSystemVersionMajor(); - if (systemVersion.v2 && systemVersion.v2.toLowerCase().includes("scale")) { + // starting with version 25 the version string no longer contains `-SCALE` + if ( + systemVersion.v2 && + (systemVersion.v2.toLowerCase().includes("scale") || Number(major) >= 20) + ) { return true; } From 957d7fc6bc16649d02ad4c4586669bdd5267ddd7 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 5 Apr 2025 17:16:52 -0600 Subject: [PATCH 11/55] support TN 25.04, env vars in config, improved Dockerfiles Signed-off-by: Travis Glenn Hansen --- Dockerfile | 14 +- Dockerfile.Windows | 119 +- bin/democratic-csi | 26 +- csi_proto/csi-v1.10.0.proto | 2103 ++++++++++++++++++++++++++++++ csi_proto/csi-v1.11.0.proto | 2078 +++++++++++++++++++++++++++++ docker/entrypoint.ps1 | 6 + docker/yq-installer.sh | 38 + docs/storage-class-parameters.md | 98 +- package-lock.json | 1551 +++++++++++++++++++++- package.json | 1 + src/driver/freenas/api.js | 18 +- src/driver/freenas/http/api.js | 70 +- src/utils/general.js | 14 + 13 files changed, 5955 insertions(+), 181 deletions(-) create mode 100644 csi_proto/csi-v1.10.0.proto create mode 100644 csi_proto/csi-v1.11.0.proto create mode 100644 docker/entrypoint.ps1 create mode 100755 docker/yq-installer.sh diff --git a/Dockerfile b/Dockerfile index e4c241a..953497f 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=v20.11.1 +ENV NODE_VERSION=v20.19.0 ENV NODE_ENV=production # install build deps @@ -80,18 +80,24 @@ RUN apt-get update && \ apt-get install -y wget netbase zip bzip2 socat e2fsprogs exfatprogs xfsprogs btrfs-progs fatresize dosfstools ntfs-3g nfs-common cifs-utils fdisk gdisk cloud-guest-utils sudo rsync procps util-linux nvme-cli fuse3 && \ rm -rf /var/lib/apt/lists/* -ARG RCLONE_VERSION=1.66.0 +# TODO: remove nvme unique files + +ARG RCLONE_VERSION=1.69.1 ADD docker/rclone-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/rclone-installer.sh && rclone-installer.sh -ARG RESTIC_VERSION=0.16.4 +ARG RESTIC_VERSION=0.18.0 ADD docker/restic-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/restic-installer.sh && restic-installer.sh -ARG KOPIA_VERSION=0.16.1 +ARG KOPIA_VERSION=0.19.0 ADD docker/kopia-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/kopia-installer.sh && kopia-installer.sh +ARG YQ_VERSION=v4.45.1 +ADD docker/yq-installer.sh /usr/local/sbin +RUN chmod +x /usr/local/sbin/yq-installer.sh && yq-installer.sh + # controller requirements #RUN apt-get update && \ # apt-get install -y ansible && \ diff --git a/Dockerfile.Windows b/Dockerfile.Windows index 2eaefc9..69cfb2d 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -3,9 +3,11 @@ # 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/ -# +# https://stackoverflow.com/questions/65104246/how-to-install-powershell-core-in-aspnet-nanoserver-docker-container +# # 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 -ti --entrypoint cmd foobar # docker run --rm foobar # docker save foobar -o foobar.tar # buildah pull docker-archive:foobar.tar @@ -16,85 +18,96 @@ ARG NANO_BASE_TAG ARG CORE_BASE_TAG -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';"] -#ENV GPG_VERSION 4.0.2 -ENV GPG_VERSION 2.3.4 +ENV POWERSHELL_TELEMETRY_OPTOUT="1" -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 +ARG PS_VERSION=7.5.0 +ADD https://github.com/PowerShell/PowerShell/releases/download/v$PS_VERSION/PowerShell-$PS_VERSION-win-x64.zip /PowerShell/powershell.zip -# 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.18.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 \ + Expand-Archive '/PowerShell/powershell.zip' -DestinationPath '/PowerShell' ; \ + cd C:\PowerShell ; \ + del powershell.zip ; \ + New-Item -ItemType SymbolicLink -Path "powershell.exe" -Target "pwsh.exe" +ENV NODE_VERSION 20.19.0 +ENV NODE_ENV=production 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' + Expand-Archive node.zip -DestinationPath C:\ ; \ + Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs' +RUN mkdir \usr\local\bin; mkdir \tmp + +ARG RCLONE_VERSION=v1.69.1 +RUN Invoke-WebRequest "https://github.com/rclone/rclone/releases/download/${env:RCLONE_VERSION}/rclone-${env:RCLONE_VERSION}-windows-amd64.zip" -OutFile '/tmp/rclone.zip' -UseBasicParsing ; \ + Expand-Archive C:\tmp\rclone.zip -DestinationPath C:\tmp ; \ + Copy-Item $('C:\tmp\rclone-{0}-windows-amd64\rclone.exe' -f $env:RCLONE_VERSION) -Destination "C:\usr\local\bin" + +ARG RESTIC_VERSION=0.18.0 +RUN Invoke-WebRequest "https://github.com/restic/restic/releases/download/v${env:RESTIC_VERSION}/restic_${env:RESTIC_VERSION}_windows_amd64.zip" -OutFile '/tmp/restic.zip' -UseBasicParsing ; \ + Expand-Archive C:\tmp\restic.zip -DestinationPath C:\tmp ; \ + Copy-Item $('C:\tmp\restic_{0}_windows_amd64.exe' -f $env:RESTIC_VERSION) -Destination "C:\usr\local\bin\restic.exe" + +ARG KOPIA_VERSION=0.19.0 +RUN Invoke-WebRequest "https://github.com/kopia/kopia/releases/download/v${env:KOPIA_VERSION}/kopia-${env:KOPIA_VERSION}-windows-x64.zip" -OutFile '/tmp/kopia.zip' -UseBasicParsing ; \ + Expand-Archive C:\tmp\kopia.zip -DestinationPath C:\tmp ; \ + Copy-Item $('C:\tmp\kopia-{0}-windows-x64\kopia.exe' -f $env:KOPIA_VERSION) -Destination "C:\usr\local\bin" + +ARG YQ_VERSION=v4.45.1 +RUN Invoke-WebRequest "https://github.com/mikefarah/yq/releases/download/${env:YQ_VERSION}/yq_windows_amd64.zip" -OutFile '/tmp/yq.zip' -UseBasicParsing ; \ + Expand-Archive C:\tmp\yq.zip -DestinationPath C:\tmp ; \ + Copy-Item $('C:\tmp\yq_windows_amd64.exe') -Destination "C:\usr\local\bin\yq.exe" + +RUN Remove-Item C:\tmp\ -Force -Recurse + +# install app #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 . . +###################### +# actual image +###################### FROM mcr.microsoft.com/windows/nanoserver:${NANO_BASE_TAG} +SHELL ["cmd.exe", "/s" , "/c"] + +#https://github.com/PowerShell/PowerShell-Docker/issues/236 +# NOTE: this works for non-host process containers, but host process containers will have specials PATH requirements +# C:\Windows\System32\WindowsPowerShell\v1.0\ +ENV PATH="C:\Windows\system32;C:\Windows;C:\PowerShell;C:\app\bin;" + +ENV DEMOCRATIC_CSI_IS_CONTAINER=true +ENV NODE_ENV=production + 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 +# install powershell +COPY --from=build /PowerShell /PowerShell +# install app 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 ./bin +COPY --from=build /usr/local/bin/ ./bin -ENTRYPOINT [ "bin/node.exe", "--expose-gc", "bin/democratic-csi" ] +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'Continue'; $verbosePreference='Continue';"] + +EXPOSE 50051 +# this works for both host-process and non-host-process container semantics +#ENTRYPOINT [ "bin/node.exe", "--expose-gc", "bin/democratic-csi" ] + +ADD docker/entrypoint.ps1 ./bin +# NOTE: this powershell.exe could be problematic based on overriding PATH in container vs host etc +ENTRYPOINT [ "powershell.exe", "bin/entrypoint.ps1" ] diff --git a/bin/democratic-csi b/bin/democratic-csi index 3d6b39e..7e99647 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -10,7 +10,12 @@ require("../src/utils/polyfills"); const yaml = require("js-yaml"); const fs = require("fs"); const { grpc } = require("../src/utils/grpc"); -const { stringify, stripWindowsDriveLetter } = require("../src/utils/general"); +const { + stringify, + stripWindowsDriveLetter, + expandenv, +} = require("../src/utils/general"); +const traverse = require("traverse"); let driverConfigFile; let options; @@ -67,6 +72,8 @@ const args = require("yargs") "1.7.0", "1.8.0", "1.9.0", + "1.10.0", + "1.11.0", ], }) .demandOption(["csi-version"], "csi-version is required") @@ -106,6 +113,20 @@ if (!args.serverSocket && !args.serverAddress && !args.serverPort) { process.exit(1); } +//console.log(JSON.stringify(options, null, 2)); +traverse(options).forEach(function (v) { + if (typeof v === "string" || v instanceof String) { + v = expandenv(v); + try { + v = JSON.parse(v); + } catch (e) { + // ignore + } + this.update(v); + } +}); +//console.log(JSON.stringify(options, null, 2)); +//process.exit(1); //console.log(args); //console.log(process.env); @@ -529,6 +550,9 @@ if (process.env.LOG_GRPC_SESSIONS == "1") { if (require.main === module) { (async function () { try { + //nvme gen-hostnqn > /etc/nvme/hostnqn + //uuidgen > /etc/nvme/hostid + if (bindAddress) { await new Promise((resolve, reject) => { csiServer.bindAsync( diff --git a/csi_proto/csi-v1.10.0.proto b/csi_proto/csi-v1.10.0.proto new file mode 100644 index 0000000..908ec24 --- /dev/null +++ b/csi_proto/csi-v1.10.0.proto @@ -0,0 +1,2103 @@ +// Code generated by make; DO NOT EDIT. +syntax = "proto3"; +package csi.v1; + +import "google/protobuf/descriptor.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +option go_package = + "github.com/container-storage-interface/spec/lib/go/csi"; + +extend google.protobuf.EnumOptions { + // Indicates that this enum is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_enum = 1060; +} +extend google.protobuf.EnumValueOptions { + // Indicates that this enum value is OPTIONAL and part of an + // experimental API that may be deprecated and eventually removed + // between minor releases. + bool alpha_enum_value = 1060; +} +extend google.protobuf.FieldOptions { + // Indicates that a field MAY contain information that is sensitive + // and MUST be treated as such (e.g. not logged). + bool csi_secret = 1059; + + // Indicates that this field is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_field = 1060; +} +extend google.protobuf.MessageOptions { + // Indicates that this message is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_message = 1060; +} +extend google.protobuf.MethodOptions { + // Indicates that this method is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_method = 1060; +} +extend google.protobuf.ServiceOptions { + // Indicates that this service is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_service = 1060; +} +service Identity { + rpc GetPluginInfo(GetPluginInfoRequest) + returns (GetPluginInfoResponse) {} + + rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) + returns (GetPluginCapabilitiesResponse) {} + + rpc Probe (ProbeRequest) + returns (ProbeResponse) {} +} + +service Controller { + rpc CreateVolume (CreateVolumeRequest) + returns (CreateVolumeResponse) {} + + rpc DeleteVolume (DeleteVolumeRequest) + returns (DeleteVolumeResponse) {} + + rpc ControllerPublishVolume (ControllerPublishVolumeRequest) + returns (ControllerPublishVolumeResponse) {} + + rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest) + returns (ControllerUnpublishVolumeResponse) {} + + rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest) + returns (ValidateVolumeCapabilitiesResponse) {} + + rpc ListVolumes (ListVolumesRequest) + returns (ListVolumesResponse) {} + + rpc GetCapacity (GetCapacityRequest) + returns (GetCapacityResponse) {} + + rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest) + returns (ControllerGetCapabilitiesResponse) {} + + rpc CreateSnapshot (CreateSnapshotRequest) + returns (CreateSnapshotResponse) {} + + rpc DeleteSnapshot (DeleteSnapshotRequest) + returns (DeleteSnapshotResponse) {} + + rpc ListSnapshots (ListSnapshotsRequest) + returns (ListSnapshotsResponse) {} + + rpc ControllerExpandVolume (ControllerExpandVolumeRequest) + returns (ControllerExpandVolumeResponse) {} + + rpc ControllerGetVolume (ControllerGetVolumeRequest) + returns (ControllerGetVolumeResponse) { + option (alpha_method) = true; + } + + rpc ControllerModifyVolume (ControllerModifyVolumeRequest) + returns (ControllerModifyVolumeResponse) { + option (alpha_method) = true; + } +} + +service GroupController { + option (alpha_service) = true; + + rpc GroupControllerGetCapabilities ( + GroupControllerGetCapabilitiesRequest) + returns (GroupControllerGetCapabilitiesResponse) {} + + rpc CreateVolumeGroupSnapshot(CreateVolumeGroupSnapshotRequest) + returns (CreateVolumeGroupSnapshotResponse) { + option (alpha_method) = true; + } + + rpc DeleteVolumeGroupSnapshot(DeleteVolumeGroupSnapshotRequest) + returns (DeleteVolumeGroupSnapshotResponse) { + option (alpha_method) = true; + } + + rpc GetVolumeGroupSnapshot( + GetVolumeGroupSnapshotRequest) + returns (GetVolumeGroupSnapshotResponse) { + option (alpha_method) = true; + } +} + +service SnapshotMetadata { + option (alpha_service) = true; + + rpc GetMetadataAllocated(GetMetadataAllocatedRequest) + returns (stream GetMetadataAllocatedResponse) {} + + rpc GetMetadataDelta(GetMetadataDeltaRequest) + returns (stream GetMetadataDeltaResponse) {} +} + +service Node { + rpc NodeStageVolume (NodeStageVolumeRequest) + returns (NodeStageVolumeResponse) {} + + rpc NodeUnstageVolume (NodeUnstageVolumeRequest) + returns (NodeUnstageVolumeResponse) {} + + rpc NodePublishVolume (NodePublishVolumeRequest) + returns (NodePublishVolumeResponse) {} + + rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest) + returns (NodeUnpublishVolumeResponse) {} + + rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest) + returns (NodeGetVolumeStatsResponse) {} + + + rpc NodeExpandVolume(NodeExpandVolumeRequest) + returns (NodeExpandVolumeResponse) {} + + + rpc NodeGetCapabilities (NodeGetCapabilitiesRequest) + returns (NodeGetCapabilitiesResponse) {} + + rpc NodeGetInfo (NodeGetInfoRequest) + returns (NodeGetInfoResponse) {} +} +message GetPluginInfoRequest { + // Intentionally empty. +} + +message GetPluginInfoResponse { + // The name MUST follow domain name notation format + // (https://tools.ietf.org/html/rfc1035#section-2.3.1). It SHOULD + // include the plugin's host company name and the plugin name, + // to minimize the possibility of collisions. It MUST be 63 + // characters or less, beginning and ending with an alphanumeric + // character ([a-z0-9A-Z]) with dashes (-), dots (.), and + // alphanumerics between. This field is REQUIRED. + string name = 1; + + // This field is REQUIRED. Value of this field is opaque to the CO. + string vendor_version = 2; + + // This field is OPTIONAL. Values are opaque to the CO. + map manifest = 3; +} +message GetPluginCapabilitiesRequest { + // Intentionally empty. +} + +message GetPluginCapabilitiesResponse { + // All the capabilities that the controller service supports. This + // field is OPTIONAL. + repeated PluginCapability capabilities = 1; +} + +// Specifies a capability of the plugin. +message PluginCapability { + message Service { + enum Type { + UNKNOWN = 0; + // CONTROLLER_SERVICE indicates that the Plugin provides RPCs for + // the ControllerService. Plugins SHOULD provide this capability. + // In rare cases certain plugins MAY wish to omit the + // ControllerService entirely from their implementation, but such + // SHOULD NOT be the common case. + // The presence of this capability determines whether the CO will + // attempt to invoke the REQUIRED ControllerService RPCs, as well + // as specific RPCs as indicated by ControllerGetCapabilities. + CONTROLLER_SERVICE = 1; + + // VOLUME_ACCESSIBILITY_CONSTRAINTS indicates that the volumes for + // this plugin MAY NOT be equally accessible by all nodes in the + // cluster. The CO MUST use the topology information returned by + // CreateVolumeRequest along with the topology information + // returned by NodeGetInfo to ensure that a given volume is + // accessible from a given node when scheduling workloads. + VOLUME_ACCESSIBILITY_CONSTRAINTS = 2; + + // GROUP_CONTROLLER_SERVICE indicates that the Plugin provides + // RPCs for operating on groups of volumes. Plugins MAY provide + // this capability. + // The presence of this capability determines whether the CO will + // attempt to invoke the REQUIRED GroupController service RPCs, as + // well as specific RPCs as indicated by + // GroupControllerGetCapabilities. + GROUP_CONTROLLER_SERVICE = 3 [(alpha_enum_value) = true]; + + // SNAPSHOT_METADATA_SERVICE indicates that the Plugin provides + // RPCs to retrieve metadata on the allocated blocks of a single + // snapshot, or the changed blocks between a pair of snapshots of + // the same block volume. + // The presence of this capability determines whether the CO will + // attempt to invoke the OPTIONAL SnapshotMetadata service RPCs. + SNAPSHOT_METADATA_SERVICE = 4 [(alpha_enum_value) = true]; + } + Type type = 1; + } + + message VolumeExpansion { + enum Type { + UNKNOWN = 0; + + // ONLINE indicates that volumes may be expanded when published to + // a node. When a Plugin implements this capability it MUST + // implement either the EXPAND_VOLUME controller capability or the + // EXPAND_VOLUME node capability or both. When a plugin supports + // ONLINE volume expansion and also has the EXPAND_VOLUME + // controller capability then the plugin MUST support expansion of + // volumes currently published and available on a node. When a + // plugin supports ONLINE volume expansion and also has the + // EXPAND_VOLUME node capability then the plugin MAY support + // expansion of node-published volume via NodeExpandVolume. + // + // Example 1: Given a shared filesystem volume (e.g. GlusterFs), + // the Plugin may set the ONLINE volume expansion capability and + // implement ControllerExpandVolume but not NodeExpandVolume. + // + // Example 2: Given a block storage volume type (e.g. EBS), the + // Plugin may set the ONLINE volume expansion capability and + // implement both ControllerExpandVolume and NodeExpandVolume. + // + // Example 3: Given a Plugin that supports volume expansion only + // upon a node, the Plugin may set the ONLINE volume + // expansion capability and implement NodeExpandVolume but not + // ControllerExpandVolume. + ONLINE = 1; + + // OFFLINE indicates that volumes currently published and + // available on a node SHALL NOT be expanded via + // ControllerExpandVolume. When a plugin supports OFFLINE volume + // expansion it MUST implement either the EXPAND_VOLUME controller + // capability or both the EXPAND_VOLUME controller capability and + // the EXPAND_VOLUME node capability. + // + // Example 1: Given a block storage volume type (e.g. Azure Disk) + // that does not support expansion of "node-attached" (i.e. + // controller-published) volumes, the Plugin may indicate + // OFFLINE volume expansion support and implement both + // ControllerExpandVolume and NodeExpandVolume. + OFFLINE = 2; + } + Type type = 1; + } + + oneof type { + // Service that the plugin supports. + Service service = 1; + VolumeExpansion volume_expansion = 2; + } +} +message ProbeRequest { + // Intentionally empty. +} + +message ProbeResponse { + // Readiness allows a plugin to report its initialization status back + // to the CO. Initialization for some plugins MAY be time consuming + // and it is important for a CO to distinguish between the following + // cases: + // + // 1) The plugin is in an unhealthy state and MAY need restarting. In + // this case a gRPC error code SHALL be returned. + // 2) The plugin is still initializing, but is otherwise perfectly + // healthy. In this case a successful response SHALL be returned + // with a readiness value of `false`. Calls to the plugin's + // Controller and/or Node services MAY fail due to an incomplete + // initialization state. + // 3) The plugin has finished initializing and is ready to service + // calls to its Controller and/or Node services. A successful + // response is returned with a readiness value of `true`. + // + // This field is OPTIONAL. If not present, the caller SHALL assume + // that the plugin is in a ready state and is accepting calls to its + // Controller and/or Node services (according to the plugin's reported + // capabilities). + .google.protobuf.BoolValue ready = 1; +} +message CreateVolumeRequest { + // The suggested name for the storage space. This field is REQUIRED. + // It serves two purposes: + // 1) Idempotency - This name is generated by the CO to achieve + // idempotency. The Plugin SHOULD ensure that multiple + // `CreateVolume` calls for the same name do not result in more + // than one piece of storage provisioned corresponding to that + // name. If a Plugin is unable to enforce idempotency, the CO's + // error recovery logic could result in multiple (unused) volumes + // being provisioned. + // In the case of error, the CO MUST handle the gRPC error codes + // per the recovery behavior defined in the "CreateVolume Errors" + // section below. + // The CO is responsible for cleaning up volumes it provisioned + // that it no longer needs. If the CO is uncertain whether a volume + // was provisioned or not when a `CreateVolume` call fails, the CO + // MAY call `CreateVolume` again, with the same name, to ensure the + // volume exists and to retrieve the volume's `volume_id` (unless + // otherwise prohibited by "CreateVolume Errors"). + // 2) Suggested name - Some storage systems allow callers to specify + // an identifier by which to refer to the newly provisioned + // storage. If a storage system supports this, it can optionally + // use this name as the identifier for the new volume. + // Any Unicode string that conforms to the length limit is allowed + // except those containing the following banned characters: + // U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F. + // (These are control characters other than commonly used whitespace.) + string name = 1; + + // This field is OPTIONAL. This allows the CO to specify the capacity + // requirement of the volume to be provisioned. If not specified, the + // Plugin MAY choose an implementation-defined capacity range. If + // specified it MUST always be honored, even when creating volumes + // from a source; which MAY force some backends to internally extend + // the volume after creating it. + CapacityRange capacity_range = 2; + + // The capabilities that the provisioned volume MUST have. SP MUST + // provision a volume that will satisfy ALL of the capabilities + // specified in this list. Otherwise SP MUST return the appropriate + // gRPC error code. + // The Plugin MUST assume that the CO MAY use the provisioned volume + // with ANY of the capabilities specified in this list. + // For example, a CO MAY specify two volume capabilities: one with + // access mode SINGLE_NODE_WRITER and another with access mode + // MULTI_NODE_READER_ONLY. In this case, the SP MUST verify that the + // provisioned volume can be used in either mode. + // This also enables the CO to do early validation: If ANY of the + // specified volume capabilities are not supported by the SP, the call + // MUST return the appropriate gRPC error code. + // This field is REQUIRED. + repeated VolumeCapability volume_capabilities = 3; + + // Plugin specific creation-time parameters passed in as opaque + // key-value pairs. This field is OPTIONAL. The Plugin is responsible + // for parsing and validating these parameters. COs will treat + // these as opaque. + map parameters = 4; + + // Secrets required by plugin to complete volume creation request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // If specified, the new volume will be pre-populated with data from + // this source. This field is OPTIONAL. + VolumeContentSource volume_content_source = 6; + + // Specifies where (regions, zones, racks, etc.) the provisioned + // volume MUST be accessible from. + // An SP SHALL advertise the requirements for topological + // accessibility information in documentation. COs SHALL only specify + // topological accessibility information supported by the SP. + // This field is OPTIONAL. + // This field SHALL NOT be specified unless the SP has the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability. + // If this field is not specified and the SP has the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability, the SP MAY + // choose where the provisioned volume is accessible from. + TopologyRequirement accessibility_requirements = 7; + + // Plugin specific creation-time parameters passed in as opaque + // key-value pairs. These mutable_parameteres MAY also be + // changed during the lifetime of the volume via a subsequent + // `ControllerModifyVolume` RPC. This field is OPTIONAL. + // The Plugin is responsible for parsing and validating these + // parameters. COs will treat these as opaque. + + // Plugins MUST treat these + // as if they take precedence over the parameters field. + // This field SHALL NOT be specified unless the SP has the + // MODIFY_VOLUME plugin capability. + map mutable_parameters = 8 [(alpha_field) = true]; +} + +// Specifies what source the volume will be created from. One of the +// type fields MUST be specified. +message VolumeContentSource { + message SnapshotSource { + // Contains identity information for the existing source snapshot. + // This field is REQUIRED. Plugin is REQUIRED to support creating + // volume from snapshot if it supports the capability + // CREATE_DELETE_SNAPSHOT. + string snapshot_id = 1; + } + + message VolumeSource { + // Contains identity information for the existing source volume. + // This field is REQUIRED. Plugins reporting CLONE_VOLUME + // capability MUST support creating a volume from another volume. + string volume_id = 1; + } + + oneof type { + SnapshotSource snapshot = 1; + VolumeSource volume = 2; + } +} + +message CreateVolumeResponse { + // Contains all attributes of the newly created volume that are + // relevant to the CO along with information required by the Plugin + // to uniquely identify the volume. This field is REQUIRED. + Volume volume = 1; +} + +// Specify a capability of a volume. +message VolumeCapability { + // Indicate that the volume will be accessed via the block device API. + message BlockVolume { + // Intentionally empty, for now. + } + + // Indicate that the volume will be accessed via the filesystem API. + message MountVolume { + // The filesystem type. This field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string fs_type = 1; + + // The mount options that can be used for the volume. This field is + // OPTIONAL. `mount_flags` MAY contain sensitive information. + // Therefore, the CO and the Plugin MUST NOT leak this information + // to untrusted entities. The total size of this repeated field + // SHALL NOT exceed 4 KiB. + repeated string mount_flags = 2; + + // If SP has VOLUME_MOUNT_GROUP node capability and CO provides + // this field then SP MUST ensure that the volume_mount_group + // parameter is passed as the group identifier to the underlying + // operating system mount system call, with the understanding + // that the set of available mount call parameters and/or + // mount implementations may vary across operating systems. + // Additionally, new file and/or directory entries written to + // the underlying filesystem SHOULD be permission-labeled in such a + // manner, unless otherwise modified by a workload, that they are + // both readable and writable by said mount group identifier. + // This is an OPTIONAL field. + string volume_mount_group = 3; + } + + // Specify how a volume can be accessed. + message AccessMode { + enum Mode { + UNKNOWN = 0; + + // Can only be published once as read/write on a single node, at + // any given time. + SINGLE_NODE_WRITER = 1; + + // Can only be published once as readonly on a single node, at + // any given time. + SINGLE_NODE_READER_ONLY = 2; + + // Can be published as readonly at multiple nodes simultaneously. + MULTI_NODE_READER_ONLY = 3; + + // Can be published at multiple nodes simultaneously. Only one of + // the node can be used as read/write. The rest will be readonly. + MULTI_NODE_SINGLE_WRITER = 4; + + // Can be published as read/write at multiple nodes + // simultaneously. + MULTI_NODE_MULTI_WRITER = 5; + + // Can only be published once as read/write at a single workload + // on a single node, at any given time. SHOULD be used instead of + // SINGLE_NODE_WRITER for COs using the experimental + // SINGLE_NODE_MULTI_WRITER capability. + SINGLE_NODE_SINGLE_WRITER = 6 [(alpha_enum_value) = true]; + + // Can be published as read/write at multiple workloads on a + // single node simultaneously. SHOULD be used instead of + // SINGLE_NODE_WRITER for COs using the experimental + // SINGLE_NODE_MULTI_WRITER capability. + SINGLE_NODE_MULTI_WRITER = 7 [(alpha_enum_value) = true]; + } + + // This field is REQUIRED. + Mode mode = 1; + } + + // Specifies what API the volume will be accessed using. One of the + // following fields MUST be specified. + oneof access_type { + BlockVolume block = 1; + MountVolume mount = 2; + } + + // This is a REQUIRED field. + AccessMode access_mode = 3; +} + +// The capacity of the storage space in bytes. To specify an exact size, +// `required_bytes` and `limit_bytes` SHALL be set to the same value. At +// least one of the these fields MUST be specified. +message CapacityRange { + // Volume MUST be at least this big. This field is OPTIONAL. + // A value of 0 is equal to an unspecified field value. + // The value of this field MUST NOT be negative. + int64 required_bytes = 1; + + // Volume MUST not be bigger than this. This field is OPTIONAL. + // A value of 0 is equal to an unspecified field value. + // The value of this field MUST NOT be negative. + int64 limit_bytes = 2; +} + +// Information about a specific volume. +message Volume { + // The capacity of the volume in bytes. This field is OPTIONAL. If not + // set (value of 0), it indicates that the capacity of the volume is + // unknown (e.g., NFS share). + // The value of this field MUST NOT be negative. + int64 capacity_bytes = 1; + + // The identifier for this volume, generated by the plugin. + // This field is REQUIRED. + // This field MUST contain enough information to uniquely identify + // this specific volume vs all other volumes supported by this plugin. + // This field SHALL be used by the CO in subsequent calls to refer to + // this volume. + // The SP is NOT responsible for global uniqueness of volume_id across + // multiple SPs. + string volume_id = 2; + + // Opaque static properties of the volume. SP MAY use this field to + // ensure subsequent volume validation and publishing calls have + // contextual information. + // The contents of this field SHALL be opaque to a CO. + // The contents of this field SHALL NOT be mutable. + // The contents of this field SHALL be safe for the CO to cache. + // The contents of this field SHOULD NOT contain sensitive + // information. + // The contents of this field SHOULD NOT be used for uniquely + // identifying a volume. The `volume_id` alone SHOULD be sufficient to + // identify the volume. + // A volume uniquely identified by `volume_id` SHALL always report the + // same volume_context. + // This field is OPTIONAL and when present MUST be passed to volume + // validation and publishing calls. + map volume_context = 3; + + // If specified, indicates that the volume is not empty and is + // pre-populated with data from the specified source. + // This field is OPTIONAL. + VolumeContentSource content_source = 4; + + // Specifies where (regions, zones, racks, etc.) the provisioned + // volume is accessible from. + // A plugin that returns this field MUST also set the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability. + // An SP MAY specify multiple topologies to indicate the volume is + // accessible from multiple locations. + // COs MAY use this information along with the topology information + // returned by NodeGetInfo to ensure that a given volume is accessible + // from a given node when scheduling workloads. + // This field is OPTIONAL. If it is not specified, the CO MAY assume + // the volume is equally accessible from all nodes in the cluster and + // MAY schedule workloads referencing the volume on any available + // node. + // + // Example 1: + // accessible_topology = {"region": "R1", "zone": "Z2"} + // Indicates a volume accessible only from the "region" "R1" and the + // "zone" "Z2". + // + // Example 2: + // accessible_topology = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"} + // Indicates a volume accessible from both "zone" "Z2" and "zone" "Z3" + // in the "region" "R1". + repeated Topology accessible_topology = 5; +} + +message TopologyRequirement { + // Specifies the list of topologies the provisioned volume MUST be + // accessible from. + // This field is OPTIONAL. If TopologyRequirement is specified either + // requisite or preferred or both MUST be specified. + // + // If requisite is specified, the provisioned volume MUST be + // accessible from at least one of the requisite topologies. + // + // Given + // x = number of topologies provisioned volume is accessible from + // n = number of requisite topologies + // The CO MUST ensure n >= 1. The SP MUST ensure x >= 1 + // If x==n, then the SP MUST make the provisioned volume available to + // all topologies from the list of requisite topologies. If it is + // unable to do so, the SP MUST fail the CreateVolume call. + // For example, if a volume should be accessible from a single zone, + // and requisite = + // {"region": "R1", "zone": "Z2"} + // then the provisioned volume MUST be accessible from the "region" + // "R1" and the "zone" "Z2". + // Similarly, if a volume should be accessible from two zones, and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"} + // then the provisioned volume MUST be accessible from the "region" + // "R1" and both "zone" "Z2" and "zone" "Z3". + // + // If xn, then the SP MUST make the provisioned volume available from + // all topologies from the list of requisite topologies and MAY choose + // the remaining x-n unique topologies from the list of all possible + // topologies. If it is unable to do so, the SP MUST fail the + // CreateVolume call. + // For example, if a volume should be accessible from two zones, and + // requisite = + // {"region": "R1", "zone": "Z2"} + // then the provisioned volume MUST be accessible from the "region" + // "R1" and the "zone" "Z2" and the SP may select the second zone + // independently, e.g. "R1/Z4". + repeated Topology requisite = 1; + + // Specifies the list of topologies the CO would prefer the volume to + // be provisioned in. + // + // This field is OPTIONAL. If TopologyRequirement is specified either + // requisite or preferred or both MUST be specified. + // + // An SP MUST attempt to make the provisioned volume available using + // the preferred topologies in order from first to last. + // + // If requisite is specified, all topologies in preferred list MUST + // also be present in the list of requisite topologies. + // + // If the SP is unable to to make the provisioned volume available + // from any of the preferred topologies, the SP MAY choose a topology + // from the list of requisite topologies. + // If the list of requisite topologies is not specified, then the SP + // MAY choose from the list of all possible topologies. + // If the list of requisite topologies is specified and the SP is + // unable to to make the provisioned volume available from any of the + // requisite topologies it MUST fail the CreateVolume call. + // + // Example 1: + // Given a volume should be accessible from a single zone, and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"} + // preferred = + // {"region": "R1", "zone": "Z3"} + // then the SP SHOULD first attempt to make the provisioned volume + // available from "zone" "Z3" in the "region" "R1" and fall back to + // "zone" "Z2" in the "region" "R1" if that is not possible. + // + // Example 2: + // Given a volume should be accessible from a single zone, and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"}, + // {"region": "R1", "zone": "Z4"}, + // {"region": "R1", "zone": "Z5"} + // preferred = + // {"region": "R1", "zone": "Z4"}, + // {"region": "R1", "zone": "Z2"} + // then the SP SHOULD first attempt to make the provisioned volume + // accessible from "zone" "Z4" in the "region" "R1" and fall back to + // "zone" "Z2" in the "region" "R1" if that is not possible. If that + // is not possible, the SP may choose between either the "zone" + // "Z3" or "Z5" in the "region" "R1". + // + // Example 3: + // Given a volume should be accessible from TWO zones (because an + // opaque parameter in CreateVolumeRequest, for example, specifies + // the volume is accessible from two zones, aka synchronously + // replicated), and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"}, + // {"region": "R1", "zone": "Z4"}, + // {"region": "R1", "zone": "Z5"} + // preferred = + // {"region": "R1", "zone": "Z5"}, + // {"region": "R1", "zone": "Z3"} + // then the SP SHOULD first attempt to make the provisioned volume + // accessible from the combination of the two "zones" "Z5" and "Z3" in + // the "region" "R1". If that's not possible, it should fall back to + // a combination of "Z5" and other possibilities from the list of + // requisite. If that's not possible, it should fall back to a + // combination of "Z3" and other possibilities from the list of + // requisite. If that's not possible, it should fall back to a + // combination of other possibilities from the list of requisite. + repeated Topology preferred = 2; +} + +// Topology is a map of topological domains to topological segments. +// A topological domain is a sub-division of a cluster, like "region", +// "zone", "rack", etc. +// A topological segment is a specific instance of a topological domain, +// like "zone3", "rack3", etc. +// For example {"com.company/zone": "Z1", "com.company/rack": "R3"} +// Valid keys have two segments: an OPTIONAL prefix and name, separated +// by a slash (/), for example: "com.company.example/zone". +// The key name segment is REQUIRED. The prefix is OPTIONAL. +// The key name MUST be 63 characters or less, begin and end with an +// alphanumeric character ([a-z0-9A-Z]), and contain only dashes (-), +// underscores (_), dots (.), or alphanumerics in between, for example +// "zone". +// The key prefix MUST be 63 characters or less, begin and end with a +// lower-case alphanumeric character ([a-z0-9]), contain only +// dashes (-), dots (.), or lower-case alphanumerics in between, and +// follow domain name notation format +// (https://tools.ietf.org/html/rfc1035#section-2.3.1). +// The key prefix SHOULD include the plugin's host company name and/or +// the plugin name, to minimize the possibility of collisions with keys +// from other plugins. +// If a key prefix is specified, it MUST be identical across all +// topology keys returned by the SP (across all RPCs). +// Keys MUST be case-insensitive. Meaning the keys "Zone" and "zone" +// MUST not both exist. +// Each value (topological segment) MUST contain 1 or more strings. +// Each string MUST be 63 characters or less and begin and end with an +// alphanumeric character with '-', '_', '.', or alphanumerics in +// between. +message Topology { + map segments = 1; +} +message DeleteVolumeRequest { + // The ID of the volume to be deprovisioned. + // This field is REQUIRED. + string volume_id = 1; + + // Secrets required by plugin to complete volume deletion request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; +} + +message DeleteVolumeResponse { + // Intentionally empty. +} +message ControllerPublishVolumeRequest { + // The ID of the volume to be used on a node. + // This field is REQUIRED. + string volume_id = 1; + + // The ID of the node. This field is REQUIRED. The CO SHALL set this + // field to match the node ID returned by `NodeGetInfo`. + string node_id = 2; + + // Volume capability describing how the CO intends to use this volume. + // SP MUST ensure the CO can use the published volume as described. + // Otherwise SP MUST return the appropriate gRPC error code. + // This is a REQUIRED field. + VolumeCapability volume_capability = 3; + + // Indicates SP MUST publish the volume in readonly mode. + // CO MUST set this field to false if SP does not have the + // PUBLISH_READONLY controller capability. + // This is a REQUIRED field. + bool readonly = 4; + + // Secrets required by plugin to complete controller publish volume + // request. This field is OPTIONAL. Refer to the + // `Secrets Requirements` section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 6; +} + +message ControllerPublishVolumeResponse { + // Opaque static publish properties of the volume. SP MAY use this + // field to ensure subsequent `NodeStageVolume` or `NodePublishVolume` + // calls calls have contextual information. + // The contents of this field SHALL be opaque to a CO. + // The contents of this field SHALL NOT be mutable. + // The contents of this field SHALL be safe for the CO to cache. + // The contents of this field SHOULD NOT contain sensitive + // information. + // The contents of this field SHOULD NOT be used for uniquely + // identifying a volume. The `volume_id` alone SHOULD be sufficient to + // identify the volume. + // This field is OPTIONAL and when present MUST be passed to + // subsequent `NodeStageVolume` or `NodePublishVolume` calls + map publish_context = 1; +} +message ControllerUnpublishVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The ID of the node. This field is OPTIONAL. The CO SHOULD set this + // field to match the node ID returned by `NodeGetInfo` or leave it + // unset. If the value is set, the SP MUST unpublish the volume from + // the specified node. If the value is unset, the SP MUST unpublish + // the volume from all nodes it is published to. + string node_id = 2; + + // Secrets required by plugin to complete controller unpublish volume + // request. This SHOULD be the same secrets passed to the + // ControllerPublishVolume call for the specified volume. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 3 [(csi_secret) = true]; +} + +message ControllerUnpublishVolumeResponse { + // Intentionally empty. +} +message ValidateVolumeCapabilitiesRequest { + // The ID of the volume to check. This field is REQUIRED. + string volume_id = 1; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 2; + + // The capabilities that the CO wants to check for the volume. This + // call SHALL return "confirmed" only if all the volume capabilities + // specified below are supported. This field is REQUIRED. + repeated VolumeCapability volume_capabilities = 3; + + // See CreateVolumeRequest.parameters. + // This field is OPTIONAL. + map parameters = 4; + + // Secrets required by plugin to complete volume validation request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // See CreateVolumeRequest.mutable_parameters. + // This field is OPTIONAL. + map mutable_parameters = 6 [(alpha_field) = true]; +} + +message ValidateVolumeCapabilitiesResponse { + message Confirmed { + // Volume context validated by the plugin. + // This field is OPTIONAL. + map volume_context = 1; + + // Volume capabilities supported by the plugin. + // This field is REQUIRED. + repeated VolumeCapability volume_capabilities = 2; + + // The volume creation parameters validated by the plugin. + // This field is OPTIONAL. + map parameters = 3; + + // The volume creation mutable_parameters validated by the plugin. + // This field is OPTIONAL. + map mutable_parameters = 4 [(alpha_field) = true]; + } + + // Confirmed indicates to the CO the set of capabilities that the + // plugin has validated. This field SHALL only be set to a non-empty + // value for successful validation responses. + // For successful validation responses, the CO SHALL compare the + // fields of this message to the originally requested capabilities in + // order to guard against an older plugin reporting "valid" for newer + // capability fields that it does not yet understand. + // This field is OPTIONAL. + Confirmed confirmed = 1; + + // Message to the CO if `confirmed` above is empty. This field is + // OPTIONAL. + // An empty string is equal to an unspecified field value. + string message = 2; +} +message ListVolumesRequest { + // If specified (non-zero value), the Plugin MUST NOT return more + // entries than this number in the response. If the actual number of + // entries is more than this number, the Plugin MUST set `next_token` + // in the response which can be used to get the next page of entries + // in the subsequent `ListVolumes` call. This field is OPTIONAL. If + // not specified (zero value), it means there is no restriction on the + // number of entries that can be returned. + // The value of this field MUST NOT be negative. + int32 max_entries = 1; + + // A token to specify where to start paginating. Set this field to + // `next_token` returned by a previous `ListVolumes` call to get the + // next page of entries. This field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string starting_token = 2; +} + +message ListVolumesResponse { + message VolumeStatus{ + // A list of all `node_id` of nodes that the volume in this entry + // is controller published on. + // This field is OPTIONAL. If it is not specified and the SP has + // the LIST_VOLUMES_PUBLISHED_NODES controller capability, the CO + // MAY assume the volume is not controller published to any nodes. + // If the field is not specified and the SP does not have the + // LIST_VOLUMES_PUBLISHED_NODES controller capability, the CO MUST + // not interpret this field. + // published_node_ids MAY include nodes not published to or + // reported by the SP. The CO MUST be resilient to that. + repeated string published_node_ids = 1; + + // Information about the current condition of the volume. + // This field is OPTIONAL. + // This field MUST be specified if the + // VOLUME_CONDITION controller capability is supported. + VolumeCondition volume_condition = 2 [(alpha_field) = true]; + } + + message Entry { + // This field is REQUIRED + Volume volume = 1; + + // This field is OPTIONAL. This field MUST be specified if the + // LIST_VOLUMES_PUBLISHED_NODES controller capability is + // supported. + VolumeStatus status = 2; + } + + repeated Entry entries = 1; + + // This token allows you to get the next page of entries for + // `ListVolumes` request. If the number of entries is larger than + // `max_entries`, use the `next_token` as a value for the + // `starting_token` field in the next `ListVolumes` request. This + // field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string next_token = 2; +} +message ControllerGetVolumeRequest { + option (alpha_message) = true; + + // The ID of the volume to fetch current volume information for. + // This field is REQUIRED. + string volume_id = 1; +} + +message ControllerGetVolumeResponse { + option (alpha_message) = true; + + message VolumeStatus{ + // A list of all the `node_id` of nodes that this volume is + // controller published on. + // This field is OPTIONAL. + // This field MUST be specified if the LIST_VOLUMES_PUBLISHED_NODES + // controller capability is supported. + // published_node_ids MAY include nodes not published to or + // reported by the SP. The CO MUST be resilient to that. + repeated string published_node_ids = 1; + + // Information about the current condition of the volume. + // This field is OPTIONAL. + // This field MUST be specified if the + // VOLUME_CONDITION controller capability is supported. + VolumeCondition volume_condition = 2; + } + + // This field is REQUIRED + Volume volume = 1; + + // This field is REQUIRED. + VolumeStatus status = 2; +} +message ControllerModifyVolumeRequest { + option (alpha_message) = true; + + // Contains identity information for the existing volume. + // This field is REQUIRED. + string volume_id = 1; + + // Secrets required by plugin to complete modify volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; + + // Plugin specific volume attributes to mutate, passed in as + // opaque key-value pairs. + // This field is REQUIRED. The Plugin is responsible for + // parsing and validating these parameters. COs will treat these + // as opaque. The CO SHOULD specify the intended values of all mutable + // parameters it intends to modify. SPs MUST NOT modify volumes based + // on the absence of keys, only keys that are specified should result + // in modifications to the volume. + map mutable_parameters = 3; +} + +message ControllerModifyVolumeResponse { + option (alpha_message) = true; +} + +message GetCapacityRequest { + // If specified, the Plugin SHALL report the capacity of the storage + // that can be used to provision volumes that satisfy ALL of the + // specified `volume_capabilities`. These are the same + // `volume_capabilities` the CO will use in `CreateVolumeRequest`. + // This field is OPTIONAL. + repeated VolumeCapability volume_capabilities = 1; + + // If specified, the Plugin SHALL report the capacity of the storage + // that can be used to provision volumes with the given Plugin + // specific `parameters`. These are the same `parameters` the CO will + // use in `CreateVolumeRequest`. This field is OPTIONAL. + map parameters = 2; + + // If specified, the Plugin SHALL report the capacity of the storage + // that can be used to provision volumes that in the specified + // `accessible_topology`. This is the same as the + // `accessible_topology` the CO returns in a `CreateVolumeResponse`. + // This field is OPTIONAL. This field SHALL NOT be set unless the + // plugin advertises the VOLUME_ACCESSIBILITY_CONSTRAINTS capability. + Topology accessible_topology = 3; +} + +message GetCapacityResponse { + // The available capacity, in bytes, of the storage that can be used + // to provision volumes. If `volume_capabilities` or `parameters` is + // specified in the request, the Plugin SHALL take those into + // consideration when calculating the available capacity of the + // storage. This field is REQUIRED. + // The value of this field MUST NOT be negative. + int64 available_capacity = 1; + + // The largest size that may be used in a + // CreateVolumeRequest.capacity_range.required_bytes field + // to create a volume with the same parameters as those in + // GetCapacityRequest. + // + // If `volume_capabilities` or `parameters` is + // specified in the request, the Plugin SHALL take those into + // consideration when calculating the minimum volume size of the + // storage. + // + // This field is OPTIONAL. MUST NOT be negative. + // The Plugin SHOULD provide a value for this field if it has + // a maximum size for individual volumes and leave it unset + // otherwise. COs MAY use it to make decision about + // where to create volumes. + google.protobuf.Int64Value maximum_volume_size = 2; + + // The smallest size that may be used in a + // CreateVolumeRequest.capacity_range.limit_bytes field + // to create a volume with the same parameters as those in + // GetCapacityRequest. + // + // If `volume_capabilities` or `parameters` is + // specified in the request, the Plugin SHALL take those into + // consideration when calculating the maximum volume size of the + // storage. + // + // This field is OPTIONAL. MUST NOT be negative. + // The Plugin SHOULD provide a value for this field if it has + // a minimum size for individual volumes and leave it unset + // otherwise. COs MAY use it to make decision about + // where to create volumes. + google.protobuf.Int64Value minimum_volume_size = 3 + [(alpha_field) = true]; +} +message ControllerGetCapabilitiesRequest { + // Intentionally empty. +} + +message ControllerGetCapabilitiesResponse { + // All the capabilities that the controller service supports. This + // field is OPTIONAL. + repeated ControllerServiceCapability capabilities = 1; +} + +// Specifies a capability of the controller service. +message ControllerServiceCapability { + message RPC { + enum Type { + UNKNOWN = 0; + CREATE_DELETE_VOLUME = 1; + PUBLISH_UNPUBLISH_VOLUME = 2; + LIST_VOLUMES = 3; + GET_CAPACITY = 4; + // Currently the only way to consume a snapshot is to create + // a volume from it. Therefore plugins supporting + // CREATE_DELETE_SNAPSHOT MUST support creating volume from + // snapshot. + CREATE_DELETE_SNAPSHOT = 5; + LIST_SNAPSHOTS = 6; + + // Plugins supporting volume cloning at the storage level MAY + // report this capability. The source volume MUST be managed by + // the same plugin. Not all volume sources and parameters + // combinations MAY work. + CLONE_VOLUME = 7; + + // Indicates the SP supports ControllerPublishVolume.readonly + // field. + PUBLISH_READONLY = 8; + + // See VolumeExpansion for details. + EXPAND_VOLUME = 9; + + // Indicates the SP supports the + // ListVolumesResponse.entry.published_node_ids field and the + // ControllerGetVolumeResponse.published_node_ids field. + // The SP MUST also support PUBLISH_UNPUBLISH_VOLUME. + LIST_VOLUMES_PUBLISHED_NODES = 10; + + // Indicates that the Controller service can report volume + // conditions. + // An SP MAY implement `VolumeCondition` in only the Controller + // Plugin, only the Node Plugin, or both. + // If `VolumeCondition` is implemented in both the Controller and + // Node Plugins, it SHALL report from different perspectives. + // If for some reason Controller and Node Plugins report + // misaligned volume conditions, CO SHALL assume the worst case + // is the truth. + // Note that, for alpha, `VolumeCondition` is intended be + // informative for humans only, not for automation. + VOLUME_CONDITION = 11 [(alpha_enum_value) = true]; + + // Indicates the SP supports the ControllerGetVolume RPC. + // This enables COs to, for example, fetch per volume + // condition after a volume is provisioned. + GET_VOLUME = 12 [(alpha_enum_value) = true]; + + // Indicates the SP supports the SINGLE_NODE_SINGLE_WRITER and/or + // SINGLE_NODE_MULTI_WRITER access modes. + // These access modes are intended to replace the + // SINGLE_NODE_WRITER access mode to clarify the number of writers + // for a volume on a single node. Plugins MUST accept and allow + // use of the SINGLE_NODE_WRITER access mode when either + // SINGLE_NODE_SINGLE_WRITER and/or SINGLE_NODE_MULTI_WRITER are + // supported, in order to permit older COs to continue working. + SINGLE_NODE_MULTI_WRITER = 13 [(alpha_enum_value) = true]; + + // Indicates the SP supports modifying volume with mutable + // parameters. See ControllerModifyVolume for details. + MODIFY_VOLUME = 14 [(alpha_enum_value) = true]; + } + + Type type = 1; + } + + oneof type { + // RPC that the controller supports. + RPC rpc = 1; + } +} +message CreateSnapshotRequest { + // The ID of the source volume to be snapshotted. + // This field is REQUIRED. + string source_volume_id = 1; + + // The suggested name for the snapshot. This field is REQUIRED for + // idempotency. + // Any Unicode string that conforms to the length limit is allowed + // except those containing the following banned characters: + // U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F. + // (These are control characters other than commonly used whitespace.) + string name = 2; + + // Secrets required by plugin to complete snapshot creation request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 3 [(csi_secret) = true]; + + // Plugin specific parameters passed in as opaque key-value pairs. + // This field is OPTIONAL. The Plugin is responsible for parsing and + // validating these parameters. COs will treat these as opaque. + // Use cases for opaque parameters: + // - Specify a policy to automatically clean up the snapshot. + // - Specify an expiration date for the snapshot. + // - Specify whether the snapshot is readonly or read/write. + // - Specify if the snapshot should be replicated to some place. + // - Specify primary or secondary for replication systems that + // support snapshotting only on primary. + map parameters = 4; +} + +message CreateSnapshotResponse { + // Contains all attributes of the newly created snapshot that are + // relevant to the CO along with information required by the Plugin + // to uniquely identify the snapshot. This field is REQUIRED. + Snapshot snapshot = 1; +} + +// Information about a specific snapshot. +message Snapshot { + // This is the complete size of the snapshot in bytes. The purpose of + // this field is to give CO guidance on how much space is needed to + // create a volume from this snapshot. The size of the volume MUST NOT + // be less than the size of the source snapshot. This field is + // OPTIONAL. If this field is not set, it indicates that this size is + // unknown. The value of this field MUST NOT be negative and a size of + // zero means it is unspecified. + int64 size_bytes = 1; + + // The identifier for this snapshot, generated by the plugin. + // This field is REQUIRED. + // This field MUST contain enough information to uniquely identify + // this specific snapshot vs all other snapshots supported by this + // plugin. + // This field SHALL be used by the CO in subsequent calls to refer to + // this snapshot. + // The SP is NOT responsible for global uniqueness of snapshot_id + // across multiple SPs. + string snapshot_id = 2; + + // Identity information for the source volume. Note that creating a + // snapshot from a snapshot is not supported here so the source has to + // be a volume. This field is REQUIRED. + string source_volume_id = 3; + + // Timestamp when the point-in-time snapshot is taken on the storage + // system. This field is REQUIRED. + .google.protobuf.Timestamp creation_time = 4; + + // Indicates if a snapshot is ready to use as a + // `volume_content_source` in a `CreateVolumeRequest`. The default + // value is false. This field is REQUIRED. + bool ready_to_use = 5; + + // The ID of the volume group snapshot that this snapshot is part of. + // It uniquely identifies the group snapshot on the storage system. + // This field is OPTIONAL. + // If this snapshot is a member of a volume group snapshot, and it + // MUST NOT be deleted as a stand alone snapshot, then the SP + // MUST provide the ID of the volume group snapshot in this field. + // If provided, CO MUST use this field in subsequent volume group + // snapshot operations to indicate that this snapshot is part of the + // specified group snapshot. + // If not provided, CO SHALL treat the snapshot as independent, + // and SP SHALL allow it to be deleted separately. + // If this message is inside a VolumeGroupSnapshot message, the value + // MUST be the same as the group_snapshot_id in that message. + string group_snapshot_id = 6 [(alpha_field) = true]; +} +message DeleteSnapshotRequest { + // The ID of the snapshot to be deleted. + // This field is REQUIRED. + string snapshot_id = 1; + + // Secrets required by plugin to complete snapshot deletion request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; +} + +message DeleteSnapshotResponse {} +// List all snapshots on the storage system regardless of how they were +// created. +message ListSnapshotsRequest { + // If specified (non-zero value), the Plugin MUST NOT return more + // entries than this number in the response. If the actual number of + // entries is more than this number, the Plugin MUST set `next_token` + // in the response which can be used to get the next page of entries + // in the subsequent `ListSnapshots` call. This field is OPTIONAL. If + // not specified (zero value), it means there is no restriction on the + // number of entries that can be returned. + // The value of this field MUST NOT be negative. + int32 max_entries = 1; + + // A token to specify where to start paginating. Set this field to + // `next_token` returned by a previous `ListSnapshots` call to get the + // next page of entries. This field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string starting_token = 2; + + // Identity information for the source volume. This field is OPTIONAL. + // It can be used to list snapshots by volume. + string source_volume_id = 3; + + // Identity information for a specific snapshot. This field is + // OPTIONAL. It can be used to list only a specific snapshot. + // ListSnapshots will return with current snapshot information + // and will not block if the snapshot is being processed after + // it is cut. + string snapshot_id = 4; + + // Secrets required by plugin to complete ListSnapshot request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; +} + +message ListSnapshotsResponse { + message Entry { + Snapshot snapshot = 1; + } + + repeated Entry entries = 1; + + // This token allows you to get the next page of entries for + // `ListSnapshots` request. If the number of entries is larger than + // `max_entries`, use the `next_token` as a value for the + // `starting_token` field in the next `ListSnapshots` request. This + // field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string next_token = 2; +} +message ControllerExpandVolumeRequest { + // The ID of the volume to expand. This field is REQUIRED. + string volume_id = 1; + + // This allows CO to specify the capacity requirements of the volume + // after expansion. This field is REQUIRED. + CapacityRange capacity_range = 2; + + // Secrets required by the plugin for expanding the volume. + // This field is OPTIONAL. + map secrets = 3 [(csi_secret) = true]; + + // Volume capability describing how the CO intends to use this volume. + // This allows SP to determine if volume is being used as a block + // device or mounted file system. For example - if volume is + // being used as a block device - the SP MAY set + // node_expansion_required to false in ControllerExpandVolumeResponse + // to skip invocation of NodeExpandVolume on the node by the CO. + // This is an OPTIONAL field. + VolumeCapability volume_capability = 4; +} + +message ControllerExpandVolumeResponse { + // Capacity of volume after expansion. This field is REQUIRED. + int64 capacity_bytes = 1; + + // Whether node expansion is required for the volume. When true + // the CO MUST make NodeExpandVolume RPC call on the node. This field + // is REQUIRED. + bool node_expansion_required = 2; +} +message NodeStageVolumeRequest { + // The ID of the volume to publish. This field is REQUIRED. + string volume_id = 1; + + // The CO SHALL set this field to the value returned by + // `ControllerPublishVolume` if the corresponding Controller Plugin + // has `PUBLISH_UNPUBLISH_VOLUME` controller capability, and SHALL be + // left unset if the corresponding Controller Plugin does not have + // this capability. This is an OPTIONAL field. + map publish_context = 2; + + // The path to which the volume MAY be staged. It MUST be an + // absolute path in the root filesystem of the process serving this + // request, and MUST be a directory. The CO SHALL ensure that there + // is only one `staging_target_path` per volume. The CO SHALL ensure + // that the path is directory and that the process serving the + // request has `read` and `write` permission to that directory. The + // CO SHALL be responsible for creating the directory if it does not + // exist. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 3; + + // Volume capability describing how the CO intends to use this volume. + // SP MUST ensure the CO can use the staged volume as described. + // Otherwise SP MUST return the appropriate gRPC error code. + // This is a REQUIRED field. + VolumeCapability volume_capability = 4; + + // Secrets required by plugin to complete node stage volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 6; +} + +message NodeStageVolumeResponse { + // Intentionally empty. +} +message NodeUnstageVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The path at which the volume was staged. It MUST be an absolute + // path in the root filesystem of the process serving this request. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 2; +} + +message NodeUnstageVolumeResponse { + // Intentionally empty. +} +message NodePublishVolumeRequest { + // The ID of the volume to publish. This field is REQUIRED. + string volume_id = 1; + + // The CO SHALL set this field to the value returned by + // `ControllerPublishVolume` if the corresponding Controller Plugin + // has `PUBLISH_UNPUBLISH_VOLUME` controller capability, and SHALL be + // left unset if the corresponding Controller Plugin does not have + // this capability. This is an OPTIONAL field. + map publish_context = 2; + + // The path to which the volume was staged by `NodeStageVolume`. + // It MUST be an absolute path in the root filesystem of the process + // serving this request. + // It MUST be set if the Node Plugin implements the + // `STAGE_UNSTAGE_VOLUME` node capability. + // This is an OPTIONAL field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 3; + + // The path to which the volume will be published. It MUST be an + // absolute path in the root filesystem of the process serving this + // request. The CO SHALL ensure uniqueness of target_path per volume. + // The CO SHALL ensure that the parent directory of this path exists + // and that the process serving the request has `read` and `write` + // permissions to that parent directory. + // For volumes with an access type of block, the SP SHALL place the + // block device at target_path. + // For volumes with an access type of mount, the SP SHALL place the + // mounted directory at target_path. + // Creation of target_path is the responsibility of the SP. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string target_path = 4; + + // Volume capability describing how the CO intends to use this volume. + // SP MUST ensure the CO can use the published volume as described. + // Otherwise SP MUST return the appropriate gRPC error code. + // This is a REQUIRED field. + VolumeCapability volume_capability = 5; + + // Indicates SP MUST publish the volume in readonly mode. + // This field is REQUIRED. + bool readonly = 6; + + // Secrets required by plugin to complete node publish volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 7 [(csi_secret) = true]; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 8; +} + +message NodePublishVolumeResponse { + // Intentionally empty. +} +message NodeUnpublishVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The path at which the volume was published. It MUST be an absolute + // path in the root filesystem of the process serving this request. + // The SP MUST delete the file or directory it created at this path. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string target_path = 2; +} + +message NodeUnpublishVolumeResponse { + // Intentionally empty. +} +message NodeGetVolumeStatsRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // It can be any valid path where volume was previously + // staged or published. + // It MUST be an absolute path in the root filesystem of + // the process serving this request. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string volume_path = 2; + + // The path where the volume is staged, if the plugin has the + // STAGE_UNSTAGE_VOLUME capability, otherwise empty. + // If not empty, it MUST be an absolute path in the root + // filesystem of the process serving this request. + // This field is OPTIONAL. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 3; +} + +message NodeGetVolumeStatsResponse { + // This field is OPTIONAL. + repeated VolumeUsage usage = 1; + // Information about the current condition of the volume. + // This field is OPTIONAL. + // This field MUST be specified if the VOLUME_CONDITION node + // capability is supported. + VolumeCondition volume_condition = 2 [(alpha_field) = true]; +} + +message VolumeUsage { + enum Unit { + UNKNOWN = 0; + BYTES = 1; + INODES = 2; + } + // The available capacity in specified Unit. This field is OPTIONAL. + // The value of this field MUST NOT be negative. + int64 available = 1; + + // The total capacity in specified Unit. This field is REQUIRED. + // The value of this field MUST NOT be negative. + int64 total = 2; + + // The used capacity in specified Unit. This field is OPTIONAL. + // The value of this field MUST NOT be negative. + int64 used = 3; + + // Units by which values are measured. This field is REQUIRED. + Unit unit = 4; +} + +// VolumeCondition represents the current condition of a volume. +message VolumeCondition { + option (alpha_message) = true; + + // Normal volumes are available for use and operating optimally. + // An abnormal volume does not meet these criteria. + // This field is REQUIRED. + bool abnormal = 1; + + // The message describing the condition of the volume. + // This field is REQUIRED. + string message = 2; +} +message NodeGetCapabilitiesRequest { + // Intentionally empty. +} + +message NodeGetCapabilitiesResponse { + // All the capabilities that the node service supports. This field + // is OPTIONAL. + repeated NodeServiceCapability capabilities = 1; +} + +// Specifies a capability of the node service. +message NodeServiceCapability { + message RPC { + enum Type { + UNKNOWN = 0; + STAGE_UNSTAGE_VOLUME = 1; + // If Plugin implements GET_VOLUME_STATS capability + // then it MUST implement NodeGetVolumeStats RPC + // call for fetching volume statistics. + GET_VOLUME_STATS = 2; + // See VolumeExpansion for details. + EXPAND_VOLUME = 3; + // Indicates that the Node service can report volume conditions. + // An SP MAY implement `VolumeCondition` in only the Node + // Plugin, only the Controller Plugin, or both. + // If `VolumeCondition` is implemented in both the Node and + // Controller Plugins, it SHALL report from different + // perspectives. + // If for some reason Node and Controller Plugins report + // misaligned volume conditions, CO SHALL assume the worst case + // is the truth. + // Note that, for alpha, `VolumeCondition` is intended to be + // informative for humans only, not for automation. + VOLUME_CONDITION = 4 [(alpha_enum_value) = true]; + + // Indicates the SP supports the SINGLE_NODE_SINGLE_WRITER and/or + // SINGLE_NODE_MULTI_WRITER access modes. + // These access modes are intended to replace the + // SINGLE_NODE_WRITER access mode to clarify the number of writers + // for a volume on a single node. Plugins MUST accept and allow + // use of the SINGLE_NODE_WRITER access mode (subject to the + // processing rules for NodePublishVolume), when either + // SINGLE_NODE_SINGLE_WRITER and/or SINGLE_NODE_MULTI_WRITER are + // supported, in order to permit older COs to continue working. + SINGLE_NODE_MULTI_WRITER = 5 [(alpha_enum_value) = true]; + + // Indicates that Node service supports mounting volumes + // with provided volume group identifier during node stage + // or node publish RPC calls. + VOLUME_MOUNT_GROUP = 6; + } + + Type type = 1; + } + + oneof type { + // RPC that the controller supports. + RPC rpc = 1; + } +} +message NodeGetInfoRequest { +} + +message NodeGetInfoResponse { + // The identifier of the node as understood by the SP. + // This field is REQUIRED. + // This field MUST contain enough information to uniquely identify + // this specific node vs all other nodes supported by this plugin. + // This field SHALL be used by the CO in subsequent calls, including + // `ControllerPublishVolume`, to refer to this node. + // The SP is NOT responsible for global uniqueness of node_id across + // multiple SPs. + // This field overrides the general CSI size limit. + // The size of this field SHALL NOT exceed 256 bytes. The general + // CSI size limit, 128 byte, is RECOMMENDED for best backwards + // compatibility. + string node_id = 1; + + // Maximum number of volumes that controller can publish to the node. + // If value is not set or zero CO SHALL decide how many volumes of + // this type can be published by the controller to the node. The + // plugin MUST NOT set negative values here. + // This field is OPTIONAL. + int64 max_volumes_per_node = 2; + + // Specifies where (regions, zones, racks, etc.) the node is + // accessible from. + // A plugin that returns this field MUST also set the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability. + // COs MAY use this information along with the topology information + // returned in CreateVolumeResponse to ensure that a given volume is + // accessible from a given node when scheduling workloads. + // This field is OPTIONAL. If it is not specified, the CO MAY assume + // the node is not subject to any topological constraint, and MAY + // schedule workloads that reference any volume V, such that there are + // no topological constraints declared for V. + // + // Example 1: + // accessible_topology = + // {"region": "R1", "zone": "Z2"} + // Indicates the node exists within the "region" "R1" and the "zone" + // "Z2". + Topology accessible_topology = 3; +} +message NodeExpandVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The path on which volume is available. This field is REQUIRED. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string volume_path = 2; + + // This allows CO to specify the capacity requirements of the volume + // after expansion. If capacity_range is omitted then a plugin MAY + // inspect the file system of the volume to determine the maximum + // capacity to which the volume can be expanded. In such cases a + // plugin MAY expand the volume to its maximum capacity. + // This field is OPTIONAL. + CapacityRange capacity_range = 3; + + // The path where the volume is staged, if the plugin has the + // STAGE_UNSTAGE_VOLUME capability, otherwise empty. + // If not empty, it MUST be an absolute path in the root + // filesystem of the process serving this request. + // This field is OPTIONAL. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 4; + + // Volume capability describing how the CO intends to use this volume. + // This allows SP to determine if volume is being used as a block + // device or mounted file system. For example - if volume is being + // used as a block device the SP MAY choose to skip expanding the + // filesystem in NodeExpandVolume implementation but still perform + // rest of the housekeeping needed for expanding the volume. If + // volume_capability is omitted the SP MAY determine + // access_type from given volume_path for the volume and perform + // node expansion. This is an OPTIONAL field. + VolumeCapability volume_capability = 5; + + // Secrets required by plugin to complete node expand volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 6 + [(csi_secret) = true, (alpha_field) = true]; +} + +message NodeExpandVolumeResponse { + // The capacity of the volume in bytes. This field is OPTIONAL. + int64 capacity_bytes = 1; +} +message GroupControllerGetCapabilitiesRequest { + option (alpha_message) = true; + + // Intentionally empty. +} + +message GroupControllerGetCapabilitiesResponse { + option (alpha_message) = true; + + // All the capabilities that the group controller service supports. + // This field is OPTIONAL. + repeated GroupControllerServiceCapability capabilities = 1; +} + +// Specifies a capability of the group controller service. +message GroupControllerServiceCapability { + option (alpha_message) = true; + + message RPC { + enum Type { + UNKNOWN = 0; + + // Indicates that the group controller plugin supports + // creating, deleting, and getting details of a volume + // group snapshot. + CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT = 1 + [(alpha_enum_value) = true]; + } + + Type type = 1; + } + + oneof type { + // RPC that the controller supports. + RPC rpc = 1; + } +} +message CreateVolumeGroupSnapshotRequest { + option (alpha_message) = true; + + // The suggested name for the group snapshot. This field is REQUIRED + // for idempotency. + // Any Unicode string that conforms to the length limit is allowed + // except those containing the following banned characters: + // U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F. + // (These are control characters other than commonly used whitespace.) + string name = 1; + + // volume IDs of the source volumes to be snapshotted together. + // This field is REQUIRED. + repeated string source_volume_ids = 2; + + // Secrets required by plugin to complete + // ControllerCreateVolumeGroupSnapshot request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + // The secrets provided in this field SHOULD be the same for + // all group snapshot operations on the same group snapshot. + map secrets = 3 [(csi_secret) = true]; + + // Plugin specific parameters passed in as opaque key-value pairs. + // This field is OPTIONAL. The Plugin is responsible for parsing and + // validating these parameters. COs will treat these as opaque. + map parameters = 4; +} + +message CreateVolumeGroupSnapshotResponse { + option (alpha_message) = true; + + // Contains all attributes of the newly created group snapshot. + // This field is REQUIRED. + VolumeGroupSnapshot group_snapshot = 1; +} + +message VolumeGroupSnapshot { + option (alpha_message) = true; + + // The identifier for this group snapshot, generated by the plugin. + // This field MUST contain enough information to uniquely identify + // this specific snapshot vs all other group snapshots supported by + // this plugin. + // This field SHALL be used by the CO in subsequent calls to refer to + // this group snapshot. + // The SP is NOT responsible for global uniqueness of + // group_snapshot_id across multiple SPs. + // This field is REQUIRED. + string group_snapshot_id = 1; + + // A list of snapshots belonging to this group. + // This field is REQUIRED. + repeated Snapshot snapshots = 2; + + // Timestamp of when the volume group snapshot was taken. + // This field is REQUIRED. + .google.protobuf.Timestamp creation_time = 3; + + // Indicates if all individual snapshots in the group snapshot + // are ready to use as a `volume_content_source` in a + // `CreateVolumeRequest`. The default value is false. + // If any snapshot in the list of snapshots in this message have + // ready_to_use set to false, the SP MUST set this field to false. + // If all of the snapshots in the list of snapshots in this message + // have ready_to_use set to true, the SP SHOULD set this field to + // true. + // This field is REQUIRED. + bool ready_to_use = 4; +} +message DeleteVolumeGroupSnapshotRequest { + option (alpha_message) = true; + + // The ID of the group snapshot to be deleted. + // This field is REQUIRED. + string group_snapshot_id = 1; + + // A list of snapshot IDs that are part of this group snapshot. + // If SP does not need to rely on this field to delete the snapshots + // in the group, it SHOULD check this field and report an error + // if it has the ability to detect a mismatch. + // Some SPs require this list to delete the snapshots in the group. + // If SP needs to use this field to delete the snapshots in the + // group, it MUST report an error if it has the ability to detect + // a mismatch. + // This field is REQUIRED. + repeated string snapshot_ids = 2; + + // Secrets required by plugin to complete group snapshot deletion + // request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + // The secrets provided in this field SHOULD be the same for + // all group snapshot operations on the same group snapshot. + map secrets = 3 [(csi_secret) = true]; +} + +message DeleteVolumeGroupSnapshotResponse { + // Intentionally empty. + option (alpha_message) = true; +} +message GetVolumeGroupSnapshotRequest { + option (alpha_message) = true; + + // The ID of the group snapshot to fetch current group snapshot + // information for. + // This field is REQUIRED. + string group_snapshot_id = 1; + + // A list of snapshot IDs that are part of this group snapshot. + // If SP does not need to rely on this field to get the snapshots + // in the group, it SHOULD check this field and report an error + // if it has the ability to detect a mismatch. + // Some SPs require this list to get the snapshots in the group. + // If SP needs to use this field to get the snapshots in the + // group, it MUST report an error if it has the ability to detect + // a mismatch. + // This field is REQUIRED. + repeated string snapshot_ids = 2; + + // Secrets required by plugin to complete + // GetVolumeGroupSnapshot request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + // The secrets provided in this field SHOULD be the same for + // all group snapshot operations on the same group snapshot. + map secrets = 3 [(csi_secret) = true]; +} + +message GetVolumeGroupSnapshotResponse { + option (alpha_message) = true; + + // This field is REQUIRED + VolumeGroupSnapshot group_snapshot = 1; +} +// BlockMetadata specifies a data range. +message BlockMetadata { + // This is the zero based byte position in the volume or snapshot, + // measured from the start of the object. + // This field is REQUIRED. + int64 byte_offset = 1; + + // This is the size of the data range. + // size_bytes MUST be greater than zero. + // This field is REQUIRED. + int64 size_bytes = 2; +} +enum BlockMetadataType { + UNKNOWN = 0; + + // The FIXED_LENGTH value indicates that data ranges are + // returned in fixed size blocks. + FIXED_LENGTH = 1; + + // The VARIABLE_LENGTH value indicates that data ranges + // are returned in potentially variable sized extents. + VARIABLE_LENGTH = 2; +} +// The GetMetadataAllocatedRequest message is used to solicit metadata +// on the allocated blocks of a snapshot: i.e. this identifies the +// data ranges that have valid data as they were the target of some +// previous write operation on the volume. +message GetMetadataAllocatedRequest { + // This is the identifier of the snapshot. + // This field is REQUIRED. + string snapshot_id = 1; + + // This indicates the zero based starting byte position in the volume + // snapshot from which the result should be computed. + // It is intended to be used to continue a previously interrupted + // call. + // The CO SHOULD specify this value to be the offset of the byte + // position immediately after the last byte of the last data range + // received, if continuing an interrupted operation, or zero if not. + // The SP MUST ensure that the returned response stream does not + // contain BlockMetadata tuples that end before the requested + // starting_offset: i.e. if S is the requested starting_offset, and + // B0 is block_metadata[0] of the first message in the response + // stream, then (S < B0.byte_offset + B0.size_bytes) must be true. + // This field is REQUIRED. + int64 starting_offset = 2; + + // This is an optional parameter, and if non-zero it specifies the + // maximum number of tuples to be returned in each + // GetMetadataAllocatedResponse message returned by the RPC stream. + // The plugin will determine an appropriate value if 0, and is + // always free to send less than the requested value. + // This field is OPTIONAL. + int32 max_results = 3; + + // Secrets required by plugin to complete the request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 4 [(csi_secret) = true]; +} + +// GetMetadataAllocatedResponse messages are returned in a gRPC stream. +// Cumulatively, they provide information on the allocated data +// ranges in the snapshot. +message GetMetadataAllocatedResponse { + // This specifies the style used in the BlockMetadata sequence. + // This value must be the same in all such messages returned by + // the stream. + // If block_metadata_type is FIXED_LENGTH, then the size_bytes field + // of each message in the block_metadata list MUST be constant. + // This field is REQUIRED. + BlockMetadataType block_metadata_type = 1; + + // This returns the capacity of the underlying volume in bytes. + // This value must be the same in all such messages returned by + // the stream. + // This field is REQUIRED. + int64 volume_capacity_bytes = 2; + + // This is a list of data range tuples. + // If the value of max_results in the GetMetadataAllocatedRequest + // message is greater than zero, then the number of entries in this + // list MUST be less than or equal to that value. + // The SP MUST respect the value of starting_offset in the request. + // The byte_offset fields of adjacent BlockMetadata messages + // MUST be strictly increasing and messages MUST NOT overlap: + // i.e. for any two BlockMetadata messages, A and B, if A is returned + // before B, then (A.byte_offset + A.size_bytes <= B.byte_offset) + // MUST be true. + // This MUST also be true if A and B are from block_metadata lists in + // different GetMetadataAllocatedResponse messages in the gRPC stream. + // This field is OPTIONAL. + repeated BlockMetadata block_metadata = 3; +} +// The GetMetadataDeltaRequest message is used to solicit metadata on +// the data ranges that have changed between two snapshots. +message GetMetadataDeltaRequest { + // This is the identifier of the snapshot against which changes + // are to be computed. + // This field is REQUIRED. + string base_snapshot_id = 1; + + // This is the identifier of a second snapshot in the same volume, + // created after the base snapshot. + // This field is REQUIRED. + string target_snapshot_id = 2; + + // This indicates the zero based starting byte position in the volume + // snapshot from which the result should be computed. + // It is intended to be used to continue a previously interrupted + // call. + // The CO SHOULD specify this value to be the offset of the byte + // position immediately after the last byte of the last data range + // received, if continuing an interrupted operation, or zero if not. + // The SP MUST ensure that the returned response stream does not + // contain BlockMetadata tuples that end before the requested + // starting_offset: i.e. if S is the requested starting_offset, and + // B0 is block_metadata[0] of the first message in the response + // stream, then (S < B0.byte_offset + B0.size_bytes) must be true. + // This field is REQUIRED. + int64 starting_offset = 3; + + // This is an optional parameter, and if non-zero it specifies the + // maximum number of tuples to be returned in each + // GetMetadataDeltaResponse message returned by the RPC stream. + // The plugin will determine an appropriate value if 0, and is + // always free to send less than the requested value. + // This field is OPTIONAL. + int32 max_results = 4; + + // Secrets required by plugin to complete the request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; +} + +// GetMetadataDeltaResponse messages are returned in a gRPC stream. +// Cumulatively, they provide information on the data ranges that +// have changed between the base and target snapshots specified +// in the GetMetadataDeltaRequest message. +message GetMetadataDeltaResponse { + // This specifies the style used in the BlockMetadata sequence. + // This value must be the same in all such messages returned by + // the stream. + // If block_metadata_type is FIXED_LENGTH, then the size_bytes field + // of each message in the block_metadata list MUST be constant. + // This field is REQUIRED. + BlockMetadataType block_metadata_type = 1; + + // This returns the capacity of the underlying volume in bytes. + // This value must be the same in all such messages returned by + // the stream. + // This field is REQUIRED. + int64 volume_capacity_bytes = 2; + + // This is a list of data range tuples. + // If the value of max_results in the GetMetadataDeltaRequest message + // is greater than zero, then the number of entries in this list MUST + // be less than or equal to that value. + // The SP MUST respect the value of starting_offset in the request. + // The byte_offset fields of adjacent BlockMetadata messages + // MUST be strictly increasing and messages MUST NOT overlap: + // i.e. for any two BlockMetadata messages, A and B, if A is returned + // before B, then (A.byte_offset + A.size_bytes <= B.byte_offset) + // MUST be true. + // This MUST also be true if A and B are from block_metadata lists in + // different GetMetadataDeltaResponse messages in the gRPC stream. + // This field is OPTIONAL. + repeated BlockMetadata block_metadata = 3; +} diff --git a/csi_proto/csi-v1.11.0.proto b/csi_proto/csi-v1.11.0.proto new file mode 100644 index 0000000..0f8a793 --- /dev/null +++ b/csi_proto/csi-v1.11.0.proto @@ -0,0 +1,2078 @@ +// Code generated by make; DO NOT EDIT. +syntax = "proto3"; +package csi.v1; + +import "google/protobuf/descriptor.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +option go_package = + "github.com/container-storage-interface/spec/lib/go/csi"; + +extend google.protobuf.EnumOptions { + // Indicates that this enum is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_enum = 1060; +} +extend google.protobuf.EnumValueOptions { + // Indicates that this enum value is OPTIONAL and part of an + // experimental API that may be deprecated and eventually removed + // between minor releases. + bool alpha_enum_value = 1060; +} +extend google.protobuf.FieldOptions { + // Indicates that a field MAY contain information that is sensitive + // and MUST be treated as such (e.g. not logged). + bool csi_secret = 1059; + + // Indicates that this field is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_field = 1060; +} +extend google.protobuf.MessageOptions { + // Indicates that this message is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_message = 1060; +} +extend google.protobuf.MethodOptions { + // Indicates that this method is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_method = 1060; +} +extend google.protobuf.ServiceOptions { + // Indicates that this service is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_service = 1060; +} +service Identity { + rpc GetPluginInfo(GetPluginInfoRequest) + returns (GetPluginInfoResponse) {} + + rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) + returns (GetPluginCapabilitiesResponse) {} + + rpc Probe (ProbeRequest) + returns (ProbeResponse) {} +} + +service Controller { + rpc CreateVolume (CreateVolumeRequest) + returns (CreateVolumeResponse) {} + + rpc DeleteVolume (DeleteVolumeRequest) + returns (DeleteVolumeResponse) {} + + rpc ControllerPublishVolume (ControllerPublishVolumeRequest) + returns (ControllerPublishVolumeResponse) {} + + rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest) + returns (ControllerUnpublishVolumeResponse) {} + + rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest) + returns (ValidateVolumeCapabilitiesResponse) {} + + rpc ListVolumes (ListVolumesRequest) + returns (ListVolumesResponse) {} + + rpc GetCapacity (GetCapacityRequest) + returns (GetCapacityResponse) {} + + rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest) + returns (ControllerGetCapabilitiesResponse) {} + + rpc CreateSnapshot (CreateSnapshotRequest) + returns (CreateSnapshotResponse) {} + + rpc DeleteSnapshot (DeleteSnapshotRequest) + returns (DeleteSnapshotResponse) {} + + rpc ListSnapshots (ListSnapshotsRequest) + returns (ListSnapshotsResponse) {} + + rpc ControllerExpandVolume (ControllerExpandVolumeRequest) + returns (ControllerExpandVolumeResponse) {} + + rpc ControllerGetVolume (ControllerGetVolumeRequest) + returns (ControllerGetVolumeResponse) { + option (alpha_method) = true; + } + + rpc ControllerModifyVolume (ControllerModifyVolumeRequest) + returns (ControllerModifyVolumeResponse) { + option (alpha_method) = true; + } +} + +service GroupController { + rpc GroupControllerGetCapabilities ( + GroupControllerGetCapabilitiesRequest) + returns (GroupControllerGetCapabilitiesResponse) {} + + rpc CreateVolumeGroupSnapshot(CreateVolumeGroupSnapshotRequest) + returns (CreateVolumeGroupSnapshotResponse) { + } + + rpc DeleteVolumeGroupSnapshot(DeleteVolumeGroupSnapshotRequest) + returns (DeleteVolumeGroupSnapshotResponse) { + } + + rpc GetVolumeGroupSnapshot( + GetVolumeGroupSnapshotRequest) + returns (GetVolumeGroupSnapshotResponse) { + } +} + +service SnapshotMetadata { + option (alpha_service) = true; + + rpc GetMetadataAllocated(GetMetadataAllocatedRequest) + returns (stream GetMetadataAllocatedResponse) {} + + rpc GetMetadataDelta(GetMetadataDeltaRequest) + returns (stream GetMetadataDeltaResponse) {} +} + +service Node { + rpc NodeStageVolume (NodeStageVolumeRequest) + returns (NodeStageVolumeResponse) {} + + rpc NodeUnstageVolume (NodeUnstageVolumeRequest) + returns (NodeUnstageVolumeResponse) {} + + rpc NodePublishVolume (NodePublishVolumeRequest) + returns (NodePublishVolumeResponse) {} + + rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest) + returns (NodeUnpublishVolumeResponse) {} + + rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest) + returns (NodeGetVolumeStatsResponse) {} + + + rpc NodeExpandVolume(NodeExpandVolumeRequest) + returns (NodeExpandVolumeResponse) {} + + + rpc NodeGetCapabilities (NodeGetCapabilitiesRequest) + returns (NodeGetCapabilitiesResponse) {} + + rpc NodeGetInfo (NodeGetInfoRequest) + returns (NodeGetInfoResponse) {} +} +message GetPluginInfoRequest { + // Intentionally empty. +} + +message GetPluginInfoResponse { + // The name MUST follow domain name notation format + // (https://tools.ietf.org/html/rfc1035#section-2.3.1). It SHOULD + // include the plugin's host company name and the plugin name, + // to minimize the possibility of collisions. It MUST be 63 + // characters or less, beginning and ending with an alphanumeric + // character ([a-z0-9A-Z]) with dashes (-), dots (.), and + // alphanumerics between. This field is REQUIRED. + string name = 1; + + // This field is REQUIRED. Value of this field is opaque to the CO. + string vendor_version = 2; + + // This field is OPTIONAL. Values are opaque to the CO. + map manifest = 3; +} +message GetPluginCapabilitiesRequest { + // Intentionally empty. +} + +message GetPluginCapabilitiesResponse { + // All the capabilities that the controller service supports. This + // field is OPTIONAL. + repeated PluginCapability capabilities = 1; +} + +// Specifies a capability of the plugin. +message PluginCapability { + message Service { + enum Type { + UNKNOWN = 0; + // CONTROLLER_SERVICE indicates that the Plugin provides RPCs for + // the ControllerService. Plugins SHOULD provide this capability. + // In rare cases certain plugins MAY wish to omit the + // ControllerService entirely from their implementation, but such + // SHOULD NOT be the common case. + // The presence of this capability determines whether the CO will + // attempt to invoke the REQUIRED ControllerService RPCs, as well + // as specific RPCs as indicated by ControllerGetCapabilities. + CONTROLLER_SERVICE = 1; + + // VOLUME_ACCESSIBILITY_CONSTRAINTS indicates that the volumes for + // this plugin MAY NOT be equally accessible by all nodes in the + // cluster. The CO MUST use the topology information returned by + // CreateVolumeRequest along with the topology information + // returned by NodeGetInfo to ensure that a given volume is + // accessible from a given node when scheduling workloads. + VOLUME_ACCESSIBILITY_CONSTRAINTS = 2; + + // GROUP_CONTROLLER_SERVICE indicates that the Plugin provides + // RPCs for operating on groups of volumes. Plugins MAY provide + // this capability. + // The presence of this capability determines whether the CO will + // attempt to invoke the REQUIRED GroupController service RPCs, as + // well as specific RPCs as indicated by + // GroupControllerGetCapabilities. + GROUP_CONTROLLER_SERVICE = 3; + + // SNAPSHOT_METADATA_SERVICE indicates that the Plugin provides + // RPCs to retrieve metadata on the allocated blocks of a single + // snapshot, or the changed blocks between a pair of snapshots of + // the same block volume. + // The presence of this capability determines whether the CO will + // attempt to invoke the OPTIONAL SnapshotMetadata service RPCs. + SNAPSHOT_METADATA_SERVICE = 4 [(alpha_enum_value) = true]; + } + Type type = 1; + } + + message VolumeExpansion { + enum Type { + UNKNOWN = 0; + + // ONLINE indicates that volumes may be expanded when published to + // a node. When a Plugin implements this capability it MUST + // implement either the EXPAND_VOLUME controller capability or the + // EXPAND_VOLUME node capability or both. When a plugin supports + // ONLINE volume expansion and also has the EXPAND_VOLUME + // controller capability then the plugin MUST support expansion of + // volumes currently published and available on a node. When a + // plugin supports ONLINE volume expansion and also has the + // EXPAND_VOLUME node capability then the plugin MAY support + // expansion of node-published volume via NodeExpandVolume. + // + // Example 1: Given a shared filesystem volume (e.g. GlusterFs), + // the Plugin may set the ONLINE volume expansion capability and + // implement ControllerExpandVolume but not NodeExpandVolume. + // + // Example 2: Given a block storage volume type (e.g. EBS), the + // Plugin may set the ONLINE volume expansion capability and + // implement both ControllerExpandVolume and NodeExpandVolume. + // + // Example 3: Given a Plugin that supports volume expansion only + // upon a node, the Plugin may set the ONLINE volume + // expansion capability and implement NodeExpandVolume but not + // ControllerExpandVolume. + ONLINE = 1; + + // OFFLINE indicates that volumes currently published and + // available on a node SHALL NOT be expanded via + // ControllerExpandVolume. When a plugin supports OFFLINE volume + // expansion it MUST implement either the EXPAND_VOLUME controller + // capability or both the EXPAND_VOLUME controller capability and + // the EXPAND_VOLUME node capability. + // + // Example 1: Given a block storage volume type (e.g. Azure Disk) + // that does not support expansion of "node-attached" (i.e. + // controller-published) volumes, the Plugin may indicate + // OFFLINE volume expansion support and implement both + // ControllerExpandVolume and NodeExpandVolume. + OFFLINE = 2; + } + Type type = 1; + } + + oneof type { + // Service that the plugin supports. + Service service = 1; + VolumeExpansion volume_expansion = 2; + } +} +message ProbeRequest { + // Intentionally empty. +} + +message ProbeResponse { + // Readiness allows a plugin to report its initialization status back + // to the CO. Initialization for some plugins MAY be time consuming + // and it is important for a CO to distinguish between the following + // cases: + // + // 1) The plugin is in an unhealthy state and MAY need restarting. In + // this case a gRPC error code SHALL be returned. + // 2) The plugin is still initializing, but is otherwise perfectly + // healthy. In this case a successful response SHALL be returned + // with a readiness value of `false`. Calls to the plugin's + // Controller and/or Node services MAY fail due to an incomplete + // initialization state. + // 3) The plugin has finished initializing and is ready to service + // calls to its Controller and/or Node services. A successful + // response is returned with a readiness value of `true`. + // + // This field is OPTIONAL. If not present, the caller SHALL assume + // that the plugin is in a ready state and is accepting calls to its + // Controller and/or Node services (according to the plugin's reported + // capabilities). + .google.protobuf.BoolValue ready = 1; +} +message CreateVolumeRequest { + // The suggested name for the storage space. This field is REQUIRED. + // It serves two purposes: + // 1) Idempotency - This name is generated by the CO to achieve + // idempotency. The Plugin SHOULD ensure that multiple + // `CreateVolume` calls for the same name do not result in more + // than one piece of storage provisioned corresponding to that + // name. If a Plugin is unable to enforce idempotency, the CO's + // error recovery logic could result in multiple (unused) volumes + // being provisioned. + // In the case of error, the CO MUST handle the gRPC error codes + // per the recovery behavior defined in the "CreateVolume Errors" + // section below. + // The CO is responsible for cleaning up volumes it provisioned + // that it no longer needs. If the CO is uncertain whether a volume + // was provisioned or not when a `CreateVolume` call fails, the CO + // MAY call `CreateVolume` again, with the same name, to ensure the + // volume exists and to retrieve the volume's `volume_id` (unless + // otherwise prohibited by "CreateVolume Errors"). + // 2) Suggested name - Some storage systems allow callers to specify + // an identifier by which to refer to the newly provisioned + // storage. If a storage system supports this, it can optionally + // use this name as the identifier for the new volume. + // Any Unicode string that conforms to the length limit is allowed + // except those containing the following banned characters: + // U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F. + // (These are control characters other than commonly used whitespace.) + string name = 1; + + // This field is OPTIONAL. This allows the CO to specify the capacity + // requirement of the volume to be provisioned. If not specified, the + // Plugin MAY choose an implementation-defined capacity range. If + // specified it MUST always be honored, even when creating volumes + // from a source; which MAY force some backends to internally extend + // the volume after creating it. + CapacityRange capacity_range = 2; + + // The capabilities that the provisioned volume MUST have. SP MUST + // provision a volume that will satisfy ALL of the capabilities + // specified in this list. Otherwise SP MUST return the appropriate + // gRPC error code. + // The Plugin MUST assume that the CO MAY use the provisioned volume + // with ANY of the capabilities specified in this list. + // For example, a CO MAY specify two volume capabilities: one with + // access mode SINGLE_NODE_WRITER and another with access mode + // MULTI_NODE_READER_ONLY. In this case, the SP MUST verify that the + // provisioned volume can be used in either mode. + // This also enables the CO to do early validation: If ANY of the + // specified volume capabilities are not supported by the SP, the call + // MUST return the appropriate gRPC error code. + // This field is REQUIRED. + repeated VolumeCapability volume_capabilities = 3; + + // Plugin specific creation-time parameters passed in as opaque + // key-value pairs. This field is OPTIONAL. The Plugin is responsible + // for parsing and validating these parameters. COs will treat + // these as opaque. + map parameters = 4; + + // Secrets required by plugin to complete volume creation request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // If specified, the new volume will be pre-populated with data from + // this source. This field is OPTIONAL. + VolumeContentSource volume_content_source = 6; + + // Specifies where (regions, zones, racks, etc.) the provisioned + // volume MUST be accessible from. + // An SP SHALL advertise the requirements for topological + // accessibility information in documentation. COs SHALL only specify + // topological accessibility information supported by the SP. + // This field is OPTIONAL. + // This field SHALL NOT be specified unless the SP has the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability. + // If this field is not specified and the SP has the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability, the SP MAY + // choose where the provisioned volume is accessible from. + TopologyRequirement accessibility_requirements = 7; + + // Plugin specific creation-time parameters passed in as opaque + // key-value pairs. These mutable_parameteres MAY also be + // changed during the lifetime of the volume via a subsequent + // `ControllerModifyVolume` RPC. This field is OPTIONAL. + // The Plugin is responsible for parsing and validating these + // parameters. COs will treat these as opaque. + + // Plugins MUST treat these + // as if they take precedence over the parameters field. + // This field SHALL NOT be specified unless the SP has the + // MODIFY_VOLUME plugin capability. + map mutable_parameters = 8 [(alpha_field) = true]; +} + +// Specifies what source the volume will be created from. One of the +// type fields MUST be specified. +message VolumeContentSource { + message SnapshotSource { + // Contains identity information for the existing source snapshot. + // This field is REQUIRED. Plugin is REQUIRED to support creating + // volume from snapshot if it supports the capability + // CREATE_DELETE_SNAPSHOT. + string snapshot_id = 1; + } + + message VolumeSource { + // Contains identity information for the existing source volume. + // This field is REQUIRED. Plugins reporting CLONE_VOLUME + // capability MUST support creating a volume from another volume. + string volume_id = 1; + } + + oneof type { + SnapshotSource snapshot = 1; + VolumeSource volume = 2; + } +} + +message CreateVolumeResponse { + // Contains all attributes of the newly created volume that are + // relevant to the CO along with information required by the Plugin + // to uniquely identify the volume. This field is REQUIRED. + Volume volume = 1; +} + +// Specify a capability of a volume. +message VolumeCapability { + // Indicate that the volume will be accessed via the block device API. + message BlockVolume { + // Intentionally empty, for now. + } + + // Indicate that the volume will be accessed via the filesystem API. + message MountVolume { + // The filesystem type. This field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string fs_type = 1; + + // The mount options that can be used for the volume. This field is + // OPTIONAL. `mount_flags` MAY contain sensitive information. + // Therefore, the CO and the Plugin MUST NOT leak this information + // to untrusted entities. The total size of this repeated field + // SHALL NOT exceed 4 KiB. + repeated string mount_flags = 2; + + // If SP has VOLUME_MOUNT_GROUP node capability and CO provides + // this field then SP MUST ensure that the volume_mount_group + // parameter is passed as the group identifier to the underlying + // operating system mount system call, with the understanding + // that the set of available mount call parameters and/or + // mount implementations may vary across operating systems. + // Additionally, new file and/or directory entries written to + // the underlying filesystem SHOULD be permission-labeled in such a + // manner, unless otherwise modified by a workload, that they are + // both readable and writable by said mount group identifier. + // This is an OPTIONAL field. + string volume_mount_group = 3; + } + + // Specify how a volume can be accessed. + message AccessMode { + enum Mode { + UNKNOWN = 0; + + // Can only be published once as read/write on a single node, at + // any given time. + SINGLE_NODE_WRITER = 1; + + // Can only be published once as readonly on a single node, at + // any given time. + SINGLE_NODE_READER_ONLY = 2; + + // Can be published as readonly at multiple nodes simultaneously. + MULTI_NODE_READER_ONLY = 3; + + // Can be published at multiple nodes simultaneously. Only one of + // the node can be used as read/write. The rest will be readonly. + MULTI_NODE_SINGLE_WRITER = 4; + + // Can be published as read/write at multiple nodes + // simultaneously. + MULTI_NODE_MULTI_WRITER = 5; + + // Can only be published once as read/write at a single workload + // on a single node, at any given time. SHOULD be used instead of + // SINGLE_NODE_WRITER for COs using the experimental + // SINGLE_NODE_MULTI_WRITER capability. + SINGLE_NODE_SINGLE_WRITER = 6 [(alpha_enum_value) = true]; + + // Can be published as read/write at multiple workloads on a + // single node simultaneously. SHOULD be used instead of + // SINGLE_NODE_WRITER for COs using the experimental + // SINGLE_NODE_MULTI_WRITER capability. + SINGLE_NODE_MULTI_WRITER = 7 [(alpha_enum_value) = true]; + } + + // This field is REQUIRED. + Mode mode = 1; + } + + // Specifies what API the volume will be accessed using. One of the + // following fields MUST be specified. + oneof access_type { + BlockVolume block = 1; + MountVolume mount = 2; + } + + // This is a REQUIRED field. + AccessMode access_mode = 3; +} + +// The capacity of the storage space in bytes. To specify an exact size, +// `required_bytes` and `limit_bytes` SHALL be set to the same value. At +// least one of the these fields MUST be specified. +message CapacityRange { + // Volume MUST be at least this big. This field is OPTIONAL. + // A value of 0 is equal to an unspecified field value. + // The value of this field MUST NOT be negative. + int64 required_bytes = 1; + + // Volume MUST not be bigger than this. This field is OPTIONAL. + // A value of 0 is equal to an unspecified field value. + // The value of this field MUST NOT be negative. + int64 limit_bytes = 2; +} + +// Information about a specific volume. +message Volume { + // The capacity of the volume in bytes. This field is OPTIONAL. If not + // set (value of 0), it indicates that the capacity of the volume is + // unknown (e.g., NFS share). + // The value of this field MUST NOT be negative. + int64 capacity_bytes = 1; + + // The identifier for this volume, generated by the plugin. + // This field is REQUIRED. + // This field MUST contain enough information to uniquely identify + // this specific volume vs all other volumes supported by this plugin. + // This field SHALL be used by the CO in subsequent calls to refer to + // this volume. + // The SP is NOT responsible for global uniqueness of volume_id across + // multiple SPs. + string volume_id = 2; + + // Opaque static properties of the volume. SP MAY use this field to + // ensure subsequent volume validation and publishing calls have + // contextual information. + // The contents of this field SHALL be opaque to a CO. + // The contents of this field SHALL NOT be mutable. + // The contents of this field SHALL be safe for the CO to cache. + // The contents of this field SHOULD NOT contain sensitive + // information. + // The contents of this field SHOULD NOT be used for uniquely + // identifying a volume. The `volume_id` alone SHOULD be sufficient to + // identify the volume. + // A volume uniquely identified by `volume_id` SHALL always report the + // same volume_context. + // This field is OPTIONAL and when present MUST be passed to volume + // validation and publishing calls. + map volume_context = 3; + + // If specified, indicates that the volume is not empty and is + // pre-populated with data from the specified source. + // This field is OPTIONAL. + VolumeContentSource content_source = 4; + + // Specifies where (regions, zones, racks, etc.) the provisioned + // volume is accessible from. + // A plugin that returns this field MUST also set the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability. + // An SP MAY specify multiple topologies to indicate the volume is + // accessible from multiple locations. + // COs MAY use this information along with the topology information + // returned by NodeGetInfo to ensure that a given volume is accessible + // from a given node when scheduling workloads. + // This field is OPTIONAL. If it is not specified, the CO MAY assume + // the volume is equally accessible from all nodes in the cluster and + // MAY schedule workloads referencing the volume on any available + // node. + // + // Example 1: + // accessible_topology = {"region": "R1", "zone": "Z2"} + // Indicates a volume accessible only from the "region" "R1" and the + // "zone" "Z2". + // + // Example 2: + // accessible_topology = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"} + // Indicates a volume accessible from both "zone" "Z2" and "zone" "Z3" + // in the "region" "R1". + repeated Topology accessible_topology = 5; +} + +message TopologyRequirement { + // Specifies the list of topologies the provisioned volume MUST be + // accessible from. + // This field is OPTIONAL. If TopologyRequirement is specified either + // requisite or preferred or both MUST be specified. + // + // If requisite is specified, the provisioned volume MUST be + // accessible from at least one of the requisite topologies. + // + // Given + // x = number of topologies provisioned volume is accessible from + // n = number of requisite topologies + // The CO MUST ensure n >= 1. The SP MUST ensure x >= 1 + // If x==n, then the SP MUST make the provisioned volume available to + // all topologies from the list of requisite topologies. If it is + // unable to do so, the SP MUST fail the CreateVolume call. + // For example, if a volume should be accessible from a single zone, + // and requisite = + // {"region": "R1", "zone": "Z2"} + // then the provisioned volume MUST be accessible from the "region" + // "R1" and the "zone" "Z2". + // Similarly, if a volume should be accessible from two zones, and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"} + // then the provisioned volume MUST be accessible from the "region" + // "R1" and both "zone" "Z2" and "zone" "Z3". + // + // If xn, then the SP MUST make the provisioned volume available from + // all topologies from the list of requisite topologies and MAY choose + // the remaining x-n unique topologies from the list of all possible + // topologies. If it is unable to do so, the SP MUST fail the + // CreateVolume call. + // For example, if a volume should be accessible from two zones, and + // requisite = + // {"region": "R1", "zone": "Z2"} + // then the provisioned volume MUST be accessible from the "region" + // "R1" and the "zone" "Z2" and the SP may select the second zone + // independently, e.g. "R1/Z4". + repeated Topology requisite = 1; + + // Specifies the list of topologies the CO would prefer the volume to + // be provisioned in. + // + // This field is OPTIONAL. If TopologyRequirement is specified either + // requisite or preferred or both MUST be specified. + // + // An SP MUST attempt to make the provisioned volume available using + // the preferred topologies in order from first to last. + // + // If requisite is specified, all topologies in preferred list MUST + // also be present in the list of requisite topologies. + // + // If the SP is unable to to make the provisioned volume available + // from any of the preferred topologies, the SP MAY choose a topology + // from the list of requisite topologies. + // If the list of requisite topologies is not specified, then the SP + // MAY choose from the list of all possible topologies. + // If the list of requisite topologies is specified and the SP is + // unable to to make the provisioned volume available from any of the + // requisite topologies it MUST fail the CreateVolume call. + // + // Example 1: + // Given a volume should be accessible from a single zone, and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"} + // preferred = + // {"region": "R1", "zone": "Z3"} + // then the SP SHOULD first attempt to make the provisioned volume + // available from "zone" "Z3" in the "region" "R1" and fall back to + // "zone" "Z2" in the "region" "R1" if that is not possible. + // + // Example 2: + // Given a volume should be accessible from a single zone, and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"}, + // {"region": "R1", "zone": "Z4"}, + // {"region": "R1", "zone": "Z5"} + // preferred = + // {"region": "R1", "zone": "Z4"}, + // {"region": "R1", "zone": "Z2"} + // then the SP SHOULD first attempt to make the provisioned volume + // accessible from "zone" "Z4" in the "region" "R1" and fall back to + // "zone" "Z2" in the "region" "R1" if that is not possible. If that + // is not possible, the SP may choose between either the "zone" + // "Z3" or "Z5" in the "region" "R1". + // + // Example 3: + // Given a volume should be accessible from TWO zones (because an + // opaque parameter in CreateVolumeRequest, for example, specifies + // the volume is accessible from two zones, aka synchronously + // replicated), and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"}, + // {"region": "R1", "zone": "Z4"}, + // {"region": "R1", "zone": "Z5"} + // preferred = + // {"region": "R1", "zone": "Z5"}, + // {"region": "R1", "zone": "Z3"} + // then the SP SHOULD first attempt to make the provisioned volume + // accessible from the combination of the two "zones" "Z5" and "Z3" in + // the "region" "R1". If that's not possible, it should fall back to + // a combination of "Z5" and other possibilities from the list of + // requisite. If that's not possible, it should fall back to a + // combination of "Z3" and other possibilities from the list of + // requisite. If that's not possible, it should fall back to a + // combination of other possibilities from the list of requisite. + repeated Topology preferred = 2; +} + +// Topology is a map of topological domains to topological segments. +// A topological domain is a sub-division of a cluster, like "region", +// "zone", "rack", etc. +// A topological segment is a specific instance of a topological domain, +// like "zone3", "rack3", etc. +// For example {"com.company/zone": "Z1", "com.company/rack": "R3"} +// Valid keys have two segments: an OPTIONAL prefix and name, separated +// by a slash (/), for example: "com.company.example/zone". +// The key name segment is REQUIRED. The prefix is OPTIONAL. +// The key name MUST be 63 characters or less, begin and end with an +// alphanumeric character ([a-z0-9A-Z]), and contain only dashes (-), +// underscores (_), dots (.), or alphanumerics in between, for example +// "zone". +// The key prefix MUST be 63 characters or less, begin and end with a +// lower-case alphanumeric character ([a-z0-9]), contain only +// dashes (-), dots (.), or lower-case alphanumerics in between, and +// follow domain name notation format +// (https://tools.ietf.org/html/rfc1035#section-2.3.1). +// The key prefix SHOULD include the plugin's host company name and/or +// the plugin name, to minimize the possibility of collisions with keys +// from other plugins. +// If a key prefix is specified, it MUST be identical across all +// topology keys returned by the SP (across all RPCs). +// Keys MUST be case-insensitive. Meaning the keys "Zone" and "zone" +// MUST not both exist. +// Each value (topological segment) MUST contain 1 or more strings. +// Each string MUST be 63 characters or less and begin and end with an +// alphanumeric character with '-', '_', '.', or alphanumerics in +// between. +message Topology { + map segments = 1; +} +message DeleteVolumeRequest { + // The ID of the volume to be deprovisioned. + // This field is REQUIRED. + string volume_id = 1; + + // Secrets required by plugin to complete volume deletion request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; +} + +message DeleteVolumeResponse { + // Intentionally empty. +} +message ControllerPublishVolumeRequest { + // The ID of the volume to be used on a node. + // This field is REQUIRED. + string volume_id = 1; + + // The ID of the node. This field is REQUIRED. The CO SHALL set this + // field to match the node ID returned by `NodeGetInfo`. + string node_id = 2; + + // Volume capability describing how the CO intends to use this volume. + // SP MUST ensure the CO can use the published volume as described. + // Otherwise SP MUST return the appropriate gRPC error code. + // This is a REQUIRED field. + VolumeCapability volume_capability = 3; + + // Indicates SP MUST publish the volume in readonly mode. + // CO MUST set this field to false if SP does not have the + // PUBLISH_READONLY controller capability. + // This is a REQUIRED field. + bool readonly = 4; + + // Secrets required by plugin to complete controller publish volume + // request. This field is OPTIONAL. Refer to the + // `Secrets Requirements` section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 6; +} + +message ControllerPublishVolumeResponse { + // Opaque static publish properties of the volume. SP MAY use this + // field to ensure subsequent `NodeStageVolume` or `NodePublishVolume` + // calls calls have contextual information. + // The contents of this field SHALL be opaque to a CO. + // The contents of this field SHALL NOT be mutable. + // The contents of this field SHALL be safe for the CO to cache. + // The contents of this field SHOULD NOT contain sensitive + // information. + // The contents of this field SHOULD NOT be used for uniquely + // identifying a volume. The `volume_id` alone SHOULD be sufficient to + // identify the volume. + // This field is OPTIONAL and when present MUST be passed to + // subsequent `NodeStageVolume` or `NodePublishVolume` calls + map publish_context = 1; +} +message ControllerUnpublishVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The ID of the node. This field is OPTIONAL. The CO SHOULD set this + // field to match the node ID returned by `NodeGetInfo` or leave it + // unset. If the value is set, the SP MUST unpublish the volume from + // the specified node. If the value is unset, the SP MUST unpublish + // the volume from all nodes it is published to. + string node_id = 2; + + // Secrets required by plugin to complete controller unpublish volume + // request. This SHOULD be the same secrets passed to the + // ControllerPublishVolume call for the specified volume. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 3 [(csi_secret) = true]; +} + +message ControllerUnpublishVolumeResponse { + // Intentionally empty. +} +message ValidateVolumeCapabilitiesRequest { + // The ID of the volume to check. This field is REQUIRED. + string volume_id = 1; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 2; + + // The capabilities that the CO wants to check for the volume. This + // call SHALL return "confirmed" only if all the volume capabilities + // specified below are supported. This field is REQUIRED. + repeated VolumeCapability volume_capabilities = 3; + + // See CreateVolumeRequest.parameters. + // This field is OPTIONAL. + map parameters = 4; + + // Secrets required by plugin to complete volume validation request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // See CreateVolumeRequest.mutable_parameters. + // This field is OPTIONAL. + map mutable_parameters = 6 [(alpha_field) = true]; +} + +message ValidateVolumeCapabilitiesResponse { + message Confirmed { + // Volume context validated by the plugin. + // This field is OPTIONAL. + map volume_context = 1; + + // Volume capabilities supported by the plugin. + // This field is REQUIRED. + repeated VolumeCapability volume_capabilities = 2; + + // The volume creation parameters validated by the plugin. + // This field is OPTIONAL. + map parameters = 3; + + // The volume creation mutable_parameters validated by the plugin. + // This field is OPTIONAL. + map mutable_parameters = 4 [(alpha_field) = true]; + } + + // Confirmed indicates to the CO the set of capabilities that the + // plugin has validated. This field SHALL only be set to a non-empty + // value for successful validation responses. + // For successful validation responses, the CO SHALL compare the + // fields of this message to the originally requested capabilities in + // order to guard against an older plugin reporting "valid" for newer + // capability fields that it does not yet understand. + // This field is OPTIONAL. + Confirmed confirmed = 1; + + // Message to the CO if `confirmed` above is empty. This field is + // OPTIONAL. + // An empty string is equal to an unspecified field value. + string message = 2; +} +message ListVolumesRequest { + // If specified (non-zero value), the Plugin MUST NOT return more + // entries than this number in the response. If the actual number of + // entries is more than this number, the Plugin MUST set `next_token` + // in the response which can be used to get the next page of entries + // in the subsequent `ListVolumes` call. This field is OPTIONAL. If + // not specified (zero value), it means there is no restriction on the + // number of entries that can be returned. + // The value of this field MUST NOT be negative. + int32 max_entries = 1; + + // A token to specify where to start paginating. Set this field to + // `next_token` returned by a previous `ListVolumes` call to get the + // next page of entries. This field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string starting_token = 2; +} + +message ListVolumesResponse { + message VolumeStatus{ + // A list of all `node_id` of nodes that the volume in this entry + // is controller published on. + // This field is OPTIONAL. If it is not specified and the SP has + // the LIST_VOLUMES_PUBLISHED_NODES controller capability, the CO + // MAY assume the volume is not controller published to any nodes. + // If the field is not specified and the SP does not have the + // LIST_VOLUMES_PUBLISHED_NODES controller capability, the CO MUST + // not interpret this field. + // published_node_ids MAY include nodes not published to or + // reported by the SP. The CO MUST be resilient to that. + repeated string published_node_ids = 1; + + // Information about the current condition of the volume. + // This field is OPTIONAL. + // This field MUST be specified if the + // VOLUME_CONDITION controller capability is supported. + VolumeCondition volume_condition = 2 [(alpha_field) = true]; + } + + message Entry { + // This field is REQUIRED + Volume volume = 1; + + // This field is OPTIONAL. This field MUST be specified if the + // LIST_VOLUMES_PUBLISHED_NODES controller capability is + // supported. + VolumeStatus status = 2; + } + + repeated Entry entries = 1; + + // This token allows you to get the next page of entries for + // `ListVolumes` request. If the number of entries is larger than + // `max_entries`, use the `next_token` as a value for the + // `starting_token` field in the next `ListVolumes` request. This + // field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string next_token = 2; +} +message ControllerGetVolumeRequest { + option (alpha_message) = true; + + // The ID of the volume to fetch current volume information for. + // This field is REQUIRED. + string volume_id = 1; +} + +message ControllerGetVolumeResponse { + option (alpha_message) = true; + + message VolumeStatus{ + // A list of all the `node_id` of nodes that this volume is + // controller published on. + // This field is OPTIONAL. + // This field MUST be specified if the LIST_VOLUMES_PUBLISHED_NODES + // controller capability is supported. + // published_node_ids MAY include nodes not published to or + // reported by the SP. The CO MUST be resilient to that. + repeated string published_node_ids = 1; + + // Information about the current condition of the volume. + // This field is OPTIONAL. + // This field MUST be specified if the + // VOLUME_CONDITION controller capability is supported. + VolumeCondition volume_condition = 2; + } + + // This field is REQUIRED + Volume volume = 1; + + // This field is REQUIRED. + VolumeStatus status = 2; +} +message ControllerModifyVolumeRequest { + option (alpha_message) = true; + + // Contains identity information for the existing volume. + // This field is REQUIRED. + string volume_id = 1; + + // Secrets required by plugin to complete modify volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; + + // Plugin specific volume attributes to mutate, passed in as + // opaque key-value pairs. + // This field is REQUIRED. The Plugin is responsible for + // parsing and validating these parameters. COs will treat these + // as opaque. The CO SHOULD specify the intended values of all mutable + // parameters it intends to modify. SPs MUST NOT modify volumes based + // on the absence of keys, only keys that are specified should result + // in modifications to the volume. + map mutable_parameters = 3; +} + +message ControllerModifyVolumeResponse { + option (alpha_message) = true; +} + +message GetCapacityRequest { + // If specified, the Plugin SHALL report the capacity of the storage + // that can be used to provision volumes that satisfy ALL of the + // specified `volume_capabilities`. These are the same + // `volume_capabilities` the CO will use in `CreateVolumeRequest`. + // This field is OPTIONAL. + repeated VolumeCapability volume_capabilities = 1; + + // If specified, the Plugin SHALL report the capacity of the storage + // that can be used to provision volumes with the given Plugin + // specific `parameters`. These are the same `parameters` the CO will + // use in `CreateVolumeRequest`. This field is OPTIONAL. + map parameters = 2; + + // If specified, the Plugin SHALL report the capacity of the storage + // that can be used to provision volumes that in the specified + // `accessible_topology`. This is the same as the + // `accessible_topology` the CO returns in a `CreateVolumeResponse`. + // This field is OPTIONAL. This field SHALL NOT be set unless the + // plugin advertises the VOLUME_ACCESSIBILITY_CONSTRAINTS capability. + Topology accessible_topology = 3; +} + +message GetCapacityResponse { + // The available capacity, in bytes, of the storage that can be used + // to provision volumes. If `volume_capabilities` or `parameters` is + // specified in the request, the Plugin SHALL take those into + // consideration when calculating the available capacity of the + // storage. This field is REQUIRED. + // The value of this field MUST NOT be negative. + int64 available_capacity = 1; + + // The largest size that may be used in a + // CreateVolumeRequest.capacity_range.required_bytes field + // to create a volume with the same parameters as those in + // GetCapacityRequest. + // + // If `volume_capabilities` or `parameters` is + // specified in the request, the Plugin SHALL take those into + // consideration when calculating the minimum volume size of the + // storage. + // + // This field is OPTIONAL. MUST NOT be negative. + // The Plugin SHOULD provide a value for this field if it has + // a maximum size for individual volumes and leave it unset + // otherwise. COs MAY use it to make decision about + // where to create volumes. + google.protobuf.Int64Value maximum_volume_size = 2; + + // The smallest size that may be used in a + // CreateVolumeRequest.capacity_range.limit_bytes field + // to create a volume with the same parameters as those in + // GetCapacityRequest. + // + // If `volume_capabilities` or `parameters` is + // specified in the request, the Plugin SHALL take those into + // consideration when calculating the maximum volume size of the + // storage. + // + // This field is OPTIONAL. MUST NOT be negative. + // The Plugin SHOULD provide a value for this field if it has + // a minimum size for individual volumes and leave it unset + // otherwise. COs MAY use it to make decision about + // where to create volumes. + google.protobuf.Int64Value minimum_volume_size = 3 + [(alpha_field) = true]; +} +message ControllerGetCapabilitiesRequest { + // Intentionally empty. +} + +message ControllerGetCapabilitiesResponse { + // All the capabilities that the controller service supports. This + // field is OPTIONAL. + repeated ControllerServiceCapability capabilities = 1; +} + +// Specifies a capability of the controller service. +message ControllerServiceCapability { + message RPC { + enum Type { + UNKNOWN = 0; + CREATE_DELETE_VOLUME = 1; + PUBLISH_UNPUBLISH_VOLUME = 2; + LIST_VOLUMES = 3; + GET_CAPACITY = 4; + // Currently the only way to consume a snapshot is to create + // a volume from it. Therefore plugins supporting + // CREATE_DELETE_SNAPSHOT MUST support creating volume from + // snapshot. + CREATE_DELETE_SNAPSHOT = 5; + LIST_SNAPSHOTS = 6; + + // Plugins supporting volume cloning at the storage level MAY + // report this capability. The source volume MUST be managed by + // the same plugin. Not all volume sources and parameters + // combinations MAY work. + CLONE_VOLUME = 7; + + // Indicates the SP supports ControllerPublishVolume.readonly + // field. + PUBLISH_READONLY = 8; + + // See VolumeExpansion for details. + EXPAND_VOLUME = 9; + + // Indicates the SP supports the + // ListVolumesResponse.entry.published_node_ids field and the + // ControllerGetVolumeResponse.published_node_ids field. + // The SP MUST also support PUBLISH_UNPUBLISH_VOLUME. + LIST_VOLUMES_PUBLISHED_NODES = 10; + + // Indicates that the Controller service can report volume + // conditions. + // An SP MAY implement `VolumeCondition` in only the Controller + // Plugin, only the Node Plugin, or both. + // If `VolumeCondition` is implemented in both the Controller and + // Node Plugins, it SHALL report from different perspectives. + // If for some reason Controller and Node Plugins report + // misaligned volume conditions, CO SHALL assume the worst case + // is the truth. + // Note that, for alpha, `VolumeCondition` is intended be + // informative for humans only, not for automation. + VOLUME_CONDITION = 11 [(alpha_enum_value) = true]; + + // Indicates the SP supports the ControllerGetVolume RPC. + // This enables COs to, for example, fetch per volume + // condition after a volume is provisioned. + GET_VOLUME = 12 [(alpha_enum_value) = true]; + + // Indicates the SP supports the SINGLE_NODE_SINGLE_WRITER and/or + // SINGLE_NODE_MULTI_WRITER access modes. + // These access modes are intended to replace the + // SINGLE_NODE_WRITER access mode to clarify the number of writers + // for a volume on a single node. Plugins MUST accept and allow + // use of the SINGLE_NODE_WRITER access mode when either + // SINGLE_NODE_SINGLE_WRITER and/or SINGLE_NODE_MULTI_WRITER are + // supported, in order to permit older COs to continue working. + SINGLE_NODE_MULTI_WRITER = 13 [(alpha_enum_value) = true]; + + // Indicates the SP supports modifying volume with mutable + // parameters. See ControllerModifyVolume for details. + MODIFY_VOLUME = 14 [(alpha_enum_value) = true]; + } + + Type type = 1; + } + + oneof type { + // RPC that the controller supports. + RPC rpc = 1; + } +} +message CreateSnapshotRequest { + // The ID of the source volume to be snapshotted. + // This field is REQUIRED. + string source_volume_id = 1; + + // The suggested name for the snapshot. This field is REQUIRED for + // idempotency. + // Any Unicode string that conforms to the length limit is allowed + // except those containing the following banned characters: + // U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F. + // (These are control characters other than commonly used whitespace.) + string name = 2; + + // Secrets required by plugin to complete snapshot creation request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 3 [(csi_secret) = true]; + + // Plugin specific parameters passed in as opaque key-value pairs. + // This field is OPTIONAL. The Plugin is responsible for parsing and + // validating these parameters. COs will treat these as opaque. + // Use cases for opaque parameters: + // - Specify a policy to automatically clean up the snapshot. + // - Specify an expiration date for the snapshot. + // - Specify whether the snapshot is readonly or read/write. + // - Specify if the snapshot should be replicated to some place. + // - Specify primary or secondary for replication systems that + // support snapshotting only on primary. + map parameters = 4; +} + +message CreateSnapshotResponse { + // Contains all attributes of the newly created snapshot that are + // relevant to the CO along with information required by the Plugin + // to uniquely identify the snapshot. This field is REQUIRED. + Snapshot snapshot = 1; +} + +// Information about a specific snapshot. +message Snapshot { + // This is the complete size of the snapshot in bytes. The purpose of + // this field is to give CO guidance on how much space is needed to + // create a volume from this snapshot. The size of the volume MUST NOT + // be less than the size of the source snapshot. This field is + // OPTIONAL. If this field is not set, it indicates that this size is + // unknown. The value of this field MUST NOT be negative and a size of + // zero means it is unspecified. + int64 size_bytes = 1; + + // The identifier for this snapshot, generated by the plugin. + // This field is REQUIRED. + // This field MUST contain enough information to uniquely identify + // this specific snapshot vs all other snapshots supported by this + // plugin. + // This field SHALL be used by the CO in subsequent calls to refer to + // this snapshot. + // The SP is NOT responsible for global uniqueness of snapshot_id + // across multiple SPs. + string snapshot_id = 2; + + // Identity information for the source volume. Note that creating a + // snapshot from a snapshot is not supported here so the source has to + // be a volume. This field is REQUIRED. + string source_volume_id = 3; + + // Timestamp when the point-in-time snapshot is taken on the storage + // system. This field is REQUIRED. + .google.protobuf.Timestamp creation_time = 4; + + // Indicates if a snapshot is ready to use as a + // `volume_content_source` in a `CreateVolumeRequest`. The default + // value is false. This field is REQUIRED. + bool ready_to_use = 5; + + // The ID of the volume group snapshot that this snapshot is part of. + // It uniquely identifies the group snapshot on the storage system. + // This field is OPTIONAL. + // If this snapshot is a member of a volume group snapshot, and it + // MUST NOT be deleted as a stand alone snapshot, then the SP + // MUST provide the ID of the volume group snapshot in this field. + // If provided, CO MUST use this field in subsequent volume group + // snapshot operations to indicate that this snapshot is part of the + // specified group snapshot. + // If not provided, CO SHALL treat the snapshot as independent, + // and SP SHALL allow it to be deleted separately. + // If this message is inside a VolumeGroupSnapshot message, the value + // MUST be the same as the group_snapshot_id in that message. + string group_snapshot_id = 6; +} +message DeleteSnapshotRequest { + // The ID of the snapshot to be deleted. + // This field is REQUIRED. + string snapshot_id = 1; + + // Secrets required by plugin to complete snapshot deletion request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; +} + +message DeleteSnapshotResponse {} +// List all snapshots on the storage system regardless of how they were +// created. +message ListSnapshotsRequest { + // If specified (non-zero value), the Plugin MUST NOT return more + // entries than this number in the response. If the actual number of + // entries is more than this number, the Plugin MUST set `next_token` + // in the response which can be used to get the next page of entries + // in the subsequent `ListSnapshots` call. This field is OPTIONAL. If + // not specified (zero value), it means there is no restriction on the + // number of entries that can be returned. + // The value of this field MUST NOT be negative. + int32 max_entries = 1; + + // A token to specify where to start paginating. Set this field to + // `next_token` returned by a previous `ListSnapshots` call to get the + // next page of entries. This field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string starting_token = 2; + + // Identity information for the source volume. This field is OPTIONAL. + // It can be used to list snapshots by volume. + string source_volume_id = 3; + + // Identity information for a specific snapshot. This field is + // OPTIONAL. It can be used to list only a specific snapshot. + // ListSnapshots will return with current snapshot information + // and will not block if the snapshot is being processed after + // it is cut. + string snapshot_id = 4; + + // Secrets required by plugin to complete ListSnapshot request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; +} + +message ListSnapshotsResponse { + message Entry { + Snapshot snapshot = 1; + } + + repeated Entry entries = 1; + + // This token allows you to get the next page of entries for + // `ListSnapshots` request. If the number of entries is larger than + // `max_entries`, use the `next_token` as a value for the + // `starting_token` field in the next `ListSnapshots` request. This + // field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string next_token = 2; +} +message ControllerExpandVolumeRequest { + // The ID of the volume to expand. This field is REQUIRED. + string volume_id = 1; + + // This allows CO to specify the capacity requirements of the volume + // after expansion. This field is REQUIRED. + CapacityRange capacity_range = 2; + + // Secrets required by the plugin for expanding the volume. + // This field is OPTIONAL. + map secrets = 3 [(csi_secret) = true]; + + // Volume capability describing how the CO intends to use this volume. + // This allows SP to determine if volume is being used as a block + // device or mounted file system. For example - if volume is + // being used as a block device - the SP MAY set + // node_expansion_required to false in ControllerExpandVolumeResponse + // to skip invocation of NodeExpandVolume on the node by the CO. + // This is an OPTIONAL field. + VolumeCapability volume_capability = 4; +} + +message ControllerExpandVolumeResponse { + // Capacity of volume after expansion. This field is REQUIRED. + int64 capacity_bytes = 1; + + // Whether node expansion is required for the volume. When true + // the CO MUST make NodeExpandVolume RPC call on the node. This field + // is REQUIRED. + bool node_expansion_required = 2; +} +message NodeStageVolumeRequest { + // The ID of the volume to publish. This field is REQUIRED. + string volume_id = 1; + + // The CO SHALL set this field to the value returned by + // `ControllerPublishVolume` if the corresponding Controller Plugin + // has `PUBLISH_UNPUBLISH_VOLUME` controller capability, and SHALL be + // left unset if the corresponding Controller Plugin does not have + // this capability. This is an OPTIONAL field. + map publish_context = 2; + + // The path to which the volume MAY be staged. It MUST be an + // absolute path in the root filesystem of the process serving this + // request, and MUST be a directory. The CO SHALL ensure that there + // is only one `staging_target_path` per volume. The CO SHALL ensure + // that the path is directory and that the process serving the + // request has `read` and `write` permission to that directory. The + // CO SHALL be responsible for creating the directory if it does not + // exist. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 3; + + // Volume capability describing how the CO intends to use this volume. + // SP MUST ensure the CO can use the staged volume as described. + // Otherwise SP MUST return the appropriate gRPC error code. + // This is a REQUIRED field. + VolumeCapability volume_capability = 4; + + // Secrets required by plugin to complete node stage volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 6; +} + +message NodeStageVolumeResponse { + // Intentionally empty. +} +message NodeUnstageVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The path at which the volume was staged. It MUST be an absolute + // path in the root filesystem of the process serving this request. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 2; +} + +message NodeUnstageVolumeResponse { + // Intentionally empty. +} +message NodePublishVolumeRequest { + // The ID of the volume to publish. This field is REQUIRED. + string volume_id = 1; + + // The CO SHALL set this field to the value returned by + // `ControllerPublishVolume` if the corresponding Controller Plugin + // has `PUBLISH_UNPUBLISH_VOLUME` controller capability, and SHALL be + // left unset if the corresponding Controller Plugin does not have + // this capability. This is an OPTIONAL field. + map publish_context = 2; + + // The path to which the volume was staged by `NodeStageVolume`. + // It MUST be an absolute path in the root filesystem of the process + // serving this request. + // It MUST be set if the Node Plugin implements the + // `STAGE_UNSTAGE_VOLUME` node capability. + // This is an OPTIONAL field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 3; + + // The path to which the volume will be published. It MUST be an + // absolute path in the root filesystem of the process serving this + // request. The CO SHALL ensure uniqueness of target_path per volume. + // The CO SHALL ensure that the parent directory of this path exists + // and that the process serving the request has `read` and `write` + // permissions to that parent directory. + // For volumes with an access type of block, the SP SHALL place the + // block device at target_path. + // For volumes with an access type of mount, the SP SHALL place the + // mounted directory at target_path. + // Creation of target_path is the responsibility of the SP. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string target_path = 4; + + // Volume capability describing how the CO intends to use this volume. + // SP MUST ensure the CO can use the published volume as described. + // Otherwise SP MUST return the appropriate gRPC error code. + // This is a REQUIRED field. + VolumeCapability volume_capability = 5; + + // Indicates SP MUST publish the volume in readonly mode. + // This field is REQUIRED. + bool readonly = 6; + + // Secrets required by plugin to complete node publish volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 7 [(csi_secret) = true]; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 8; +} + +message NodePublishVolumeResponse { + // Intentionally empty. +} +message NodeUnpublishVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The path at which the volume was published. It MUST be an absolute + // path in the root filesystem of the process serving this request. + // The SP MUST delete the file or directory it created at this path. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string target_path = 2; +} + +message NodeUnpublishVolumeResponse { + // Intentionally empty. +} +message NodeGetVolumeStatsRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // It can be any valid path where volume was previously + // staged or published. + // It MUST be an absolute path in the root filesystem of + // the process serving this request. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string volume_path = 2; + + // The path where the volume is staged, if the plugin has the + // STAGE_UNSTAGE_VOLUME capability, otherwise empty. + // If not empty, it MUST be an absolute path in the root + // filesystem of the process serving this request. + // This field is OPTIONAL. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 3; +} + +message NodeGetVolumeStatsResponse { + // This field is OPTIONAL. + repeated VolumeUsage usage = 1; + // Information about the current condition of the volume. + // This field is OPTIONAL. + // This field MUST be specified if the VOLUME_CONDITION node + // capability is supported. + VolumeCondition volume_condition = 2 [(alpha_field) = true]; +} + +message VolumeUsage { + enum Unit { + UNKNOWN = 0; + BYTES = 1; + INODES = 2; + } + // The available capacity in specified Unit. This field is OPTIONAL. + // The value of this field MUST NOT be negative. + int64 available = 1; + + // The total capacity in specified Unit. This field is REQUIRED. + // The value of this field MUST NOT be negative. + int64 total = 2; + + // The used capacity in specified Unit. This field is OPTIONAL. + // The value of this field MUST NOT be negative. + int64 used = 3; + + // Units by which values are measured. This field is REQUIRED. + Unit unit = 4; +} + +// VolumeCondition represents the current condition of a volume. +message VolumeCondition { + option (alpha_message) = true; + + // Normal volumes are available for use and operating optimally. + // An abnormal volume does not meet these criteria. + // This field is REQUIRED. + bool abnormal = 1; + + // The message describing the condition of the volume. + // This field is REQUIRED. + string message = 2; +} +message NodeGetCapabilitiesRequest { + // Intentionally empty. +} + +message NodeGetCapabilitiesResponse { + // All the capabilities that the node service supports. This field + // is OPTIONAL. + repeated NodeServiceCapability capabilities = 1; +} + +// Specifies a capability of the node service. +message NodeServiceCapability { + message RPC { + enum Type { + UNKNOWN = 0; + STAGE_UNSTAGE_VOLUME = 1; + // If Plugin implements GET_VOLUME_STATS capability + // then it MUST implement NodeGetVolumeStats RPC + // call for fetching volume statistics. + GET_VOLUME_STATS = 2; + // See VolumeExpansion for details. + EXPAND_VOLUME = 3; + // Indicates that the Node service can report volume conditions. + // An SP MAY implement `VolumeCondition` in only the Node + // Plugin, only the Controller Plugin, or both. + // If `VolumeCondition` is implemented in both the Node and + // Controller Plugins, it SHALL report from different + // perspectives. + // If for some reason Node and Controller Plugins report + // misaligned volume conditions, CO SHALL assume the worst case + // is the truth. + // Note that, for alpha, `VolumeCondition` is intended to be + // informative for humans only, not for automation. + VOLUME_CONDITION = 4 [(alpha_enum_value) = true]; + + // Indicates the SP supports the SINGLE_NODE_SINGLE_WRITER and/or + // SINGLE_NODE_MULTI_WRITER access modes. + // These access modes are intended to replace the + // SINGLE_NODE_WRITER access mode to clarify the number of writers + // for a volume on a single node. Plugins MUST accept and allow + // use of the SINGLE_NODE_WRITER access mode (subject to the + // processing rules for NodePublishVolume), when either + // SINGLE_NODE_SINGLE_WRITER and/or SINGLE_NODE_MULTI_WRITER are + // supported, in order to permit older COs to continue working. + SINGLE_NODE_MULTI_WRITER = 5 [(alpha_enum_value) = true]; + + // Indicates that Node service supports mounting volumes + // with provided volume group identifier during node stage + // or node publish RPC calls. + VOLUME_MOUNT_GROUP = 6; + } + + Type type = 1; + } + + oneof type { + // RPC that the controller supports. + RPC rpc = 1; + } +} +message NodeGetInfoRequest { +} + +message NodeGetInfoResponse { + // The identifier of the node as understood by the SP. + // This field is REQUIRED. + // This field MUST contain enough information to uniquely identify + // this specific node vs all other nodes supported by this plugin. + // This field SHALL be used by the CO in subsequent calls, including + // `ControllerPublishVolume`, to refer to this node. + // The SP is NOT responsible for global uniqueness of node_id across + // multiple SPs. + // This field overrides the general CSI size limit. + // The size of this field SHALL NOT exceed 256 bytes. The general + // CSI size limit, 128 byte, is RECOMMENDED for best backwards + // compatibility. + string node_id = 1; + + // Maximum number of volumes that controller can publish to the node. + // If value is not set or zero CO SHALL decide how many volumes of + // this type can be published by the controller to the node. The + // plugin MUST NOT set negative values here. + // This field is OPTIONAL. + int64 max_volumes_per_node = 2; + + // Specifies where (regions, zones, racks, etc.) the node is + // accessible from. + // A plugin that returns this field MUST also set the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability. + // COs MAY use this information along with the topology information + // returned in CreateVolumeResponse to ensure that a given volume is + // accessible from a given node when scheduling workloads. + // This field is OPTIONAL. If it is not specified, the CO MAY assume + // the node is not subject to any topological constraint, and MAY + // schedule workloads that reference any volume V, such that there are + // no topological constraints declared for V. + // + // Example 1: + // accessible_topology = + // {"region": "R1", "zone": "Z2"} + // Indicates the node exists within the "region" "R1" and the "zone" + // "Z2". + Topology accessible_topology = 3; +} +message NodeExpandVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The path on which volume is available. This field is REQUIRED. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string volume_path = 2; + + // This allows CO to specify the capacity requirements of the volume + // after expansion. If capacity_range is omitted then a plugin MAY + // inspect the file system of the volume to determine the maximum + // capacity to which the volume can be expanded. In such cases a + // plugin MAY expand the volume to its maximum capacity. + // This field is OPTIONAL. + CapacityRange capacity_range = 3; + + // The path where the volume is staged, if the plugin has the + // STAGE_UNSTAGE_VOLUME capability, otherwise empty. + // If not empty, it MUST be an absolute path in the root + // filesystem of the process serving this request. + // This field is OPTIONAL. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 4; + + // Volume capability describing how the CO intends to use this volume. + // This allows SP to determine if volume is being used as a block + // device or mounted file system. For example - if volume is being + // used as a block device the SP MAY choose to skip expanding the + // filesystem in NodeExpandVolume implementation but still perform + // rest of the housekeeping needed for expanding the volume. If + // volume_capability is omitted the SP MAY determine + // access_type from given volume_path for the volume and perform + // node expansion. This is an OPTIONAL field. + VolumeCapability volume_capability = 5; + + // Secrets required by plugin to complete node expand volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 6 + [(csi_secret) = true, (alpha_field) = true]; +} + +message NodeExpandVolumeResponse { + // The capacity of the volume in bytes. This field is OPTIONAL. + int64 capacity_bytes = 1; +} +message GroupControllerGetCapabilitiesRequest { + // Intentionally empty. +} + +message GroupControllerGetCapabilitiesResponse { + // All the capabilities that the group controller service supports. + // This field is OPTIONAL. + repeated GroupControllerServiceCapability capabilities = 1; +} + +// Specifies a capability of the group controller service. +message GroupControllerServiceCapability { + message RPC { + enum Type { + UNKNOWN = 0; + + // Indicates that the group controller plugin supports + // creating, deleting, and getting details of a volume + // group snapshot. + CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT = 1; + } + + Type type = 1; + } + + oneof type { + // RPC that the controller supports. + RPC rpc = 1; + } +} +message CreateVolumeGroupSnapshotRequest { + // The suggested name for the group snapshot. This field is REQUIRED + // for idempotency. + // Any Unicode string that conforms to the length limit is allowed + // except those containing the following banned characters: + // U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F. + // (These are control characters other than commonly used whitespace.) + string name = 1; + + // volume IDs of the source volumes to be snapshotted together. + // This field is REQUIRED. + repeated string source_volume_ids = 2; + + // Secrets required by plugin to complete + // ControllerCreateVolumeGroupSnapshot request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + // The secrets provided in this field SHOULD be the same for + // all group snapshot operations on the same group snapshot. + map secrets = 3 [(csi_secret) = true]; + + // Plugin specific parameters passed in as opaque key-value pairs. + // This field is OPTIONAL. The Plugin is responsible for parsing and + // validating these parameters. COs will treat these as opaque. + map parameters = 4; +} + +message CreateVolumeGroupSnapshotResponse { + // Contains all attributes of the newly created group snapshot. + // This field is REQUIRED. + VolumeGroupSnapshot group_snapshot = 1; +} + +message VolumeGroupSnapshot { + // The identifier for this group snapshot, generated by the plugin. + // This field MUST contain enough information to uniquely identify + // this specific snapshot vs all other group snapshots supported by + // this plugin. + // This field SHALL be used by the CO in subsequent calls to refer to + // this group snapshot. + // The SP is NOT responsible for global uniqueness of + // group_snapshot_id across multiple SPs. + // This field is REQUIRED. + string group_snapshot_id = 1; + + // A list of snapshots belonging to this group. + // This field is REQUIRED. + repeated Snapshot snapshots = 2; + + // Timestamp of when the volume group snapshot was taken. + // This field is REQUIRED. + .google.protobuf.Timestamp creation_time = 3; + + // Indicates if all individual snapshots in the group snapshot + // are ready to use as a `volume_content_source` in a + // `CreateVolumeRequest`. The default value is false. + // If any snapshot in the list of snapshots in this message have + // ready_to_use set to false, the SP MUST set this field to false. + // If all of the snapshots in the list of snapshots in this message + // have ready_to_use set to true, the SP SHOULD set this field to + // true. + // This field is REQUIRED. + bool ready_to_use = 4; +} +message DeleteVolumeGroupSnapshotRequest { + // The ID of the group snapshot to be deleted. + // This field is REQUIRED. + string group_snapshot_id = 1; + + // A list of snapshot IDs that are part of this group snapshot. + // If SP does not need to rely on this field to delete the snapshots + // in the group, it SHOULD check this field and report an error + // if it has the ability to detect a mismatch. + // Some SPs require this list to delete the snapshots in the group. + // If SP needs to use this field to delete the snapshots in the + // group, it MUST report an error if it has the ability to detect + // a mismatch. + // This field is REQUIRED. + repeated string snapshot_ids = 2; + + // Secrets required by plugin to complete group snapshot deletion + // request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + // The secrets provided in this field SHOULD be the same for + // all group snapshot operations on the same group snapshot. + map secrets = 3 [(csi_secret) = true]; +} + +message DeleteVolumeGroupSnapshotResponse { + // Intentionally empty. +} +message GetVolumeGroupSnapshotRequest { + // The ID of the group snapshot to fetch current group snapshot + // information for. + // This field is REQUIRED. + string group_snapshot_id = 1; + + // A list of snapshot IDs that are part of this group snapshot. + // If SP does not need to rely on this field to get the snapshots + // in the group, it SHOULD check this field and report an error + // if it has the ability to detect a mismatch. + // Some SPs require this list to get the snapshots in the group. + // If SP needs to use this field to get the snapshots in the + // group, it MUST report an error if it has the ability to detect + // a mismatch. + // This field is REQUIRED. + repeated string snapshot_ids = 2; + + // Secrets required by plugin to complete + // GetVolumeGroupSnapshot request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + // The secrets provided in this field SHOULD be the same for + // all group snapshot operations on the same group snapshot. + map secrets = 3 [(csi_secret) = true]; +} + +message GetVolumeGroupSnapshotResponse { + // This field is REQUIRED + VolumeGroupSnapshot group_snapshot = 1; +} +// BlockMetadata specifies a data range. +message BlockMetadata { + // This is the zero based byte position in the volume or snapshot, + // measured from the start of the object. + // This field is REQUIRED. + int64 byte_offset = 1; + + // This is the size of the data range. + // size_bytes MUST be greater than zero. + // This field is REQUIRED. + int64 size_bytes = 2; +} +enum BlockMetadataType { + UNKNOWN = 0; + + // The FIXED_LENGTH value indicates that data ranges are + // returned in fixed size blocks. + FIXED_LENGTH = 1; + + // The VARIABLE_LENGTH value indicates that data ranges + // are returned in potentially variable sized extents. + VARIABLE_LENGTH = 2; +} +// The GetMetadataAllocatedRequest message is used to solicit metadata +// on the allocated blocks of a snapshot: i.e. this identifies the +// data ranges that have valid data as they were the target of some +// previous write operation on the volume. +message GetMetadataAllocatedRequest { + // This is the identifier of the snapshot. + // This field is REQUIRED. + string snapshot_id = 1; + + // This indicates the zero based starting byte position in the volume + // snapshot from which the result should be computed. + // It is intended to be used to continue a previously interrupted + // call. + // The CO SHOULD specify this value to be the offset of the byte + // position immediately after the last byte of the last data range + // received, if continuing an interrupted operation, or zero if not. + // The SP MUST ensure that the returned response stream does not + // contain BlockMetadata tuples that end before the requested + // starting_offset: i.e. if S is the requested starting_offset, and + // B0 is block_metadata[0] of the first message in the response + // stream, then (S < B0.byte_offset + B0.size_bytes) must be true. + // This field is REQUIRED. + int64 starting_offset = 2; + + // This is an optional parameter, and if non-zero it specifies the + // maximum number of tuples to be returned in each + // GetMetadataAllocatedResponse message returned by the RPC stream. + // The plugin will determine an appropriate value if 0, and is + // always free to send less than the requested value. + // This field is OPTIONAL. + int32 max_results = 3; + + // Secrets required by plugin to complete the request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 4 [(csi_secret) = true]; +} + +// GetMetadataAllocatedResponse messages are returned in a gRPC stream. +// Cumulatively, they provide information on the allocated data +// ranges in the snapshot. +message GetMetadataAllocatedResponse { + // This specifies the style used in the BlockMetadata sequence. + // This value must be the same in all such messages returned by + // the stream. + // If block_metadata_type is FIXED_LENGTH, then the size_bytes field + // of each message in the block_metadata list MUST be constant. + // This field is REQUIRED. + BlockMetadataType block_metadata_type = 1; + + // This returns the capacity of the underlying volume in bytes. + // This value must be the same in all such messages returned by + // the stream. + // This field is REQUIRED. + int64 volume_capacity_bytes = 2; + + // This is a list of data range tuples. + // If the value of max_results in the GetMetadataAllocatedRequest + // message is greater than zero, then the number of entries in this + // list MUST be less than or equal to that value. + // The SP MUST respect the value of starting_offset in the request. + // The byte_offset fields of adjacent BlockMetadata messages + // MUST be strictly increasing and messages MUST NOT overlap: + // i.e. for any two BlockMetadata messages, A and B, if A is returned + // before B, then (A.byte_offset + A.size_bytes <= B.byte_offset) + // MUST be true. + // This MUST also be true if A and B are from block_metadata lists in + // different GetMetadataAllocatedResponse messages in the gRPC stream. + // This field is OPTIONAL. + repeated BlockMetadata block_metadata = 3; +} +// The GetMetadataDeltaRequest message is used to solicit metadata on +// the data ranges that have changed between two snapshots. +message GetMetadataDeltaRequest { + // This is the identifier of the snapshot against which changes + // are to be computed. + // This field is REQUIRED. + string base_snapshot_id = 1; + + // This is the identifier of a second snapshot in the same volume, + // created after the base snapshot. + // This field is REQUIRED. + string target_snapshot_id = 2; + + // This indicates the zero based starting byte position in the volume + // snapshot from which the result should be computed. + // It is intended to be used to continue a previously interrupted + // call. + // The CO SHOULD specify this value to be the offset of the byte + // position immediately after the last byte of the last data range + // received, if continuing an interrupted operation, or zero if not. + // The SP MUST ensure that the returned response stream does not + // contain BlockMetadata tuples that end before the requested + // starting_offset: i.e. if S is the requested starting_offset, and + // B0 is block_metadata[0] of the first message in the response + // stream, then (S < B0.byte_offset + B0.size_bytes) must be true. + // This field is REQUIRED. + int64 starting_offset = 3; + + // This is an optional parameter, and if non-zero it specifies the + // maximum number of tuples to be returned in each + // GetMetadataDeltaResponse message returned by the RPC stream. + // The plugin will determine an appropriate value if 0, and is + // always free to send less than the requested value. + // This field is OPTIONAL. + int32 max_results = 4; + + // Secrets required by plugin to complete the request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; +} + +// GetMetadataDeltaResponse messages are returned in a gRPC stream. +// Cumulatively, they provide information on the data ranges that +// have changed between the base and target snapshots specified +// in the GetMetadataDeltaRequest message. +message GetMetadataDeltaResponse { + // This specifies the style used in the BlockMetadata sequence. + // This value must be the same in all such messages returned by + // the stream. + // If block_metadata_type is FIXED_LENGTH, then the size_bytes field + // of each message in the block_metadata list MUST be constant. + // This field is REQUIRED. + BlockMetadataType block_metadata_type = 1; + + // This returns the capacity of the underlying volume in bytes. + // This value must be the same in all such messages returned by + // the stream. + // This field is REQUIRED. + int64 volume_capacity_bytes = 2; + + // This is a list of data range tuples. + // If the value of max_results in the GetMetadataDeltaRequest message + // is greater than zero, then the number of entries in this list MUST + // be less than or equal to that value. + // The SP MUST respect the value of starting_offset in the request. + // The byte_offset fields of adjacent BlockMetadata messages + // MUST be strictly increasing and messages MUST NOT overlap: + // i.e. for any two BlockMetadata messages, A and B, if A is returned + // before B, then (A.byte_offset + A.size_bytes <= B.byte_offset) + // MUST be true. + // This MUST also be true if A and B are from block_metadata lists in + // different GetMetadataDeltaResponse messages in the gRPC stream. + // This field is OPTIONAL. + repeated BlockMetadata block_metadata = 3; +} diff --git a/docker/entrypoint.ps1 b/docker/entrypoint.ps1 new file mode 100644 index 0000000..acd5712 --- /dev/null +++ b/docker/entrypoint.ps1 @@ -0,0 +1,6 @@ +write-host "starting democratic-csi via entrypoint.ps1" +$env:Path = "${pwd}\bin;${env:Path}" + +.\bin\node.exe --expose-gc .\bin\democratic-csi @args + +Exit $LASTEXITCODE diff --git a/docker/yq-installer.sh b/docker/yq-installer.sh new file mode 100755 index 0000000..1cb0977 --- /dev/null +++ b/docker/yq-installer.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +set -e +set -x + +PLATFORM_TYPE=${1} + +if [[ "${PLATFORM_TYPE}" == "build" ]]; then + PLATFORM=$BUILDPLATFORM +else + PLATFORM=$TARGETPLATFORM +fi + +if [[ "x${PLATFORM}" == "x" ]]; then + PLATFORM="linux/amd64" +fi + +# these come from the --platform option of buildx, indirectly from DOCKER_BUILD_PLATFORM in main.yaml +if [ "$PLATFORM" = "linux/amd64" ]; then + export PLATFORM_ARCH="amd64" +elif [ "$PLATFORM" = "linux/arm64" ]; then + export PLATFORM_ARCH="arm64" +elif [ "$PLATFORM" = "linux/arm/v7" ]; then + export PLATFORM_ARCH="arm" +elif [ "$PLATFORM" = "linux/s390x" ]; then + export PLATFORM_ARCH="s390x" +elif [ "$PLATFORM" = "linux/ppc64le" ]; then + export PLATFORM_ARCH="ppc64le" +else + echo "unsupported/unknown yq PLATFORM ${PLATFORM}" + exit 0 +fi + +echo "I am installing yq $YQ_VERSION" + +wget https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_linux_${PLATFORM_ARCH} -O /usr/local/bin/yq +chmod +x /usr/local/bin/yq + diff --git a/docs/storage-class-parameters.md b/docs/storage-class-parameters.md index d71d47f..500c85c 100644 --- a/docs/storage-class-parameters.md +++ b/docs/storage-class-parameters.md @@ -4,63 +4,67 @@ Some drivers support different settings for volumes. These can be configured via 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. These options are passed directly to the Synology API. - # The following options are known. - lunTemplate: | - 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_tpws - enable: 1 - - ... + fsType: ext4 + # The following options affect the LUN representing the volume. These options are passed directly to the Synology API. + # The following options are known. + lunTemplate: | + 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 - # The following options affect the iSCSI target. These options will be passed directly to the Synology API. - # The following options are known. - targetTemplate: | - 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 + # Only for thick provisioned volumes. Known values: + # 0: Buffered Writes + # 3: Direct Write + direct_io_pattern: 0 - # Use this to disable authentication. To configure authentication see below - auth_type: 0 + # Device Attributes. See below for more info + dev_attribs: + - 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: | + 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 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: +(with the `StorageClass` taking precedence). The following `dev_attribs` are known to work: - `emulate_tpws`: Hardware-assisted zeroing - `emulate_caw`: Hardware-assisted locking @@ -71,6 +75,7 @@ of the global configuration and in the `lunTemplate` of the `StorageClass`. If b - `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: ```yaml @@ -82,18 +87,18 @@ parameters: # This inline yaml object will be passed to the Synology API when creating the snapshot. lunSnapshotTemplate: | 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 -... ``` 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. +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 can use the same password for alle volumes of a `StorageClass` or use different passwords per volume. @@ -123,12 +128,17 @@ 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 - mutualPassword: MyOtherPassword + lunTemplate: | + ... + targetTemplate: | + # Client Credentials + user: client + password: MySecretPassword + # Mutual CHAP Credentials. If these are specified mutual CHAP will be enabled. + mutualUser: server + mutualPassword: MyOtherPassword + lunSnapshotTemplate: | + ... ``` 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/package-lock.json b/package-lock.json index 3908a56..5e3ed0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "reconnecting-websocket": "^4.4.0", "semver": "^7.3.4", "ssh2": "^1.1.0", + "traverse": "^0.6.11", "uri-js": "^4.4.1", "uuid": "^9.0.0", "winston": "^3.6.0", @@ -56,9 +57,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", - "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", + "integrity": "sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==", "dev": true, "license": "MIT", "dependencies": { @@ -119,9 +120,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.12.5", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz", - "integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.2.tgz", + "integrity": "sha512-nnR5nmL6lxF8YBqb6gWvEgLdLh/Fn+kvAdX5hUOnt48sNSb0riz/93ASd2E5gvanPA41X6Yp25bIfGRp1SMb2g==", "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.7.13", @@ -339,9 +340,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.71", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.71.tgz", - "integrity": "sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==", + "version": "18.19.86", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.86.tgz", + "integrity": "sha512-fifKayi175wLyKyc5qUfyENhQ1dCNI1UNjp653d8kuYcPQN5JhX3dGuP/XmvPTg/xRBn1VTLpbmi+H/Mr7tLfQ==", "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -372,25 +373,25 @@ "license": "MIT" }, "node_modules/@types/ws": { - "version": "8.5.13", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", - "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", "license": "MIT", "dependencies": { "@types/node": "*" } }, "node_modules/@ungap/structured-clone": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", - "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, "license": "ISC" }, "node_modules/acorn": { - "version": "8.14.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", - "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", "bin": { @@ -456,6 +457,43 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -480,6 +518,15 @@ "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==", "license": "MIT" }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/async-mutex": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.4.1.tgz", @@ -495,6 +542,21 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -511,9 +573,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -522,13 +584,14 @@ } }, "node_modules/axios/node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" }, "engines": { @@ -598,6 +661,53 @@ "node": ">=0.10.0" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -817,6 +927,57 @@ "node": ">=0.10" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -842,6 +1003,40 @@ "dev": true, "license": "MIT" }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -878,6 +1073,20 @@ "node": ">=0.10" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -900,6 +1109,133 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "license": "MIT" }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -1116,9 +1452,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", - "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", "dev": true, "license": "ISC", "dependencies": { @@ -1177,9 +1513,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -1209,6 +1545,21 @@ } } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1219,14 +1570,15 @@ } }, "node_modules/form-data": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", - "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.3.tgz", + "integrity": "sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" }, "engines": { @@ -1278,6 +1630,44 @@ "dev": true, "license": "ISC" }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -1287,6 +1677,60 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", @@ -1347,6 +1791,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1404,6 +1876,18 @@ "node": ">=6" } }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1414,6 +1898,72 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -1440,9 +1990,9 @@ } }, "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1484,12 +2034,138 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1500,6 +2176,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1509,6 +2200,24 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1522,6 +2231,34 @@ "node": ">=0.10.0" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -1532,6 +2269,51 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -1544,12 +2326,109 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", "license": "MIT" }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1760,9 +2639,9 @@ } }, "node_modules/long": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/long/-/long-5.2.4.tgz", - "integrity": "sha512-qtzLbJE8hq7VabR3mISmVGtoXP8KGc2Z/AT8OuqlYD7JTR3oqrgwdjnk07wpj1twXxYmgDXgoKVWUG/fReSzHg==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.1.tgz", + "integrity": "sha512-ka87Jz3gcx/I7Hal94xaN2tZEOPoUOEVftkQqZx2EeQRN7LGdfLlI3FvZ+7WDplm+vK2Urx9ULrvSowtdCieng==", "license": "Apache-2.0" }, "node_modules/lru-cache": { @@ -1774,6 +2653,15 @@ "node": ">=12" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1934,9 +2822,9 @@ } }, "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", "license": "MIT", "optional": true }, @@ -1982,10 +2870,51 @@ "node": ">= 6" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/oidc-token-hash": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz", - "integrity": "sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", + "integrity": "sha512-y0W+X7Ppo7oZX6eovsRkuzcSM40Bicg2JEJkDJ4irIt1wsYAP5MLSNv+QAogO8xivMffw/9OvV3um1pxXgt1uA==", "license": "MIT", "optional": true, "engines": { @@ -2058,6 +2987,23 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -2139,6 +3085,15 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "license": "MIT" }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2304,6 +3259,48 @@ "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==", "license": "MIT" }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -2380,9 +3377,9 @@ } }, "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", "dev": true, "license": "MIT", "engines": { @@ -2446,6 +3443,25 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -2473,6 +3489,39 @@ "license": "MIT", "optional": true }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-stable-stringify": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", @@ -2489,9 +3538,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -2500,6 +3549,52 @@ "node": ">=10" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -2523,6 +3618,78 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -2624,6 +3791,62 @@ "node": ">=8" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2735,6 +3958,23 @@ "node": ">=0.8" } }, + "node_modules/traverse": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.11.tgz", + "integrity": "sha512-vxXDZg8/+p3gblxB6BhhG5yWVn1kGRlaL8O78UDXc3wRnPizB5g83dcvWV1jpDMIPnjZjOFuxlMmE82XJ4407w==", + "license": "MIT", + "dependencies": { + "gopd": "^1.2.0", + "typedarray.prototype.slice": "^1.0.5", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -2794,6 +4034,102 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typedarray.prototype.slice": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/typedarray.prototype.slice/-/typedarray.prototype.slice-1.0.5.tgz", + "integrity": "sha512-q7QNVDGTdl702bVFiI5eY4l/HkgCM6at9KhcFbgUAzezHFbOVy4+0O/lCjsABEQwbZPravVfBIiBVGo89yzHFg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "math-intrinsics": "^1.1.0", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-offset": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", @@ -2807,6 +4143,24 @@ "node": ">=0.8.0" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/underscore": { "version": "1.13.7", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", @@ -2886,6 +4240,91 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/winston": { "version": "3.17.0", "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", @@ -2972,9 +4411,9 @@ "license": "ISC" }, "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/package.json b/package.json index 4bb7516..bef9f6a 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "reconnecting-websocket": "^4.4.0", "semver": "^7.3.4", "ssh2": "^1.1.0", + "traverse": "^0.6.11", "uri-js": "^4.4.1", "uuid": "^9.0.0", "winston": "^3.6.0", diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index c5df0a0..b47286b 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -1798,8 +1798,13 @@ class FreeNASApiDriver extends CsiBaseDriver { async removeSnapshotsFromDatatset(datasetName) { const httpApiClient = await this.getTrueNASHttpApiClient(); + // const httpClient = await this.getHttpClient(); + // const major = await httpApiClient.getSystemVersionMajor(); + let job_id = await httpApiClient.DatasetDestroySnapshots(datasetName); - await httpApiClient.CoreWaitForJob(job_id, 30); + if (job_id) { + await httpApiClient.CoreWaitForJob(job_id, 30); + } } /** @@ -2033,10 +2038,13 @@ class FreeNASApiDriver extends CsiBaseDriver { } async getTrueNASHttpApiClient() { - return this.ctx.registry.getAsync(`${__REGISTRY_NS__}:api_client`, async () => { - const httpClient = await this.getHttpClient(); - return new TrueNASApiClient(httpClient, this.ctx.cache); - }); + return this.ctx.registry.getAsync( + `${__REGISTRY_NS__}:api_client`, + async () => { + const httpClient = await this.getHttpClient(); + return new TrueNASApiClient(httpClient, this.ctx.cache); + } + ); } getAccessModes(capability) { diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index 700576a..79d7311 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -1,6 +1,7 @@ - +const _ = require("lodash"); const { sleep, stringify } = require("../../../utils/general"); const { Zetabyte } = require("../../../utils/zfs"); +const { Registry } = require("../../../utils/registry"); // used for in-memory cache of the version info const FREENAS_SYSTEM_VERSION_CACHE_KEY = "freenas:system_version"; @@ -11,6 +12,7 @@ class Api { this.client = client; this.cache = cache; this.options = options; + this.registry = new Registry(); } async getHttpClient() { @@ -22,7 +24,7 @@ class Api { * @returns */ async getZetabyte() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:zb`, () => { + return this.registry.get(`${__REGISTRY_NS__}:zb`, () => { return new Zetabyte({ executor: { spawn: function () { @@ -422,13 +424,13 @@ class Api { * @param {*} properties * @returns */ - async DatasetGet(datasetName, properties) { + async DatasetGet(datasetName, properties, queryParams = {}) { const httpClient = await this.getHttpClient(false); let response; let endpoint; endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; - response = await httpClient.get(endpoint); + response = await httpClient.get(endpoint, queryParams); if (response.statusCode == 200) { return this.normalizeProperties(response.body, properties); @@ -441,28 +443,60 @@ class Api { throw new Error(JSON.stringify(response.body)); } + /** + * This is meant to destroy all snapshots on the given dataset + * + * @param {*} datasetName + * @param {*} data + * @returns + */ async DatasetDestroySnapshots(datasetName, data = {}) { const httpClient = await this.getHttpClient(false); let response; let endpoint; - data.name = datasetName; + const major = await this.getSystemVersionMajor(); + if (Number(major) >= 25) { + try { + response = await this.DatasetGet( + datasetName, + ["id", "type", "name", "pool", "snapshots"], + { + "extra.snapshots": "true", + "extra.retrieve_children": "false", + } + ); - endpoint = "/pool/dataset/destroy_snapshots"; - response = await httpClient.post(endpoint, data); + for (const snapshot of _.get(response, "snapshots", [])) { + await this.SnapshotDelete(snapshot.name, { + defer: true, + }); + } + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + return; + } + throw err; + } + } else { + data.name = datasetName; - if (response.statusCode == 200) { - return response.body; + endpoint = "/pool/dataset/destroy_snapshots"; + response = await httpClient.post(endpoint, data); + + if (response.statusCode == 200) { + return response.body; + } + + if ( + response.statusCode == 422 && + JSON.stringify(response.body).includes("already exists") + ) { + return; + } + + throw new Error(JSON.stringify(response.body)); } - - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes("already exists") - ) { - return; - } - - throw new Error(JSON.stringify(response.body)); } async SnapshotSet(snapshotName, properties) { diff --git a/src/utils/general.js b/src/utils/general.js index 7ffddcb..1657a67 100644 --- a/src/utils/general.js +++ b/src/utils/general.js @@ -272,6 +272,19 @@ async function hostname_lookup(hostname) { }); } +function expandenv(string, env) { + if (!(typeof string === "string" || string instanceof String)) { + throw new Error("Please pass a string into expandenv"); + } + + env = env ? env : process.env; + + return string.replace(/\$\{?[a-zA-Z_]+[a-zA-Z0-9_]*\}?/g, function (match) { + match = match.replace(/[^A-Za-z0-9_]/g, ""); + return env[match] || ""; + }); +} + module.exports.sleep = sleep; module.exports.md5 = md5; module.exports.crc32 = crc32; @@ -292,3 +305,4 @@ module.exports.default_supported_file_filesystems = module.exports.retry = retry; module.exports.trimchar = trimchar; module.exports.hostname_lookup = hostname_lookup; +module.exports.expandenv = expandenv; From a032ed3e187a084b03d89906b608db668518ad8c Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 5 Apr 2025 17:53:59 -0600 Subject: [PATCH 12/55] fix registry usage Signed-off-by: Travis Glenn Hansen --- src/driver/controller-synology/http/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/driver/controller-synology/http/index.js b/src/driver/controller-synology/http/index.js index a16be94..206c0da 100644 --- a/src/driver/controller-synology/http/index.js +++ b/src/driver/controller-synology/http/index.js @@ -4,6 +4,7 @@ const https = require("https"); const { axios_request, stringify } = require("../../../utils/general"); const Mutex = require("async-mutex").Mutex; const { GrpcError, grpc } = require("../../../utils/grpc"); +const { Registry } = require("../../../utils/registry"); const USER_AGENT = "democratic-csi"; const __REGISTRY_NS__ = "SynologyHttpClient"; @@ -84,6 +85,7 @@ class SynologyHttpClient { this.logger = console; this.doLoginMutex = new Mutex(); this.apiSerializeMutex = new Mutex(); + this.registry = new Registry(); if (false) { setInterval(() => { @@ -94,7 +96,7 @@ class SynologyHttpClient { } getHttpAgent() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:http_agent`, () => { + return this.registry.get(`${__REGISTRY_NS__}:http_agent`, () => { return new http.Agent({ keepAlive: true, maxSockets: Infinity, @@ -104,7 +106,7 @@ class SynologyHttpClient { } getHttpsAgent() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:https_agent`, () => { + return this.registry.get(`${__REGISTRY_NS__}:https_agent`, () => { return new https.Agent({ keepAlive: true, maxSockets: Infinity, From 1ed366c545c133419d10dc78afce631181b02496 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 5 Apr 2025 19:01:48 -0600 Subject: [PATCH 13/55] remove unused deps Signed-off-by: Travis Glenn Hansen --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 953497f..0f0d82f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ ENV NODE_VERSION=v20.19.0 ENV NODE_ENV=production # install build deps -RUN apt-get update && apt-get install -y python3 make cmake gcc g++ +#RUN apt-get update && apt-get install -y python3 make cmake gcc g++ # install node RUN apt-get update && apt-get install -y wget xz-utils From 98f99bc7611cf3479b26363ee648ab2c1474631d Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 5 Apr 2025 19:47:08 -0600 Subject: [PATCH 14/55] fix node deps Signed-off-by: Travis Glenn Hansen --- Dockerfile | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Dockerfile b/Dockerfile index 0f0d82f..582494f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,6 @@ +# docker build --pull -t foobar . +# docker buildx build --pull -t foobar --platform linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le . + FROM debian:12-slim AS build #FROM --platform=$BUILDPLATFORM debian:10-slim AS build @@ -24,6 +27,13 @@ ADD docker/node-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/node-installer.sh && node-installer.sh ENV PATH=/usr/local/lib/nodejs/bin:$PATH +# Workaround for https://github.com/nodejs/node/issues/37219 +RUN test $(uname -m) != armv7l || ( \ + apt-get update \ + && apt-get install -y libatomic1 \ + && rm -rf /var/lib/apt/lists/* \ + ) + # Run as a non-root user RUN useradd --create-home csi \ && mkdir /home/csi/app \ From 73af26298cbb12fe215ffd07e21560a24b98edc6 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sun, 6 Apr 2025 11:02:21 -0600 Subject: [PATCH 15/55] handling nvme hostid/hostnqn files more robustly Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 12 ++++----- Dockerfile | 4 ++- bin/democratic-csi | 27 ++++++++++++++++--- .../scale/{24.10 => 25.04}/scale-iscsi.yaml | 0 .../scale/{24.10 => 25.04}/scale-nfs.yaml | 0 .../scale/{24.10 => 25.04}/scale-smb.yaml | 0 6 files changed, 33 insertions(+), 10 deletions(-) rename ci/configs/truenas/scale/{24.10 => 25.04}/scale-iscsi.yaml (100%) rename ci/configs/truenas/scale/{24.10 => 25.04}/scale-nfs.yaml (100%) rename ci/configs/truenas/scale/{24.10 => 25.04}/scale-smb.yaml (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 47b6c73..c7389b4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -115,7 +115,7 @@ jobs: SYNOLOGY_PASSWORD: ${{ secrets.SANITY_SYNOLOGY_PASSWORD }} SYNOLOGY_VOLUME: ${{ secrets.SANITY_SYNOLOGY_VOLUME }} - csi-sanity-truenas-scale-24_10: + csi-sanity-truenas-scale-25_04: needs: - build-npm-linux-amd64 strategy: @@ -123,10 +123,10 @@ jobs: max-parallel: 1 matrix: config: - - truenas/scale/24.10/scale-iscsi.yaml - - truenas/scale/24.10/scale-nfs.yaml + - truenas/scale/25.04/scale-iscsi.yaml + - truenas/scale/25.04/scale-nfs.yaml # 80 char limit - - truenas/scale/24.10/scale-smb.yaml + - truenas/scale/25.04/scale-smb.yaml runs-on: - self-hosted - Linux @@ -435,7 +435,7 @@ jobs: - determine-image-tag - csi-sanity-synology-dsm6 - csi-sanity-synology-dsm7 - - csi-sanity-truenas-scale-24_10 + - csi-sanity-truenas-scale-25_04 - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic - csi-sanity-objectivefs @@ -475,7 +475,7 @@ jobs: needs: - csi-sanity-synology-dsm6 - csi-sanity-synology-dsm7 - - csi-sanity-truenas-scale-24_10 + - csi-sanity-truenas-scale-25_04 - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic - csi-sanity-objectivefs diff --git a/Dockerfile b/Dockerfile index 582494f..0950c8d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -90,7 +90,9 @@ RUN apt-get update && \ apt-get install -y wget netbase zip bzip2 socat e2fsprogs exfatprogs xfsprogs btrfs-progs fatresize dosfstools ntfs-3g nfs-common cifs-utils fdisk gdisk cloud-guest-utils sudo rsync procps util-linux nvme-cli fuse3 && \ rm -rf /var/lib/apt/lists/* -# TODO: remove nvme unique files +RUN \ + echo '83e7a026-2564-455b-ada6-ddbdaf0bc519' > /etc/nvme/hostid && \ + echo 'nqn.2014-08.org.nvmexpress:uuid:941e4f03-2cd6-435e-86df-731b1c573d86' > /etc/nvme/hostnqn ARG RCLONE_VERSION=1.69.1 ADD docker/rclone-installer.sh /usr/local/sbin diff --git a/bin/democratic-csi b/bin/democratic-csi index 7e99647..df46a27 100755 --- a/bin/democratic-csi +++ b/bin/democratic-csi @@ -7,7 +7,7 @@ // polyfills require("../src/utils/polyfills"); -const yaml = require("js-yaml"); +const cp = require("child_process"); const fs = require("fs"); const { grpc } = require("../src/utils/grpc"); const { @@ -16,6 +16,8 @@ const { expandenv, } = require("../src/utils/general"); const traverse = require("traverse"); +const uuidv4 = require("uuid").v4; +const yaml = require("js-yaml"); let driverConfigFile; let options; @@ -550,8 +552,27 @@ if (process.env.LOG_GRPC_SESSIONS == "1") { if (require.main === module) { (async function () { try { - //nvme gen-hostnqn > /etc/nvme/hostnqn - //uuidgen > /etc/nvme/hostid + switch (process.platform) { + case "linux": + const nvme_dir = "/etc/nvme"; + + // ensure directory + if (!fs.existsSync(nvme_dir)) { + fs.mkdirSync(nvme_dir); + } + + //uuidgen > /etc/nvme/hostid + if (!fs.existsSync(`${nvme_dir}/hostid`)) { + fs.writeFileSync(`${nvme_dir}/hostid`, uuidv4() + "\n"); + } + + //nvme gen-hostnqn > /etc/nvme/hostnqn + if (!fs.existsSync(`${nvme_dir}/hostnqn`)) { + const nqn = String(cp.execSync(`nvme gen-hostnqn`)); + fs.writeFileSync(`${nvme_dir}/hostnqn`, nqn); + } + break; + } if (bindAddress) { await new Promise((resolve, reject) => { diff --git a/ci/configs/truenas/scale/24.10/scale-iscsi.yaml b/ci/configs/truenas/scale/25.04/scale-iscsi.yaml similarity index 100% rename from ci/configs/truenas/scale/24.10/scale-iscsi.yaml rename to ci/configs/truenas/scale/25.04/scale-iscsi.yaml diff --git a/ci/configs/truenas/scale/24.10/scale-nfs.yaml b/ci/configs/truenas/scale/25.04/scale-nfs.yaml similarity index 100% rename from ci/configs/truenas/scale/24.10/scale-nfs.yaml rename to ci/configs/truenas/scale/25.04/scale-nfs.yaml diff --git a/ci/configs/truenas/scale/24.10/scale-smb.yaml b/ci/configs/truenas/scale/25.04/scale-smb.yaml similarity index 100% rename from ci/configs/truenas/scale/24.10/scale-smb.yaml rename to ci/configs/truenas/scale/25.04/scale-smb.yaml From edfdf86f2d9bd6dbfc83e199b660223405a34eb2 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 30 May 2025 07:59:09 -0600 Subject: [PATCH 16/55] fix for scale with ssh driver Signed-off-by: Travis Glenn Hansen --- src/driver/freenas/ssh.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 3f635ac..7163d7f 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -120,6 +120,15 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { chroot: "/usr/sbin/chroot", }; } + + if (isScale && Number(majorMinor) >= 25) { + options.paths = { + zfs: "/usr/sbin/zfs", + zpool: "/usr/sbin/zpool", + sudo: "/usr/bin/sudo", + chroot: "/usr/sbin/chroot", + }; + } } } @@ -142,10 +151,13 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { } async getTrueNASHttpApiClient() { - return this.ctx.registry.getAsync(`${__REGISTRY_NS__}:api_client`, async () => { - const httpClient = await this.getHttpClient(); - return new TrueNASApiClient(httpClient, this.ctx.cache); - }); + return this.ctx.registry.getAsync( + `${__REGISTRY_NS__}:api_client`, + async () => { + const httpClient = await this.getHttpClient(); + return new TrueNASApiClient(httpClient, this.ctx.cache); + } + ); } getDriverShareType() { From 55c36d62ffb51c6ebd5c25b85300e27b9a7af803 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 30 May 2025 08:14:54 -0600 Subject: [PATCH 17/55] newer ubuntu image for ci Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c7389b4..a64234a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,7 +20,7 @@ jobs: access_token: ${{ github.token }} build-npm-linux-amd64: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From 721d37553182d0279b5bd157e80aafc55ad082e4 Mon Sep 17 00:00:00 2001 From: perf3ct Date: Wed, 20 Aug 2025 20:13:29 +0000 Subject: [PATCH 18/55] feat(ssh/zfs): update path resolution logic for scale --- src/driver/controller-zfs-generic/index.js | 20 ++++--- src/driver/freenas/ssh.js | 64 ++++++++++++++++------ 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 3e4836f..bec3c68 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -39,20 +39,26 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { } options.idempotent = true; + options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); + + // Run automatic detection first + if (typeof this.setZetabyteCustomOptions === "function") { + await this.setZetabyteCustomOptions(options); + } + + // Manual override comes after automatic detection + // Only apply if user explicitly provided non-empty paths if ( this.options.zfs.hasOwnProperty("cli") && this.options.zfs.cli && - this.options.zfs.cli.hasOwnProperty("paths") + this.options.zfs.cli.hasOwnProperty("paths") && + this.options.zfs.cli.paths && + Object.keys(this.options.zfs.cli.paths).length > 0 ) { + // User explicitly configured paths - use them options.paths = this.options.zfs.cli.paths; } - options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); - - if (typeof this.setZetabyteCustomOptions === "function") { - await this.setZetabyteCustomOptions(options); - } - return new Zetabyte(options); }); } diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 7163d7f..05a5cf3 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -71,20 +71,26 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { options.executor = new ZfsSshProcessManager(sshClient); options.idempotent = true; + options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); + + // Run automatic detection first + if (typeof this.setZetabyteCustomOptions === "function") { + await this.setZetabyteCustomOptions(options); + } + + // Manual override comes after automatic detection + // Only apply if user explicitly provided non-empty paths if ( this.options.zfs.hasOwnProperty("cli") && this.options.zfs.cli && - this.options.zfs.cli.hasOwnProperty("paths") + this.options.zfs.cli.hasOwnProperty("paths") && + this.options.zfs.cli.paths && + Object.keys(this.options.zfs.cli.paths).length > 0 ) { + // User explicitly configured paths - use them options.paths = this.options.zfs.cli.paths; } - options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); - - if (typeof this.setZetabyteCustomOptions === "function") { - await this.setZetabyteCustomOptions(options); - } - return new Zetabyte(options); }); } @@ -112,22 +118,33 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { if (!options.hasOwnProperty("paths")) { const majorMinor = await this.getSystemVersionMajorMinor(); const isScale = await this.getIsScale(); + if (!isScale && Number(majorMinor) >= 12) { + // TrueNAS CORE/Enterprise version 12+ options.paths = { zfs: "/usr/local/sbin/zfs", zpool: "/usr/local/sbin/zpool", sudo: "/usr/local/bin/sudo", chroot: "/usr/sbin/chroot", }; - } - - if (isScale && Number(majorMinor) >= 25) { - options.paths = { - zfs: "/usr/sbin/zfs", - zpool: "/usr/sbin/zpool", - sudo: "/usr/bin/sudo", - chroot: "/usr/sbin/chroot", - }; + } else if (isScale) { + if (Number(majorMinor) >= 25) { + // TrueNAS SCALE version 25+ (paths changed to /usr/sbin in 25.04) + options.paths = { + zfs: "/usr/sbin/zfs", + zpool: "/usr/sbin/zpool", + sudo: "/usr/bin/sudo", + chroot: "/usr/sbin/chroot", + }; + } else { + // TrueNAS SCALE versions before 25 + options.paths = { + zfs: "/usr/local/sbin/zfs", + zpool: "/usr/local/sbin/zpool", + sudo: "/usr/bin/sudo", + chroot: "/usr/sbin/chroot", + }; + } } } } @@ -2179,8 +2196,19 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { async getIsScale() { const systemVersion = await this.getSystemVersion(); - if (systemVersion.v2 && systemVersion.v2.toLowerCase().includes("scale")) { - return true; + if (systemVersion.v2) { + const versionLower = systemVersion.v2.toLowerCase(); + // Check for explicit "scale" in version string + if (versionLower.includes("scale")) { + return true; + } + // TrueNAS 25+ doesn't include "SCALE" in version string, but versions 25+ are SCALE-only + if (versionLower.includes("truenas")) { + const majorVersion = await this.getSystemVersionMajor(); + if (Number(majorVersion) >= 25) { + return true; + } + } } return false; From fc7ec358ab378482d05a33e918534d59d8330101 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 28 Oct 2025 16:10:35 -0600 Subject: [PATCH 19/55] support new containerd-oci-ephemeral-inline driver Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 14 +- Dockerfile | 34 +- Dockerfile.Windows | 1 + README.md | 4 +- docker/ctr-mount-labels.diff | 31 + examples/containerd-oci-ephemeral-inline.yaml | 6 + package-lock.json | 1035 ++++++++++++++++- package.json | 1 + .../ephemeral-inline-containerd-oci/index.js | 493 ++++++++ src/driver/factory.js | 5 + src/utils/ctr.js | 176 +++ 11 files changed, 1773 insertions(+), 27 deletions(-) create mode 100644 docker/ctr-mount-labels.diff create mode 100644 examples/containerd-oci-ephemeral-inline.yaml create mode 100644 src/driver/ephemeral-inline-containerd-oci/index.js create mode 100644 src/utils/ctr.js diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a64234a..3789ddf 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -487,16 +487,16 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [windows-2019, windows-2022] + os: [windows-2022, windows-2025] include: - - os: windows-2019 - core_base_tag: ltsc2019 - nano_base_tag: "1809" - file: Dockerfile.Windows - os: windows-2022 core_base_tag: ltsc2022 nano_base_tag: ltsc2022 file: Dockerfile.Windows + - os: windows-2025 + core_base_tag: ltsc2025 + nano_base_tag: ltsc2025 + file: Dockerfile.Windows steps: - uses: actions/checkout@v4 - name: docker build @@ -528,10 +528,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: - name: democratic-csi-windows-ltsc2019.tar + name: democratic-csi-windows-ltsc2022.tar - uses: actions/download-artifact@v4 with: - name: democratic-csi-windows-ltsc2022.tar + name: democratic-csi-windows-ltsc2025.tar - name: push windows images with buildah run: | #.github/bin/install_latest_buildah.sh diff --git a/Dockerfile b/Dockerfile index 0950c8d..8dfd58e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,25 @@ # docker build --pull -t foobar . # docker buildx build --pull -t foobar --platform linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le . +# docker run --rm -ti --user root --entrypoint /bin/bash foobar +###################### +# golang builder +###################### +FROM golang:1.25.3-bookworm as ctrbuilder + +# /go/containerd/ctr +ADD docker/ctr-mount-labels.diff /tmp +RUN \ + git clone https://github.com/containerd/containerd.git; \ + cd containerd && \ + git checkout v2.0.4 && \ + git apply /tmp/ctr-mount-labels.diff && \ + CGO_ENABLED=0 go build ./cmd/ctr/; + + +###################### +# nodejs builder +###################### FROM debian:12-slim AS build #FROM --platform=$BUILDPLATFORM debian:10-slim AS build @@ -78,6 +97,9 @@ RUN test $(uname -m) != armv7l || ( \ && rm -rf /var/lib/apt/lists/* \ ) +# install ctr +COPY --from=ctrbuilder /go/containerd/ctr /usr/local/bin/ctr + # install node #ENV PATH=/usr/local/lib/nodejs/bin:$PATH #COPY --from=build /usr/local/lib/nodejs /usr/local/lib/nodejs @@ -116,31 +138,27 @@ RUN chmod +x /usr/local/sbin/yq-installer.sh && yq-installer.sh # rm -rf /var/lib/apt/lists/* # install objectivefs -ARG OBJECTIVEFS_VERSION=7.2 +ARG OBJECTIVEFS_VERSION=7.3 ADD docker/objectivefs-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/objectivefs-installer.sh && objectivefs-installer.sh # install wrappers ADD docker/iscsiadm /usr/local/sbin -RUN chmod +x /usr/local/sbin/iscsiadm ADD docker/multipath /usr/local/sbin -RUN chmod +x /usr/local/sbin/multipath ## USE_HOST_MOUNT_TOOLS=1 ADD docker/mount /usr/local/bin/mount -RUN chmod +x /usr/local/bin/mount ## USE_HOST_MOUNT_TOOLS=1 ADD docker/umount /usr/local/bin/umount -RUN chmod +x /usr/local/bin/umount ADD docker/zfs /usr/local/bin/zfs -RUN chmod +x /usr/local/bin/zfs ADD docker/zpool /usr/local/bin/zpool -RUN chmod +x /usr/local/bin/zpool ADD docker/oneclient /usr/local/bin/oneclient -RUN chmod +x /usr/local/bin/oneclient + +RUN chown -R root:root /usr/local/bin/* +RUN chmod +x /usr/local/bin/* # Run as a non-root user RUN useradd --create-home csi \ diff --git a/Dockerfile.Windows b/Dockerfile.Windows index 69cfb2d..bb5c058 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -99,6 +99,7 @@ COPY --from=build /PowerShell /PowerShell COPY --from=build /app /app WORKDIR /app +ADD https://github.com/democratic-csi/democratic-csi/releases/download/v1.0.0/ctr.exe ./bin COPY --from=build /nodejs/node.exe ./bin COPY --from=build /usr/local/bin/ ./bin diff --git a/README.md b/README.md index 79992b5..e25be56 100644 --- a/README.md +++ b/README.md @@ -463,6 +463,7 @@ passwd smbroot (optional) smbpasswd -L -a smbroot ####### nvmeof +# apt-get install linux-modules-extra-$(uname -r) # ensure nvmeof target modules are loaded at startup cat < /etc/modules-load.d/nvmet.conf nvmet @@ -483,7 +484,8 @@ cd nvmetcli ## install globally python3 setup.py install --prefix=/usr -pip install configshell_fb +pip install configshell_fb # apt-get install -y pip python3-configshell-fb + ## install to root home dir python3 setup.py install --user diff --git a/docker/ctr-mount-labels.diff b/docker/ctr-mount-labels.diff new file mode 100644 index 0000000..921ab09 --- /dev/null +++ b/docker/ctr-mount-labels.diff @@ -0,0 +1,31 @@ +diff --git a/cmd/ctr/commands/images/mount.go b/cmd/ctr/commands/images/mount.go +index c97954267..63c5a7746 100644 +--- a/cmd/ctr/commands/images/mount.go ++++ b/cmd/ctr/commands/images/mount.go +@@ -25,6 +25,7 @@ import ( + "github.com/containerd/containerd/v2/cmd/ctr/commands" + "github.com/containerd/containerd/v2/core/leases" + "github.com/containerd/containerd/v2/core/mount" ++ "github.com/containerd/containerd/v2/core/snapshots" + "github.com/containerd/containerd/v2/defaults" + "github.com/containerd/errdefs" + "github.com/containerd/platforms" +@@ -114,11 +115,16 @@ When you are done, use the unmount command. + + s := client.SnapshotService(snapshotter) + ++ labels := commands.LabelArgs(cliContext.StringSlice("label")) ++ opts := []snapshots.Opt{ ++ snapshots.WithLabels(labels), ++ } ++ + var mounts []mount.Mount + if cliContext.Bool("rw") { +- mounts, err = s.Prepare(ctx, target, chainID) ++ mounts, err = s.Prepare(ctx, target, chainID, opts...) + } else { +- mounts, err = s.View(ctx, target, chainID) ++ mounts, err = s.View(ctx, target, chainID, opts...) + } + if err != nil { + if errdefs.IsAlreadyExists(err) { diff --git a/examples/containerd-oci-ephemeral-inline.yaml b/examples/containerd-oci-ephemeral-inline.yaml new file mode 100644 index 0000000..feac678 --- /dev/null +++ b/examples/containerd-oci-ephemeral-inline.yaml @@ -0,0 +1,6 @@ +driver: containerd-oci-ephemeral-inline +containerd: + #address: /run/containerd/containerd.sock + #windowsAddress: \\\\.\\pipe\\containerd-containerd + #namespace: default + #creds encryption key diff --git a/package-lock.json b/package-lock.json index 5e3ed0e..7579a70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.9.0", "license": "MIT", "dependencies": { + "@codefresh-io/docker-reference": "^0.0.11", "@grpc/grpc-js": "^1.8.4", "@grpc/proto-loader": "^0.7.0", "@kubernetes/client-node": "^0.18.0", @@ -36,6 +37,15 @@ "eslint": "^8.10.0" } }, + "node_modules/@codefresh-io/docker-reference": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@codefresh-io/docker-reference/-/docker-reference-0.0.11.tgz", + "integrity": "sha512-eLVAoDQmJN7Z2TCHyz6KhZPW32XVfODYcsPjbzH2jXds69QAevTS71PZL0oQcDc25dYodOw/G+NncQL8f44cXg==", + "license": "MIT", + "dependencies": { + "re2": "^1.16.0" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -188,6 +198,123 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/fs-minipass/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/@js-sdsl/ordered-map": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", @@ -263,6 +390,50 @@ "node": ">= 8" } }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -388,12 +559,22 @@ "dev": true, "license": "ISC" }, + "node_modules/abbrev": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -411,6 +592,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -602,7 +792,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "devOptional": true, "license": "MIT" }, "node_modules/bcrypt-pbkdf": { @@ -661,6 +850,146 @@ "node": ">=0.10.0" } }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/cacache/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -896,7 +1225,6 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -982,7 +1310,6 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -1087,6 +1414,12 @@ "node": ">= 0.4" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -1109,6 +1442,31 @@ "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", "license": "MIT" }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "license": "MIT" + }, "node_modules/es-abstract": { "version": "1.23.9", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", @@ -1265,6 +1623,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -1409,6 +1768,12 @@ "node": ">=0.10.0" } }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -1461,6 +1826,23 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -1560,6 +1942,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", @@ -1964,6 +2362,25 @@ "node": ">= 0.4" } }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -1979,6 +2396,32 @@ "npm": ">=1.3.7" } }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2010,7 +2453,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -2034,6 +2476,16 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/install-artifact-from-github": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.4.0.tgz", + "integrity": "sha512-+y6WywKZREw5rq7U2jvr2nmZpT7cbWbQQ0N/qfcseYnzHFz2cZz1Et52oY+XttYuYeTkI8Y+R2JNWj68MpQFSg==", + "license": "BSD-3-Clause", + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -2048,6 +2500,15 @@ "node": ">= 0.4" } }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -2433,7 +2894,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/isomorphic-ws": { @@ -2451,6 +2911,21 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", "license": "MIT" }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jose": { "version": "4.15.9", "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", @@ -2653,6 +3128,37 @@ "node": ">=12" } }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/make-fetch-happen/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2714,6 +3220,137 @@ "node": ">=8" } }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-fetch/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/minizlib": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", @@ -2825,8 +3462,7 @@ "version": "2.22.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", - "license": "MIT", - "optional": true + "license": "MIT" }, "node_modules/natural-compare": { "version": "1.4.0", @@ -2845,12 +3481,139 @@ "ncp": "bin/ncp" } }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, + "node_modules/node-gyp": { + "version": "11.5.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", + "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/node-gyp/node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", + "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/nopt": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", + "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -3036,6 +3799,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3073,18 +3854,52 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", "license": "MIT" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -3104,6 +3919,28 @@ "node": ">= 0.8.0" } }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prompt": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/prompt/-/prompt-1.3.0.tgz", @@ -3227,6 +4064,18 @@ ], "license": "MIT" }, + "node_modules/re2": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.22.1.tgz", + "integrity": "sha512-E4J0EtgyNLdIr0wTg0dQPefuiqNY29KaLacytiUAYYRzxCG+zOkWoUygt1rI+TA1LrhN49/njrfSO1DHtVC5Vw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "install-artifact-from-github": "^1.4.0", + "nan": "^2.22.2", + "node-gyp": "^11.2.0" + } + }, "node_modules/read": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", @@ -3376,6 +4225,15 @@ "node": ">=4" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -3599,7 +4457,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -3612,7 +4469,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -3690,6 +4546,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -3699,6 +4567,44 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3750,6 +4656,27 @@ "node": ">=0.10.0" } }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ssri/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -3791,6 +4718,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.10", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", @@ -3859,6 +4801,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3927,6 +4882,22 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -4173,6 +5144,30 @@ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "license": "MIT" }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -4228,7 +5223,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -4403,6 +5397,24 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -4415,6 +5427,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, diff --git a/package.json b/package.json index bef9f6a..9d46d56 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "url": "https://github.com/democratic-csi/democratic-csi.git" }, "dependencies": { + "@codefresh-io/docker-reference": "^0.0.11", "@grpc/grpc-js": "^1.8.4", "@grpc/proto-loader": "^0.7.0", "@kubernetes/client-node": "^0.18.0", diff --git a/src/driver/ephemeral-inline-containerd-oci/index.js b/src/driver/ephemeral-inline-containerd-oci/index.js new file mode 100644 index 0000000..4542f2d --- /dev/null +++ b/src/driver/ephemeral-inline-containerd-oci/index.js @@ -0,0 +1,493 @@ +const _ = require("lodash"); +const fs = require("fs"); +const CTR = require("../../utils/ctr").CTR; +const { CsiBaseDriver } = require("../index"); +const { GrpcError, grpc } = require("../../utils/grpc"); +const { Filesystem } = require("../../utils/filesystem"); +const { Mount } = require("../../utils/mount"); +const semver = require("semver"); +const { parseAll } = require("@codefresh-io/docker-reference"); + +const __REGISTRY_NS__ = "EphemeralInlineContainerDOciDriver"; + +/** + * https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20190122-csi-inline-volumes.md + * https://kubernetes-csi.github.io/docs/ephemeral-local-volumes.html + * + * Sample calls: + * - https://gcsweb.k8s.io/gcs/kubernetes-jenkins/pr-logs/pull/92387/pull-kubernetes-e2e-gce/1280784994997899264/artifacts/_sig-storage_CSI_Volumes/_Driver_csi-hostpath_/_Testpattern_inline_ephemeral_CSI_volume_ephemeral/should_create_read_write_inline_ephemeral_volume/ + * - https://storage.googleapis.com/kubernetes-jenkins/pr-logs/pull/92387/pull-kubernetes-e2e-gce/1280784994997899264/artifacts/_sig-storage_CSI_Volumes/_Driver_csi-hostpath_/_Testpattern_inline_ephemeral_CSI_volume_ephemeral/should_create_read-only_inline_ephemeral_volume/csi-hostpathplugin-0-hostpath.log + * + * inline drivers are assumed to be mount only (no block support) + * purposely there is no native support for size contraints + * + */ +class EphemeralInlineContainerDOciDriver extends CsiBaseDriver { + constructor(ctx, options) { + super(...arguments); + + options = options || {}; + options.service = options.service || {}; + options.service.identity = options.service.identity || {}; + options.service.controller = options.service.controller || {}; + options.service.node = options.service.node || {}; + + options.service.identity.capabilities = + options.service.identity.capabilities || {}; + + options.service.controller.capabilities = + options.service.controller.capabilities || {}; + + options.service.node.capabilities = options.service.node.capabilities || {}; + + if (!("service" in options.service.identity.capabilities)) { + this.ctx.logger.debug("setting default identity service caps"); + + options.service.identity.capabilities.service = [ + "UNKNOWN", + //"CONTROLLER_SERVICE" + //"VOLUME_ACCESSIBILITY_CONSTRAINTS" + ]; + } + + if (!("volume_expansion" in options.service.identity.capabilities)) { + this.ctx.logger.debug("setting default identity volume_expansion caps"); + + options.service.identity.capabilities.volume_expansion = [ + "UNKNOWN", + //"ONLINE", + //"OFFLINE" + ]; + } + + if (!("rpc" in options.service.controller.capabilities)) { + this.ctx.logger.debug("setting default controller caps"); + + options.service.controller.capabilities.rpc = [ + //"UNKNOWN", + //"CREATE_DELETE_VOLUME", + //"PUBLISH_UNPUBLISH_VOLUME", + //"LIST_VOLUMES", + //"GET_CAPACITY", + //"CREATE_DELETE_SNAPSHOT", + //"LIST_SNAPSHOTS", + //"CLONE_VOLUME", + //"PUBLISH_READONLY", + //"EXPAND_VOLUME" + ]; + + if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { + options.service.controller.capabilities.rpc + .push + //"VOLUME_CONDITION", + //"GET_VOLUME" + (); + } + + if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { + options.service.controller.capabilities.rpc + .push + //"SINGLE_NODE_MULTI_WRITER" + (); + } + } + + if (!("rpc" in options.service.node.capabilities)) { + this.ctx.logger.debug("setting default node caps"); + options.service.node.capabilities.rpc = [ + //"UNKNOWN", + //"STAGE_UNSTAGE_VOLUME", + "GET_VOLUME_STATS", + //"EXPAND_VOLUME", + ]; + + if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { + //options.service.node.capabilities.rpc.push("VOLUME_CONDITION"); + } + + if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { + options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER"); + /** + * This is for volumes that support a mount time gid such as smb or fat + */ + //options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP"); + } + } + } + + /** + * + * @returns CTR + */ + getCTR() { + return this.ctx.registry.get(`${__REGISTRY_NS__}:ctr`, () => { + const driver = this; + let options = _.get(driver.options, "containerd", {}); + options = options || {}; + return new CTR(options); + }); + } + + assertCapabilities(capabilities) { + this.ctx.logger.verbose("validating capabilities: %j", capabilities); + + let message = null; + //[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}] + const valid = capabilities.every((capability) => { + if (capability.access_type != "mount") { + message = `invalid access_type ${capability.access_type}`; + return false; + } + + if (capability.mount.fs_type) { + message = `invalid fs_type ${capability.mount.fs_type}`; + return false; + } + + if ( + capability.mount.mount_flags && + capability.mount.mount_flags.length > 0 + ) { + message = `invalid mount_flags ${capability.mount.mount_flags}`; + return false; + } + + if ( + ![ + "UNKNOWN", + "SINGLE_NODE_WRITER", + "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 + "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 + "SINGLE_NODE_READER_ONLY", + ].includes(capability.access_mode.mode) + ) { + message = `invalid access_mode, ${capability.access_mode.mode}`; + return false; + } + + return true; + }); + + return { valid, message }; + } + + /** + * This should create a dataset with appropriate volume properties, ensuring + * the mountpoint is the target_path + * + * Any volume_context attributes starting with property. will be set as zfs properties + * + * { + "target_path": "/var/lib/kubelet/pods/f8b237db-19e8-44ae-b1d2-740c9aeea702/volumes/kubernetes.io~csi/my-volume-0/mount", + "volume_capability": { + "AccessType": { + "Mount": {} + }, + "access_mode": { + "mode": 1 + } + }, + "volume_context": { + "csi.storage.k8s.io/ephemeral": "true", + "csi.storage.k8s.io/pod.name": "inline-volume-tester-2ptb7", + "csi.storage.k8s.io/pod.namespace": "ephemeral-468", + "csi.storage.k8s.io/pod.uid": "f8b237db-19e8-44ae-b1d2-740c9aeea702", + "csi.storage.k8s.io/serviceAccount.name": "default", + "foo": "bar" + }, + "volume_id": "csi-8228252978a824126924de00126e6aec7c989a48a39d577bd3ab718647df5555" + } + * + * @param {*} call + */ + async NodePublishVolume(call) { + const driver = this; + const ctr = driver.getCTR(); + const filesystem = new Filesystem(); + const mount = new Mount(); + + const volume_id = call.request.volume_id; + const staging_target_path = call.request.staging_target_path || ""; + const target_path = call.request.target_path; + const capability = call.request.volume_capability; + const access_type = capability.access_type || "mount"; + const readonly = call.request.readonly; + const volume_context = call.request.volume_context; + + let result; + + let imageReference; + let imagePullPolicy; + let imagePlatform; + let imageUser; + let labels = {}; + Object.keys(volume_context).forEach(function (key) { + switch (key) { + case "image.reference": + imageReference = volume_context[key]; + break; + case "image.pullPolicy": + imagePullPolicy = volume_context[key]; + break; + case "image.platform": + imagePlatform = volume_context[key]; + break; + case "image.user": + imageUser = volume_context[key]; + break; + } + + if (key.startsWith("snapshot.label.")) { + labels[key.replace(/^snapshot\.label\./, "")] = volume_context[key]; + } + }); + + if (!imageReference) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `image.reference is required` + ); + } + + if (!volume_id) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `volume_id is required` + ); + } + + if (!target_path) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `target_path is required` + ); + } + + if (capability) { + const result = driver.assertCapabilities([capability]); + + if (result.valid !== true) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); + } + } + + // create publish directory + if (!fs.existsSync(target_path)) { + await fs.mkdirSync(target_path, { recursive: true }); + } + + if (process.platform != "win32") { + result = await mount.pathIsMounted(target_path); + if (result) { + return {}; + } + } + + // normalize image reference + let parsedImageReference = parseAll(imageReference); + //console.log(parsedImageReference); + + /** + * const typesTemplates = { + 'digest': ref => `${ref.digest}`, + 'canonical': ref => `${ref.repositoryUrl}@${ref.digest}`, + 'repository': ref => `${ref.repositoryUrl}`, + 'tagged': ref => `${ref.repositoryUrl}:${ref.tag}`, + 'dual': ref => `${ref.repositoryUrl}:${ref.tag}@${ref.digest}` + }; + * + */ + switch (parsedImageReference.type) { + // repository is not enough for `ctr` + case "repository": + imageReference = `${imageReference}:latest`; + parsedImageReference = parseAll(imageReference); + break; + + case "canonical": + case "digest": + case "dual": + case "tagged": + break; + } + + driver.ctx.logger.debug( + `imageReference: ${JSON.stringify(parsedImageReference)}` + ); + + imageReference = parsedImageReference.toString(); + + // normalize image pull policy + if (!imagePullPolicy) { + imagePullPolicy = + parsedImageReference.type == "tagged" && + parsedImageReference.tag == "latest" + ? "Always" + : "IfNotPresent"; + } + + driver.ctx.logger.debug(`effective imagePullPolicy: ${imagePullPolicy}`); + + let doPull = true; + switch (String(imagePullPolicy).toLowerCase()) { + case "never": + doPull = false; + break; + case "always": + doPull = true; + break; + case "ifnotpresent": + try { + await ctr.imageInspect(imageReference); + doPull = false; + } catch (err) {} + break; + } + + if (doPull) { + let ctr_pull_args = []; + if (imagePlatform) { + ctr_pull_args.push("--platform", imagePlatform); + } + + if (imageUser) { + // TODO: decrypt as appropriate + // --user value, -u value User[:password] Registry user and password + ctr_pull_args.push("--user", imageUser); + } + + await ctr.imagePull(imageReference, ctr_pull_args); + } + + let ctr_mount_args = []; + if (imagePlatform) { + ctr_mount_args.push("--platform", imagePlatform); + } + + if (Object.keys(labels).length > 0) { + for (const label in labels) { + ctr_mount_args.push("--label", `${label}=${labels[label]}`); + } + } + + // kubelet will manage readonly for us by bind mounting and ro, it is expected that the driver mounts rw + // if (!readonly) { + // ctr_mount_args.push("--rw"); + // } + ctr_mount_args.push("--rw"); + + await ctr.imageMount(imageReference, target_path, ctr_mount_args); + + return {}; + } + + /** + * This should destroy the dataset and remove target_path as appropriate + * + *{ + "target_path": "/var/lib/kubelet/pods/f8b237db-19e8-44ae-b1d2-740c9aeea702/volumes/kubernetes.io~csi/my-volume-0/mount", + "volume_id": "csi-8228252978a824126924de00126e6aec7c989a48a39d577bd3ab718647df5555" + } + * + * @param {*} call + */ + async NodeUnpublishVolume(call) { + const driver = this; + const ctr = driver.getCTR(); + + const volume_id = call.request.volume_id; + const target_path = call.request.target_path; + + if (!volume_id) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `volume_id is required` + ); + } + + if (!target_path) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `target_path is required` + ); + } + + // unmount + await ctr.imageUnmount(target_path); + + // delete snapshot + try { + await ctr.snapshotDelete(target_path); + } catch (err) { + if (!err.stderr.includes("does not exist")) { + throw err; + } + } + + // cleanup publish directory + if (fs.existsSync(target_path) && fs.lstatSync(target_path).isDirectory()) { + fs.rmSync(target_path, { recursive: true }); + } + + return {}; + } + + /** + * TODO: consider volume_capabilities? + * + * @param {*} call + */ + async GetCapacity(call) { + const driver = this; + const zb = this.getZetabyte(); + + let datasetParentName = this.getVolumeParentDatasetName(); + + if (!datasetParentName) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: missing datasetParentName` + ); + } + + if (call.request.volume_capabilities) { + const result = this.assertCapabilities(call.request.volume_capabilities); + + if (result.valid !== true) { + return { available_capacity: 0 }; + } + } + + const datasetName = datasetParentName; + + let properties; + properties = await zb.zfs.get(datasetName, ["avail"]); + properties = properties[datasetName]; + + return { available_capacity: properties.available.value }; + } + + /** + * + * @param {*} call + */ + async ValidateVolumeCapabilities(call) { + const driver = this; + const result = this.assertCapabilities(call.request.volume_capabilities); + + if (result.valid !== true) { + return { message: result.message }; + } + + return { + confirmed: { + volume_context: call.request.volume_context, + volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested + parameters: call.request.parameters, + }, + }; + } +} + +module.exports.EphemeralInlineContainerDOciDriver = + EphemeralInlineContainerDOciDriver; diff --git a/src/driver/factory.js b/src/driver/factory.js index 0235758..4c8fd23 100644 --- a/src/driver/factory.js +++ b/src/driver/factory.js @@ -14,6 +14,9 @@ const { ControllerSmbClientDriver } = require("./controller-smb-client"); const { ControllerLustreClientDriver } = require("./controller-lustre-client"); const { ControllerObjectiveFSDriver } = require("./controller-objectivefs"); const { ControllerSynologyDriver } = require("./controller-synology"); +const { + EphemeralInlineContainerDOciDriver, +} = require("./ephemeral-inline-containerd-oci"); const { NodeManualDriver } = require("./node-manual"); function factory(ctx, options) { @@ -53,6 +56,8 @@ function factory(ctx, options) { return new ControllerLustreClientDriver(ctx, options); case "objectivefs": return new ControllerObjectiveFSDriver(ctx, options); + case "containerd-oci-ephemeral-inline": + return new EphemeralInlineContainerDOciDriver(ctx, options); case "node-manual": return new NodeManualDriver(ctx, options); default: diff --git a/src/utils/ctr.js b/src/utils/ctr.js new file mode 100644 index 0000000..bb8bbd4 --- /dev/null +++ b/src/utils/ctr.js @@ -0,0 +1,176 @@ +const cp = require("child_process"); + +class CTR { + constructor(options = {}) { + const ctr = this; + ctr.options = options; + + options.containerd = options.containerd || {}; + if (process.platform != "win32" && options.containerd.address) { + //options.containerd.address = "/run/containerd/containerd.sock"; + //options.containerd.address; + } + + if (process.platform == "win32" && options.containerd.windowsAddress) { + // --address value, -a value Address for containerd's GRPC server (default: "\\\\.\\pipe\\containerd-containerd") [%CONTAINERD_ADDRESS%] + options.containerd.address = options.containerd.windowsAddress; + } + + if (!options.containerd.namespace) { + //options.containerd.namespace = "default"; + } + + options.paths = options.paths || {}; + if (!options.paths.ctr) { + options.paths.ctr = "ctr"; + } + + if (!options.paths.sudo) { + options.paths.sudo = "/usr/bin/sudo"; + } + + if (!options.executor) { + options.executor = { + spawn: cp.spawn, + }; + } + + if (!options.env) { + options.env = {}; + } + + if (ctr.options.logger) { + ctr.logger = ctr.options.logger; + } else { + ctr.logger = console; + console.verbose = function () { + console.log(...arguments); + }; + } + } + + async info() { + const ctr = this; + let args = ["info"]; + let result = await ctr.exec(ctr.options.paths.ctr, args); + return result.parsed; + } + + // ctr images pull "${IMAGE}" + async imagePull(image, args = []) { + const ctr = this; + args.unshift("images", "pull"); + args.push(image); + let result = await ctr.exec(ctr.options.paths.ctr, args); + return result.parsed; + } + + // ctr images mount --rw "${IMAGE}" "${MOUNT_TARGET}" + async imageMount(image, target, args = []) { + const ctr = this; + args.unshift("images", "mount"); + args.push(image, target); + let result = await ctr.exec(ctr.options.paths.ctr, args); + return result; + } + + // ctr images unmount "${MOUNT_TARGET}" + async imageUnmount(target, args = []) { + const ctr = this; + args.unshift("images", "unmount"); + args.push(target); + let result = await ctr.exec(ctr.options.paths.ctr, args); + return result; + } + + // ctr image inspect docker.io/library/ubuntu:latest + async imageInspect(image, args = []) { + const ctr = this; + args.unshift("images", "inspect"); + args.push(image); + let result = await ctr.exec(ctr.options.paths.ctr, args); + return result; + } + + async snapshotList(args = []) { + const ctr = this; + args.unshift("snapshot", "list"); + let result = await ctr.exec(ctr.options.paths.ctr, args); + return result; + } + + // ctr snapshots delete [command options] [, ...] + async snapshotDelete(key) { + const ctr = this; + let args = ["snapshot", "delete"]; + args.push(key); + let result = await ctr.exec(ctr.options.paths.ctr, args); + return result; + } + + exec(command, args, options = {}) { + // if (!options.hasOwnProperty("timeout")) { + // options.timeout = DEFAULT_TIMEOUT; + // } + + const ctr = this; + args = args || []; + + // --debug + + if (process.platform != "win32" && ctr.options.sudo) { + args.unshift(command); + command = ctr.options.paths.sudo; + } + + options.env = { ...{}, ...ctr.options.env, ...options.env }; + + if (ctr.options.containerd.address) { + options.env.CONTAINERD_ADDRESS = ctr.options.containerd.address; + } + + if (ctr.options.containerd.namespace) { + options.env.CONTAINERD_NAMESPACE = ctr.options.containerd.namespace; + } + + options.env.PATH = process.env.PATH; + + ctr.logger.verbose("executing ctr command: %s %s", command, args.join(" ")); + + return new Promise((resolve, reject) => { + const child = ctr.options.executor.spawn(command, args, options); + + let stdout = ""; + let stderr = ""; + + 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 }; + try { + result.parsed = JSON.parse(result.stdout); + } catch (err) {} + + // timeout scenario + if (code === null) { + result.timeout = true; + reject(result); + } + + if (code) { + reject(result); + } else { + resolve(result); + } + }); + }); + } +} + +module.exports.CTR = CTR; From 018de54685bf9aa2a8518c9f9cc8ee59ba43b312 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 29 Oct 2025 19:31:56 -0600 Subject: [PATCH 20/55] further truenas 25.10 support Signed-off-by: Travis Glenn Hansen --- src/driver/freenas/api.js | 68 +++++++++++++++++++++++++++++----- src/driver/freenas/http/api.js | 63 +++++++++++++++++++++++++++---- src/driver/freenas/ssh.js | 39 +++++++++++++++++-- 3 files changed, 148 insertions(+), 22 deletions(-) diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index b47286b..1e83022 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -181,10 +181,7 @@ class FreeNASApiDriver extends CsiBaseDriver { const httpApiClient = await this.getTrueNASHttpApiClient(); const apiVersion = httpClient.getApiVersion(); const zb = await this.getZetabyte(); - const truenasVersion = semver.coerce( - await httpApiClient.getSystemVersionMajorMinor(), - { loose: true } - ); + const truenasVersion = await httpApiClient.getSystemVersionSemver(); if (!truenasVersion) { throw new GrpcError( @@ -506,6 +503,40 @@ class FreeNASApiDriver extends CsiBaseDriver { } } + if (isScale && semver.satisfies(truenasVersion, ">=25.10")) { + let topLevelProperties = [ + "purpose", + "name", + "path", + "enabled", + "comment", + "readonly", + "browsable", + "access_based_share_enumeration", + "audit", + ]; + let disallowedOptions = ["abe"]; + share.purpose = "LEGACY_SHARE"; + share.options = { + purpose: "LEGACY_SHARE", + }; + for (const key in share) { + switch (key) { + case "options": + // ignore + break; + default: + if (!topLevelProperties.includes(key)) { + if (!disallowedOptions.includes(key)) { + share.options[key] = share[key]; + } + delete share[key]; + } + break; + } + } + } + switch (apiVersion) { case 1: endpoint = "/sharing/cifs"; @@ -2819,13 +2850,13 @@ class FreeNASApiDriver extends CsiBaseDriver { // set quota if (this.options.zfs.datasetEnableQuotas) { setProps = true; - properties.refquota = capacity_bytes; + properties.refquota = Number(capacity_bytes); } // set reserve if (this.options.zfs.datasetEnableReservation) { setProps = true; - properties.refreservation = capacity_bytes; + properties.refreservation = Number(capacity_bytes); } // quota for dataset and all children @@ -2933,7 +2964,7 @@ class FreeNASApiDriver extends CsiBaseDriver { // this should be already set, but when coming from a volume source // it may not match that of the source - properties.volsize = capacity_bytes; + properties.volsize = Number(capacity_bytes); // dedup // on, off, verify @@ -3221,17 +3252,17 @@ class FreeNASApiDriver extends CsiBaseDriver { // set quota if (this.options.zfs.datasetEnableQuotas) { setProps = true; - properties.refquota = capacity_bytes; + properties.refquota = Number(capacity_bytes); } // set reserve if (this.options.zfs.datasetEnableReservation) { setProps = true; - properties.refreservation = capacity_bytes; + properties.refreservation = Number(capacity_bytes); } break; case "volume": - properties.volsize = capacity_bytes; + properties.volsize = Number(capacity_bytes); setProps = true; // managed automatically for zvols @@ -3532,6 +3563,7 @@ class FreeNASApiDriver extends CsiBaseDriver { const httpClient = await this.getHttpClient(); const httpApiClient = await this.getTrueNASHttpApiClient(); const zb = await this.getZetabyte(); + const truenasVersion = await httpApiClient.getSystemVersionSemver(); let entries = []; let entries_length = 0; @@ -3693,11 +3725,19 @@ class FreeNASApiDriver extends CsiBaseDriver { response = await httpClient.get(endpoint, { "extra.snapshots": 1, "extra.snapshots_properties": JSON.stringify(zfsProperties), + //"extra.snapshots_properties": "null", }); if (response.statusCode == 404) { throw new Error("dataset does not exist"); } else if (response.statusCode == 200) { for (let snapshot of response.body.snapshots) { + if (semver.satisfies(truenasVersion, ">=25.10")) { + // request the snapshot because fetching properties is broken with the dataset is broken + snapshot.properties = await httpApiClient.SnapshotGet( + snapshot.id, + zfsProperties + ); + } let row = {}; for (let p in snapshot.properties) { row[p] = snapshot.properties[p].rawvalue; @@ -3716,12 +3756,20 @@ class FreeNASApiDriver extends CsiBaseDriver { response = await httpClient.get(endpoint, { "extra.snapshots": 1, "extra.snapshots_properties": JSON.stringify(zfsProperties), + //"extra.snapshots_properties": "null", }); if (response.statusCode == 404) { throw new Error("dataset does not exist"); } else if (response.statusCode == 200) { for (let child of response.body.children) { for (let snapshot of child.snapshots) { + if (semver.satisfies(truenasVersion, ">=25.10")) { + // request the snapshot because fetching properties is broken with the dataset is broken + snapshot.properties = await httpApiClient.SnapshotGet( + snapshot.id, + zfsProperties + ); + } let row = {}; for (let p in snapshot.properties) { row[p] = snapshot.properties[p].rawvalue; diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index 79d7311..a5bb236 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -1,4 +1,5 @@ const _ = require("lodash"); +const semver = require("semver"); const { sleep, stringify } = require("../../../utils/general"); const { Zetabyte } = require("../../../utils/zfs"); const { Registry } = require("../../../utils/registry"); @@ -183,6 +184,12 @@ class Api { return majorMinor.split(".")[0]; } + async getSystemVersionSemver() { + return semver.coerce(await this.getSystemVersionMajorMinor(), { + loose: true, + }); + } + async setVersionInfoCache(versionInfo) { await this.cache.set(FREENAS_SYSTEM_VERSION_CACHE_KEY, versionInfo, { ttl: 60 * 1000, @@ -250,7 +257,7 @@ class Api { let user_properties = {}; for (const property in properties) { if (this.getIsUserProperty(property)) { - user_properties[property] = properties[property]; + user_properties[property] = String(properties[property]); } } @@ -271,7 +278,15 @@ class Api { getPropertiesKeyValueArray(properties) { let arr = []; for (const property in properties) { - arr.push({ key: property, value: properties[property] }); + let value = properties[property]; + if ( + this.getIsUserProperty(property) && + value != null && + value !== undefined + ) { + value = String(value); + } + arr.push({ key: property, value }); } return arr; @@ -501,10 +516,17 @@ class Api { async SnapshotSet(snapshotName, properties) { const httpClient = await this.getHttpClient(false); + const systemVersionSemver = await this.getSystemVersionSemver(); + let response; let endpoint; - endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = `/pool/snapshot/id/${encodeURIComponent(snapshotName)}`; + } else { + endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; + } + response = await httpClient.put(endpoint, { //...this.getSystemProperties(properties), user_properties_update: this.getPropertiesKeyValueArray( @@ -529,10 +551,17 @@ class Api { */ async SnapshotGet(snapshotName, properties) { const httpClient = await this.getHttpClient(false); + const systemVersionSemver = await this.getSystemVersionSemver(); + let response; let endpoint; - endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = `/pool/snapshot/id/${encodeURIComponent(snapshotName)}`; + } else { + endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; + } + response = await httpClient.get(endpoint); if (response.statusCode == 200) { @@ -540,7 +569,7 @@ class Api { } if (response.statusCode == 404) { - throw new Error("dataset does not exist"); + throw new Error("snapshot does not exist"); } throw new Error(JSON.stringify(response.body)); @@ -549,6 +578,7 @@ class Api { async SnapshotCreate(snapshotName, data = {}) { const httpClient = await this.getHttpClient(false); const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); let response; let endpoint; @@ -559,7 +589,12 @@ class Api { data.dataset = dataset; data.name = snapshot; - endpoint = "/zfs/snapshot"; + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = "/pool/snapshot"; + } else { + endpoint = "/zfs/snapshot"; + } + response = await httpClient.post(endpoint, data); if (response.statusCode == 200) { @@ -579,11 +614,17 @@ class Api { async SnapshotDelete(snapshotName, data = {}) { const httpClient = await this.getHttpClient(false); const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); let response; let endpoint; - endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = `/pool/snapshot/id/${encodeURIComponent(snapshotName)}`; + } else { + endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; + } + response = await httpClient.delete(endpoint, data); if (response.statusCode == 200) { @@ -607,6 +648,7 @@ class Api { async CloneCreate(snapshotName, datasetName, data = {}) { const httpClient = await this.getHttpClient(false); const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); let response; let endpoint; @@ -614,7 +656,12 @@ class Api { data.snapshot = snapshotName; data.dataset_dst = datasetName; - endpoint = "/zfs/snapshot/clone"; + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = "/pool/snapshot/clone"; + } else { + endpoint = "/zfs/snapshot/clone"; + } + response = await httpClient.post(endpoint, data); if (response.statusCode == 200) { diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 7163d7f..27324a5 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -269,10 +269,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { const httpApiClient = await this.getTrueNASHttpApiClient(); const apiVersion = httpClient.getApiVersion(); const zb = await this.getZetabyte(); - const truenasVersion = semver.coerce( - await httpApiClient.getSystemVersionMajorMinor(), - { loose: true } - ); + const truenasVersion = await httpApiClient.getSystemVersionSemver(); if (!truenasVersion) { throw new GrpcError( @@ -594,6 +591,40 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { } } + if (isScale && semver.satisfies(truenasVersion, ">=25.10")) { + let topLevelProperties = [ + "purpose", + "name", + "path", + "enabled", + "comment", + "readonly", + "browsable", + "access_based_share_enumeration", + "audit", + ]; + let disallowedOptions = ["abe"]; + share.purpose = "LEGACY_SHARE"; + share.options = { + purpose: "LEGACY_SHARE", + }; + for (const key in share) { + switch (key) { + case "options": + // ignore + break; + default: + if (!topLevelProperties.includes(key)) { + if (!disallowedOptions.includes(key)) { + share.options[key] = share[key]; + } + delete share[key]; + } + break; + } + } + } + switch (apiVersion) { case 1: endpoint = "/sharing/cifs"; From 5d8cd2253a30f823e55155775845b8c9b9e1a257 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 29 Oct 2025 22:38:14 -0600 Subject: [PATCH 21/55] better zfs path support Signed-off-by: Travis Glenn Hansen --- Dockerfile | 2 +- src/driver/controller-zfs-generic/index.js | 21 ++--- src/driver/freenas/ssh.js | 88 ++++++++----------- .../zfs-local-ephemeral-inline/index.js | 19 +++- 4 files changed, 59 insertions(+), 71 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8dfd58e..4d8bf62 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ ENV NODE_VERSION=v20.19.0 ENV NODE_ENV=production # install build deps -#RUN apt-get update && apt-get install -y python3 make cmake gcc g++ +RUN apt-get update && apt-get install -y python3 make cmake gcc g++ # install node RUN apt-get update && apt-get install -y wget xz-utils diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index bec3c68..45146cd 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -38,26 +38,17 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { options.executor = execClient; } options.idempotent = true; - options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); - - // Run automatic detection first if (typeof this.setZetabyteCustomOptions === "function") { await this.setZetabyteCustomOptions(options); } - // Manual override comes after automatic detection - // Only apply if user explicitly provided non-empty paths - if ( - this.options.zfs.hasOwnProperty("cli") && - this.options.zfs.cli && - this.options.zfs.cli.hasOwnProperty("paths") && - this.options.zfs.cli.paths && - Object.keys(this.options.zfs.cli.paths).length > 0 - ) { - // User explicitly configured paths - use them - options.paths = this.options.zfs.cli.paths; - } + options.paths = options.paths || {}; + options.paths = Object.assign( + {}, + options.paths, + _.get(this.options, "zfs.cli.paths", {}) + ); return new Zetabyte(options); }); diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index f897b73..f18bb09 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -70,26 +70,17 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { const options = {}; options.executor = new ZfsSshProcessManager(sshClient); options.idempotent = true; - options.sudo = _.get(this.options, "zfs.cli.sudoEnabled", false); - - // Run automatic detection first if (typeof this.setZetabyteCustomOptions === "function") { await this.setZetabyteCustomOptions(options); } - // Manual override comes after automatic detection - // Only apply if user explicitly provided non-empty paths - if ( - this.options.zfs.hasOwnProperty("cli") && - this.options.zfs.cli && - this.options.zfs.cli.hasOwnProperty("paths") && - this.options.zfs.cli.paths && - Object.keys(this.options.zfs.cli.paths).length > 0 - ) { - // User explicitly configured paths - use them - options.paths = this.options.zfs.cli.paths; - } + options.paths = options.paths || {}; + options.paths = Object.assign( + {}, + options.paths, + _.get(this.options, "zfs.cli.paths", {}) + ); return new Zetabyte(options); }); @@ -108,6 +99,8 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { return "filesystem"; case "freenas-iscsi": case "truenas-iscsi": + case "freenas-nvmeof": + case "truenas-nvmeof": return "volume"; default: throw new Error("unknown driver: " + this.ctx.args.driver); @@ -115,36 +108,31 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { } async setZetabyteCustomOptions(options) { - if (!options.hasOwnProperty("paths")) { - const majorMinor = await this.getSystemVersionMajorMinor(); - const isScale = await this.getIsScale(); - - if (!isScale && Number(majorMinor) >= 12) { - // TrueNAS CORE/Enterprise version 12+ + const major = await this.getSystemVersionMajor(); + const isScale = await this.getIsScale(); + + if (!isScale && Number(major) >= 12) { + options.paths = { + zfs: "/usr/local/sbin/zfs", + zpool: "/usr/local/sbin/zpool", + sudo: "/usr/local/bin/sudo", + chroot: "/usr/sbin/chroot", + }; + } else if (isScale) { + if (Number(major) >= 25) { + options.paths = { + zfs: "/usr/sbin/zfs", + zpool: "/usr/sbin/zpool", + sudo: "/usr/bin/sudo", + chroot: "/usr/sbin/chroot", + }; + } else { options.paths = { zfs: "/usr/local/sbin/zfs", zpool: "/usr/local/sbin/zpool", - sudo: "/usr/local/bin/sudo", + sudo: "/usr/bin/sudo", chroot: "/usr/sbin/chroot", }; - } else if (isScale) { - if (Number(majorMinor) >= 25) { - // TrueNAS SCALE version 25+ (paths changed to /usr/sbin in 25.04) - options.paths = { - zfs: "/usr/sbin/zfs", - zpool: "/usr/sbin/zpool", - sudo: "/usr/bin/sudo", - chroot: "/usr/sbin/chroot", - }; - } else { - // TrueNAS SCALE versions before 25 - options.paths = { - zfs: "/usr/local/sbin/zfs", - zpool: "/usr/local/sbin/zpool", - sudo: "/usr/bin/sudo", - chroot: "/usr/sbin/chroot", - }; - } } } } @@ -2226,20 +2214,14 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { async getIsScale() { const systemVersion = await this.getSystemVersion(); + const major = await this.getSystemVersionMajor(); - if (systemVersion.v2) { - const versionLower = systemVersion.v2.toLowerCase(); - // Check for explicit "scale" in version string - if (versionLower.includes("scale")) { - return true; - } - // TrueNAS 25+ doesn't include "SCALE" in version string, but versions 25+ are SCALE-only - if (versionLower.includes("truenas")) { - const majorVersion = await this.getSystemVersionMajor(); - if (Number(majorVersion) >= 25) { - return true; - } - } + // starting with version 25 the version string no longer contains `-SCALE` + if ( + systemVersion.v2 && + (systemVersion.v2.toLowerCase().includes("scale") || Number(major) >= 20) + ) { + return true; } return false; diff --git a/src/driver/zfs-local-ephemeral-inline/index.js b/src/driver/zfs-local-ephemeral-inline/index.js index 276b1f4..d231d32 100644 --- a/src/driver/zfs-local-ephemeral-inline/index.js +++ b/src/driver/zfs-local-ephemeral-inline/index.js @@ -140,7 +140,8 @@ class ZfsLocalEphemeralInlineDriver extends CsiBaseDriver { sshClient = this.getSshClient(); executor = new ZfsSshProcessManager(sshClient); } - return new Zetabyte({ + + const options = { executor, idempotent: true, chroot: this.options.zfs.chroot, @@ -148,7 +149,21 @@ class ZfsLocalEphemeralInlineDriver extends CsiBaseDriver { zpool: "/usr/sbin/zpool", zfs: "/usr/sbin/zfs", }, - }); + }; + + if (process.env.DEMOCRATIC_CSI_IS_CONTAINER == "true") { + delete options.chroot; + options.paths.zpool = "/usr/local/bin/zpool"; + options.paths.zfs = "/usr/local/bin/zfs"; + } + + options.paths = Object.assign( + {}, + options.paths, + _.get(this.options, "zfs.cli.paths", {}) + ); + + return new Zetabyte(options); }); } From be57b695ed533e952e8f80b6f0fcb4bfe1238af6 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 30 Oct 2025 11:57:59 -0600 Subject: [PATCH 22/55] prebuilt re2 binaries for image Signed-off-by: Travis Glenn Hansen --- Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4d8bf62..5d70bfa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -38,7 +38,7 @@ ENV NODE_VERSION=v20.19.0 ENV NODE_ENV=production # install build deps -RUN apt-get update && apt-get install -y python3 make cmake gcc g++ +# RUN apt-get update && apt-get install -y python3 make cmake gcc g++ # install node RUN apt-get update && apt-get install -y wget xz-utils @@ -60,6 +60,10 @@ RUN useradd --create-home csi \ WORKDIR /home/csi/app USER csi +# prevent need to build re2 module +ENV RE2_DOWNLOAD_MIRROR="https://grpc-uds-binaries.s3-us-west-2.amazonaws.com/re2" +ENV RE2_DOWNLOAD_SKIP_PATH=1 + COPY --chown=csi:csi package*.json ./ RUN npm install --only=production --grpc_node_binary_host_mirror=https://grpc-uds-binaries.s3-us-west-2.amazonaws.com/debian-buster COPY --chown=csi:csi . . From 2a6fd4f91d37c42b25735d37617e78053eef0f42 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 30 Oct 2025 13:39:21 -0600 Subject: [PATCH 23/55] limit build architectures Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 15 ++++++++------- .../scale/{25.04 => 25.10}/scale-iscsi.yaml | 0 .../truenas/scale/{25.04 => 25.10}/scale-nfs.yaml | 0 .../truenas/scale/{25.04 => 25.10}/scale-smb.yaml | 0 4 files changed, 8 insertions(+), 7 deletions(-) rename ci/configs/truenas/scale/{25.04 => 25.10}/scale-iscsi.yaml (100%) rename ci/configs/truenas/scale/{25.04 => 25.10}/scale-nfs.yaml (100%) rename ci/configs/truenas/scale/{25.04 => 25.10}/scale-smb.yaml (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3789ddf..1a0d09d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -115,7 +115,7 @@ jobs: SYNOLOGY_PASSWORD: ${{ secrets.SANITY_SYNOLOGY_PASSWORD }} SYNOLOGY_VOLUME: ${{ secrets.SANITY_SYNOLOGY_VOLUME }} - csi-sanity-truenas-scale-25_04: + csi-sanity-truenas-scale-25_10: needs: - build-npm-linux-amd64 strategy: @@ -123,10 +123,10 @@ jobs: max-parallel: 1 matrix: config: - - truenas/scale/25.04/scale-iscsi.yaml - - truenas/scale/25.04/scale-nfs.yaml + - truenas/scale/25.10/scale-iscsi.yaml + - truenas/scale/25.10/scale-nfs.yaml # 80 char limit - - truenas/scale/25.04/scale-smb.yaml + - truenas/scale/25.10/scale-smb.yaml runs-on: - self-hosted - Linux @@ -435,7 +435,7 @@ jobs: - determine-image-tag - csi-sanity-synology-dsm6 - csi-sanity-synology-dsm7 - - csi-sanity-truenas-scale-25_04 + - csi-sanity-truenas-scale-25_10 - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic - csi-sanity-objectivefs @@ -468,14 +468,15 @@ jobs: GHCR_PASSWORD: ${{ secrets.GHCR_PASSWORD }} OBJECTIVEFS_DOWNLOAD_ID: ${{ secrets.OBJECTIVEFS_DOWNLOAD_ID }} DOCKER_CLI_EXPERIMENTAL: enabled - DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le + # DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le + DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64 IMAGE_TAG: ${{needs.determine-image-tag.outputs.tag}} build-docker-windows: needs: - csi-sanity-synology-dsm6 - csi-sanity-synology-dsm7 - - csi-sanity-truenas-scale-25_04 + - csi-sanity-truenas-scale-25_10 - csi-sanity-truenas-core-13_0 - csi-sanity-zfs-generic - csi-sanity-objectivefs diff --git a/ci/configs/truenas/scale/25.04/scale-iscsi.yaml b/ci/configs/truenas/scale/25.10/scale-iscsi.yaml similarity index 100% rename from ci/configs/truenas/scale/25.04/scale-iscsi.yaml rename to ci/configs/truenas/scale/25.10/scale-iscsi.yaml diff --git a/ci/configs/truenas/scale/25.04/scale-nfs.yaml b/ci/configs/truenas/scale/25.10/scale-nfs.yaml similarity index 100% rename from ci/configs/truenas/scale/25.04/scale-nfs.yaml rename to ci/configs/truenas/scale/25.10/scale-nfs.yaml diff --git a/ci/configs/truenas/scale/25.04/scale-smb.yaml b/ci/configs/truenas/scale/25.10/scale-smb.yaml similarity index 100% rename from ci/configs/truenas/scale/25.04/scale-smb.yaml rename to ci/configs/truenas/scale/25.10/scale-smb.yaml From 78a5342809f972456436cfba68cb4c291d0efabe Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 30 Oct 2025 14:44:17 -0600 Subject: [PATCH 24/55] build windows 2022 and 2025 Signed-off-by: Travis Glenn Hansen --- .github/bin/docker-release-windows.sh | 46 +++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/.github/bin/docker-release-windows.sh b/.github/bin/docker-release-windows.sh index 21e2e13..cc2ba72 100755 --- a/.github/bin/docker-release-windows.sh +++ b/.github/bin/docker-release-windows.sh @@ -3,7 +3,7 @@ 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 +echo "$GHCR_PASSWORD" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin export DOCKER_ORG="democraticcsi" export DOCKER_PROJECT="democratic-csi" @@ -16,29 +16,29 @@ 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 + # create local manifest to work with + buildah manifest rm "${MANIFEST_NAME}" || true + buildah manifest create "${MANIFEST_NAME}" - # 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}" + # all all the existing linux data to the manifest + buildah manifest add "${MANIFEST_NAME}" --all "${DOCKER_REPO}:${IMAGE_TAG}" + 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} + # import pre-built images + buildah pull docker-archive:democratic-csi-windows-ltsc2022.tar + buildah pull docker-archive:democratic-csi-windows-ltsc2025.tar - # cleanup - buildah manifest rm "${MANIFEST_NAME}" || true + # add pre-built images to manifest + buildah manifest add "${MANIFEST_NAME}" democratic-csi-windows:${GITHUB_RUN_ID}-ltsc2022 + 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 + : +fi From b3292da53decad61979614df20f67e15a54bf3bf Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 31 Oct 2025 02:31:21 -0600 Subject: [PATCH 25/55] introduce -nvmeof drivers for TrueNAS Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 1 + Dockerfile | 3 +- .../truenas/scale/25.10/scale-nvmeof.yaml | 32 + src/driver/factory.js | 9 +- src/driver/freenas/api.js | 3185 ++++++++-------- src/driver/freenas/http/api.js | 350 ++ src/driver/freenas/ssh.js | 3193 +++++++++-------- src/driver/index.js | 131 +- src/utils/nvmeof.js | 124 +- 9 files changed, 4061 insertions(+), 2967 deletions(-) create mode 100644 ci/configs/truenas/scale/25.10/scale-nvmeof.yaml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1a0d09d..16cdb78 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -125,6 +125,7 @@ jobs: config: - truenas/scale/25.10/scale-iscsi.yaml - truenas/scale/25.10/scale-nfs.yaml + - truenas/scale/25.10/scale-nvmeof.yaml # 80 char limit - truenas/scale/25.10/scale-smb.yaml runs-on: diff --git a/Dockerfile b/Dockerfile index 5d70bfa..28bab3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,7 @@ ###################### # golang builder ###################### -FROM golang:1.25.3-bookworm as ctrbuilder +FROM golang:1.25.3-bookworm AS ctrbuilder # /go/containerd/ctr ADD docker/ctr-mount-labels.diff /tmp @@ -61,6 +61,7 @@ WORKDIR /home/csi/app USER csi # prevent need to build re2 module +# https://github.com/uhop/install-artifact-from-github/wiki/Making-local-mirror ENV RE2_DOWNLOAD_MIRROR="https://grpc-uds-binaries.s3-us-west-2.amazonaws.com/re2" ENV RE2_DOWNLOAD_SKIP_PATH=1 diff --git a/ci/configs/truenas/scale/25.10/scale-nvmeof.yaml b/ci/configs/truenas/scale/25.10/scale-nvmeof.yaml new file mode 100644 index 0000000..34c5828 --- /dev/null +++ b/ci/configs/truenas/scale/25.10/scale-nvmeof.yaml @@ -0,0 +1,32 @@ +driver: freenas-api-nvmeof + +httpConnection: + protocol: http + host: ${TRUENAS_HOST} + port: 80 + #apiKey: + username: ${TRUENAS_USERNAME} + password: ${TRUENAS_PASSWORD} + +zfs: + datasetParentName: tank/ci/${CI_BUILD_KEY}/v + detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s + + zvolCompression: + zvolDedup: + zvolEnableReservation: false + zvolBlocksize: + +nvmeof: + transports: + - tcp://${TRUENAS_HOST}:4420 + namePrefix: "csi-ci-${CI_BUILD_KEY}-" + ports: + - 1 + +# https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 +_private: + csi: + volume: + idHash: + strategy: crc16 diff --git a/src/driver/factory.js b/src/driver/factory.js index 4c8fd23..da6874e 100644 --- a/src/driver/factory.js +++ b/src/driver/factory.js @@ -24,13 +24,20 @@ function factory(ctx, options) { case "freenas-nfs": case "freenas-smb": case "freenas-iscsi": + case "freenas-nvmeof": case "truenas-nfs": case "truenas-smb": case "truenas-iscsi": + case "truenas-nvmeof": return new FreeNASSshDriver(ctx, options); - case "freenas-api-iscsi": case "freenas-api-nfs": case "freenas-api-smb": + case "freenas-api-iscsi": + case "freenas-api-nvmeof": + case "truenas-api-nfs": + case "truenas-api-smb": + case "truenas-api-iscsi": + case "truenas-api-nvmeof": return new FreeNASApiDriver(ctx, options); case "synology-nfs": case "synology-smb": diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 1e83022..65cf970 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -13,6 +13,8 @@ const semver = require("semver"); // freenas properties const FREENAS_NFS_SHARE_PROPERTY_NAME = "democratic-csi:freenas_nfs_share_id"; const FREENAS_SMB_SHARE_PROPERTY_NAME = "democratic-csi:freenas_smb_share_id"; + +// iscsi const FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME = "democratic-csi:freenas_iscsi_target_id"; const FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME = @@ -22,6 +24,14 @@ const FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME = const FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:freenas_iscsi_assets_name"; +// nvmeof +const FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME = + "democratic-csi:freenas_nvmeof_subsystem_id"; +const FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME = + "democratic-csi:freenas_nvmeof_namespace_id"; +const FREENAS_NVMEOF_ASSETS_NAME_PROPERTY_NAME = + "democratic-csi:freenas_nvmeof_assets_name"; + // zfs common properties const MANAGED_PROPERTY_NAME = "democratic-csi:managed_resource"; const SUCCESS_PROPERTY_NAME = "democratic-csi:provision_success"; @@ -200,22 +210,572 @@ class FreeNASApiDriver extends CsiBaseDriver { switch (driverShareType) { case "nfs": - properties = await httpApiClient.DatasetGet(datasetName, [ - "mountpoint", - FREENAS_NFS_SHARE_PROPERTY_NAME, - ]); - this.ctx.logger.debug("zfs props data: %j", properties); + { + properties = await httpApiClient.DatasetGet(datasetName, [ + "mountpoint", + FREENAS_NFS_SHARE_PROPERTY_NAME, + ]); + this.ctx.logger.debug("zfs props data: %j", properties); - // create nfs share - if ( - !zb.helpers.isPropertyValueSet( - properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value - ) - ) { - let nfsShareComment; - if (this.options.nfs.shareCommentTemplate) { - nfsShareComment = Handlebars.compile( - this.options.nfs.shareCommentTemplate + // create nfs share + if ( + !zb.helpers.isPropertyValueSet( + properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value + ) + ) { + let nfsShareComment; + if (this.options.nfs.shareCommentTemplate) { + nfsShareComment = Handlebars.compile( + this.options.nfs.shareCommentTemplate + )({ + name: call.request.name, + parameters: call.request.parameters, + csi: { + name: this.ctx.args.csiName, + version: this.ctx.args.csiVersion, + }, + zfs: { + datasetName: datasetName, + }, + }); + } else { + nfsShareComment = `democratic-csi (${this.ctx.args.csiName}): ${datasetName}`; + } + + switch (apiVersion) { + case 1: + case 2: + switch (apiVersion) { + case 1: + share = { + nfs_paths: [properties.mountpoint.value], + nfs_comment: nfsShareComment || "", + nfs_network: + this.options.nfs.shareAllowedNetworks.join(","), + nfs_hosts: this.options.nfs.shareAllowedHosts.join(","), + nfs_alldirs: this.options.nfs.shareAlldirs, + nfs_ro: false, + nfs_quiet: false, + nfs_maproot_user: this.options.nfs.shareMaprootUser, + nfs_maproot_group: this.options.nfs.shareMaprootGroup, + nfs_mapall_user: this.options.nfs.shareMapallUser, + nfs_mapall_group: this.options.nfs.shareMapallGroup, + nfs_security: [], + }; + break; + case 2: + share = { + paths: [properties.mountpoint.value], + comment: nfsShareComment || "", + networks: this.options.nfs.shareAllowedNetworks, + hosts: this.options.nfs.shareAllowedHosts, + alldirs: this.options.nfs.shareAlldirs, + ro: false, + quiet: false, + maproot_user: this.options.nfs.shareMaprootUser, + maproot_group: this.options.nfs.shareMaprootGroup, + mapall_user: this.options.nfs.shareMapallUser, + mapall_group: this.options.nfs.shareMapallGroup, + security: [], + }; + break; + } + + if (isScale && semver.satisfies(truenasVersion, ">=23.10")) { + delete share.quiet; + delete share.nfs_quiet; + } + + if (isScale && semver.satisfies(truenasVersion, ">=22.12")) { + share.path = share.paths[0]; + delete share.paths; + delete share.alldirs; + } + + 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 + * v2 = 200 + */ + if ([200, 201].includes(response.statusCode)) { + let sharePaths; + switch (apiVersion) { + case 1: + sharePaths = response.body.nfs_paths; + break; + case 2: + if (response.body.path) { + sharePaths = [response.body.path]; + } else { + sharePaths = response.body.paths; + } + break; + } + + // FreeNAS responding with bad data + if (!sharePaths.includes(properties.mountpoint.value)) { + throw new GrpcError( + grpc.status.UNKNOWN, + `FreeNAS responded with incorrect share data: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + //set zfs property + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_NFS_SHARE_PROPERTY_NAME]: response.body.id, + }); + } else { + /** + * v1 = 409 + * v2 = 422 + */ + if ( + [409, 422].includes(response.statusCode) && + (JSON.stringify(response.body).includes( + "You can't share same filesystem with all hosts twice." + ) || + JSON.stringify(response.body).includes( + "Another NFS share already exports this dataset for some network" + )) + ) { + let lookupShare = + await httpApiClient.findResourceByProperties( + "/sharing/nfs", + (item) => { + if ( + (item.nfs_paths && + item.nfs_paths.includes( + properties.mountpoint.value + )) || + (item.paths && + item.paths.includes( + properties.mountpoint.value + )) || + (item.path && + item.path == properties.mountpoint.value) + ) { + return true; + } + return false; + } + ); + + if (!lookupShare) { + throw new GrpcError( + grpc.status.UNKNOWN, + `FreeNAS failed to find matching share` + ); + } + + //set zfs property + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_NFS_SHARE_PROPERTY_NAME]: lookupShare.id, + }); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating nfs share - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } + } + + volume_context = { + node_attach_driver: "nfs", + server: this.options.nfs.shareHost, + share: properties.mountpoint.value, + }; + return volume_context; + } + break; + /** + * TODO: smb need to be more defensive like iscsi and nfs + * ensuring the path is valid and the shareName + */ + case "smb": + { + properties = await httpApiClient.DatasetGet(datasetName, [ + "mountpoint", + FREENAS_SMB_SHARE_PROPERTY_NAME, + ]); + this.ctx.logger.debug("zfs props data: %j", properties); + + let smbName; + + if (this.options.smb.nameTemplate) { + smbName = Handlebars.compile(this.options.smb.nameTemplate)({ + name: call.request.name, + parameters: call.request.parameters, + }); + } else { + smbName = zb.helpers.extractLeafName(datasetName); + } + + if (this.options.smb.namePrefix) { + smbName = this.options.smb.namePrefix + smbName; + } + + if (this.options.smb.nameSuffix) { + smbName += this.options.smb.nameSuffix; + } + + smbName = smbName.toLowerCase(); + + this.ctx.logger.info( + "FreeNAS creating smb share with name: " + smbName + ); + + // create smb share + if ( + !zb.helpers.isPropertyValueSet( + properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value + ) + ) { + /** + * The only required parameters are: + * - path + * - name + * + * Note that over time it appears the list of available parameters has increased + * so in an effort to best support old versions of FreeNAS we should check the + * presense of each parameter in the config and set the corresponding parameter in + * the API request *only* if present in the config. + */ + switch (apiVersion) { + case 1: + case 2: + share = { + name: smbName, + path: properties.mountpoint.value, + }; + + let propertyMapping = { + shareAuxiliaryConfigurationTemplate: "auxsmbconf", + shareHome: "home", + shareAllowedHosts: "hostsallow", + shareDeniedHosts: "hostsdeny", + shareDefaultPermissions: "default_permissions", + shareGuestOk: "guestok", + shareGuestOnly: "guestonly", + shareShowHiddenFiles: "showhiddenfiles", + shareRecycleBin: "recyclebin", + shareBrowsable: "browsable", + shareAccessBasedEnumeration: "abe", + shareTimeMachine: "timemachine", + shareStorageTask: "storage_task", + }; + + for (const key in propertyMapping) { + if (this.options.smb.hasOwnProperty(key)) { + let value; + switch (key) { + case "shareAuxiliaryConfigurationTemplate": + value = Handlebars.compile( + this.options.smb.shareAuxiliaryConfigurationTemplate + )({ + name: call.request.name, + parameters: call.request.parameters, + }); + break; + default: + value = this.options.smb[key]; + break; + } + share[propertyMapping[key]] = value; + } + } + + if (isScale && semver.satisfies(truenasVersion, ">=25.10")) { + let topLevelProperties = [ + "purpose", + "name", + "path", + "enabled", + "comment", + "readonly", + "browsable", + "access_based_share_enumeration", + "audit", + ]; + let disallowedOptions = ["abe"]; + share.purpose = "LEGACY_SHARE"; + share.options = { + purpose: "LEGACY_SHARE", + }; + for (const key in share) { + switch (key) { + case "options": + // ignore + break; + default: + if (!topLevelProperties.includes(key)) { + if (!disallowedOptions.includes(key)) { + share.options[key] = share[key]; + } + delete share[key]; + } + break; + } + } + } + + switch (apiVersion) { + case 1: + endpoint = "/sharing/cifs"; + + // rename keys with cifs_ prefix + for (const key in share) { + share["cifs_" + key] = share[key]; + delete share[key]; + } + + // convert to comma-separated list + if (share.cifs_hostsallow) { + share.cifs_hostsallow = share.cifs_hostsallow.join(","); + } + + // convert to comma-separated list + if (share.cifs_hostsdeny) { + share.cifs_hostsdeny = share.cifs_hostsdeny.join(","); + } + break; + case 2: + endpoint = "/sharing/smb"; + break; + } + + response = await 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 + * v2 = 200 + */ + if ([200, 201].includes(response.statusCode)) { + share = response.body; + let sharePath; + let shareName; + switch (apiVersion) { + case 1: + sharePath = response.body.cifs_path; + shareName = response.body.cifs_name; + break; + case 2: + sharePath = response.body.path; + shareName = response.body.name; + break; + } + + if (shareName != smbName) { + throw new GrpcError( + grpc.status.UNKNOWN, + `FreeNAS responded with incorrect share data: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + if (sharePath != properties.mountpoint.value) { + throw new GrpcError( + grpc.status.UNKNOWN, + `FreeNAS responded with incorrect share data: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + //set zfs property + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_SMB_SHARE_PROPERTY_NAME]: response.body.id, + }); + } else { + /** + * v1 = 409 + * v2 = 422 + */ + if ( + [409, 422].includes(response.statusCode) && + JSON.stringify(response.body).includes( + "A share with this name already exists." + ) + ) { + let lookupShare = + await httpApiClient.findResourceByProperties( + endpoint, + (item) => { + if ( + (item.cifs_path && + item.cifs_path == properties.mountpoint.value && + item.cifs_name && + item.cifs_name == smbName) || + (item.path && + item.path == properties.mountpoint.value && + item.name && + item.name == smbName) + ) { + return true; + } + return false; + } + ); + + if (!lookupShare) { + throw new GrpcError( + grpc.status.UNKNOWN, + `FreeNAS failed to find matching share` + ); + } + + //set zfs property + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_SMB_SHARE_PROPERTY_NAME]: lookupShare.id, + }); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating smb share - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } + } + + volume_context = { + node_attach_driver: "smb", + server: this.options.smb.shareHost, + share: smbName, + }; + return volume_context; + } + break; + case "iscsi": + { + properties = await httpApiClient.DatasetGet(datasetName, [ + FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, + FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME, + FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME, + ]); + this.ctx.logger.debug("zfs props data: %j", properties); + + let basename; + let iscsiName; + + if (this.options.iscsi.nameTemplate) { + iscsiName = Handlebars.compile(this.options.iscsi.nameTemplate)({ + name: call.request.name, + parameters: call.request.parameters, + }); + } else { + iscsiName = zb.helpers.extractLeafName(datasetName); + } + + if (this.options.iscsi.namePrefix) { + iscsiName = this.options.iscsi.namePrefix + iscsiName; + } + + if (this.options.iscsi.nameSuffix) { + iscsiName += this.options.iscsi.nameSuffix; + } + + // According to RFC3270, 'Each iSCSI node, whether an initiator or target, MUST have an iSCSI name. Initiators and targets MUST support the receipt of iSCSI names of up to the maximum length of 223 bytes.' + // https://kb.netapp.com/Advice_and_Troubleshooting/Miscellaneous/What_is_the_maximum_length_of_a_iSCSI_iqn_name + // https://tools.ietf.org/html/rfc3720 + // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 + iscsiName = iscsiName.toLowerCase(); + + let extentDiskName = "zvol/" + datasetName; + let maxZvolNameLength = await driver.getMaxZvolNameLength(); + driver.ctx.logger.debug( + "max zvol name length: %s", + maxZvolNameLength + ); + + /** + * limit is a FreeBSD limitation + * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab + */ + if (extentDiskName.length > maxZvolNameLength) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` + ); + } + + // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 + if (isScale && iscsiName.length > 64) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `extent name cannot exceed 64 characters: ${iscsiName}` + ); + } + + this.ctx.logger.info( + "FreeNAS creating iscsi assets with name: " + iscsiName + ); + + let extentComment; + if (this.options.iscsi.extentCommentTemplate) { + extentComment = Handlebars.compile( + this.options.iscsi.extentCommentTemplate )({ name: call.request.name, parameters: call.request.parameters, @@ -228,1178 +788,810 @@ class FreeNASApiDriver extends CsiBaseDriver { }, }); } else { - nfsShareComment = `democratic-csi (${this.ctx.args.csiName}): ${datasetName}`; + extentComment = ""; } - switch (apiVersion) { - case 1: - case 2: - switch (apiVersion) { - case 1: - share = { - nfs_paths: [properties.mountpoint.value], - nfs_comment: nfsShareComment || "", - nfs_network: - this.options.nfs.shareAllowedNetworks.join(","), - nfs_hosts: this.options.nfs.shareAllowedHosts.join(","), - nfs_alldirs: this.options.nfs.shareAlldirs, - nfs_ro: false, - nfs_quiet: false, - nfs_maproot_user: this.options.nfs.shareMaprootUser, - nfs_maproot_group: this.options.nfs.shareMaprootGroup, - nfs_mapall_user: this.options.nfs.shareMapallUser, - nfs_mapall_group: this.options.nfs.shareMapallGroup, - nfs_security: [], - }; - break; - case 2: - share = { - paths: [properties.mountpoint.value], - comment: nfsShareComment || "", - networks: this.options.nfs.shareAllowedNetworks, - hosts: this.options.nfs.shareAllowedHosts, - alldirs: this.options.nfs.shareAlldirs, - ro: false, - quiet: false, - maproot_user: this.options.nfs.shareMaprootUser, - maproot_group: this.options.nfs.shareMaprootGroup, - mapall_user: this.options.nfs.shareMapallUser, - mapall_group: this.options.nfs.shareMapallGroup, - security: [], - }; - break; - } - - if (isScale && semver.satisfies(truenasVersion, ">=23.10")) { - delete share.quiet; - delete share.nfs_quiet; - } - - if (isScale && semver.satisfies(truenasVersion, ">=22.12")) { - share.path = share.paths[0]; - delete share.paths; - delete share.alldirs; - } - - 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 - * v2 = 200 - */ - if ([200, 201].includes(response.statusCode)) { - let sharePaths; - switch (apiVersion) { - case 1: - sharePaths = response.body.nfs_paths; - break; - case 2: - if (response.body.path) { - sharePaths = [response.body.path]; - } else { - sharePaths = response.body.paths; - } - break; - } - - // FreeNAS responding with bad data - if (!sharePaths.includes(properties.mountpoint.value)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS responded with incorrect share data: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - //set zfs property - await httpApiClient.DatasetSet(datasetName, { - [FREENAS_NFS_SHARE_PROPERTY_NAME]: response.body.id, - }); - } else { - /** - * v1 = 409 - * v2 = 422 - */ - if ( - [409, 422].includes(response.statusCode) && - (JSON.stringify(response.body).includes( - "You can't share same filesystem with all hosts twice." - ) || - JSON.stringify(response.body).includes( - "Another NFS share already exports this dataset for some network" - )) - ) { - let lookupShare = - await httpApiClient.findResourceByProperties( - "/sharing/nfs", - (item) => { - if ( - (item.nfs_paths && - item.nfs_paths.includes( - properties.mountpoint.value - )) || - (item.paths && - item.paths.includes(properties.mountpoint.value)) || - (item.path && - item.path == properties.mountpoint.value) - ) { - return true; - } - return false; - } - ); - - if (!lookupShare) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS failed to find matching share` - ); - } - - //set zfs property - await httpApiClient.DatasetSet(datasetName, { - [FREENAS_NFS_SHARE_PROPERTY_NAME]: lookupShare.id, - }); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating nfs share - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - - volume_context = { - node_attach_driver: "nfs", - server: this.options.nfs.shareHost, - share: properties.mountpoint.value, - }; - return volume_context; - - break; - /** - * TODO: smb need to be more defensive like iscsi and nfs - * ensuring the path is valid and the shareName - */ - case "smb": - properties = await httpApiClient.DatasetGet(datasetName, [ - "mountpoint", - FREENAS_SMB_SHARE_PROPERTY_NAME, - ]); - this.ctx.logger.debug("zfs props data: %j", properties); - - let smbName; - - if (this.options.smb.nameTemplate) { - smbName = Handlebars.compile(this.options.smb.nameTemplate)({ - name: call.request.name, - parameters: call.request.parameters, - }); - } else { - smbName = zb.helpers.extractLeafName(datasetName); - } - - if (this.options.smb.namePrefix) { - smbName = this.options.smb.namePrefix + smbName; - } - - if (this.options.smb.nameSuffix) { - smbName += this.options.smb.nameSuffix; - } - - smbName = smbName.toLowerCase(); - - this.ctx.logger.info( - "FreeNAS creating smb share with name: " + smbName - ); - - // create smb share - if ( - !zb.helpers.isPropertyValueSet( - properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value + const extentInsecureTpc = this.options.iscsi.hasOwnProperty( + "extentInsecureTpc" ) - ) { - /** - * The only required parameters are: - * - path - * - name - * - * Note that over time it appears the list of available parameters has increased - * so in an effort to best support old versions of FreeNAS we should check the - * presense of each parameter in the config and set the corresponding parameter in - * the API request *only* if present in the config. - */ - switch (apiVersion) { - case 1: - case 2: - share = { - name: smbName, - path: properties.mountpoint.value, - }; - - let propertyMapping = { - shareAuxiliaryConfigurationTemplate: "auxsmbconf", - shareHome: "home", - shareAllowedHosts: "hostsallow", - shareDeniedHosts: "hostsdeny", - shareDefaultPermissions: "default_permissions", - shareGuestOk: "guestok", - shareGuestOnly: "guestonly", - shareShowHiddenFiles: "showhiddenfiles", - shareRecycleBin: "recyclebin", - shareBrowsable: "browsable", - shareAccessBasedEnumeration: "abe", - shareTimeMachine: "timemachine", - shareStorageTask: "storage_task", - }; - - for (const key in propertyMapping) { - if (this.options.smb.hasOwnProperty(key)) { - let value; - switch (key) { - case "shareAuxiliaryConfigurationTemplate": - value = Handlebars.compile( - this.options.smb.shareAuxiliaryConfigurationTemplate - )({ - name: call.request.name, - parameters: call.request.parameters, - }); - break; - default: - value = this.options.smb[key]; - break; - } - share[propertyMapping[key]] = value; - } - } - - if (isScale && semver.satisfies(truenasVersion, ">=25.10")) { - let topLevelProperties = [ - "purpose", - "name", - "path", - "enabled", - "comment", - "readonly", - "browsable", - "access_based_share_enumeration", - "audit", - ]; - let disallowedOptions = ["abe"]; - share.purpose = "LEGACY_SHARE"; - share.options = { - purpose: "LEGACY_SHARE", - }; - for (const key in share) { - switch (key) { - case "options": - // ignore - break; - default: - if (!topLevelProperties.includes(key)) { - if (!disallowedOptions.includes(key)) { - share.options[key] = share[key]; - } - delete share[key]; - } - break; - } - } - } - - switch (apiVersion) { - case 1: - endpoint = "/sharing/cifs"; - - // rename keys with cifs_ prefix - for (const key in share) { - share["cifs_" + key] = share[key]; - delete share[key]; - } - - // convert to comma-separated list - if (share.cifs_hostsallow) { - share.cifs_hostsallow = share.cifs_hostsallow.join(","); - } - - // convert to comma-separated list - if (share.cifs_hostsdeny) { - share.cifs_hostsdeny = share.cifs_hostsdeny.join(","); - } - break; - case 2: - endpoint = "/sharing/smb"; - break; - } - - response = await 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 - * v2 = 200 - */ - if ([200, 201].includes(response.statusCode)) { - share = response.body; - let sharePath; - let shareName; - switch (apiVersion) { - case 1: - sharePath = response.body.cifs_path; - shareName = response.body.cifs_name; - break; - case 2: - sharePath = response.body.path; - shareName = response.body.name; - break; - } - - if (shareName != smbName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS responded with incorrect share data: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - if (sharePath != properties.mountpoint.value) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS responded with incorrect share data: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - //set zfs property - await httpApiClient.DatasetSet(datasetName, { - [FREENAS_SMB_SHARE_PROPERTY_NAME]: response.body.id, - }); - } else { - /** - * v1 = 409 - * v2 = 422 - */ - if ( - [409, 422].includes(response.statusCode) && - JSON.stringify(response.body).includes( - "A share with this name already exists." - ) - ) { - let lookupShare = - await httpApiClient.findResourceByProperties( - endpoint, - (item) => { - if ( - (item.cifs_path && - item.cifs_path == properties.mountpoint.value && - item.cifs_name && - item.cifs_name == smbName) || - (item.path && - item.path == properties.mountpoint.value && - item.name && - item.name == smbName) - ) { - return true; - } - return false; - } - ); - - if (!lookupShare) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS failed to find matching share` - ); - } - - //set zfs property - await httpApiClient.DatasetSet(datasetName, { - [FREENAS_SMB_SHARE_PROPERTY_NAME]: lookupShare.id, - }); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating smb share - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - - volume_context = { - node_attach_driver: "smb", - server: this.options.smb.shareHost, - share: smbName, - }; - return volume_context; - - break; - case "iscsi": - properties = await httpApiClient.DatasetGet(datasetName, [ - FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, - FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME, - FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME, - ]); - this.ctx.logger.debug("zfs props data: %j", properties); - - let basename; - let iscsiName; - - if (this.options.iscsi.nameTemplate) { - iscsiName = Handlebars.compile(this.options.iscsi.nameTemplate)({ - name: call.request.name, - parameters: call.request.parameters, - }); - } else { - iscsiName = zb.helpers.extractLeafName(datasetName); - } - - if (this.options.iscsi.namePrefix) { - iscsiName = this.options.iscsi.namePrefix + iscsiName; - } - - if (this.options.iscsi.nameSuffix) { - iscsiName += this.options.iscsi.nameSuffix; - } - - // According to RFC3270, 'Each iSCSI node, whether an initiator or target, MUST have an iSCSI name. Initiators and targets MUST support the receipt of iSCSI names of up to the maximum length of 223 bytes.' - // https://kb.netapp.com/Advice_and_Troubleshooting/Miscellaneous/What_is_the_maximum_length_of_a_iSCSI_iqn_name - // https://tools.ietf.org/html/rfc3720 - // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 - iscsiName = iscsiName.toLowerCase(); - - let extentDiskName = "zvol/" + datasetName; - let maxZvolNameLength = await driver.getMaxZvolNameLength(); - driver.ctx.logger.debug("max zvol name length: %s", maxZvolNameLength); - - /** - * limit is a FreeBSD limitation - * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab - */ - if (extentDiskName.length > maxZvolNameLength) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` - ); - } - - // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 - if (isScale && iscsiName.length > 64) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `extent name cannot exceed 64 characters: ${iscsiName}` - ); - } - - this.ctx.logger.info( - "FreeNAS creating iscsi assets with name: " + iscsiName - ); - - let extentComment; - if (this.options.iscsi.extentCommentTemplate) { - extentComment = Handlebars.compile( - this.options.iscsi.extentCommentTemplate - )({ - name: call.request.name, - parameters: call.request.parameters, - csi: { - name: this.ctx.args.csiName, - version: this.ctx.args.csiVersion, - }, - zfs: { - datasetName: datasetName, - }, - }); - } else { - extentComment = ""; - } - - const extentInsecureTpc = this.options.iscsi.hasOwnProperty( - "extentInsecureTpc" - ) - ? this.options.iscsi.extentInsecureTpc - : true; - - const extentXenCompat = this.options.iscsi.hasOwnProperty( - "extentXenCompat" - ) - ? this.options.iscsi.extentXenCompat - : false; - - const extentBlocksize = this.options.iscsi.hasOwnProperty( - "extentBlocksize" - ) - ? this.options.iscsi.extentBlocksize - : 512; - - const extentDisablePhysicalBlocksize = - this.options.iscsi.hasOwnProperty("extentDisablePhysicalBlocksize") - ? this.options.iscsi.extentDisablePhysicalBlocksize + ? this.options.iscsi.extentInsecureTpc : true; - const extentRpm = this.options.iscsi.hasOwnProperty("extentRpm") - ? this.options.iscsi.extentRpm - : "SSD"; - - let extentAvailThreshold = this.options.iscsi.hasOwnProperty( - "extentAvailThreshold" - ) - ? Number(this.options.iscsi.extentAvailThreshold) - : null; - - if (!(extentAvailThreshold > 0 && extentAvailThreshold <= 100)) { - extentAvailThreshold = null; - } - - switch (apiVersion) { - case 1: - response = await httpClient.get( - "/services/iscsi/globalconfiguration" - ); - if (response.statusCode != 200) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error getting iscsi configuration - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - basename = response.body.iscsi_basename; - this.ctx.logger.verbose("FreeNAS ISCSI BASENAME: " + basename); - break; - case 2: - response = await httpClient.get("/iscsi/global"); - if (response.statusCode != 200) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error getting iscsi configuration - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - basename = response.body.basename; - this.ctx.logger.verbose("FreeNAS ISCSI BASENAME: " + basename); - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - - // if we got all the way to the TARGETTOEXTENT then we fully finished - // otherwise we must do all assets every time due to the interdependence of IDs etc - if ( - !zb.helpers.isPropertyValueSet( - properties[FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME].value + const extentXenCompat = this.options.iscsi.hasOwnProperty( + "extentXenCompat" ) - ) { + ? this.options.iscsi.extentXenCompat + : false; + + const extentBlocksize = this.options.iscsi.hasOwnProperty( + "extentBlocksize" + ) + ? this.options.iscsi.extentBlocksize + : 512; + + const extentDisablePhysicalBlocksize = + this.options.iscsi.hasOwnProperty("extentDisablePhysicalBlocksize") + ? this.options.iscsi.extentDisablePhysicalBlocksize + : true; + + const extentRpm = this.options.iscsi.hasOwnProperty("extentRpm") + ? this.options.iscsi.extentRpm + : "SSD"; + + let extentAvailThreshold = this.options.iscsi.hasOwnProperty( + "extentAvailThreshold" + ) + ? Number(this.options.iscsi.extentAvailThreshold) + : null; + + if (!(extentAvailThreshold > 0 && extentAvailThreshold <= 100)) { + extentAvailThreshold = null; + } + switch (apiVersion) { - case 1: { - // create target - let target = { - iscsi_target_name: iscsiName, - iscsi_target_alias: "", // TODO: allow template for this - }; - - response = await httpClient.post( - "/services/iscsi/target", - target + case 1: + response = await httpClient.get( + "/services/iscsi/globalconfiguration" ); - - // 409 if invalid - if (response.statusCode != 201) { - target = null; - if ( - response.statusCode == 409 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) - ) { - target = await httpApiClient.findResourceByProperties( - "/services/iscsi/target", - { - iscsi_target_name: iscsiName, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi target - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - target = response.body; - } - - if (!target) { + if (response.statusCode != 200) { throw new GrpcError( grpc.status.UNKNOWN, - `unknown error creating iscsi target` + `error getting iscsi configuration - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` ); } - - if (target.iscsi_target_name != iscsiName) { + basename = response.body.iscsi_basename; + this.ctx.logger.verbose("FreeNAS ISCSI BASENAME: " + basename); + break; + case 2: + response = await httpClient.get("/iscsi/global"); + if (response.statusCode != 200) { throw new GrpcError( grpc.status.UNKNOWN, - `mismatch name error creating iscsi target` + `error getting iscsi configuration - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` ); } + basename = response.body.basename; + this.ctx.logger.verbose("FreeNAS ISCSI BASENAME: " + basename); + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } - this.ctx.logger.verbose("FreeNAS ISCSI TARGET: %j", target); - - // set target.id on zvol - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME]: target.id, - }); - - // create targetgroup(s) - // targetgroups do have IDs - for (let targetGroupConfig of this.options.iscsi.targetGroups) { - let targetGroup = { - iscsi_target: target.id, - iscsi_target_authgroup: - targetGroupConfig.targetGroupAuthGroup, - iscsi_target_authtype: targetGroupConfig.targetGroupAuthType - ? targetGroupConfig.targetGroupAuthType - : "None", - iscsi_target_portalgroup: - targetGroupConfig.targetGroupPortalGroup, - iscsi_target_initiatorgroup: - targetGroupConfig.targetGroupInitiatorGroup, - iscsi_target_initialdigest: "Auto", + // if we got all the way to the TARGETTOEXTENT then we fully finished + // otherwise we must do all assets every time due to the interdependence of IDs etc + if ( + !zb.helpers.isPropertyValueSet( + properties[FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME].value + ) + ) { + switch (apiVersion) { + case 1: { + // create target + let target = { + iscsi_target_name: iscsiName, + iscsi_target_alias: "", // TODO: allow template for this }; + response = await httpClient.post( - "/services/iscsi/targetgroup", - targetGroup + "/services/iscsi/target", + target ); // 409 if invalid if (response.statusCode != 201) { - targetGroup = null; - /** - * 404 gets returned with an unable to process response when the DB is corrupted (has invalid entries in essense) - * - * To resolve properly the DB should be cleaned up - * /usr/local/etc/rc.d/django stop - * /usr/local/etc/rc.d/nginx stop - * sqlite3 /data/freenas-v1.db - * - * // this deletes everything, probably not what you want - * // should have a better query to only find entries where associated assets no longer exist - * DELETE from services_iscsitargetgroups; - * - * /usr/local/etc/rc.d/django restart - * /usr/local/etc/rc.d/nginx restart - */ + target = null; if ( - response.statusCode == 404 || - (response.statusCode == 409 && - JSON.stringify(response.body).includes( - "cannot be duplicated on a target" - )) + response.statusCode == 409 && + JSON.stringify(response.body).includes( + "Target name already exists" + ) ) { - targetGroup = await httpApiClient.findResourceByProperties( - "/services/iscsi/targetgroup", + target = await httpApiClient.findResourceByProperties( + "/services/iscsi/target", { - iscsi_target: target.id, - iscsi_target_portalgroup: - targetGroupConfig.targetGroupPortalGroup, - iscsi_target_initiatorgroup: - targetGroupConfig.targetGroupInitiatorGroup, + iscsi_target_name: iscsiName, } ); } else { throw new GrpcError( grpc.status.UNKNOWN, - `received error creating iscsi targetgroup - code: ${ + `received error creating iscsi target - code: ${ response.statusCode } body: ${JSON.stringify(response.body)}` ); } } else { - targetGroup = response.body; + target = response.body; } - if (!targetGroup) { + if (!target) { throw new GrpcError( grpc.status.UNKNOWN, - `unknown error creating iscsi targetgroup` + `unknown error creating iscsi target` ); } - this.ctx.logger.verbose( - "FreeNAS ISCSI TARGET_GROUP: %j", - targetGroup - ); - } - - let extent = { - iscsi_target_extent_comment: extentComment, - iscsi_target_extent_type: "Disk", // Disk/File, after save Disk becomes "ZVOL" - iscsi_target_extent_name: iscsiName, - iscsi_target_extent_insecure_tpc: extentInsecureTpc, - //iscsi_target_extent_naa: "0x3822690834aae6c5", - iscsi_target_extent_disk: extentDiskName, - iscsi_target_extent_xen: extentXenCompat, - iscsi_target_extent_avail_threshold: extentAvailThreshold, - iscsi_target_extent_blocksize: Number(extentBlocksize), - iscsi_target_extent_pblocksize: extentDisablePhysicalBlocksize, - iscsi_target_extent_rpm: isNaN(Number(extentRpm)) - ? "SSD" - : Number(extentRpm), - iscsi_target_extent_ro: false, - }; - response = await httpClient.post( - "/services/iscsi/extent", - extent - ); - - // 409 if invalid - if (response.statusCode != 201) { - extent = null; - if ( - response.statusCode == 409 && - JSON.stringify(response.body).includes( - "Extent name must be unique" - ) - ) { - extent = await httpApiClient.findResourceByProperties( - "/services/iscsi/extent", - { iscsi_target_extent_name: iscsiName } - ); - } else { + if (target.iscsi_target_name != iscsiName) { throw new GrpcError( grpc.status.UNKNOWN, - `received error creating iscsi extent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` + `mismatch name error creating iscsi target` ); } - } else { - extent = response.body; - } - if (!extent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi extent` - ); - } + this.ctx.logger.verbose("FreeNAS ISCSI TARGET: %j", target); - if (extent.iscsi_target_extent_name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi extent` - ); - } - - this.ctx.logger.verbose("FreeNAS ISCSI EXTENT: %j", extent); - - await httpApiClient.DatasetSet(datasetName, { - [FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME]: extent.id, - }); - - // create targettoextent - let targetToExtent = { - iscsi_target: target.id, - iscsi_extent: extent.id, - iscsi_lunid: 0, - }; - response = await httpClient.post( - "/services/iscsi/targettoextent", - targetToExtent - ); - - // 409 if invalid - if (response.statusCode != 201) { - targetToExtent = null; - - // LUN ID is already being used for this target. - // Extent is already in this target. - if ( - response.statusCode == 409 && - (JSON.stringify(response.body).includes( - "Extent is already in this target." - ) || - JSON.stringify(response.body).includes( - "LUN ID is already being used for this target." - )) - ) { - targetToExtent = await httpApiClient.findResourceByProperties( - "/services/iscsi/targettoextent", - { - iscsi_target: target.id, - iscsi_extent: extent.id, - iscsi_lunid: 0, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi targettoextent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - targetToExtent = response.body; - } - - if (!targetToExtent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi targettoextent` - ); - } - this.ctx.logger.verbose( - "FreeNAS ISCSI TARGET_TO_EXTENT: %j", - targetToExtent - ); - - await httpApiClient.DatasetSet(datasetName, { - [FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME]: - targetToExtent.id, - }); - - break; - } - case 2: - // create target and targetgroup - //let targetId; - let targetGroups = []; - for (let targetGroupConfig of this.options.iscsi.targetGroups) { - targetGroups.push({ - portal: targetGroupConfig.targetGroupPortalGroup, - initiator: targetGroupConfig.targetGroupInitiatorGroup, - auth: - targetGroupConfig.targetGroupAuthGroup > 0 - ? targetGroupConfig.targetGroupAuthGroup - : null, - authmethod: - targetGroupConfig.targetGroupAuthType.length > 0 - ? targetGroupConfig.targetGroupAuthType - .toUpperCase() - .replace(" ", "_") - : "NONE", + // set target.id on zvol + await zb.zfs.set(datasetName, { + [FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME]: target.id, }); - } - let target = { - name: iscsiName, - alias: null, // cannot send "" error: handler error - driver: FreeNASDriver method: CreateVolume error: {"name":"GrpcError","code":2,"message":"received error creating iscsi target - code: 422 body: {\"iscsi_target_create.alias\":[{\"message\":\"Alias already exists\",\"errno\":22}]}"} - mode: "ISCSI", - groups: targetGroups, - }; - response = await httpClient.post("/iscsi/target", target); - - // 409 if invalid - if (response.statusCode != 200) { - target = null; - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) - ) { - target = await httpApiClient.findResourceByProperties( - "/iscsi/target", - { - name: iscsiName, - } + // create targetgroup(s) + // targetgroups do have IDs + for (let targetGroupConfig of this.options.iscsi.targetGroups) { + let targetGroup = { + iscsi_target: target.id, + iscsi_target_authgroup: + targetGroupConfig.targetGroupAuthGroup, + iscsi_target_authtype: targetGroupConfig.targetGroupAuthType + ? targetGroupConfig.targetGroupAuthType + : "None", + iscsi_target_portalgroup: + targetGroupConfig.targetGroupPortalGroup, + iscsi_target_initiatorgroup: + targetGroupConfig.targetGroupInitiatorGroup, + iscsi_target_initialdigest: "Auto", + }; + response = await httpClient.post( + "/services/iscsi/targetgroup", + targetGroup ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi target - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` + + // 409 if invalid + if (response.statusCode != 201) { + targetGroup = null; + /** + * 404 gets returned with an unable to process response when the DB is corrupted (has invalid entries in essense) + * + * To resolve properly the DB should be cleaned up + * /usr/local/etc/rc.d/django stop + * /usr/local/etc/rc.d/nginx stop + * sqlite3 /data/freenas-v1.db + * + * // this deletes everything, probably not what you want + * // should have a better query to only find entries where associated assets no longer exist + * DELETE from services_iscsitargetgroups; + * + * /usr/local/etc/rc.d/django restart + * /usr/local/etc/rc.d/nginx restart + */ + if ( + response.statusCode == 404 || + (response.statusCode == 409 && + JSON.stringify(response.body).includes( + "cannot be duplicated on a target" + )) + ) { + targetGroup = + await httpApiClient.findResourceByProperties( + "/services/iscsi/targetgroup", + { + iscsi_target: target.id, + iscsi_target_portalgroup: + targetGroupConfig.targetGroupPortalGroup, + iscsi_target_initiatorgroup: + targetGroupConfig.targetGroupInitiatorGroup, + } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi targetgroup - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } else { + targetGroup = response.body; + } + + if (!targetGroup) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown error creating iscsi targetgroup` + ); + } + + this.ctx.logger.verbose( + "FreeNAS ISCSI TARGET_GROUP: %j", + targetGroup ); } - } else { - target = response.body; - } - if (!target) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi target` + let extent = { + iscsi_target_extent_comment: extentComment, + iscsi_target_extent_type: "Disk", // Disk/File, after save Disk becomes "ZVOL" + iscsi_target_extent_name: iscsiName, + iscsi_target_extent_insecure_tpc: extentInsecureTpc, + //iscsi_target_extent_naa: "0x3822690834aae6c5", + iscsi_target_extent_disk: extentDiskName, + iscsi_target_extent_xen: extentXenCompat, + iscsi_target_extent_avail_threshold: extentAvailThreshold, + iscsi_target_extent_blocksize: Number(extentBlocksize), + iscsi_target_extent_pblocksize: + extentDisablePhysicalBlocksize, + iscsi_target_extent_rpm: isNaN(Number(extentRpm)) + ? "SSD" + : Number(extentRpm), + iscsi_target_extent_ro: false, + }; + response = await httpClient.post( + "/services/iscsi/extent", + extent ); - } - if (target.name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi target` - ); - } - - // handle situations/race conditions where groups failed to be added/created on the target - // groups":[{"portal":1,"initiator":1,"auth":null,"authmethod":"NONE"},{"portal":2,"initiator":1,"auth":null,"authmethod":"NONE"}] - // TODO: this logic could be more intelligent but this should do for now as it appears in the failure scenario no groups are added - // in other words, I have never seen them invalid, only omitted so this should be enough - if (target.groups.length != targetGroups.length) { - response = await httpClient.put( - `/iscsi/target/id/${target.id}`, - { - groups: targetGroups, + // 409 if invalid + if (response.statusCode != 201) { + extent = null; + if ( + response.statusCode == 409 && + JSON.stringify(response.body).includes( + "Extent name must be unique" + ) + ) { + extent = await httpApiClient.findResourceByProperties( + "/services/iscsi/extent", + { iscsi_target_extent_name: iscsiName } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi extent - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); } - ); + } else { + extent = response.body; + } - if (response.statusCode != 200) { + if (!extent) { throw new GrpcError( grpc.status.UNKNOWN, - `failed setting target groups` + `unknown error creating iscsi extent` ); + } + + if (extent.iscsi_target_extent_name != iscsiName) { + throw new GrpcError( + grpc.status.UNKNOWN, + `mismatch name error creating iscsi extent` + ); + } + + this.ctx.logger.verbose("FreeNAS ISCSI EXTENT: %j", extent); + + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME]: extent.id, + }); + + // create targettoextent + let targetToExtent = { + iscsi_target: target.id, + iscsi_extent: extent.id, + iscsi_lunid: 0, + }; + response = await httpClient.post( + "/services/iscsi/targettoextent", + targetToExtent + ); + + // 409 if invalid + if (response.statusCode != 201) { + targetToExtent = null; + + // LUN ID is already being used for this target. + // Extent is already in this target. + if ( + response.statusCode == 409 && + (JSON.stringify(response.body).includes( + "Extent is already in this target." + ) || + JSON.stringify(response.body).includes( + "LUN ID is already being used for this target." + )) + ) { + targetToExtent = + await httpApiClient.findResourceByProperties( + "/services/iscsi/targettoextent", + { + iscsi_target: target.id, + iscsi_extent: extent.id, + iscsi_lunid: 0, + } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi targettoextent - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } else { + targetToExtent = response.body; + } + + if (!targetToExtent) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown error creating iscsi targettoextent` + ); + } + this.ctx.logger.verbose( + "FreeNAS ISCSI TARGET_TO_EXTENT: %j", + targetToExtent + ); + + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME]: + targetToExtent.id, + }); + + break; + } + case 2: + // create target and targetgroup + //let targetId; + let targetGroups = []; + for (let targetGroupConfig of this.options.iscsi.targetGroups) { + targetGroups.push({ + portal: targetGroupConfig.targetGroupPortalGroup, + initiator: targetGroupConfig.targetGroupInitiatorGroup, + auth: + targetGroupConfig.targetGroupAuthGroup > 0 + ? targetGroupConfig.targetGroupAuthGroup + : null, + authmethod: + targetGroupConfig.targetGroupAuthType.length > 0 + ? targetGroupConfig.targetGroupAuthType + .toUpperCase() + .replace(" ", "_") + : "NONE", + }); + } + let target = { + name: iscsiName, + alias: null, // cannot send "" error: handler error - driver: FreeNASDriver method: CreateVolume error: {"name":"GrpcError","code":2,"message":"received error creating iscsi target - code: 422 body: {\"iscsi_target_create.alias\":[{\"message\":\"Alias already exists\",\"errno\":22}]}"} + mode: "ISCSI", + groups: targetGroups, + }; + + response = await httpClient.post("/iscsi/target", target); + + // 409 if invalid + if (response.statusCode != 200) { + target = null; + if ( + response.statusCode == 422 && + JSON.stringify(response.body).includes( + "Target name already exists" + ) + ) { + target = await httpApiClient.findResourceByProperties( + "/iscsi/target", + { + name: iscsiName, + } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi target - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } } else { target = response.body; + } - // re-run sanity checks - if (!target) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi target` - ); - } + if (!target) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown error creating iscsi target` + ); + } - if (target.name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi target` - ); - } + if (target.name != iscsiName) { + throw new GrpcError( + grpc.status.UNKNOWN, + `mismatch name error creating iscsi target` + ); + } - if (target.groups.length != targetGroups.length) { + // handle situations/race conditions where groups failed to be added/created on the target + // groups":[{"portal":1,"initiator":1,"auth":null,"authmethod":"NONE"},{"portal":2,"initiator":1,"auth":null,"authmethod":"NONE"}] + // TODO: this logic could be more intelligent but this should do for now as it appears in the failure scenario no groups are added + // in other words, I have never seen them invalid, only omitted so this should be enough + if (target.groups.length != targetGroups.length) { + response = await httpClient.put( + `/iscsi/target/id/${target.id}`, + { + groups: targetGroups, + } + ); + + if (response.statusCode != 200) { throw new GrpcError( grpc.status.UNKNOWN, `failed setting target groups` ); + } else { + target = response.body; + + // re-run sanity checks + if (!target) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown error creating iscsi target` + ); + } + + if (target.name != iscsiName) { + throw new GrpcError( + grpc.status.UNKNOWN, + `mismatch name error creating iscsi target` + ); + } + + if (target.groups.length != targetGroups.length) { + throw new GrpcError( + grpc.status.UNKNOWN, + `failed setting target groups` + ); + } } } - } - this.ctx.logger.verbose("FreeNAS ISCSI TARGET: %j", target); + this.ctx.logger.verbose("FreeNAS ISCSI TARGET: %j", target); - // set target.id on zvol - await httpApiClient.DatasetSet(datasetName, { - [FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME]: target.id, - }); + // set target.id on zvol + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME]: target.id, + }); - let extent = { - comment: extentComment, - type: "DISK", // Disk/File, after save Disk becomes "ZVOL" - name: iscsiName, - //iscsi_target_extent_naa: "0x3822690834aae6c5", - disk: extentDiskName, - insecure_tpc: extentInsecureTpc, - xen: extentXenCompat, - avail_threshold: extentAvailThreshold, - blocksize: Number(extentBlocksize), - pblocksize: extentDisablePhysicalBlocksize, - rpm: "" + extentRpm, // should be a string - ro: false, - }; + let extent = { + comment: extentComment, + type: "DISK", // Disk/File, after save Disk becomes "ZVOL" + name: iscsiName, + //iscsi_target_extent_naa: "0x3822690834aae6c5", + disk: extentDiskName, + insecure_tpc: extentInsecureTpc, + xen: extentXenCompat, + avail_threshold: extentAvailThreshold, + blocksize: Number(extentBlocksize), + pblocksize: extentDisablePhysicalBlocksize, + rpm: "" + extentRpm, // should be a string + ro: false, + }; - response = await httpClient.post("/iscsi/extent", extent); + response = await httpClient.post("/iscsi/extent", extent); - // 409 if invalid - if (response.statusCode != 200) { - extent = null; - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes( - "Extent name must be unique" - ) - ) { - extent = await httpApiClient.findResourceByProperties( - "/iscsi/extent", - { - name: iscsiName, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi extent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - extent = response.body; - } - - if (!extent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi extent` - ); - } - - if (extent.name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi extent` - ); - } - - this.ctx.logger.verbose("FreeNAS ISCSI EXTENT: %j", extent); - - await httpApiClient.DatasetSet(datasetName, { - [FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME]: extent.id, - }); - - // create targettoextent - let targetToExtent = { - target: target.id, - extent: extent.id, - lunid: 0, - }; - response = await httpClient.post( - "/iscsi/targetextent", - targetToExtent - ); - - if (response.statusCode != 200) { - targetToExtent = null; - - // LUN ID is already being used for this target. - // Extent is already in this target. - if ( - response.statusCode == 422 && - (JSON.stringify(response.body).includes( - "Extent is already in this target." - ) || + // 409 if invalid + if (response.statusCode != 200) { + extent = null; + if ( + response.statusCode == 422 && JSON.stringify(response.body).includes( - "LUN ID is already being used for this target." - )) - ) { - targetToExtent = await httpApiClient.findResourceByProperties( - "/iscsi/targetextent", - { - target: target.id, - extent: extent.id, - lunid: 0, - } - ); + "Extent name must be unique" + ) + ) { + extent = await httpApiClient.findResourceByProperties( + "/iscsi/extent", + { + name: iscsiName, + } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi extent - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } } else { + extent = response.body; + } + + if (!extent) { throw new GrpcError( grpc.status.UNKNOWN, - `received error creating iscsi targetextent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` + `unknown error creating iscsi extent` ); } - } else { - targetToExtent = response.body; - } - if (!targetToExtent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi targetextent` + if (extent.name != iscsiName) { + throw new GrpcError( + grpc.status.UNKNOWN, + `mismatch name error creating iscsi extent` + ); + } + + this.ctx.logger.verbose("FreeNAS ISCSI EXTENT: %j", extent); + + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME]: extent.id, + }); + + // create targettoextent + let targetToExtent = { + target: target.id, + extent: extent.id, + lunid: 0, + }; + response = await httpClient.post( + "/iscsi/targetextent", + targetToExtent ); - } - this.ctx.logger.verbose( - "FreeNAS ISCSI TARGET_TO_EXTENT: %j", - targetToExtent - ); - await httpApiClient.DatasetSet(datasetName, { - [FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME]: - targetToExtent.id, - }); + if (response.statusCode != 200) { + targetToExtent = null; - break; - default: + // LUN ID is already being used for this target. + // Extent is already in this target. + if ( + response.statusCode == 422 && + (JSON.stringify(response.body).includes( + "Extent is already in this target." + ) || + JSON.stringify(response.body).includes( + "LUN ID is already being used for this target." + )) + ) { + targetToExtent = + await httpApiClient.findResourceByProperties( + "/iscsi/targetextent", + { + target: target.id, + extent: extent.id, + lunid: 0, + } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi targetextent - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } else { + targetToExtent = response.body; + } + + if (!targetToExtent) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown error creating iscsi targetextent` + ); + } + this.ctx.logger.verbose( + "FreeNAS ISCSI TARGET_TO_EXTENT: %j", + targetToExtent + ); + + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME]: + targetToExtent.id, + }); + + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } + } + + // iqn = target + let iqn = basename + ":" + iscsiName; + this.ctx.logger.info("FreeNAS iqn: " + iqn); + + // store this off to make delete process more bullet proof + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME]: iscsiName, + }); + + volume_context = { + node_attach_driver: "iscsi", + portal: this.options.iscsi.targetPortal || "", + portals: this.options.iscsi.targetPortals + ? this.options.iscsi.targetPortals.join(",") + : "", + interface: this.options.iscsi.interface || "", + iqn: iqn, + lun: 0, + }; + return volume_context; + } + break; + + case "nvmeof": + { + switch (apiVersion) { + case 1: throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` + `nvmeof feature is only available with version 2 of the api` ); + break; } + + if (!isScale || semver.satisfies(truenasVersion, "<25.10")) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `nvmeof feature is only available with TrueNAS version 25.10 and above` + ); + } + + properties = await httpApiClient.DatasetGet(datasetName, [ + FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME, + FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME, + FREENAS_NVMEOF_ASSETS_NAME_PROPERTY_NAME, + ]); + this.ctx.logger.debug("zfs props data: %j", properties); + + let nvmeofName; + + if (this.options.nvmeof.nameTemplate) { + nvmeofName = Handlebars.compile(this.options.nvmeof.nameTemplate)({ + name: call.request.name, + parameters: call.request.parameters, + }); + } else { + nvmeofName = zb.helpers.extractLeafName(datasetName); + } + + if (this.options.nvmeof.namePrefix) { + nvmeofName = this.options.nvmeof.namePrefix + nvmeofName; + } + + if (this.options.nvmeof.nameSuffix) { + nvmeofName += this.options.nvmeof.nameSuffix; + } + + // According to RFC3270, 'Each iSCSI node, whether an initiator or target, MUST have an iSCSI name. Initiators and targets MUST support the receipt of iSCSI names of up to the maximum length of 223 bytes.' + // https://kb.netapp.com/Advice_and_Troubleshooting/Miscellaneous/What_is_the_maximum_length_of_a_iSCSI_iqn_name + // https://tools.ietf.org/html/rfc3720 + // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 + nvmeofName = nvmeofName.toLowerCase(); + + let namespaceDiskName = "zvol/" + datasetName; + let maxZvolNameLength = await driver.getMaxZvolNameLength(); + driver.ctx.logger.debug( + "max zvol name length: %s", + maxZvolNameLength + ); + + if (namespaceDiskName.length > maxZvolNameLength) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `namespace disk name cannot exceed ${maxZvolNameLength} characters: ${namespaceDiskName}` + ); + } + + // TODO: get basenqn from global config, add nvemofName to it and ensure full nqn is <= 223 + // // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 + // if (isScale && nvmeofName.length > 64) { + // throw new GrpcError( + // grpc.status.FAILED_PRECONDITION, + // `extent name cannot exceed 64 characters: ${nvmeofName}` + // ); + // } + + this.ctx.logger.info( + "FreeNAS creating nvmeof assets with name: " + nvmeofName + ); + + // http:///api/docs/current/api_methods_nvmet.subsys.create.html + let subsystemTemplate = _.get( + this.options, + "nvmeof.subsystemTemplate", + {} + ); + subsystemTemplate = subsystemTemplate || {}; + + // http:///api/docs/current/api_methods_nvmet.namespace.create.html + let namespaceTemplate = _.get( + this.options, + "nvmeof.namespaceTemplate", + {} + ); + namespaceTemplate = namespaceTemplate || {}; + + // create subsystem + let subsystem; + switch (apiVersion) { + case 2: + subsystem = await httpApiClient.NvmetSubsysCreate( + nvmeofName, + subsystemTemplate + ); + + break; + } + if (!subsystem) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `unable to find nvmeof subsystem: ${nvmeofName}` + ); + } + this.ctx.logger.verbose("FreeNAS NVMEOF SUBSYSTEM: %j", subsystem); + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME]: subsystem.id, + }); + + // create subsystem + let namespace; + switch (apiVersion) { + case 2: + namespace = await httpApiClient.NvmetNamespaceCreate( + namespaceDiskName, + subsystem.id, + namespaceTemplate + ); + + break; + } + if (!namespace) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `unable to find nvmeof namespace: ${namespaceDiskName}` + ); + } + this.ctx.logger.verbose("FreeNAS NVMEOF NAMESPACE: %j", namespace); + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME]: namespace.id, + }); + + // assign ports to subsystem + let ports = _.get(this.options, "nvmeof.ports", []); + for (const port_i of ports) { + const port = await httpApiClient.NvmetPortSubsysCreate( + port_i, + subsystem.id + ); + this.ctx.logger.verbose("FreeNAS NVMEOF PORT: %j", port); + } + + // TODO: assign hosts + + // store this off to make delete process more bullet proof + await httpApiClient.DatasetSet(datasetName, { + [FREENAS_NVMEOF_ASSETS_NAME_PROPERTY_NAME]: nvmeofName, + }); + + volume_context = { + node_attach_driver: "nvmeof", + transport: this.options.nvmeof.transport || "", + transports: this.options.nvmeof.transports + ? this.options.nvmeof.transports.join(",") + : "", + nqn: subsystem.subnqn, + nsid: namespace.nsid, + }; + return volume_context; } - - // iqn = target - let iqn = basename + ":" + iscsiName; - this.ctx.logger.info("FreeNAS iqn: " + iqn); - - // store this off to make delete process more bullet proof - await httpApiClient.DatasetSet(datasetName, { - [FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME]: iscsiName, - }); - - volume_context = { - node_attach_driver: "iscsi", - portal: this.options.iscsi.targetPortal || "", - portals: this.options.iscsi.targetPortals - ? this.options.iscsi.targetPortals.join(",") - : "", - interface: this.options.iscsi.interface || "", - iqn: iqn, - lun: 0, - }; - return volume_context; + break; default: throw new GrpcError( @@ -1415,6 +1607,16 @@ class FreeNASApiDriver extends CsiBaseDriver { const httpApiClient = await this.getTrueNASHttpApiClient(); const apiVersion = httpClient.getApiVersion(); const zb = await this.getZetabyte(); + const truenasVersion = await httpApiClient.getSystemVersionSemver(); + + if (!truenasVersion) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unable to detect TrueNAS version` + ); + } + + const isScale = await httpApiClient.getIsScale(); let properties; let response; @@ -1425,200 +1627,397 @@ class FreeNASApiDriver extends CsiBaseDriver { switch (driverShareType) { case "nfs": - try { - properties = await httpApiClient.DatasetGet(datasetName, [ - "mountpoint", - FREENAS_NFS_SHARE_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; + { + try { + properties = await httpApiClient.DatasetGet(datasetName, [ + "mountpoint", + FREENAS_NFS_SHARE_PROPERTY_NAME, + ]); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + return; + } + throw err; } - throw err; - } - this.ctx.logger.debug("zfs props data: %j", properties); + this.ctx.logger.debug("zfs props data: %j", properties); - shareId = properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value; + shareId = properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value; - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(shareId)) { - // remove nfs share - switch (apiVersion) { - case 1: - case 2: - endpoint = "/sharing/nfs/"; - if (apiVersion == 2) { - endpoint += "id/"; - } - endpoint += shareId; - - response = await httpClient.get(endpoint); - - // assume share is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { - switch (apiVersion) { - case 1: - sharePaths = response.body.nfs_paths; - break; - case 2: - if (response.body.path) { - sharePaths = [response.body.path]; - } else { - sharePaths = response.body.paths; - } - break; + // only remove if the process has not succeeded already + if (zb.helpers.isPropertyValueSet(shareId)) { + // remove nfs share + switch (apiVersion) { + case 1: + case 2: + endpoint = "/sharing/nfs/"; + if (apiVersion == 2) { + endpoint += "id/"; } + endpoint += shareId; - deleteAsset = sharePaths.some((value) => { - return value == properties.mountpoint.value; - }); + response = await httpClient.get(endpoint); - if (deleteAsset) { - 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 - // v2 = 200 - if (![200, 204].includes(response.statusCode)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error deleting nfs share - share: ${shareId} code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); + // assume share is gone for now + if ([404, 500].includes(response.statusCode)) { + } else { + switch (apiVersion) { + case 1: + sharePaths = response.body.nfs_paths; + break; + case 2: + if (response.body.path) { + sharePaths = [response.body.path]; + } else { + sharePaths = response.body.paths; + } + break; } - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await httpApiClient.DatasetInherit( - datasetName, - FREENAS_NFS_SHARE_PROPERTY_NAME - ); + deleteAsset = sharePaths.some((value) => { + return value == properties.mountpoint.value; + }); + + if (deleteAsset) { + 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 + // v2 = 200 + if (![200, 204].includes(response.statusCode)) { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error deleting nfs share - share: ${shareId} code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + // remove property to prevent delete race conditions + // due to id re-use by FreeNAS/TrueNAS + await httpApiClient.DatasetInherit( + datasetName, + FREENAS_NFS_SHARE_PROPERTY_NAME + ); + } } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } } } break; case "smb": - try { - properties = await httpApiClient.DatasetGet(datasetName, [ - "mountpoint", - FREENAS_SMB_SHARE_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; + { + try { + properties = await httpApiClient.DatasetGet(datasetName, [ + "mountpoint", + FREENAS_SMB_SHARE_PROPERTY_NAME, + ]); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + return; + } + throw err; } - throw err; - } - this.ctx.logger.debug("zfs props data: %j", properties); + this.ctx.logger.debug("zfs props data: %j", properties); - shareId = properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value; + shareId = properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value; - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(shareId)) { - // remove smb share - switch (apiVersion) { - case 1: - case 2: - switch (apiVersion) { - case 1: - endpoint = `/sharing/cifs/${shareId}`; - break; - case 2: - endpoint = `/sharing/smb/id/${shareId}`; - break; - } - - response = await httpClient.get(endpoint); - - // assume share is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { + // only remove if the process has not succeeded already + if (zb.helpers.isPropertyValueSet(shareId)) { + // remove smb share + switch (apiVersion) { + case 1: + case 2: switch (apiVersion) { case 1: - sharePaths = [response.body.cifs_path]; + endpoint = `/sharing/cifs/${shareId}`; break; case 2: - sharePaths = [response.body.path]; + endpoint = `/sharing/smb/id/${shareId}`; break; } - deleteAsset = sharePaths.some((value) => { - return value == properties.mountpoint.value; - }); + response = await httpClient.get(endpoint); - if (deleteAsset) { - 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 - // v2 = 200 - 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: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); + // assume share is gone for now + if ([404, 500].includes(response.statusCode)) { + } else { + switch (apiVersion) { + case 1: + sharePaths = [response.body.cifs_path]; + break; + case 2: + sharePaths = [response.body.path]; + break; } - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await httpApiClient.DatasetInherit( - datasetName, - FREENAS_SMB_SHARE_PROPERTY_NAME - ); + deleteAsset = sharePaths.some((value) => { + return value == properties.mountpoint.value; + }); + + if (deleteAsset) { + 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 + // v2 = 200 + 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: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + // remove property to prevent delete race conditions + // due to id re-use by FreeNAS/TrueNAS + await httpApiClient.DatasetInherit( + datasetName, + FREENAS_SMB_SHARE_PROPERTY_NAME + ); + } + } + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } + } + } + break; + case "iscsi": + { + // Delete target + // NOTE: deleting a target inherently deletes associated targetgroup(s) and targettoextent(s) + + // Delete extent + try { + properties = await httpApiClient.DatasetGet(datasetName, [ + FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, + FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME, + FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME, + FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME, + ]); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + return; + } + throw err; + } + + this.ctx.logger.debug("zfs props data: %j", properties); + + let targetId = + properties[FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME].value; + let extentId = + properties[FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME].value; + let iscsiName = + properties[FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME].value; + let assetName; + + switch (apiVersion) { + case 1: + case 2: + // only remove if the process has not succeeded already + if (zb.helpers.isPropertyValueSet(targetId)) { + // https://jira.ixsystems.com/browse/NAS-103952 + + // v1 - /services/iscsi/target/{id}/ + // v2 - /iscsi/target/id/{id} + endpoint = ""; + if (apiVersion == 1) { + endpoint += "/services"; + } + endpoint += "/iscsi/target/"; + if (apiVersion == 2) { + endpoint += "id/"; + } + endpoint += targetId; + response = await httpClient.get(endpoint); + + // assume is gone for now + if ([404, 500].includes(response.statusCode)) { + } else { + deleteAsset = true; + assetName = null; + + // checking if set for backwards compatibility + if (zb.helpers.isPropertyValueSet(iscsiName)) { + switch (apiVersion) { + case 1: + assetName = response.body.iscsi_target_name; + break; + case 2: + assetName = response.body.name; + break; + } + + if (assetName != iscsiName) { + deleteAsset = false; + } + } + + if (deleteAsset) { + let retries = 0; + let maxRetries = 5; + let retryWait = 1000; + response = await httpClient.delete(endpoint); + + // sometimes after an initiator has detached it takes a moment for TrueNAS to settle + // code: 422 body: {\"message\":\"Target csi-ci-55877e95sanity-node-expand-volume-e54f81fa-cd38e798 is in use.\",\"errno\":14} + while ( + response.statusCode == 422 && + retries < maxRetries && + _.get(response, "body.message").includes("Target") && + _.get(response, "body.message").includes("is in use") && + _.get(response, "body.errno") == 14 + ) { + retries++; + this.ctx.logger.debug( + "target: %s is in use, retry %s shortly", + targetId, + retries + ); + await GeneralUtils.sleep(retryWait); + response = await httpClient.delete(endpoint); + } + + if (![200, 204, 404].includes(response.statusCode)) { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error deleting iscsi target - target: ${targetId} code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + // remove property to prevent delete race conditions + // due to id re-use by FreeNAS/TrueNAS + await httpApiClient.DatasetInherit( + datasetName, + FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME + ); + } else { + this.ctx.logger.debug( + "not deleting iscsitarget asset as it appears ID %s has been re-used: zfs name - %s, iscsitarget name - %s", + targetId, + iscsiName, + assetName + ); + } + } + } + + // only remove if the process has not succeeded already + if (zb.helpers.isPropertyValueSet(extentId)) { + // v1 - /services/iscsi/targettoextent/{id}/ + // v2 - /iscsi/targetextent/id/{id} + if (apiVersion == 1) { + endpoint = "/services/iscsi/extent/"; + } else { + endpoint = "/iscsi/extent/id/"; + } + endpoint += extentId; + response = await httpClient.get(endpoint); + + // assume is gone for now + if ([404, 500].includes(response.statusCode)) { + } else { + deleteAsset = true; + assetName = null; + + // checking if set for backwards compatibility + if (zb.helpers.isPropertyValueSet(iscsiName)) { + switch (apiVersion) { + case 1: + assetName = response.body.iscsi_target_extent_name; + break; + case 2: + assetName = response.body.name; + break; + } + + if (assetName != iscsiName) { + deleteAsset = false; + } + } + + if (deleteAsset) { + response = await httpClient.delete(endpoint); + if (![200, 204, 404].includes(response.statusCode)) { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error deleting iscsi extent - extent: ${extentId} code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + // remove property to prevent delete race conditions + // due to id re-use by FreeNAS/TrueNAS + await httpApiClient.DatasetInherit( + datasetName, + FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME + ); + } else { + this.ctx.logger.debug( + "not deleting iscsiextent asset as it appears ID %s has been re-used: zfs name - %s, iscsiextent name - %s", + extentId, + iscsiName, + assetName + ); + } } } break; @@ -1630,195 +2029,90 @@ class FreeNASApiDriver extends CsiBaseDriver { } } break; - case "iscsi": - // Delete target - // NOTE: deleting a target inherently deletes associated targetgroup(s) and targettoextent(s) - // Delete extent - try { - properties = await httpApiClient.DatasetGet(datasetName, [ - FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, - FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME, - FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME, - FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; + case "nvmeof": + { + switch (apiVersion) { + case 1: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `nvmeof feature is only available with version 2 of the api` + ); + break; } - throw err; - } - this.ctx.logger.debug("zfs props data: %j", properties); - - let targetId = properties[FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME].value; - let extentId = properties[FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME].value; - let iscsiName = - properties[FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME].value; - let assetName; - - switch (apiVersion) { - case 1: - case 2: - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(targetId)) { - // https://jira.ixsystems.com/browse/NAS-103952 - - // v1 - /services/iscsi/target/{id}/ - // v2 - /iscsi/target/id/{id} - endpoint = ""; - if (apiVersion == 1) { - endpoint += "/services"; - } - endpoint += "/iscsi/target/"; - if (apiVersion == 2) { - endpoint += "id/"; - } - endpoint += targetId; - response = await httpClient.get(endpoint); - - // assume is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { - deleteAsset = true; - assetName = null; - - // checking if set for backwards compatibility - if (zb.helpers.isPropertyValueSet(iscsiName)) { - switch (apiVersion) { - case 1: - assetName = response.body.iscsi_target_name; - break; - case 2: - assetName = response.body.name; - break; - } - - if (assetName != iscsiName) { - deleteAsset = false; - } - } - - if (deleteAsset) { - let retries = 0; - let maxRetries = 5; - let retryWait = 1000; - response = await httpClient.delete(endpoint); - - // sometimes after an initiator has detached it takes a moment for TrueNAS to settle - // code: 422 body: {\"message\":\"Target csi-ci-55877e95sanity-node-expand-volume-e54f81fa-cd38e798 is in use.\",\"errno\":14} - while ( - response.statusCode == 422 && - retries < maxRetries && - _.get(response, "body.message").includes("Target") && - _.get(response, "body.message").includes("is in use") && - _.get(response, "body.errno") == 14 - ) { - retries++; - this.ctx.logger.debug( - "target: %s is in use, retry %s shortly", - targetId, - retries - ); - await GeneralUtils.sleep(retryWait); - response = await httpClient.delete(endpoint); - } - - if (![200, 204, 404].includes(response.statusCode)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error deleting iscsi target - target: ${targetId} code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await httpApiClient.DatasetInherit( - datasetName, - FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME - ); - } else { - this.ctx.logger.debug( - "not deleting iscsitarget asset as it appears ID %s has been re-used: zfs name - %s, iscsitarget name - %s", - targetId, - iscsiName, - assetName - ); - } - } - } - - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(extentId)) { - // v1 - /services/iscsi/targettoextent/{id}/ - // v2 - /iscsi/targetextent/id/{id} - if (apiVersion == 1) { - endpoint = "/services/iscsi/extent/"; - } else { - endpoint = "/iscsi/extent/id/"; - } - endpoint += extentId; - response = await httpClient.get(endpoint); - - // assume is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { - deleteAsset = true; - assetName = null; - - // checking if set for backwards compatibility - if (zb.helpers.isPropertyValueSet(iscsiName)) { - switch (apiVersion) { - case 1: - assetName = response.body.iscsi_target_extent_name; - break; - case 2: - assetName = response.body.name; - break; - } - - if (assetName != iscsiName) { - deleteAsset = false; - } - } - - if (deleteAsset) { - response = await httpClient.delete(endpoint); - if (![200, 204, 404].includes(response.statusCode)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error deleting iscsi extent - extent: ${extentId} code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await httpApiClient.DatasetInherit( - datasetName, - FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME - ); - } else { - this.ctx.logger.debug( - "not deleting iscsiextent asset as it appears ID %s has been re-used: zfs name - %s, iscsiextent name - %s", - extentId, - iscsiName, - assetName - ); - } - } - } - break; - default: + if (!isScale || semver.satisfies(truenasVersion, "<25.10")) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` + `nvmeof feature is only available with TrueNAS version 25.10 and above` ); + } + + try { + properties = await httpApiClient.DatasetGet(datasetName, [ + FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME, + FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME, + FREENAS_NVMEOF_ASSETS_NAME_PROPERTY_NAME, + ]); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + return; + } + throw err; + } + this.ctx.logger.debug("zfs props data: %j", properties); + + let subsystemId = + properties[FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME].value; + let namespaceId = + properties[FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME].value; + + // remove namespace + if (zb.helpers.isPropertyValueSet(namespaceId)) { + await GeneralUtils.retry( + 15, + 2000, + async () => { + await httpApiClient.NvmetNamespaceDeleteById(namespaceId); + }, + { + retryCondition: (err) => { + return true; + }, + } + ); + + await httpApiClient.DatasetInherit( + datasetName, + FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME + ); + } + + // remove subsystem + if (zb.helpers.isPropertyValueSet(subsystemId)) { + await GeneralUtils.retry( + 15, + 2000, + async () => { + await httpApiClient.NvmetSubsysDeleteById(subsystemId, { + force: true, + }); + }, + { + retryCondition: (err) => { + return true; + }, + } + ); + + await httpApiClient.DatasetInherit( + datasetName, + FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME + ); + } } break; + default: throw new GrpcError( grpc.status.FAILED_PRECONDITION, @@ -2008,6 +2302,8 @@ class FreeNASApiDriver extends CsiBaseDriver { return "filesystem"; case "freenas-api-iscsi": case "truenas-api-iscsi": + case "freenas-api-nvmeof": + case "truenas-api-nvmeof": return "volume"; default: throw new Error("unknown driver: " + this.ctx.args.driver); @@ -2025,6 +2321,9 @@ class FreeNASApiDriver extends CsiBaseDriver { case "freenas-api-iscsi": case "truenas-api-iscsi": return "iscsi"; + case "freenas-api-nvmeof": + case "truenas-api-nvmeof": + return "nvmeof"; default: throw new Error("unknown driver: " + this.ctx.args.driver); } diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index a5bb236..4687ce1 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -645,6 +645,356 @@ class Api { throw new Error(JSON.stringify(response.body)); } + async NvmetSubsysList(data = {}) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = "/nvmet/subsys"; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + response = await httpClient.get(endpoint, data); + + if (response.statusCode == 200) { + return response.body; + } + + throw new Error(JSON.stringify(response.body)); + } + + async NvmetSubsysCreate(subsysName, data = {}) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + data.name = subsysName; + data.allow_any_host = true; + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = "/nvmet/subsys"; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + response = await httpClient.post(endpoint, data); + + if (response.statusCode == 200) { + return response.body; + } + + if ( + response.statusCode == 422 && + JSON.stringify(response.body).includes("already exists") + ) { + return this.NvmetSubsysGetByName(subsysName); + } + + throw new Error(JSON.stringify(response.body)); + } + + async NvmetSubsysGetByName(subsysName, data = {}) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + data.name = subsysName; + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = "/nvmet/subsys"; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + response = await httpClient.get(endpoint, data); + + if (response.statusCode == 200) { + for (const subsys of response.body) { + if (subsys.name == subsysName) { + return subsys; + } + } + } + + throw new Error(JSON.stringify(response.body)); + } + + async NvmetSubsysGetById(id, data = {}) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = `/nvmet/subsys/id/${id}`; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + response = await httpClient.get(endpoint, data); + + if (response.statusCode == 200) { + return response.body; + } + + throw new Error(JSON.stringify(response.body)); + } + + async NvmetSubsysDeleteById(id, data = {}) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = `/nvmet/subsys/id/${id}`; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + response = await httpClient.delete(endpoint, data); + + if (response.statusCode == 200) { + return; + } + + if ( + response.statusCode == 422 && + JSON.stringify(response.body).includes("does not exist") + ) { + return; + } + + throw new Error(JSON.stringify(response.body)); + } + + async NvmetPortList(data = {}) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = "/nvmet/port"; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + response = await httpClient.get(endpoint, data); + + if (response.statusCode == 200) { + return response.body; + } + + throw new Error(JSON.stringify(response.body)); + } + + async NvmetPortSubsysList(data = {}) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = "/nvmet/port_subsys"; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + response = await httpClient.get(endpoint, data); + + if (response.statusCode == 200) { + return response.body; + } + + throw new Error(JSON.stringify(response.body)); + } + + async NvmetPortSubsysCreate(port_id, subsys_id) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + let data = { + port_id, + subsys_id, + }; + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = "/nvmet/port_subsys"; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + response = await httpClient.post(endpoint, data); + + if (response.statusCode == 200) { + return response.body; + } + + //already exists + if ( + response.statusCode == 422 && + JSON.stringify(response.body).includes("already exists") + ) { + response = await this.NvmetPortSubsysList({ port_id, subsys_id }); + if (Array.isArray(response) && response.length == 1) { + return response[0]; + } + } + + throw new Error(JSON.stringify(response.body)); + } + + async NvmetNamespaceCreate(zvol, subsysId, data = {}) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + zvol = String(zvol); + if (zvol.startsWith("/dev/")) { + zvol = zvol.substring(5); + } + + if (zvol.startsWith("/")) { + zvol = zvol.substring(1); + } + + if (!zvol.startsWith("zvol/")) { + zvol = `zvol/${zvol}`; + } + + data.device_type = "ZVOL"; + data.device_path = zvol; + data.subsys_id = subsysId; + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = "/nvmet/namespace"; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + response = await httpClient.post(endpoint, data); + + if (response.statusCode == 200) { + return response.body; + } + + if ( + response.statusCode == 422 && + JSON.stringify(response.body).includes("already exists") + ) { + return this.NvmetSubsysGetByName(subsysName); + } + + if ( + response.statusCode == 422 && + JSON.stringify(response.body).includes("already used by subsystem") + ) { + //This device_path already used by subsystem: csi-pvc-111-clustera + return this.NvmetNamespaceGetByDeivcePath(zvol); + } + + throw new Error(JSON.stringify(response.body)); + } + + async NvmetNamespaceGetByDeivcePath(zvol) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + zvol = String(zvol); + if (zvol.startsWith("/dev/")) { + zvol = zvol.substring(5); + } + + if (zvol.startsWith("/")) { + zvol = zvol.substring(1); + } + + if (!zvol.startsWith("zvol/")) { + zvol = `zvol/${zvol}`; + } + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = "/nvmet/namespace"; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + let data = { + device_path: zvol, + }; + + response = await httpClient.get(endpoint, data); + + if (response.statusCode == 200) { + if (Array.isArray(response.body) && response.body.length == 1) { + return response.body[0]; + } + } + + throw new Error(JSON.stringify(response.body)); + } + + async NvmetNamespaceDeleteById(id) { + const httpClient = await this.getHttpClient(false); + const zb = await this.getZetabyte(); + const systemVersionSemver = await this.getSystemVersionSemver(); + + let response; + let endpoint; + + if (semver.satisfies(systemVersionSemver, ">=25.10")) { + endpoint = `/nvmet/namespace/id/${id}`; + } else { + throw new Error("nvmet is unavailable with TrueNAS versions <25.10"); + } + + response = await httpClient.delete(endpoint); + + if (response.statusCode == 200) { + return; + } + + if ( + response.statusCode == 422 && + JSON.stringify(response.body).includes("does not exist") + ) { + return; + } + + throw new Error(JSON.stringify(response.body)); + } + async CloneCreate(snapshotName, datasetName, data = {}) { const httpClient = await this.getHttpClient(false); const zb = await this.getZetabyte(); diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index f18bb09..ab26768 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -13,6 +13,8 @@ const semver = require("semver"); // freenas properties const FREENAS_NFS_SHARE_PROPERTY_NAME = "democratic-csi:freenas_nfs_share_id"; const FREENAS_SMB_SHARE_PROPERTY_NAME = "democratic-csi:freenas_smb_share_id"; + +// iscsi const FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME = "democratic-csi:freenas_iscsi_target_id"; const FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME = @@ -22,6 +24,14 @@ const FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME = const FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:freenas_iscsi_assets_name"; +// nvmeof +const FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME = + "democratic-csi:freenas_nvmeof_subsystem_id"; +const FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME = + "democratic-csi:freenas_nvmeof_namespace_id"; +const FREENAS_NVMEOF_ASSETS_NAME_PROPERTY_NAME = + "democratic-csi:freenas_nvmeof_assets_name"; + // used for in-memory cache of the version info const FREENAS_SYSTEM_VERSION_CACHE_KEY = "freenas:system_version"; const __REGISTRY_NS__ = "FreeNASSshDriver"; @@ -176,6 +186,9 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { case "freenas-iscsi": case "truenas-iscsi": return "iscsi"; + case "freenas-nvmeof": + case "truenas-nvmeof": + return "nvmeof"; default: throw new Error("unknown driver: " + this.ctx.args.driver); } @@ -293,23 +306,574 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { switch (driverShareType) { case "nfs": - properties = await zb.zfs.get(datasetName, [ - "mountpoint", - FREENAS_NFS_SHARE_PROPERTY_NAME, - ]); - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); + { + properties = await zb.zfs.get(datasetName, [ + "mountpoint", + FREENAS_NFS_SHARE_PROPERTY_NAME, + ]); + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); - // create nfs share - if ( - !zb.helpers.isPropertyValueSet( - properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value - ) - ) { - let nfsShareComment; - if (this.options.nfs.shareCommentTemplate) { - nfsShareComment = Handlebars.compile( - this.options.nfs.shareCommentTemplate + // create nfs share + if ( + !zb.helpers.isPropertyValueSet( + properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value + ) + ) { + let nfsShareComment; + if (this.options.nfs.shareCommentTemplate) { + nfsShareComment = Handlebars.compile( + this.options.nfs.shareCommentTemplate + )({ + name: call.request.name, + parameters: call.request.parameters, + csi: { + name: this.ctx.args.csiName, + version: this.ctx.args.csiVersion, + }, + zfs: { + datasetName: datasetName, + }, + }); + } else { + nfsShareComment = `democratic-csi (${this.ctx.args.csiName}): ${datasetName}`; + } + + switch (apiVersion) { + case 1: + case 2: + switch (apiVersion) { + case 1: + share = { + nfs_paths: [properties.mountpoint.value], + nfs_comment: nfsShareComment || "", + nfs_network: + this.options.nfs.shareAllowedNetworks.join(","), + nfs_hosts: this.options.nfs.shareAllowedHosts.join(","), + nfs_alldirs: this.options.nfs.shareAlldirs, + nfs_ro: false, + nfs_quiet: false, + nfs_maproot_user: this.options.nfs.shareMaprootUser, + nfs_maproot_group: this.options.nfs.shareMaprootGroup, + nfs_mapall_user: this.options.nfs.shareMapallUser, + nfs_mapall_group: this.options.nfs.shareMapallGroup, + nfs_security: [], + }; + break; + case 2: + share = { + paths: [properties.mountpoint.value], + comment: nfsShareComment || "", + networks: this.options.nfs.shareAllowedNetworks, + hosts: this.options.nfs.shareAllowedHosts, + alldirs: this.options.nfs.shareAlldirs, + ro: false, + quiet: false, + maproot_user: this.options.nfs.shareMaprootUser, + maproot_group: this.options.nfs.shareMaprootGroup, + mapall_user: this.options.nfs.shareMapallUser, + mapall_group: this.options.nfs.shareMapallGroup, + security: [], + }; + break; + } + + if (isScale && semver.satisfies(truenasVersion, ">=23.10")) { + delete share.quiet; + delete share.nfs_quiet; + } + + if (isScale && semver.satisfies(truenasVersion, ">=22.12")) { + share.path = share.paths[0]; + delete share.paths; + delete share.alldirs; + } + + 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 + * v2 = 200 + */ + if ([200, 201].includes(response.statusCode)) { + let sharePaths; + switch (apiVersion) { + case 1: + sharePaths = response.body.nfs_paths; + break; + case 2: + if (response.body.path) { + sharePaths = [response.body.path]; + } else { + sharePaths = response.body.paths; + } + break; + } + + // FreeNAS responding with bad data + if (!sharePaths.includes(properties.mountpoint.value)) { + throw new GrpcError( + grpc.status.UNKNOWN, + `FreeNAS responded with incorrect share data: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + //set zfs property + await zb.zfs.set(datasetName, { + [FREENAS_NFS_SHARE_PROPERTY_NAME]: response.body.id, + }); + } else { + /** + * v1 = 409 + * v2 = 422 + */ + if ( + [409, 422].includes(response.statusCode) && + (JSON.stringify(response.body).includes( + "You can't share same filesystem with all hosts twice." + ) || + JSON.stringify(response.body).includes( + "Another NFS share already exports this dataset for some network" + )) + ) { + let lookupShare = await this.findResourceByProperties( + "/sharing/nfs", + (item) => { + if ( + (item.nfs_paths && + item.nfs_paths.includes( + properties.mountpoint.value + )) || + (item.paths && + item.paths.includes(properties.mountpoint.value)) || + (item.path && + item.path == properties.mountpoint.value) + ) { + return true; + } + return false; + } + ); + + if (!lookupShare) { + throw new GrpcError( + grpc.status.UNKNOWN, + `FreeNAS failed to find matching share` + ); + } + + //set zfs property + await zb.zfs.set(datasetName, { + [FREENAS_NFS_SHARE_PROPERTY_NAME]: lookupShare.id, + }); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating nfs share - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } + } + + volume_context = { + node_attach_driver: "nfs", + server: this.options.nfs.shareHost, + share: properties.mountpoint.value, + }; + return volume_context; + } + + break; + /** + * TODO: smb need to be more defensive like iscsi and nfs + * ensuring the path is valid and the shareName + */ + case "smb": + { + properties = await zb.zfs.get(datasetName, [ + "mountpoint", + FREENAS_SMB_SHARE_PROPERTY_NAME, + ]); + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); + + let smbName; + + if (this.options.smb.nameTemplate) { + smbName = Handlebars.compile(this.options.smb.nameTemplate)({ + name: call.request.name, + parameters: call.request.parameters, + }); + } else { + smbName = zb.helpers.extractLeafName(datasetName); + } + + if (this.options.smb.namePrefix) { + smbName = this.options.smb.namePrefix + smbName; + } + + if (this.options.smb.nameSuffix) { + smbName += this.options.smb.nameSuffix; + } + + smbName = smbName.toLowerCase(); + + this.ctx.logger.info( + "FreeNAS creating smb share with name: " + smbName + ); + + // create smb share + if ( + !zb.helpers.isPropertyValueSet( + properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value + ) + ) { + /** + * The only required parameters are: + * - path + * - name + * + * Note that over time it appears the list of available parameters has increased + * so in an effort to best support old versions of FreeNAS we should check the + * presense of each parameter in the config and set the corresponding parameter in + * the API request *only* if present in the config. + */ + switch (apiVersion) { + case 1: + case 2: + share = { + name: smbName, + path: properties.mountpoint.value, + }; + + let propertyMapping = { + shareAuxiliaryConfigurationTemplate: "auxsmbconf", + shareHome: "home", + shareAllowedHosts: "hostsallow", + shareDeniedHosts: "hostsdeny", + shareDefaultPermissions: "default_permissions", + shareGuestOk: "guestok", + shareGuestOnly: "guestonly", + shareShowHiddenFiles: "showhiddenfiles", + shareRecycleBin: "recyclebin", + shareBrowsable: "browsable", + shareAccessBasedEnumeration: "abe", + shareTimeMachine: "timemachine", + shareStorageTask: "storage_task", + }; + + for (const key in propertyMapping) { + if (this.options.smb.hasOwnProperty(key)) { + let value; + switch (key) { + case "shareAuxiliaryConfigurationTemplate": + value = Handlebars.compile( + this.options.smb.shareAuxiliaryConfigurationTemplate + )({ + name: call.request.name, + parameters: call.request.parameters, + }); + break; + default: + value = this.options.smb[key]; + break; + } + share[propertyMapping[key]] = value; + } + } + + if (isScale && semver.satisfies(truenasVersion, ">=25.10")) { + let topLevelProperties = [ + "purpose", + "name", + "path", + "enabled", + "comment", + "readonly", + "browsable", + "access_based_share_enumeration", + "audit", + ]; + let disallowedOptions = ["abe"]; + share.purpose = "LEGACY_SHARE"; + share.options = { + purpose: "LEGACY_SHARE", + }; + for (const key in share) { + switch (key) { + case "options": + // ignore + break; + default: + if (!topLevelProperties.includes(key)) { + if (!disallowedOptions.includes(key)) { + share.options[key] = share[key]; + } + delete share[key]; + } + break; + } + } + } + + switch (apiVersion) { + case 1: + endpoint = "/sharing/cifs"; + + // rename keys with cifs_ prefix + for (const key in share) { + share["cifs_" + key] = share[key]; + delete share[key]; + } + + // convert to comma-separated list + if (share.cifs_hostsallow) { + share.cifs_hostsallow = share.cifs_hostsallow.join(","); + } + + // convert to comma-separated list + if (share.cifs_hostsdeny) { + share.cifs_hostsdeny = share.cifs_hostsdeny.join(","); + } + break; + case 2: + endpoint = "/sharing/smb"; + break; + } + + response = await 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 + * v2 = 200 + */ + if ([200, 201].includes(response.statusCode)) { + share = response.body; + let sharePath; + let shareName; + switch (apiVersion) { + case 1: + sharePath = response.body.cifs_path; + shareName = response.body.cifs_name; + break; + case 2: + sharePath = response.body.path; + shareName = response.body.name; + break; + } + + if (shareName != smbName) { + throw new GrpcError( + grpc.status.UNKNOWN, + `FreeNAS responded with incorrect share data: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + if (sharePath != properties.mountpoint.value) { + throw new GrpcError( + grpc.status.UNKNOWN, + `FreeNAS responded with incorrect share data: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + //set zfs property + await zb.zfs.set(datasetName, { + [FREENAS_SMB_SHARE_PROPERTY_NAME]: response.body.id, + }); + } else { + /** + * v1 = 409 + * v2 = 422 + */ + if ( + [409, 422].includes(response.statusCode) && + JSON.stringify(response.body).includes( + "A share with this name already exists." + ) + ) { + let lookupShare = await this.findResourceByProperties( + endpoint, + (item) => { + if ( + (item.cifs_path && + item.cifs_path == properties.mountpoint.value && + item.cifs_name && + item.cifs_name == smbName) || + (item.path && + item.path == properties.mountpoint.value && + item.name && + item.name == smbName) + ) { + return true; + } + return false; + } + ); + + if (!lookupShare) { + throw new GrpcError( + grpc.status.UNKNOWN, + `FreeNAS failed to find matching share` + ); + } + + //set zfs property + await zb.zfs.set(datasetName, { + [FREENAS_SMB_SHARE_PROPERTY_NAME]: lookupShare.id, + }); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating smb share - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } + } + + volume_context = { + node_attach_driver: "smb", + server: this.options.smb.shareHost, + share: smbName, + }; + return volume_context; + } + + break; + case "iscsi": + { + properties = await zb.zfs.get(datasetName, [ + FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, + FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME, + FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME, + ]); + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); + + let basename; + let iscsiName; + + if (this.options.iscsi.nameTemplate) { + iscsiName = Handlebars.compile(this.options.iscsi.nameTemplate)({ + name: call.request.name, + parameters: call.request.parameters, + }); + } else { + iscsiName = zb.helpers.extractLeafName(datasetName); + } + + if (this.options.iscsi.namePrefix) { + iscsiName = this.options.iscsi.namePrefix + iscsiName; + } + + if (this.options.iscsi.nameSuffix) { + iscsiName += this.options.iscsi.nameSuffix; + } + + // According to RFC3270, 'Each iSCSI node, whether an initiator or target, MUST have an iSCSI name. Initiators and targets MUST support the receipt of iSCSI names of up to the maximum length of 223 bytes.' + // https://kb.netapp.com/Advice_and_Troubleshooting/Miscellaneous/What_is_the_maximum_length_of_a_iSCSI_iqn_name + // https://tools.ietf.org/html/rfc3720 + // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 + iscsiName = iscsiName.toLowerCase(); + + let extentDiskName = "zvol/" + datasetName; + let maxZvolNameLength = await driver.getMaxZvolNameLength(); + driver.ctx.logger.debug( + "max zvol name length: %s", + maxZvolNameLength + ); + + /** + * limit is a FreeBSD limitation + * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab + */ + + if (extentDiskName.length > maxZvolNameLength) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` + ); + } + + // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 + if (isScale && iscsiName.length > 64) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `extent name cannot exceed 64 characters: ${iscsiName}` + ); + } + + this.ctx.logger.info( + "FreeNAS creating iscsi assets with name: " + iscsiName + ); + + let extentComment; + if (this.options.iscsi.extentCommentTemplate) { + extentComment = Handlebars.compile( + this.options.iscsi.extentCommentTemplate )({ name: call.request.name, parameters: call.request.parameters, @@ -322,1178 +886,808 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { }, }); } else { - nfsShareComment = `democratic-csi (${this.ctx.args.csiName}): ${datasetName}`; + extentComment = ""; } - switch (apiVersion) { - case 1: - case 2: - switch (apiVersion) { - case 1: - share = { - nfs_paths: [properties.mountpoint.value], - nfs_comment: nfsShareComment || "", - nfs_network: - this.options.nfs.shareAllowedNetworks.join(","), - nfs_hosts: this.options.nfs.shareAllowedHosts.join(","), - nfs_alldirs: this.options.nfs.shareAlldirs, - nfs_ro: false, - nfs_quiet: false, - nfs_maproot_user: this.options.nfs.shareMaprootUser, - nfs_maproot_group: this.options.nfs.shareMaprootGroup, - nfs_mapall_user: this.options.nfs.shareMapallUser, - nfs_mapall_group: this.options.nfs.shareMapallGroup, - nfs_security: [], - }; - break; - case 2: - share = { - paths: [properties.mountpoint.value], - comment: nfsShareComment || "", - networks: this.options.nfs.shareAllowedNetworks, - hosts: this.options.nfs.shareAllowedHosts, - alldirs: this.options.nfs.shareAlldirs, - ro: false, - quiet: false, - maproot_user: this.options.nfs.shareMaprootUser, - maproot_group: this.options.nfs.shareMaprootGroup, - mapall_user: this.options.nfs.shareMapallUser, - mapall_group: this.options.nfs.shareMapallGroup, - security: [], - }; - break; - } - - if (isScale && semver.satisfies(truenasVersion, ">=23.10")) { - delete share.quiet; - delete share.nfs_quiet; - } - - if (isScale && semver.satisfies(truenasVersion, ">=22.12")) { - share.path = share.paths[0]; - delete share.paths; - delete share.alldirs; - } - - 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 - * v2 = 200 - */ - if ([200, 201].includes(response.statusCode)) { - let sharePaths; - switch (apiVersion) { - case 1: - sharePaths = response.body.nfs_paths; - break; - case 2: - if (response.body.path) { - sharePaths = [response.body.path]; - } else { - sharePaths = response.body.paths; - } - break; - } - - // FreeNAS responding with bad data - if (!sharePaths.includes(properties.mountpoint.value)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS responded with incorrect share data: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - //set zfs property - await zb.zfs.set(datasetName, { - [FREENAS_NFS_SHARE_PROPERTY_NAME]: response.body.id, - }); - } else { - /** - * v1 = 409 - * v2 = 422 - */ - if ( - [409, 422].includes(response.statusCode) && - (JSON.stringify(response.body).includes( - "You can't share same filesystem with all hosts twice." - ) || - JSON.stringify(response.body).includes( - "Another NFS share already exports this dataset for some network" - )) - ) { - let lookupShare = await this.findResourceByProperties( - "/sharing/nfs", - (item) => { - if ( - (item.nfs_paths && - item.nfs_paths.includes( - properties.mountpoint.value - )) || - (item.paths && - item.paths.includes(properties.mountpoint.value)) || - (item.path && item.path == properties.mountpoint.value) - ) { - return true; - } - return false; - } - ); - - if (!lookupShare) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS failed to find matching share` - ); - } - - //set zfs property - await zb.zfs.set(datasetName, { - [FREENAS_NFS_SHARE_PROPERTY_NAME]: lookupShare.id, - }); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating nfs share - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - - volume_context = { - node_attach_driver: "nfs", - server: this.options.nfs.shareHost, - share: properties.mountpoint.value, - }; - return volume_context; - - break; - /** - * TODO: smb need to be more defensive like iscsi and nfs - * ensuring the path is valid and the shareName - */ - case "smb": - properties = await zb.zfs.get(datasetName, [ - "mountpoint", - FREENAS_SMB_SHARE_PROPERTY_NAME, - ]); - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - let smbName; - - if (this.options.smb.nameTemplate) { - smbName = Handlebars.compile(this.options.smb.nameTemplate)({ - name: call.request.name, - parameters: call.request.parameters, - }); - } else { - smbName = zb.helpers.extractLeafName(datasetName); - } - - if (this.options.smb.namePrefix) { - smbName = this.options.smb.namePrefix + smbName; - } - - if (this.options.smb.nameSuffix) { - smbName += this.options.smb.nameSuffix; - } - - smbName = smbName.toLowerCase(); - - this.ctx.logger.info( - "FreeNAS creating smb share with name: " + smbName - ); - - // create smb share - if ( - !zb.helpers.isPropertyValueSet( - properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value + const extentInsecureTpc = this.options.iscsi.hasOwnProperty( + "extentInsecureTpc" ) - ) { - /** - * The only required parameters are: - * - path - * - name - * - * Note that over time it appears the list of available parameters has increased - * so in an effort to best support old versions of FreeNAS we should check the - * presense of each parameter in the config and set the corresponding parameter in - * the API request *only* if present in the config. - */ - switch (apiVersion) { - case 1: - case 2: - share = { - name: smbName, - path: properties.mountpoint.value, - }; - - let propertyMapping = { - shareAuxiliaryConfigurationTemplate: "auxsmbconf", - shareHome: "home", - shareAllowedHosts: "hostsallow", - shareDeniedHosts: "hostsdeny", - shareDefaultPermissions: "default_permissions", - shareGuestOk: "guestok", - shareGuestOnly: "guestonly", - shareShowHiddenFiles: "showhiddenfiles", - shareRecycleBin: "recyclebin", - shareBrowsable: "browsable", - shareAccessBasedEnumeration: "abe", - shareTimeMachine: "timemachine", - shareStorageTask: "storage_task", - }; - - for (const key in propertyMapping) { - if (this.options.smb.hasOwnProperty(key)) { - let value; - switch (key) { - case "shareAuxiliaryConfigurationTemplate": - value = Handlebars.compile( - this.options.smb.shareAuxiliaryConfigurationTemplate - )({ - name: call.request.name, - parameters: call.request.parameters, - }); - break; - default: - value = this.options.smb[key]; - break; - } - share[propertyMapping[key]] = value; - } - } - - if (isScale && semver.satisfies(truenasVersion, ">=25.10")) { - let topLevelProperties = [ - "purpose", - "name", - "path", - "enabled", - "comment", - "readonly", - "browsable", - "access_based_share_enumeration", - "audit", - ]; - let disallowedOptions = ["abe"]; - share.purpose = "LEGACY_SHARE"; - share.options = { - purpose: "LEGACY_SHARE", - }; - for (const key in share) { - switch (key) { - case "options": - // ignore - break; - default: - if (!topLevelProperties.includes(key)) { - if (!disallowedOptions.includes(key)) { - share.options[key] = share[key]; - } - delete share[key]; - } - break; - } - } - } - - switch (apiVersion) { - case 1: - endpoint = "/sharing/cifs"; - - // rename keys with cifs_ prefix - for (const key in share) { - share["cifs_" + key] = share[key]; - delete share[key]; - } - - // convert to comma-separated list - if (share.cifs_hostsallow) { - share.cifs_hostsallow = share.cifs_hostsallow.join(","); - } - - // convert to comma-separated list - if (share.cifs_hostsdeny) { - share.cifs_hostsdeny = share.cifs_hostsdeny.join(","); - } - break; - case 2: - endpoint = "/sharing/smb"; - break; - } - - response = await 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 - * v2 = 200 - */ - if ([200, 201].includes(response.statusCode)) { - share = response.body; - let sharePath; - let shareName; - switch (apiVersion) { - case 1: - sharePath = response.body.cifs_path; - shareName = response.body.cifs_name; - break; - case 2: - sharePath = response.body.path; - shareName = response.body.name; - break; - } - - if (shareName != smbName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS responded with incorrect share data: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - if (sharePath != properties.mountpoint.value) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS responded with incorrect share data: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - //set zfs property - await zb.zfs.set(datasetName, { - [FREENAS_SMB_SHARE_PROPERTY_NAME]: response.body.id, - }); - } else { - /** - * v1 = 409 - * v2 = 422 - */ - if ( - [409, 422].includes(response.statusCode) && - JSON.stringify(response.body).includes( - "A share with this name already exists." - ) - ) { - let lookupShare = await this.findResourceByProperties( - endpoint, - (item) => { - if ( - (item.cifs_path && - item.cifs_path == properties.mountpoint.value && - item.cifs_name && - item.cifs_name == smbName) || - (item.path && - item.path == properties.mountpoint.value && - item.name && - item.name == smbName) - ) { - return true; - } - return false; - } - ); - - if (!lookupShare) { - throw new GrpcError( - grpc.status.UNKNOWN, - `FreeNAS failed to find matching share` - ); - } - - //set zfs property - await zb.zfs.set(datasetName, { - [FREENAS_SMB_SHARE_PROPERTY_NAME]: lookupShare.id, - }); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating smb share - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - } - - volume_context = { - node_attach_driver: "smb", - server: this.options.smb.shareHost, - share: smbName, - }; - return volume_context; - - break; - case "iscsi": - properties = await zb.zfs.get(datasetName, [ - FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, - FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME, - FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME, - ]); - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - let basename; - let iscsiName; - - if (this.options.iscsi.nameTemplate) { - iscsiName = Handlebars.compile(this.options.iscsi.nameTemplate)({ - name: call.request.name, - parameters: call.request.parameters, - }); - } else { - iscsiName = zb.helpers.extractLeafName(datasetName); - } - - if (this.options.iscsi.namePrefix) { - iscsiName = this.options.iscsi.namePrefix + iscsiName; - } - - if (this.options.iscsi.nameSuffix) { - iscsiName += this.options.iscsi.nameSuffix; - } - - // According to RFC3270, 'Each iSCSI node, whether an initiator or target, MUST have an iSCSI name. Initiators and targets MUST support the receipt of iSCSI names of up to the maximum length of 223 bytes.' - // https://kb.netapp.com/Advice_and_Troubleshooting/Miscellaneous/What_is_the_maximum_length_of_a_iSCSI_iqn_name - // https://tools.ietf.org/html/rfc3720 - // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 - iscsiName = iscsiName.toLowerCase(); - - let extentDiskName = "zvol/" + datasetName; - let maxZvolNameLength = await driver.getMaxZvolNameLength(); - driver.ctx.logger.debug("max zvol name length: %s", maxZvolNameLength); - - /** - * limit is a FreeBSD limitation - * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab - */ - - if (extentDiskName.length > maxZvolNameLength) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `extent disk name cannot exceed ${maxZvolNameLength} characters: ${extentDiskName}` - ); - } - - // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 - if (isScale && iscsiName.length > 64) { - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `extent name cannot exceed 64 characters: ${iscsiName}` - ); - } - - this.ctx.logger.info( - "FreeNAS creating iscsi assets with name: " + iscsiName - ); - - let extentComment; - if (this.options.iscsi.extentCommentTemplate) { - extentComment = Handlebars.compile( - this.options.iscsi.extentCommentTemplate - )({ - name: call.request.name, - parameters: call.request.parameters, - csi: { - name: this.ctx.args.csiName, - version: this.ctx.args.csiVersion, - }, - zfs: { - datasetName: datasetName, - }, - }); - } else { - extentComment = ""; - } - - const extentInsecureTpc = this.options.iscsi.hasOwnProperty( - "extentInsecureTpc" - ) - ? this.options.iscsi.extentInsecureTpc - : true; - - const extentXenCompat = this.options.iscsi.hasOwnProperty( - "extentXenCompat" - ) - ? this.options.iscsi.extentXenCompat - : false; - - const extentBlocksize = this.options.iscsi.hasOwnProperty( - "extentBlocksize" - ) - ? this.options.iscsi.extentBlocksize - : 512; - - const extentDisablePhysicalBlocksize = - this.options.iscsi.hasOwnProperty("extentDisablePhysicalBlocksize") - ? this.options.iscsi.extentDisablePhysicalBlocksize + ? this.options.iscsi.extentInsecureTpc : true; - const extentRpm = this.options.iscsi.hasOwnProperty("extentRpm") - ? this.options.iscsi.extentRpm - : "SSD"; - - let extentAvailThreshold = this.options.iscsi.hasOwnProperty( - "extentAvailThreshold" - ) - ? Number(this.options.iscsi.extentAvailThreshold) - : null; - - if (!(extentAvailThreshold > 0 && extentAvailThreshold <= 100)) { - extentAvailThreshold = null; - } - - switch (apiVersion) { - case 1: - response = await httpClient.get( - "/services/iscsi/globalconfiguration" - ); - if (response.statusCode != 200) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error getting iscsi configuration - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - basename = response.body.iscsi_basename; - this.ctx.logger.verbose("FreeNAS ISCSI BASENAME: " + basename); - break; - case 2: - response = await httpClient.get("/iscsi/global"); - if (response.statusCode != 200) { - throw new GrpcError( - grpc.status.UNKNOWN, - `error getting iscsi configuration - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - basename = response.body.basename; - this.ctx.logger.verbose("FreeNAS ISCSI BASENAME: " + basename); - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); - } - - // if we got all the way to the TARGETTOEXTENT then we fully finished - // otherwise we must do all assets every time due to the interdependence of IDs etc - if ( - !zb.helpers.isPropertyValueSet( - properties[FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME].value + const extentXenCompat = this.options.iscsi.hasOwnProperty( + "extentXenCompat" ) - ) { + ? this.options.iscsi.extentXenCompat + : false; + + const extentBlocksize = this.options.iscsi.hasOwnProperty( + "extentBlocksize" + ) + ? this.options.iscsi.extentBlocksize + : 512; + + const extentDisablePhysicalBlocksize = + this.options.iscsi.hasOwnProperty("extentDisablePhysicalBlocksize") + ? this.options.iscsi.extentDisablePhysicalBlocksize + : true; + + const extentRpm = this.options.iscsi.hasOwnProperty("extentRpm") + ? this.options.iscsi.extentRpm + : "SSD"; + + let extentAvailThreshold = this.options.iscsi.hasOwnProperty( + "extentAvailThreshold" + ) + ? Number(this.options.iscsi.extentAvailThreshold) + : null; + + if (!(extentAvailThreshold > 0 && extentAvailThreshold <= 100)) { + extentAvailThreshold = null; + } + switch (apiVersion) { - case 1: { - // create target - let target = { - iscsi_target_name: iscsiName, - iscsi_target_alias: "", // TODO: allow template for this - }; - - response = await httpClient.post( - "/services/iscsi/target", - target + case 1: + response = await httpClient.get( + "/services/iscsi/globalconfiguration" ); - - // 409 if invalid - if (response.statusCode != 201) { - target = null; - if ( - response.statusCode == 409 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) - ) { - target = await this.findResourceByProperties( - "/services/iscsi/target", - { - iscsi_target_name: iscsiName, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi target - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - target = response.body; - } - - if (!target) { + if (response.statusCode != 200) { throw new GrpcError( grpc.status.UNKNOWN, - `unknown error creating iscsi target` + `error getting iscsi configuration - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` ); } - - if (target.iscsi_target_name != iscsiName) { + basename = response.body.iscsi_basename; + this.ctx.logger.verbose("FreeNAS ISCSI BASENAME: " + basename); + break; + case 2: + response = await httpClient.get("/iscsi/global"); + if (response.statusCode != 200) { throw new GrpcError( grpc.status.UNKNOWN, - `mismatch name error creating iscsi target` + `error getting iscsi configuration - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` ); } + basename = response.body.basename; + this.ctx.logger.verbose("FreeNAS ISCSI BASENAME: " + basename); + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } - this.ctx.logger.verbose("FreeNAS ISCSI TARGET: %j", target); - - // set target.id on zvol - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME]: target.id, - }); - - // create targetgroup(s) - // targetgroups do have IDs - for (let targetGroupConfig of this.options.iscsi.targetGroups) { - let targetGroup = { - iscsi_target: target.id, - iscsi_target_authgroup: - targetGroupConfig.targetGroupAuthGroup, - iscsi_target_authtype: targetGroupConfig.targetGroupAuthType - ? targetGroupConfig.targetGroupAuthType - : "None", - iscsi_target_portalgroup: - targetGroupConfig.targetGroupPortalGroup, - iscsi_target_initiatorgroup: - targetGroupConfig.targetGroupInitiatorGroup, - iscsi_target_initialdigest: "Auto", + // if we got all the way to the TARGETTOEXTENT then we fully finished + // otherwise we must do all assets every time due to the interdependence of IDs etc + if ( + !zb.helpers.isPropertyValueSet( + properties[FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME].value + ) + ) { + switch (apiVersion) { + case 1: { + // create target + let target = { + iscsi_target_name: iscsiName, + iscsi_target_alias: "", // TODO: allow template for this }; + response = await httpClient.post( - "/services/iscsi/targetgroup", - targetGroup + "/services/iscsi/target", + target ); // 409 if invalid if (response.statusCode != 201) { - targetGroup = null; - /** - * 404 gets returned with an unable to process response when the DB is corrupted (has invalid entries in essense) - * - * To resolve properly the DB should be cleaned up - * /usr/local/etc/rc.d/django stop - * /usr/local/etc/rc.d/nginx stop - * sqlite3 /data/freenas-v1.db - * - * // this deletes everything, probably not what you want - * // should have a better query to only find entries where associated assets no longer exist - * DELETE from services_iscsitargetgroups; - * - * /usr/local/etc/rc.d/django restart - * /usr/local/etc/rc.d/nginx restart - */ + target = null; if ( - response.statusCode == 404 || - (response.statusCode == 409 && - JSON.stringify(response.body).includes( - "cannot be duplicated on a target" - )) + response.statusCode == 409 && + JSON.stringify(response.body).includes( + "Target name already exists" + ) ) { - targetGroup = await this.findResourceByProperties( - "/services/iscsi/targetgroup", + target = await this.findResourceByProperties( + "/services/iscsi/target", { - iscsi_target: target.id, - iscsi_target_portalgroup: - targetGroupConfig.targetGroupPortalGroup, - iscsi_target_initiatorgroup: - targetGroupConfig.targetGroupInitiatorGroup, + iscsi_target_name: iscsiName, } ); } else { throw new GrpcError( grpc.status.UNKNOWN, - `received error creating iscsi targetgroup - code: ${ + `received error creating iscsi target - code: ${ response.statusCode } body: ${JSON.stringify(response.body)}` ); } } else { - targetGroup = response.body; + target = response.body; } - if (!targetGroup) { + if (!target) { throw new GrpcError( grpc.status.UNKNOWN, - `unknown error creating iscsi targetgroup` + `unknown error creating iscsi target` ); } - this.ctx.logger.verbose( - "FreeNAS ISCSI TARGET_GROUP: %j", - targetGroup - ); - } - - let extent = { - iscsi_target_extent_comment: extentComment, - iscsi_target_extent_type: "Disk", // Disk/File, after save Disk becomes "ZVOL" - iscsi_target_extent_name: iscsiName, - iscsi_target_extent_insecure_tpc: extentInsecureTpc, - //iscsi_target_extent_naa: "0x3822690834aae6c5", - iscsi_target_extent_disk: extentDiskName, - iscsi_target_extent_xen: extentXenCompat, - iscsi_target_extent_avail_threshold: extentAvailThreshold, - iscsi_target_extent_blocksize: Number(extentBlocksize), - iscsi_target_extent_pblocksize: extentDisablePhysicalBlocksize, - iscsi_target_extent_rpm: isNaN(Number(extentRpm)) - ? "SSD" - : Number(extentRpm), - iscsi_target_extent_ro: false, - }; - response = await httpClient.post( - "/services/iscsi/extent", - extent - ); - - // 409 if invalid - if (response.statusCode != 201) { - extent = null; - if ( - response.statusCode == 409 && - JSON.stringify(response.body).includes( - "Extent name must be unique" - ) - ) { - extent = await this.findResourceByProperties( - "/services/iscsi/extent", - { iscsi_target_extent_name: iscsiName } - ); - } else { + if (target.iscsi_target_name != iscsiName) { throw new GrpcError( grpc.status.UNKNOWN, - `received error creating iscsi extent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` + `mismatch name error creating iscsi target` ); } - } else { - extent = response.body; - } - if (!extent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi extent` - ); - } + this.ctx.logger.verbose("FreeNAS ISCSI TARGET: %j", target); - if (extent.iscsi_target_extent_name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi extent` - ); - } - - this.ctx.logger.verbose("FreeNAS ISCSI EXTENT: %j", extent); - - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME]: extent.id, - }); - - // create targettoextent - let targetToExtent = { - iscsi_target: target.id, - iscsi_extent: extent.id, - iscsi_lunid: 0, - }; - response = await httpClient.post( - "/services/iscsi/targettoextent", - targetToExtent - ); - - // 409 if invalid - if (response.statusCode != 201) { - targetToExtent = null; - - // LUN ID is already being used for this target. - // Extent is already in this target. - if ( - response.statusCode == 409 && - (JSON.stringify(response.body).includes( - "Extent is already in this target." - ) || - JSON.stringify(response.body).includes( - "LUN ID is already being used for this target." - )) - ) { - targetToExtent = await this.findResourceByProperties( - "/services/iscsi/targettoextent", - { - iscsi_target: target.id, - iscsi_extent: extent.id, - iscsi_lunid: 0, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi targettoextent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - targetToExtent = response.body; - } - - if (!targetToExtent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi targettoextent` - ); - } - this.ctx.logger.verbose( - "FreeNAS ISCSI TARGET_TO_EXTENT: %j", - targetToExtent - ); - - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME]: - targetToExtent.id, - }); - - break; - } - case 2: - // create target and targetgroup - //let targetId; - let targetGroups = []; - for (let targetGroupConfig of this.options.iscsi.targetGroups) { - targetGroups.push({ - portal: targetGroupConfig.targetGroupPortalGroup, - initiator: targetGroupConfig.targetGroupInitiatorGroup, - auth: - targetGroupConfig.targetGroupAuthGroup > 0 - ? targetGroupConfig.targetGroupAuthGroup - : null, - authmethod: - targetGroupConfig.targetGroupAuthType.length > 0 - ? targetGroupConfig.targetGroupAuthType - .toUpperCase() - .replace(" ", "_") - : "NONE", + // set target.id on zvol + await zb.zfs.set(datasetName, { + [FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME]: target.id, }); - } - let target = { - name: iscsiName, - alias: null, // cannot send "" error: handler error - driver: FreeNASDriver method: CreateVolume error: {"name":"GrpcError","code":2,"message":"received error creating iscsi target - code: 422 body: {\"iscsi_target_create.alias\":[{\"message\":\"Alias already exists\",\"errno\":22}]}"} - mode: "ISCSI", - groups: targetGroups, - }; - response = await httpClient.post("/iscsi/target", target); - - // 409 if invalid - if (response.statusCode != 200) { - target = null; - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) - ) { - target = await this.findResourceByProperties( - "/iscsi/target", - { - name: iscsiName, - } + // create targetgroup(s) + // targetgroups do have IDs + for (let targetGroupConfig of this.options.iscsi.targetGroups) { + let targetGroup = { + iscsi_target: target.id, + iscsi_target_authgroup: + targetGroupConfig.targetGroupAuthGroup, + iscsi_target_authtype: targetGroupConfig.targetGroupAuthType + ? targetGroupConfig.targetGroupAuthType + : "None", + iscsi_target_portalgroup: + targetGroupConfig.targetGroupPortalGroup, + iscsi_target_initiatorgroup: + targetGroupConfig.targetGroupInitiatorGroup, + iscsi_target_initialdigest: "Auto", + }; + response = await httpClient.post( + "/services/iscsi/targetgroup", + targetGroup ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi target - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` + + // 409 if invalid + if (response.statusCode != 201) { + targetGroup = null; + /** + * 404 gets returned with an unable to process response when the DB is corrupted (has invalid entries in essense) + * + * To resolve properly the DB should be cleaned up + * /usr/local/etc/rc.d/django stop + * /usr/local/etc/rc.d/nginx stop + * sqlite3 /data/freenas-v1.db + * + * // this deletes everything, probably not what you want + * // should have a better query to only find entries where associated assets no longer exist + * DELETE from services_iscsitargetgroups; + * + * /usr/local/etc/rc.d/django restart + * /usr/local/etc/rc.d/nginx restart + */ + if ( + response.statusCode == 404 || + (response.statusCode == 409 && + JSON.stringify(response.body).includes( + "cannot be duplicated on a target" + )) + ) { + targetGroup = await this.findResourceByProperties( + "/services/iscsi/targetgroup", + { + iscsi_target: target.id, + iscsi_target_portalgroup: + targetGroupConfig.targetGroupPortalGroup, + iscsi_target_initiatorgroup: + targetGroupConfig.targetGroupInitiatorGroup, + } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi targetgroup - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } else { + targetGroup = response.body; + } + + if (!targetGroup) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown error creating iscsi targetgroup` + ); + } + + this.ctx.logger.verbose( + "FreeNAS ISCSI TARGET_GROUP: %j", + targetGroup ); } - } else { - target = response.body; - } - if (!target) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi target` + let extent = { + iscsi_target_extent_comment: extentComment, + iscsi_target_extent_type: "Disk", // Disk/File, after save Disk becomes "ZVOL" + iscsi_target_extent_name: iscsiName, + iscsi_target_extent_insecure_tpc: extentInsecureTpc, + //iscsi_target_extent_naa: "0x3822690834aae6c5", + iscsi_target_extent_disk: extentDiskName, + iscsi_target_extent_xen: extentXenCompat, + iscsi_target_extent_avail_threshold: extentAvailThreshold, + iscsi_target_extent_blocksize: Number(extentBlocksize), + iscsi_target_extent_pblocksize: + extentDisablePhysicalBlocksize, + iscsi_target_extent_rpm: isNaN(Number(extentRpm)) + ? "SSD" + : Number(extentRpm), + iscsi_target_extent_ro: false, + }; + response = await httpClient.post( + "/services/iscsi/extent", + extent ); - } - if (target.name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi target` - ); - } - - // handle situations/race conditions where groups failed to be added/created on the target - // groups":[{"portal":1,"initiator":1,"auth":null,"authmethod":"NONE"},{"portal":2,"initiator":1,"auth":null,"authmethod":"NONE"}] - // TODO: this logic could be more intelligent but this should do for now as it appears in the failure scenario no groups are added - // in other words, I have never seen them invalid, only omitted so this should be enough - if (target.groups.length != targetGroups.length) { - response = await httpClient.put( - `/iscsi/target/id/${target.id}`, - { - groups: targetGroups, + // 409 if invalid + if (response.statusCode != 201) { + extent = null; + if ( + response.statusCode == 409 && + JSON.stringify(response.body).includes( + "Extent name must be unique" + ) + ) { + extent = await this.findResourceByProperties( + "/services/iscsi/extent", + { iscsi_target_extent_name: iscsiName } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi extent - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); } - ); + } else { + extent = response.body; + } - if (response.statusCode != 200) { + if (!extent) { throw new GrpcError( grpc.status.UNKNOWN, - `failed setting target groups` + `unknown error creating iscsi extent` ); + } + + if (extent.iscsi_target_extent_name != iscsiName) { + throw new GrpcError( + grpc.status.UNKNOWN, + `mismatch name error creating iscsi extent` + ); + } + + this.ctx.logger.verbose("FreeNAS ISCSI EXTENT: %j", extent); + + await zb.zfs.set(datasetName, { + [FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME]: extent.id, + }); + + // create targettoextent + let targetToExtent = { + iscsi_target: target.id, + iscsi_extent: extent.id, + iscsi_lunid: 0, + }; + response = await httpClient.post( + "/services/iscsi/targettoextent", + targetToExtent + ); + + // 409 if invalid + if (response.statusCode != 201) { + targetToExtent = null; + + // LUN ID is already being used for this target. + // Extent is already in this target. + if ( + response.statusCode == 409 && + (JSON.stringify(response.body).includes( + "Extent is already in this target." + ) || + JSON.stringify(response.body).includes( + "LUN ID is already being used for this target." + )) + ) { + targetToExtent = await this.findResourceByProperties( + "/services/iscsi/targettoextent", + { + iscsi_target: target.id, + iscsi_extent: extent.id, + iscsi_lunid: 0, + } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi targettoextent - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } else { + targetToExtent = response.body; + } + + if (!targetToExtent) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown error creating iscsi targettoextent` + ); + } + this.ctx.logger.verbose( + "FreeNAS ISCSI TARGET_TO_EXTENT: %j", + targetToExtent + ); + + await zb.zfs.set(datasetName, { + [FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME]: + targetToExtent.id, + }); + + break; + } + case 2: + // create target and targetgroup + //let targetId; + let targetGroups = []; + for (let targetGroupConfig of this.options.iscsi.targetGroups) { + targetGroups.push({ + portal: targetGroupConfig.targetGroupPortalGroup, + initiator: targetGroupConfig.targetGroupInitiatorGroup, + auth: + targetGroupConfig.targetGroupAuthGroup > 0 + ? targetGroupConfig.targetGroupAuthGroup + : null, + authmethod: + targetGroupConfig.targetGroupAuthType.length > 0 + ? targetGroupConfig.targetGroupAuthType + .toUpperCase() + .replace(" ", "_") + : "NONE", + }); + } + let target = { + name: iscsiName, + alias: null, // cannot send "" error: handler error - driver: FreeNASDriver method: CreateVolume error: {"name":"GrpcError","code":2,"message":"received error creating iscsi target - code: 422 body: {\"iscsi_target_create.alias\":[{\"message\":\"Alias already exists\",\"errno\":22}]}"} + mode: "ISCSI", + groups: targetGroups, + }; + + response = await httpClient.post("/iscsi/target", target); + + // 409 if invalid + if (response.statusCode != 200) { + target = null; + if ( + response.statusCode == 422 && + JSON.stringify(response.body).includes( + "Target name already exists" + ) + ) { + target = await this.findResourceByProperties( + "/iscsi/target", + { + name: iscsiName, + } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi target - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } } else { target = response.body; + } - // re-run sanity checks - if (!target) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi target` - ); - } + if (!target) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown error creating iscsi target` + ); + } - if (target.name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi target` - ); - } + if (target.name != iscsiName) { + throw new GrpcError( + grpc.status.UNKNOWN, + `mismatch name error creating iscsi target` + ); + } - if (target.groups.length != targetGroups.length) { + // handle situations/race conditions where groups failed to be added/created on the target + // groups":[{"portal":1,"initiator":1,"auth":null,"authmethod":"NONE"},{"portal":2,"initiator":1,"auth":null,"authmethod":"NONE"}] + // TODO: this logic could be more intelligent but this should do for now as it appears in the failure scenario no groups are added + // in other words, I have never seen them invalid, only omitted so this should be enough + if (target.groups.length != targetGroups.length) { + response = await httpClient.put( + `/iscsi/target/id/${target.id}`, + { + groups: targetGroups, + } + ); + + if (response.statusCode != 200) { throw new GrpcError( grpc.status.UNKNOWN, `failed setting target groups` ); + } else { + target = response.body; + + // re-run sanity checks + if (!target) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown error creating iscsi target` + ); + } + + if (target.name != iscsiName) { + throw new GrpcError( + grpc.status.UNKNOWN, + `mismatch name error creating iscsi target` + ); + } + + if (target.groups.length != targetGroups.length) { + throw new GrpcError( + grpc.status.UNKNOWN, + `failed setting target groups` + ); + } } } - } - this.ctx.logger.verbose("FreeNAS ISCSI TARGET: %j", target); + this.ctx.logger.verbose("FreeNAS ISCSI TARGET: %j", target); - // set target.id on zvol - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME]: target.id, - }); + // set target.id on zvol + await zb.zfs.set(datasetName, { + [FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME]: target.id, + }); - let extent = { - comment: extentComment, - type: "DISK", // Disk/File, after save Disk becomes "ZVOL" - name: iscsiName, - //iscsi_target_extent_naa: "0x3822690834aae6c5", - disk: extentDiskName, - insecure_tpc: extentInsecureTpc, - xen: extentXenCompat, - avail_threshold: extentAvailThreshold, - blocksize: Number(extentBlocksize), - pblocksize: extentDisablePhysicalBlocksize, - rpm: "" + extentRpm, // should be a string - ro: false, - }; + let extent = { + comment: extentComment, + type: "DISK", // Disk/File, after save Disk becomes "ZVOL" + name: iscsiName, + //iscsi_target_extent_naa: "0x3822690834aae6c5", + disk: extentDiskName, + insecure_tpc: extentInsecureTpc, + xen: extentXenCompat, + avail_threshold: extentAvailThreshold, + blocksize: Number(extentBlocksize), + pblocksize: extentDisablePhysicalBlocksize, + rpm: "" + extentRpm, // should be a string + ro: false, + }; - response = await httpClient.post("/iscsi/extent", extent); + response = await httpClient.post("/iscsi/extent", extent); - // 409 if invalid - if (response.statusCode != 200) { - extent = null; - if ( - response.statusCode == 422 && - JSON.stringify(response.body).includes( - "Extent name must be unique" - ) - ) { - extent = await this.findResourceByProperties( - "/iscsi/extent", - { - name: iscsiName, - } - ); - } else { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error creating iscsi extent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - } else { - extent = response.body; - } - - if (!extent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi extent` - ); - } - - if (extent.name != iscsiName) { - throw new GrpcError( - grpc.status.UNKNOWN, - `mismatch name error creating iscsi extent` - ); - } - - this.ctx.logger.verbose("FreeNAS ISCSI EXTENT: %j", extent); - - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME]: extent.id, - }); - - // create targettoextent - let targetToExtent = { - target: target.id, - extent: extent.id, - lunid: 0, - }; - response = await httpClient.post( - "/iscsi/targetextent", - targetToExtent - ); - - if (response.statusCode != 200) { - targetToExtent = null; - - // LUN ID is already being used for this target. - // Extent is already in this target. - if ( - response.statusCode == 422 && - (JSON.stringify(response.body).includes( - "Extent is already in this target." - ) || + // 409 if invalid + if (response.statusCode != 200) { + extent = null; + if ( + response.statusCode == 422 && JSON.stringify(response.body).includes( - "LUN ID is already being used for this target." - )) - ) { - targetToExtent = await this.findResourceByProperties( - "/iscsi/targetextent", - { - target: target.id, - extent: extent.id, - lunid: 0, - } - ); + "Extent name must be unique" + ) + ) { + extent = await this.findResourceByProperties( + "/iscsi/extent", + { + name: iscsiName, + } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi extent - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } } else { + extent = response.body; + } + + if (!extent) { throw new GrpcError( grpc.status.UNKNOWN, - `received error creating iscsi targetextent - code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` + `unknown error creating iscsi extent` ); } - } else { - targetToExtent = response.body; - } - if (!targetToExtent) { - throw new GrpcError( - grpc.status.UNKNOWN, - `unknown error creating iscsi targetextent` + if (extent.name != iscsiName) { + throw new GrpcError( + grpc.status.UNKNOWN, + `mismatch name error creating iscsi extent` + ); + } + + this.ctx.logger.verbose("FreeNAS ISCSI EXTENT: %j", extent); + + await zb.zfs.set(datasetName, { + [FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME]: extent.id, + }); + + // create targettoextent + let targetToExtent = { + target: target.id, + extent: extent.id, + lunid: 0, + }; + response = await httpClient.post( + "/iscsi/targetextent", + targetToExtent ); - } - this.ctx.logger.verbose( - "FreeNAS ISCSI TARGET_TO_EXTENT: %j", - targetToExtent - ); - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME]: - targetToExtent.id, - }); + if (response.statusCode != 200) { + targetToExtent = null; - break; - default: + // LUN ID is already being used for this target. + // Extent is already in this target. + if ( + response.statusCode == 422 && + (JSON.stringify(response.body).includes( + "Extent is already in this target." + ) || + JSON.stringify(response.body).includes( + "LUN ID is already being used for this target." + )) + ) { + targetToExtent = await this.findResourceByProperties( + "/iscsi/targetextent", + { + target: target.id, + extent: extent.id, + lunid: 0, + } + ); + } else { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error creating iscsi targetextent - code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + } else { + targetToExtent = response.body; + } + + if (!targetToExtent) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unknown error creating iscsi targetextent` + ); + } + this.ctx.logger.verbose( + "FreeNAS ISCSI TARGET_TO_EXTENT: %j", + targetToExtent + ); + + await zb.zfs.set(datasetName, { + [FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME]: + targetToExtent.id, + }); + + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } + } + + // iqn = target + let iqn = basename + ":" + iscsiName; + this.ctx.logger.info("FreeNAS iqn: " + iqn); + + // store this off to make delete process more bullet proof + await zb.zfs.set(datasetName, { + [FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME]: iscsiName, + }); + + volume_context = { + node_attach_driver: "iscsi", + portal: this.options.iscsi.targetPortal || "", + portals: this.options.iscsi.targetPortals + ? this.options.iscsi.targetPortals.join(",") + : "", + interface: this.options.iscsi.interface || "", + iqn: iqn, + lun: 0, + }; + return volume_context; + } + break; + + case "nvmeof": + { + switch (apiVersion) { + case 1: throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` + `nvmeof feature is only available with version 2 of the api` ); + break; } + + if (!isScale || semver.satisfies(truenasVersion, "<25.10")) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `nvmeof feature is only available with TrueNAS version 25.10 and above` + ); + } + + properties = await zb.zfs.get(datasetName, [ + FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME, + FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME, + FREENAS_NVMEOF_ASSETS_NAME_PROPERTY_NAME, + ]); + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); + + let nvmeofName; + + if (this.options.nvmeof.nameTemplate) { + nvmeofName = Handlebars.compile(this.options.nvmeof.nameTemplate)({ + name: call.request.name, + parameters: call.request.parameters, + }); + } else { + nvmeofName = zb.helpers.extractLeafName(datasetName); + } + + if (this.options.nvmeof.namePrefix) { + nvmeofName = this.options.nvmeof.namePrefix + nvmeofName; + } + + if (this.options.nvmeof.nameSuffix) { + nvmeofName += this.options.nvmeof.nameSuffix; + } + + // According to RFC3270, 'Each iSCSI node, whether an initiator or target, MUST have an iSCSI name. Initiators and targets MUST support the receipt of iSCSI names of up to the maximum length of 223 bytes.' + // https://kb.netapp.com/Advice_and_Troubleshooting/Miscellaneous/What_is_the_maximum_length_of_a_iSCSI_iqn_name + // https://tools.ietf.org/html/rfc3720 + // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 + nvmeofName = nvmeofName.toLowerCase(); + + let namespaceDiskName = "zvol/" + datasetName; + let maxZvolNameLength = await driver.getMaxZvolNameLength(); + driver.ctx.logger.debug( + "max zvol name length: %s", + maxZvolNameLength + ); + + if (namespaceDiskName.length > maxZvolNameLength) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `namespace disk name cannot exceed ${maxZvolNameLength} characters: ${namespaceDiskName}` + ); + } + + // TODO: get basenqn from global config, add nvemofName to it and ensure full nqn is <= 223 + // // https://github.com/SCST-project/scst/blob/master/scst/src/dev_handlers/scst_vdisk.c#L203 + // if (isScale && nvmeofName.length > 64) { + // throw new GrpcError( + // grpc.status.FAILED_PRECONDITION, + // `extent name cannot exceed 64 characters: ${nvmeofName}` + // ); + // } + + this.ctx.logger.info( + "FreeNAS creating nvmeof assets with name: " + nvmeofName + ); + + // http:///api/docs/current/api_methods_nvmet.subsys.create.html + let subsystemTemplate = _.get( + this.options, + "nvmeof.subsystemTemplate", + {} + ); + subsystemTemplate = subsystemTemplate || {}; + + // http:///api/docs/current/api_methods_nvmet.namespace.create.html + let namespaceTemplate = _.get( + this.options, + "nvmeof.namespaceTemplate", + {} + ); + namespaceTemplate = namespaceTemplate || {}; + + // create subsystem + let subsystem; + switch (apiVersion) { + case 2: + subsystem = await httpApiClient.NvmetSubsysCreate( + nvmeofName, + subsystemTemplate + ); + + break; + } + if (!subsystem) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `unable to find nvmeof subsystem: ${nvmeofName}` + ); + } + this.ctx.logger.verbose("FreeNAS NVMEOF SUBSYSTEM: %j", subsystem); + await zb.zfs.set(datasetName, { + [FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME]: subsystem.id, + }); + + // create subsystem + let namespace; + switch (apiVersion) { + case 2: + namespace = await httpApiClient.NvmetNamespaceCreate( + namespaceDiskName, + subsystem.id, + namespaceTemplate + ); + + break; + } + if (!namespace) { + throw new GrpcError( + grpc.status.NOT_FOUND, + `unable to find nvmeof namespace: ${namespaceDiskName}` + ); + } + this.ctx.logger.verbose("FreeNAS NVMEOF NAMESPACE: %j", namespace); + await zb.zfs.set(datasetName, { + [FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME]: namespace.id, + }); + + // assign ports to subsystem + let ports = _.get(this.options, "nvmeof.ports", []); + for (const port_i of ports) { + const port = await httpApiClient.NvmetPortSubsysCreate( + port_i, + subsystem.id + ); + this.ctx.logger.verbose("FreeNAS NVMEOF PORT: %j", port); + } + + // TODO: assign hosts + + // store this off to make delete process more bullet proof + await zb.zfs.set(datasetName, { + [FREENAS_NVMEOF_ASSETS_NAME_PROPERTY_NAME]: nvmeofName, + }); + + volume_context = { + node_attach_driver: "nvmeof", + transport: this.options.nvmeof.transport || "", + transports: this.options.nvmeof.transports + ? this.options.nvmeof.transports.join(",") + : "", + nqn: subsystem.subnqn, + nsid: namespace.nsid, + }; + return volume_context; } - - // iqn = target - let iqn = basename + ":" + iscsiName; - this.ctx.logger.info("FreeNAS iqn: " + iqn); - - // store this off to make delete process more bullet proof - await zb.zfs.set(datasetName, { - [FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME]: iscsiName, - }); - - volume_context = { - node_attach_driver: "iscsi", - portal: this.options.iscsi.targetPortal || "", - portals: this.options.iscsi.targetPortals - ? this.options.iscsi.targetPortals.join(",") - : "", - interface: this.options.iscsi.interface || "", - iqn: iqn, - lun: 0, - }; - return volume_context; + break; default: throw new GrpcError( @@ -1506,8 +1700,19 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { async deleteShare(call, datasetName) { const driverShareType = this.getDriverShareType(); const httpClient = await this.getHttpClient(); + const httpApiClient = await this.getTrueNASHttpApiClient(); const apiVersion = httpClient.getApiVersion(); const zb = await this.getZetabyte(); + const truenasVersion = await httpApiClient.getSystemVersionSemver(); + + if (!truenasVersion) { + throw new GrpcError( + grpc.status.UNKNOWN, + `unable to detect TrueNAS version` + ); + } + + const isScale = await httpApiClient.getIsScale(); let properties; let response; @@ -1518,202 +1723,400 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { switch (driverShareType) { case "nfs": - try { - properties = await zb.zfs.get(datasetName, [ - "mountpoint", - FREENAS_NFS_SHARE_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; + { + try { + properties = await zb.zfs.get(datasetName, [ + "mountpoint", + FREENAS_NFS_SHARE_PROPERTY_NAME, + ]); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + return; + } + throw err; } - throw err; - } - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); - shareId = properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value; + shareId = properties[FREENAS_NFS_SHARE_PROPERTY_NAME].value; - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(shareId)) { - // remove nfs share - switch (apiVersion) { - case 1: - case 2: - endpoint = "/sharing/nfs/"; - if (apiVersion == 2) { - endpoint += "id/"; - } - endpoint += shareId; - - response = await httpClient.get(endpoint); - - // assume share is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { - switch (apiVersion) { - case 1: - sharePaths = response.body.nfs_paths; - break; - case 2: - if (response.body.path) { - sharePaths = [response.body.path]; - } else { - sharePaths = response.body.paths; - } - break; + // only remove if the process has not succeeded already + if (zb.helpers.isPropertyValueSet(shareId)) { + // remove nfs share + switch (apiVersion) { + case 1: + case 2: + endpoint = "/sharing/nfs/"; + if (apiVersion == 2) { + endpoint += "id/"; } + endpoint += shareId; - deleteAsset = sharePaths.some((value) => { - return value == properties.mountpoint.value; - }); + response = await httpClient.get(endpoint); - if (deleteAsset) { - 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 - // v2 = 200 - if (![200, 204].includes(response.statusCode)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error deleting nfs share - share: ${shareId} code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); + // assume share is gone for now + if ([404, 500].includes(response.statusCode)) { + } else { + switch (apiVersion) { + case 1: + sharePaths = response.body.nfs_paths; + break; + case 2: + if (response.body.path) { + sharePaths = [response.body.path]; + } else { + sharePaths = response.body.paths; + } + break; } - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await zb.zfs.inherit( - datasetName, - FREENAS_NFS_SHARE_PROPERTY_NAME - ); + deleteAsset = sharePaths.some((value) => { + return value == properties.mountpoint.value; + }); + + if (deleteAsset) { + 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 + // v2 = 200 + if (![200, 204].includes(response.statusCode)) { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error deleting nfs share - share: ${shareId} code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + // remove property to prevent delete race conditions + // due to id re-use by FreeNAS/TrueNAS + await zb.zfs.inherit( + datasetName, + FREENAS_NFS_SHARE_PROPERTY_NAME + ); + } } - } - break; - default: - throw new GrpcError( - grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` - ); + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } } } break; case "smb": - try { - properties = await zb.zfs.get(datasetName, [ - "mountpoint", - FREENAS_SMB_SHARE_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; + { + try { + properties = await zb.zfs.get(datasetName, [ + "mountpoint", + FREENAS_SMB_SHARE_PROPERTY_NAME, + ]); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + return; + } + throw err; } - throw err; - } - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); - shareId = properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value; + shareId = properties[FREENAS_SMB_SHARE_PROPERTY_NAME].value; - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(shareId)) { - // remove smb share - switch (apiVersion) { - case 1: - case 2: - switch (apiVersion) { - case 1: - endpoint = `/sharing/cifs/${shareId}`; - break; - case 2: - endpoint = `/sharing/smb/id/${shareId}`; - break; - } - - response = await httpClient.get(endpoint); - - // assume share is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { + // only remove if the process has not succeeded already + if (zb.helpers.isPropertyValueSet(shareId)) { + // remove smb share + switch (apiVersion) { + case 1: + case 2: switch (apiVersion) { case 1: - sharePaths = [response.body.cifs_path]; + endpoint = `/sharing/cifs/${shareId}`; break; case 2: - sharePaths = [response.body.path]; + endpoint = `/sharing/smb/id/${shareId}`; break; } - deleteAsset = sharePaths.some((value) => { - return value == properties.mountpoint.value; - }); + response = await httpClient.get(endpoint); - if (deleteAsset) { - 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 - // v2 = 200 - 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: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); + // assume share is gone for now + if ([404, 500].includes(response.statusCode)) { + } else { + switch (apiVersion) { + case 1: + sharePaths = [response.body.cifs_path]; + break; + case 2: + sharePaths = [response.body.path]; + break; } - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await zb.zfs.inherit( - datasetName, - FREENAS_SMB_SHARE_PROPERTY_NAME - ); + deleteAsset = sharePaths.some((value) => { + return value == properties.mountpoint.value; + }); + + if (deleteAsset) { + 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 + // v2 = 200 + 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: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + // remove property to prevent delete race conditions + // due to id re-use by FreeNAS/TrueNAS + await zb.zfs.inherit( + datasetName, + FREENAS_SMB_SHARE_PROPERTY_NAME + ); + } + } + break; + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown apiVersion ${apiVersion}` + ); + } + } + } + break; + case "iscsi": + { + // Delete target + // NOTE: deletting a target inherently deletes associated targetgroup(s) and targettoextent(s) + + // Delete extent + try { + properties = await zb.zfs.get(datasetName, [ + FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, + FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME, + FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME, + FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME, + ]); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + return; + } + throw err; + } + + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); + + let targetId = + properties[FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME].value; + let extentId = + properties[FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME].value; + let iscsiName = + properties[FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME].value; + let assetName; + + switch (apiVersion) { + case 1: + case 2: + // only remove if the process has not succeeded already + if (zb.helpers.isPropertyValueSet(targetId)) { + // https://jira.ixsystems.com/browse/NAS-103952 + + // v1 - /services/iscsi/target/{id}/ + // v2 - /iscsi/target/id/{id} + endpoint = ""; + if (apiVersion == 1) { + endpoint += "/services"; + } + endpoint += "/iscsi/target/"; + if (apiVersion == 2) { + endpoint += "id/"; + } + endpoint += targetId; + response = await httpClient.get(endpoint); + + // assume is gone for now + if ([404, 500].includes(response.statusCode)) { + } else { + deleteAsset = true; + assetName = null; + + // checking if set for backwards compatibility + if (zb.helpers.isPropertyValueSet(iscsiName)) { + switch (apiVersion) { + case 1: + assetName = response.body.iscsi_target_name; + break; + case 2: + assetName = response.body.name; + break; + } + + if (assetName != iscsiName) { + deleteAsset = false; + } + } + + if (deleteAsset) { + let retries = 0; + let maxRetries = 5; + let retryWait = 1000; + response = await httpClient.delete(endpoint); + + // sometimes after an initiator has detached it takes a moment for TrueNAS to settle + // code: 422 body: {\"message\":\"Target csi-ci-55877e95sanity-node-expand-volume-e54f81fa-cd38e798 is in use.\",\"errno\":14} + while ( + response.statusCode == 422 && + retries < maxRetries && + _.get(response, "body.message").includes("Target") && + _.get(response, "body.message").includes("is in use") && + _.get(response, "body.errno") == 14 + ) { + retries++; + this.ctx.logger.debug( + "target: %s is in use, retry %s shortly", + targetId, + retries + ); + await GeneralUtils.sleep(retryWait); + response = await httpClient.delete(endpoint); + } + + if (![200, 204].includes(response.statusCode)) { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error deleting iscsi target - target: ${targetId} code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + // remove property to prevent delete race conditions + // due to id re-use by FreeNAS/TrueNAS + await zb.zfs.inherit( + datasetName, + FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME + ); + } else { + this.ctx.logger.debug( + "not deleting iscsitarget asset as it appears ID %s has been re-used: zfs name - %s, iscsitarget name - %s", + targetId, + iscsiName, + assetName + ); + } + } + } + + // only remove if the process has not succeeded already + if (zb.helpers.isPropertyValueSet(extentId)) { + // v1 - /services/iscsi/targettoextent/{id}/ + // v2 - /iscsi/targetextent/id/{id} + if (apiVersion == 1) { + endpoint = "/services/iscsi/extent/"; + } else { + endpoint = "/iscsi/extent/id/"; + } + endpoint += extentId; + response = await httpClient.get(endpoint); + + // assume is gone for now + if ([404, 500].includes(response.statusCode)) { + } else { + deleteAsset = true; + assetName = null; + + // checking if set for backwards compatibility + if (zb.helpers.isPropertyValueSet(iscsiName)) { + switch (apiVersion) { + case 1: + assetName = response.body.iscsi_target_extent_name; + break; + case 2: + assetName = response.body.name; + break; + } + + if (assetName != iscsiName) { + deleteAsset = false; + } + } + + if (deleteAsset) { + response = await httpClient.delete(endpoint); + if (![200, 204].includes(response.statusCode)) { + throw new GrpcError( + grpc.status.UNKNOWN, + `received error deleting iscsi extent - extent: ${extentId} code: ${ + response.statusCode + } body: ${JSON.stringify(response.body)}` + ); + } + + // remove property to prevent delete race conditions + // due to id re-use by FreeNAS/TrueNAS + await zb.zfs.inherit( + datasetName, + FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME + ); + } else { + this.ctx.logger.debug( + "not deleting iscsiextent asset as it appears ID %s has been re-used: zfs name - %s, iscsiextent name - %s", + extentId, + iscsiName, + assetName + ); + } } } break; @@ -1725,196 +2128,92 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { } } break; - case "iscsi": - // Delete target - // NOTE: deletting a target inherently deletes associated targetgroup(s) and targettoextent(s) - // Delete extent - try { - properties = await zb.zfs.get(datasetName, [ - FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME, - FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME, - FREENAS_ISCSI_TARGETTOEXTENT_ID_PROPERTY_NAME, - FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME, - ]); - } catch (err) { - if (err.toString().includes("dataset does not exist")) { - return; + case "nvmeof": + { + switch (apiVersion) { + case 1: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `nvmeof feature is only available with version 2 of the api` + ); + break; } - throw err; - } - properties = properties[datasetName]; - this.ctx.logger.debug("zfs props data: %j", properties); - - let targetId = properties[FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME].value; - let extentId = properties[FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME].value; - let iscsiName = - properties[FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME].value; - let assetName; - - switch (apiVersion) { - case 1: - case 2: - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(targetId)) { - // https://jira.ixsystems.com/browse/NAS-103952 - - // v1 - /services/iscsi/target/{id}/ - // v2 - /iscsi/target/id/{id} - endpoint = ""; - if (apiVersion == 1) { - endpoint += "/services"; - } - endpoint += "/iscsi/target/"; - if (apiVersion == 2) { - endpoint += "id/"; - } - endpoint += targetId; - response = await httpClient.get(endpoint); - - // assume is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { - deleteAsset = true; - assetName = null; - - // checking if set for backwards compatibility - if (zb.helpers.isPropertyValueSet(iscsiName)) { - switch (apiVersion) { - case 1: - assetName = response.body.iscsi_target_name; - break; - case 2: - assetName = response.body.name; - break; - } - - if (assetName != iscsiName) { - deleteAsset = false; - } - } - - if (deleteAsset) { - let retries = 0; - let maxRetries = 5; - let retryWait = 1000; - response = await httpClient.delete(endpoint); - - // sometimes after an initiator has detached it takes a moment for TrueNAS to settle - // code: 422 body: {\"message\":\"Target csi-ci-55877e95sanity-node-expand-volume-e54f81fa-cd38e798 is in use.\",\"errno\":14} - while ( - response.statusCode == 422 && - retries < maxRetries && - _.get(response, "body.message").includes("Target") && - _.get(response, "body.message").includes("is in use") && - _.get(response, "body.errno") == 14 - ) { - retries++; - this.ctx.logger.debug( - "target: %s is in use, retry %s shortly", - targetId, - retries - ); - await GeneralUtils.sleep(retryWait); - response = await httpClient.delete(endpoint); - } - - if (![200, 204].includes(response.statusCode)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error deleting iscsi target - target: ${targetId} code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await zb.zfs.inherit( - datasetName, - FREENAS_ISCSI_TARGET_ID_PROPERTY_NAME - ); - } else { - this.ctx.logger.debug( - "not deleting iscsitarget asset as it appears ID %s has been re-used: zfs name - %s, iscsitarget name - %s", - targetId, - iscsiName, - assetName - ); - } - } - } - - // only remove if the process has not succeeded already - if (zb.helpers.isPropertyValueSet(extentId)) { - // v1 - /services/iscsi/targettoextent/{id}/ - // v2 - /iscsi/targetextent/id/{id} - if (apiVersion == 1) { - endpoint = "/services/iscsi/extent/"; - } else { - endpoint = "/iscsi/extent/id/"; - } - endpoint += extentId; - response = await httpClient.get(endpoint); - - // assume is gone for now - if ([404, 500].includes(response.statusCode)) { - } else { - deleteAsset = true; - assetName = null; - - // checking if set for backwards compatibility - if (zb.helpers.isPropertyValueSet(iscsiName)) { - switch (apiVersion) { - case 1: - assetName = response.body.iscsi_target_extent_name; - break; - case 2: - assetName = response.body.name; - break; - } - - if (assetName != iscsiName) { - deleteAsset = false; - } - } - - if (deleteAsset) { - response = await httpClient.delete(endpoint); - if (![200, 204].includes(response.statusCode)) { - throw new GrpcError( - grpc.status.UNKNOWN, - `received error deleting iscsi extent - extent: ${extentId} code: ${ - response.statusCode - } body: ${JSON.stringify(response.body)}` - ); - } - - // remove property to prevent delete race conditions - // due to id re-use by FreeNAS/TrueNAS - await zb.zfs.inherit( - datasetName, - FREENAS_ISCSI_EXTENT_ID_PROPERTY_NAME - ); - } else { - this.ctx.logger.debug( - "not deleting iscsiextent asset as it appears ID %s has been re-used: zfs name - %s, iscsiextent name - %s", - extentId, - iscsiName, - assetName - ); - } - } - } - break; - default: + if (!isScale || semver.satisfies(truenasVersion, "<25.10")) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `invalid configuration: unknown apiVersion ${apiVersion}` + `nvmeof feature is only available with TrueNAS version 25.10 and above` ); + } + + try { + properties = await zb.zfs.get(datasetName, [ + FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME, + FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME, + FREENAS_NVMEOF_ASSETS_NAME_PROPERTY_NAME, + ]); + } catch (err) { + if (err.toString().includes("dataset does not exist")) { + return; + } + throw err; + } + + properties = properties[datasetName]; + this.ctx.logger.debug("zfs props data: %j", properties); + + let subsystemId = + properties[FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME].value; + let namespaceId = + properties[FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME].value; + + // remove namespace + if (zb.helpers.isPropertyValueSet(namespaceId)) { + await GeneralUtils.retry( + 15, + 2000, + async () => { + await httpApiClient.NvmetNamespaceDeleteById(namespaceId); + }, + { + retryCondition: (err) => { + return true; + }, + } + ); + + await zb.zfs.inherit( + datasetName, + FREENAS_NVMEOF_NAMESPACE_ID_PROPERTY_NAME + ); + } + + // remove subsystem + if (zb.helpers.isPropertyValueSet(subsystemId)) { + await GeneralUtils.retry( + 15, + 2000, + async () => { + await httpApiClient.NvmetSubsysDeleteById(subsystemId, { + force: true, + }); + }, + { + retryCondition: (err) => { + return true; + }, + } + ); + + await zb.zfs.inherit( + datasetName, + FREENAS_NVMEOF_SUBSYSTEM_ID_PROPERTY_NAME + ); + } } break; + default: throw new GrpcError( grpc.status.FAILED_PRECONDITION, diff --git a/src/driver/index.js b/src/driver/index.js index f0fe630..406111c 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -124,10 +124,13 @@ class CsiBaseDriver { * @returns Mount */ getDefaultMountInstance() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:default_mount_instance`, () => { - const filesystem = this.getDefaultFilesystemInstance(); - return new Mount({ filesystem }); - }); + return this.ctx.registry.get( + `${__REGISTRY_NS__}:default_mount_instance`, + () => { + const filesystem = this.getDefaultFilesystemInstance(); + return new Mount({ filesystem }); + } + ); } /** @@ -136,9 +139,12 @@ class CsiBaseDriver { * @returns ISCSI */ getDefaultISCSIInstance() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:default_iscsi_instance`, () => { - return new ISCSI(); - }); + return this.ctx.registry.get( + `${__REGISTRY_NS__}:default_iscsi_instance`, + () => { + return new ISCSI(); + } + ); } /** @@ -148,37 +154,46 @@ class CsiBaseDriver { */ getDefaultNVMEoFInstance() { const driver = this; - return this.ctx.registry.get(`${__REGISTRY_NS__}:default_nvmeof_instance`, () => { - return new NVMEoF({ logger: driver.ctx.logger }); - }); + return this.ctx.registry.get( + `${__REGISTRY_NS__}:default_nvmeof_instance`, + () => { + return new NVMEoF({ logger: driver.ctx.logger }); + } + ); } getDefaultZetabyteInstance() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:default_zb_instance`, () => { - return new Zetabyte({ - idempotent: true, - paths: { - zfs: "zfs", - zpool: "zpool", - sudo: "sudo", - chroot: "chroot", - }, - //logger: driver.ctx.logger, - executor: { - spawn: function () { - const command = `${arguments[0]} ${arguments[1].join(" ")}`; - return cp.exec(command); + return this.ctx.registry.get( + `${__REGISTRY_NS__}:default_zb_instance`, + () => { + return new Zetabyte({ + idempotent: true, + paths: { + zfs: "zfs", + zpool: "zpool", + sudo: "sudo", + chroot: "chroot", }, - }, - log_commands: true, - }); - }); + //logger: driver.ctx.logger, + executor: { + spawn: function () { + const command = `${arguments[0]} ${arguments[1].join(" ")}`; + return cp.exec(command); + }, + }, + log_commands: true, + }); + } + ); } getDefaultOneClientInstance() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:default_oneclient_instance`, () => { - return new OneClient(); - }); + return this.ctx.registry.get( + `${__REGISTRY_NS__}:default_oneclient_instance`, + () => { + return new OneClient(); + } + ); } getDefaultObjectiveFSInstance() { @@ -198,11 +213,14 @@ class CsiBaseDriver { * @returns CsiProxyClient */ getDefaultCsiProxyClientInstance() { - return this.ctx.registry.get(`${__REGISTRY_NS__}:default_csi_proxy_instance`, () => { - const options = {}; - options.services = _.get(this.options, "node.csiProxy.services", {}); - return new CsiProxyClient(options); - }); + return this.ctx.registry.get( + `${__REGISTRY_NS__}:default_csi_proxy_instance`, + () => { + const options = {}; + options.services = _.get(this.options, "node.csiProxy.services", {}); + return new CsiProxyClient(options); + } + ); } getDefaultKubernetsConfigInstance() { @@ -1054,7 +1072,7 @@ class CsiBaseDriver { for (let nvmeofConnection of nvmeofConnections) { // connect try { - await GeneralUtils.retry(15, 2000, async () => { + await GeneralUtils.retry(30, 2000, async () => { await nvmeof.connectByNQNTransport( nvmeofConnection.nqn, nvmeofConnection.transport @@ -1069,15 +1087,36 @@ class CsiBaseDriver { continue; } + // wait for connection to actually be connected + try { + await GeneralUtils.retry(30, 2000, async () => { + let state = await nvmeof.getSubsystemStateByNQNTransport( + nvmeofConnection.nqn, + nvmeofConnection.transport + ); + if (state != "live") { + throw new Error("nvmeof connection is not live"); + } + }); + } catch (err) { + driver.ctx.logger.warn( + `error: ${JSON.stringify( + err + )} transport never became live: ${ + nvmeofConnection.transport + }` + ); + continue; + } + // find controller device let controllerDevice; try { - await GeneralUtils.retry(15, 2000, async () => { + await GeneralUtils.retry(30, 2000, async () => { controllerDevice = await nvmeof.controllerDevicePathByTransportNQN( nvmeofConnection.transport, - nvmeofConnection.nqn, - nvmeofConnection.nsid + nvmeofConnection.nqn ); if (!controllerDevice) { @@ -1488,11 +1527,13 @@ class CsiBaseDriver { // format result = await filesystem.deviceIsFormatted(device); if (!result) { - let formatOptions = _.get( - driver.options.node.format, - [fs_type, "customOptions"], - [] - ); + let formatOptions = [ + ..._.get( + driver.options.node.format, + [fs_type, "customOptions"], + [] + ), + ]; if (!Array.isArray(formatOptions)) { formatOptions = []; } diff --git a/src/utils/nvmeof.js b/src/utils/nvmeof.js index e2f47e1..82c7b63 100644 --- a/src/utils/nvmeof.js +++ b/src/utils/nvmeof.js @@ -218,6 +218,50 @@ class NVMEoF { return false; } + async parseTransportFromPath(path) { + let address; + let service; + switch (path.Transport) { + case "fc": + case "rdma": + case "tcp": + let controllerAddress = path.Address; + /** + * For backwards compatibility with older nvme-cli versions (at least < 2.2.1) + * old: "Address":"traddr=127.0.0.1 trsvcid=4420" + * new: "Address":"traddr=127.0.0.1,trsvcid=4420" + */ + controllerAddress = controllerAddress.replace( + new RegExp(/ ([a-z_]*=)/, "g"), + ",$1" + ); + let parts = controllerAddress.split(","); + + for (let i_part of parts) { + let i_parts = i_part.split("="); + switch (i_parts[0].trim()) { + case "traddr": + address = i_parts[1].trim(); + break; + case "trsvcid": + service = i_parts[1].trim(); + break; + } + } + + break; + case "pcie": + address = path.Address; + break; + } + + return { + type: path.Transport, + address, + service, + }; + } + async parseTransport(transport) { if (typeof transport === "object") { return transport; @@ -338,11 +382,17 @@ class NVMEoF { async controllerDevicePathByTransportNQN(transport, nqn) { const nvmeof = this; + transport = await nvmeof.parseTransport(transport); - let controller = await nvmeof.getControllerByTransportNQN(transport, nqn); - if (controller) { - return `/dev/${controller.Controller}`; + let path = await nvmeof.getSubsystemPathByNQNTransport(nqn, transport); + if (path) { + return `/dev/${path.Name}`; } + + // let controller = await nvmeof.getControllerByTransportNQN(transport, nqn); + // if (controller) { + // return `/dev/${controller.Controller}`; + // } } async getSubsystems() { @@ -396,7 +446,7 @@ class NVMEoF { for (let subsystem of subsystems) { if (subsystem.Namespaces) { for (let namespace of subsystem.Namespaces) { - if (namespace.NameSpace == name) { + if (namespace.NameSpace == name && subsystem.Controllers) { return subsystem.Controllers; } } @@ -433,37 +483,18 @@ class NVMEoF { continue; } - let controllerAddress = controller.Address; - /** - * For backwards compatibility with older nvme-cli versions (at least < 2.2.1) - * old: "Address":"traddr=127.0.0.1 trsvcid=4420" - * new: "Address":"traddr=127.0.0.1,trsvcid=4420" - */ - controllerAddress = controllerAddress.replace( - new RegExp(/ ([a-z_]*=)/, "g"), - ",$1" + let controller_transport = await nvmeof.parseTransportFromPath( + controller ); - let parts = controllerAddress.split(","); - let traddr; - let trsvcid; - for (let i_part of parts) { - let i_parts = i_part.split("="); - switch (i_parts[0].trim()) { - case "traddr": - traddr = i_parts[1].trim(); - break; - case "trsvcid": - trsvcid = i_parts[1].trim(); - break; - } - } - - if (traddr != transport.address) { + if (controller_transport.address != transport.address) { continue; } - if (transport.service && trsvcid != transport.service) { + if ( + transport.service && + controller_transport.service != transport.service + ) { continue; } @@ -515,6 +546,39 @@ class NVMEoF { nvmeof.logger.warn(`failed to find nqn for device: ${name}`); } + async getSubsystemStateByNQNTransport(nqn, transport) { + const nvmeof = this; + transport = await nvmeof.parseTransport(transport); + const path = await nvmeof.getSubsystemPathByNQNTransport(nqn, transport); + return path?.State; + } + + async getSubsystemPathByNQNTransport(nqn, transport) { + const nvmeof = this; + transport = await nvmeof.parseTransport(transport); + const subsysList = await nvmeof.listSubsys(["-v"]); + host_label: for (const host of subsysList) { + subsys_label: for (const subsys of host.Subsystems) { + if (subsys.NQN != nqn) { + continue; + } + path_label: for (const path of subsys.Paths) { + let parsed_path_transport = await nvmeof.parseTransportFromPath(path); + for (const key of Object.keys(transport)) { + if ( + ["type", "address", "service"].includes(key) && + transport[key] != parsed_path_transport[key] + ) { + break path_label; + } + } + + return path; + } + } + } + } + devicePathByModelNumberSerialNumber(modelNumber, serialNumber) { modelNumber = modelNumber.replaceAll(" ", "_"); serialNumber = serialNumber.replaceAll(" ", "_"); From ddf6b0d3209f64ce1944f6638453027bfd2dec84 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 31 Oct 2025 09:45:28 -0600 Subject: [PATCH 26/55] fix target idempotency, bump binary versions Signed-off-by: Travis Glenn Hansen --- Dockerfile | 8 ++-- Dockerfile.Windows | 8 ++-- src/driver/freenas/api.js | 77 +++++++++++++++++++++++++++++---- src/driver/freenas/http/api.js | 2 +- src/driver/freenas/ssh.js | 79 ++++++++++++++++++++++++++++++---- 5 files changed, 148 insertions(+), 26 deletions(-) diff --git a/Dockerfile b/Dockerfile index 28bab3d..16817ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -121,19 +121,19 @@ RUN \ echo '83e7a026-2564-455b-ada6-ddbdaf0bc519' > /etc/nvme/hostid && \ echo 'nqn.2014-08.org.nvmexpress:uuid:941e4f03-2cd6-435e-86df-731b1c573d86' > /etc/nvme/hostnqn -ARG RCLONE_VERSION=1.69.1 +ARG RCLONE_VERSION=1.71.2 ADD docker/rclone-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/rclone-installer.sh && rclone-installer.sh -ARG RESTIC_VERSION=0.18.0 +ARG RESTIC_VERSION=0.18.1 ADD docker/restic-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/restic-installer.sh && restic-installer.sh -ARG KOPIA_VERSION=0.19.0 +ARG KOPIA_VERSION=0.21.1 ADD docker/kopia-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/kopia-installer.sh && kopia-installer.sh -ARG YQ_VERSION=v4.45.1 +ARG YQ_VERSION=v4.48.1 ADD docker/yq-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/yq-installer.sh && yq-installer.sh diff --git a/Dockerfile.Windows b/Dockerfile.Windows index bb5c058..0c0c2a0 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -41,22 +41,22 @@ RUN Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f RUN mkdir \usr\local\bin; mkdir \tmp -ARG RCLONE_VERSION=v1.69.1 +ARG RCLONE_VERSION=v1.71.2 RUN Invoke-WebRequest "https://github.com/rclone/rclone/releases/download/${env:RCLONE_VERSION}/rclone-${env:RCLONE_VERSION}-windows-amd64.zip" -OutFile '/tmp/rclone.zip' -UseBasicParsing ; \ Expand-Archive C:\tmp\rclone.zip -DestinationPath C:\tmp ; \ Copy-Item $('C:\tmp\rclone-{0}-windows-amd64\rclone.exe' -f $env:RCLONE_VERSION) -Destination "C:\usr\local\bin" -ARG RESTIC_VERSION=0.18.0 +ARG RESTIC_VERSION=0.18.1 RUN Invoke-WebRequest "https://github.com/restic/restic/releases/download/v${env:RESTIC_VERSION}/restic_${env:RESTIC_VERSION}_windows_amd64.zip" -OutFile '/tmp/restic.zip' -UseBasicParsing ; \ Expand-Archive C:\tmp\restic.zip -DestinationPath C:\tmp ; \ Copy-Item $('C:\tmp\restic_{0}_windows_amd64.exe' -f $env:RESTIC_VERSION) -Destination "C:\usr\local\bin\restic.exe" -ARG KOPIA_VERSION=0.19.0 +ARG KOPIA_VERSION=0.21.1 RUN Invoke-WebRequest "https://github.com/kopia/kopia/releases/download/v${env:KOPIA_VERSION}/kopia-${env:KOPIA_VERSION}-windows-x64.zip" -OutFile '/tmp/kopia.zip' -UseBasicParsing ; \ Expand-Archive C:\tmp\kopia.zip -DestinationPath C:\tmp ; \ Copy-Item $('C:\tmp\kopia-{0}-windows-x64\kopia.exe' -f $env:KOPIA_VERSION) -Destination "C:\usr\local\bin" -ARG YQ_VERSION=v4.45.1 +ARG YQ_VERSION=v4.48.1 RUN Invoke-WebRequest "https://github.com/mikefarah/yq/releases/download/${env:YQ_VERSION}/yq_windows_amd64.zip" -OutFile '/tmp/yq.zip' -UseBasicParsing ; \ Expand-Archive C:\tmp\yq.zip -DestinationPath C:\tmp ; \ Copy-Item $('C:\tmp\yq_windows_amd64.exe') -Destination "C:\usr\local\bin\yq.exe" diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 65cf970..9c9395d 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -178,6 +178,49 @@ class FreeNASApiDriver extends CsiBaseDriver { }); } + /** + * Check if an error response indicates a target already exists. + * This method handles variations in TrueNAS API error messages across different API versions. + * + * @param {string|Object} responseBody - The HTTP response body (string or object) + * @returns {boolean} - true if the error indicates target already exists + */ + isTargetAlreadyExistsError(responseBody) { + // Extract error message more efficiently + let errorString = ""; + + if (typeof responseBody === "string") { + errorString = responseBody; + } else if (responseBody && typeof responseBody === "object") { + // Try common error message fields first to avoid full JSON.stringify + errorString = + responseBody.message || + responseBody.error || + responseBody.detail || + JSON.stringify(responseBody); + } else { + return false; + } + + // Handle multiple variations of the target already exists error message + const targetExistsPatterns = [ + "Target name already exists", // Original pattern in code (API v1) + "Target with this name already exists", // Actual TrueNAS error message (API v2) + "Target\\b.*\\balready\\b.*\\bexists", // Flexible pattern with word boundaries + ]; + + return targetExistsPatterns.some((pattern) => { + if (pattern.includes("\\")) { + // Use regex for flexible patterns with word boundaries + const regex = new RegExp(pattern, "i"); + return regex.test(errorString); + } else { + // Use case-insensitive simple string matching for exact patterns + return errorString.toLowerCase().includes(pattern.toLowerCase()); + } + }); + } + /** * should create any necessary share resources * should set the SHARE_VOLUME_CONTEXT_PROPERTY_NAME propery @@ -884,21 +927,30 @@ class FreeNASApiDriver extends CsiBaseDriver { target ); - // 409 if invalid + // 409 Conflict - target already exists or other validation errors if (response.statusCode != 201) { target = null; if ( response.statusCode == 409 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) + this.isTargetAlreadyExistsError(response.body) ) { + this.ctx.logger.debug( + "iSCSI target already exists, attempting to find existing target with name: %s", + iscsiName + ); target = await httpApiClient.findResourceByProperties( "/services/iscsi/target", { iscsi_target_name: iscsiName, } ); + if (target) { + this.ctx.logger.debug( + "Found existing iSCSI target with ID: %s, name: %s", + target.id, + target.iscsi_target_name + ); + } } else { throw new GrpcError( grpc.status.UNKNOWN, @@ -1175,21 +1227,30 @@ class FreeNASApiDriver extends CsiBaseDriver { response = await httpClient.post("/iscsi/target", target); - // 409 if invalid + // 422 Unprocessable Entity - validation errors including duplicate targets if (response.statusCode != 200) { target = null; if ( response.statusCode == 422 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) + this.isTargetAlreadyExistsError(response.body) ) { + this.ctx.logger.debug( + "iSCSI target already exists, attempting to find existing target with name: %s", + iscsiName + ); target = await httpApiClient.findResourceByProperties( "/iscsi/target", { name: iscsiName, } ); + if (target) { + this.ctx.logger.debug( + "Found existing iSCSI target with ID: %s, name: %s", + target.id, + target.name + ); + } } else { throw new GrpcError( grpc.status.UNKNOWN, diff --git a/src/driver/freenas/http/api.js b/src/driver/freenas/http/api.js index 4687ce1..de78bf6 100644 --- a/src/driver/freenas/http/api.js +++ b/src/driver/freenas/http/api.js @@ -67,7 +67,7 @@ class Api { // crude stoppage attempt let response = await httpClient.get(endpoint, queryParams); if (lastReponse) { - if (JSON.stringify(lastReponse) == JSON.stringify(response)) { + if (JSON.stringify(lastReponse.body) == JSON.stringify(response.body)) { break; } } diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index ab26768..0840b1a 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -228,7 +228,7 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { // crude stoppage attempt let response = await httpClient.get(endpoint, queryParams); if (lastReponse) { - if (JSON.stringify(lastReponse) == JSON.stringify(response)) { + if (JSON.stringify(lastReponse.body) == JSON.stringify(response.body)) { break; } } @@ -273,6 +273,49 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { return target; } + /** + * Check if an error response indicates a target already exists. + * This method handles variations in TrueNAS API error messages across different API versions. + * + * @param {string|Object} responseBody - The HTTP response body (string or object) + * @returns {boolean} - true if the error indicates target already exists + */ + isTargetAlreadyExistsError(responseBody) { + // Extract error message more efficiently + let errorString = ""; + + if (typeof responseBody === "string") { + errorString = responseBody; + } else if (responseBody && typeof responseBody === "object") { + // Try common error message fields first to avoid full JSON.stringify + errorString = + responseBody.message || + responseBody.error || + responseBody.detail || + JSON.stringify(responseBody); + } else { + return false; + } + + // Handle multiple variations of the target already exists error message + const targetExistsPatterns = [ + "Target name already exists", // Original pattern in code (API v1) + "Target with this name already exists", // Actual TrueNAS error message (API v2) + "Target\\b.*\\balready\\b.*\\bexists", // Flexible pattern with word boundaries + ]; + + return targetExistsPatterns.some((pattern) => { + if (pattern.includes("\\")) { + // Use regex for flexible patterns with word boundaries + const regex = new RegExp(pattern, "i"); + return regex.test(errorString); + } else { + // Use case-insensitive simple string matching for exact patterns + return errorString.toLowerCase().includes(pattern.toLowerCase()); + } + }); + } + /** * should create any necessary share resources * should set the SHARE_VOLUME_CONTEXT_PROPERTY_NAME propery @@ -982,21 +1025,30 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { target ); - // 409 if invalid + // 409 Conflict - target already exists or other validation errors if (response.statusCode != 201) { target = null; if ( response.statusCode == 409 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) + this.isTargetAlreadyExistsError(response.body) ) { + this.ctx.logger.debug( + "iSCSI target already exists, attempting to find existing target with name: %s", + iscsiName + ); target = await this.findResourceByProperties( "/services/iscsi/target", { iscsi_target_name: iscsiName, } ); + if (target) { + this.ctx.logger.debug( + "Found existing iSCSI target with ID: %s, name: %s", + target.id, + target.iscsi_target_name + ); + } } else { throw new GrpcError( grpc.status.UNKNOWN, @@ -1271,21 +1323,30 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { response = await httpClient.post("/iscsi/target", target); - // 409 if invalid + // 422 Unprocessable Entity - validation errors including duplicate targets if (response.statusCode != 200) { target = null; if ( response.statusCode == 422 && - JSON.stringify(response.body).includes( - "Target name already exists" - ) + this.isTargetAlreadyExistsError(response.body) ) { + this.ctx.logger.debug( + "iSCSI target already exists, attempting to find existing target with name: %s", + iscsiName + ); target = await this.findResourceByProperties( "/iscsi/target", { name: iscsiName, } ); + if (target) { + this.ctx.logger.debug( + "Found existing iSCSI target with ID: %s, name: %s", + target.id, + target.name + ); + } } else { throw new GrpcError( grpc.status.UNKNOWN, From 9f7d4019c96d8b9e5519e45ffee8cf84ce4f7bb0 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Fri, 31 Oct 2025 13:08:07 -0600 Subject: [PATCH 27/55] use mutex for zfs-generic commands to ensure no concurrent execution Signed-off-by: Travis Glenn Hansen --- src/driver/controller-zfs-generic/index.js | 80 ++++++++++++++-------- 1 file changed, 51 insertions(+), 29 deletions(-) diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 7ddc66e..1c7c32f 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -4,6 +4,7 @@ const { GrpcError, grpc } = require("../../utils/grpc"); const GeneralUtils = require("../../utils/general"); const LocalCliExecClient = require("../../utils/zfs_local_exec_client").LocalCliClient; +const Mutex = require("async-mutex").Mutex; const SshClient = require("../../utils/zfs_ssh_exec_client").SshClient; const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); @@ -13,6 +14,14 @@ const ISCSI_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:iscsi_assets_name"; const NVMEOF_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:nvmeof_assets_name"; const __REGISTRY_NS__ = "ControllerZfsGenericDriver"; class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { + constructor(ctx, options) { + super(...arguments); + + this.targetCliMutex = new Mutex(); + this.nvmetCliMutex = new Mutex(); + this.spdkCliMutex = new Mutex(); + } + getExecClient() { return this.ctx.registry.get(`${__REGISTRY_NS__}:exec_client`, () => { if (this.options.sshConnection) { @@ -894,17 +903,20 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} let options = { pty: true, }; - let response = await execClient.exec( - execClient.buildCommand(command, args), - options - ); - driver.ctx.logger.verbose( - "TargetCLI response: " + JSON.stringify(response) - ); - if (response.code != 0) { - throw response; - } - return response; + + return driver.targetCliMutex.runExclusive(async () => { + let response = await execClient.exec( + execClient.buildCommand(command, args), + options + ); + driver.ctx.logger.verbose( + "TargetCLI response: " + JSON.stringify(response) + ); + if (response.code != 0) { + throw response; + } + return response; + }); } async nvmetCliCommand(data) { @@ -982,15 +994,20 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} let options = { pty: true, }; - let response = await execClient.exec( - execClient.buildCommand(command, args), - options - ); - driver.ctx.logger.verbose("nvmetCLI response: " + JSON.stringify(response)); - if (response.code != 0) { - throw response; - } - return response; + + return driver.nvmetCliMutex.runExclusive(async () => { + let response = await execClient.exec( + execClient.buildCommand(command, args), + options + ); + driver.ctx.logger.verbose( + "nvmetCLI response: " + JSON.stringify(response) + ); + if (response.code != 0) { + throw response; + } + return response; + }); } async spdkCliCommand(data) { @@ -1041,15 +1058,20 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} let options = { pty: true, }; - let response = await execClient.exec( - execClient.buildCommand(command, args), - options - ); - driver.ctx.logger.verbose("spdkCLI response: " + JSON.stringify(response)); - if (response.code != 0) { - throw response; - } - return response; + + return driver.spdkCliMutex.runExclusive(async () => { + let response = await execClient.exec( + execClient.buildCommand(command, args), + options + ); + driver.ctx.logger.verbose( + "spdkCLI response: " + JSON.stringify(response) + ); + if (response.code != 0) { + throw response; + } + return response; + }); } } From 3eb18d80994b2b5f9d040d015e1c4b2a2fae4755 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 1 Nov 2025 09:22:32 -0600 Subject: [PATCH 28/55] cleanse secrets from iscsi command logging Signed-off-by: Travis Glenn Hansen --- src/driver/freenas/api.js | 7 +++++-- src/driver/freenas/ssh.js | 5 ++++- src/utils/iscsi.js | 24 +++++++++++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 1c96c33..3cf4616 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -377,7 +377,10 @@ class FreeNASApiDriver extends CsiBaseDriver { } // FreeNAS responding with bad data - if (!sharePaths.includes(properties.mountpoint.value)) { + if ( + !Array.isArray(sharePaths) || + !sharePaths.includes(properties.mountpoint.value) + ) { throw new GrpcError( grpc.status.UNKNOWN, `FreeNAS responded with incorrect share data: ${ @@ -2606,7 +2609,7 @@ class FreeNASApiDriver extends CsiBaseDriver { if (!(await httpApiClient.getIsScale())) { throw new GrpcError( grpc.status.FAILED_PRECONDITION, - `driver is only availalbe with TrueNAS SCALE` + `driver is only available with TrueNAS SCALE` ); } diff --git a/src/driver/freenas/ssh.js b/src/driver/freenas/ssh.js index 9156d9d..37a593f 100644 --- a/src/driver/freenas/ssh.js +++ b/src/driver/freenas/ssh.js @@ -474,7 +474,10 @@ class FreeNASSshDriver extends ControllerZfsBaseDriver { } // FreeNAS responding with bad data - if (!sharePaths.includes(properties.mountpoint.value)) { + if ( + !Array.isArray(sharePaths) || + !sharePaths.includes(properties.mountpoint.value) + ) { throw new GrpcError( grpc.status.UNKNOWN, `FreeNAS responded with incorrect share data: ${ diff --git a/src/utils/iscsi.js b/src/utils/iscsi.js index 019f539..f62e0ab 100644 --- a/src/utils/iscsi.js +++ b/src/utils/iscsi.js @@ -613,7 +613,29 @@ class ISCSI { args.unshift(command); command = iscsi.options.paths.sudo; } - console.log("executing iscsi command: %s %s", command, args.join(" ")); + + // --name node.session.auth.password --value FOOBAR + let argIndex; + let cleansedArgs = [...args]; + argIndex = args.findIndex((value) => { + return value.trim() == "node.session.auth.password"; + }); + + if (argIndex >= 0 && cleansedArgs[argIndex + 1].trim() == "--value") { + cleansedArgs[argIndex + 2] = "redacted"; + } + + // --name node.session.auth.password_id --value FOOBAR + argIndex = args.findIndex((value) => { + return value.trim() == "node.session.auth.password_in"; + }); + + if (argIndex >= 0 && cleansedArgs[argIndex + 1].trim() == "--value") { + cleansedArgs[argIndex + 2] = "redacted"; + } + + const cleansedLog = `${command} ${cleansedArgs.join(" ")}`; + console.log("executing iscsi command: %s", cleansedLog); return new Promise((resolve, reject) => { const child = iscsi.options.executor.spawn(command, args, options); From 6f47a506d00c16786a8eba651f67f3723b9c11b6 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Sat, 1 Nov 2025 10:07:51 -0600 Subject: [PATCH 29/55] ensure string values on args for later processing Signed-off-by: Travis Glenn Hansen --- src/utils/iscsi.js | 3 +++ src/utils/kopia.js | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/utils/iscsi.js b/src/utils/iscsi.js index f62e0ab..281324e 100644 --- a/src/utils/iscsi.js +++ b/src/utils/iscsi.js @@ -614,6 +614,9 @@ class ISCSI { command = iscsi.options.paths.sudo; } + // ensure all args are converted to string values + args = args.map(String); + // --name node.session.auth.password --value FOOBAR let argIndex; let cleansedArgs = [...args]; diff --git a/src/utils/kopia.js b/src/utils/kopia.js index faad548..05dcf54 100644 --- a/src/utils/kopia.js +++ b/src/utils/kopia.js @@ -254,6 +254,9 @@ class Kopia { command = kopia.options.paths.sudo; } + // ensure all args are converted to string values + args = args.map(String); + options.env = { ...{}, ...process.env, From 5b0b1049e5cb69260f53006da9e510748e6cb72e Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 3 Nov 2025 14:36:00 -0700 Subject: [PATCH 30/55] initial support for overriding driver options on a per-class and per-pvc basis Signed-off-by: Travis Glenn Hansen --- src/driver/controller-synology/index.js | 2 +- src/driver/controller-zfs/index.js | 154 +++++++++++++++++------- src/driver/index.js | 37 +++++- src/utils/iscsi.js | 4 +- 4 files changed, 151 insertions(+), 46 deletions(-) diff --git a/src/driver/controller-synology/index.js b/src/driver/controller-synology/index.js index 9c1396b..29e6817 100644 --- a/src/driver/controller-synology/index.js +++ b/src/driver/controller-synology/index.js @@ -163,7 +163,7 @@ class ControllerSynologyDriver extends CsiBaseDriver { parseParameterYamlData(data, fieldHint = "") { try { return yaml.load(data); - } catch { + } catch (err) { if (err instanceof yaml.YAMLException) { throw new GrpcError( grpc.status.INVALID_ARGUMENT, diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index 2865df4..e65e14d 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -7,6 +7,7 @@ const getLargestNumber = require("../../utils/general").getLargestNumber; const Handlebars = require("handlebars"); const uuidv4 = require("uuid").v4; const semver = require("semver"); +const yaml = require("js-yaml"); // zfs common properties const MANAGED_PROPERTY_NAME = "democratic-csi:managed_resource"; @@ -32,7 +33,7 @@ const VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME = const MAX_ZVOL_NAME_LENGTH_CACHE_KEY = "controller-zfs:max_zvol_name_length"; /** - * Base driver to provisin zfs assets using zfs cli commands. + * Base driver to provision zfs assets using zfs cli commands. * Derived drivers only need to implement: * - getExecClient() * - async getZetabyte() @@ -640,9 +641,67 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { const execClient = this.getExecClient(); const zb = await this.getZetabyte(); + const normalizedParameters = driver.getNormalizedParameters( + call.request.parameters, + driver.options.driver, + driver.options.instance_id + ); + + let parametersOptions = {}; + if (normalizedParameters["config"]) { + try { + parametersOptions = yaml.load(normalizedParameters["config"]); + } catch (err) { + if (err instanceof yaml.YAMLException) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `parameter 'config' not a valid YAML/JSON document.`.trim() + ); + } else { + throw err; + } + } + } + + let pvcOptions = {}; + if ( + normalizedParameters["load-config-from-pvc"] == "true" && + call.request.parameters["csi.storage.k8s.io/pvc/name"] && + call.request.parameters["csi.storage.k8s.io/pvc/namespace"] + ) { + let pvc = await driver.getPersistentVolumeClaim( + call.request.parameters["csi.storage.k8s.io/pvc/name"], + call.request.parameters["csi.storage.k8s.io/pvc/namespace"] + ); + + if ( + _.has(pvc, ["metadata", "annotations", "democratic-csi.org/config"]) + ) { + try { + pvcOptions = yaml.load( + _.get(pvc, ["metadata", "annotations", "democratic-csi.org/config"]) + ); + } catch (err) { + if (err instanceof yaml.YAMLException) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `pvc 'democratic-csi.org/config' annotation not a valid YAML/JSON document.`.trim() + ); + } else { + throw err; + } + } + } + } + + const driverOptions = driver.getMergedDriverOptions([ + parametersOptions, + pvcOptions, + ]); + let datasetParentName = this.getVolumeParentDatasetName(); let snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName(); - let zvolBlocksize = this.options.zfs.zvolBlocksize || "16K"; + let zvolBlocksize = driverOptions.zfs.zvolBlocksize || "16K"; let name = call.request.name; let volume_id = await driver.getVolumeIdFromCall(call); let volume_content_source = call.request.volume_content_source; @@ -755,7 +814,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { if ( driverZfsResourceType == "filesystem" && - this.options.zfs.datasetEnableQuotas + driverOptions.zfs.datasetEnableQuotas ) { check = true; } @@ -811,9 +870,9 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { // user-supplied properties // put early to prevent stupid (user-supplied values overwriting system values) - if (driver.options.zfs.datasetProperties) { - for (let property in driver.options.zfs.datasetProperties) { - let value = driver.options.zfs.datasetProperties[property]; + if (driverOptions.zfs.datasetProperties) { + for (let property in driverOptions.zfs.datasetProperties) { + let value = driverOptions.zfs.datasetProperties[property]; const template = Handlebars.compile(value); volumeProperties[property] = template({ @@ -822,13 +881,15 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { } } + // TODO: add call.request.parameters properties here + volumeProperties[VOLUME_CSI_NAME_PROPERTY_NAME] = name; volumeProperties[MANAGED_PROPERTY_NAME] = "true"; volumeProperties[VOLUME_CONTEXT_PROVISIONER_DRIVER_PROPERTY_NAME] = - driver.options.driver; - if (driver.options.instance_id) { + driverOptions.driver; + if (driverOptions.instance_id) { volumeProperties[VOLUME_CONTEXT_PROVISIONER_INSTANCE_ID_PROPERTY_NAME] = - driver.options.instance_id; + driverOptions.instance_id; } // TODO: also set access_mode as property? @@ -837,7 +898,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { // zvol enables reservation by default // this implements 'sparse' zvols if (driverZfsResourceType == "volume") { - if (!this.options.zfs.zvolEnableReservation) { + if (!driverOptions.zfs.zvolEnableReservation) { volumeProperties.refreservation = 0; } } @@ -1097,13 +1158,13 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { switch (driverZfsResourceType) { case "filesystem": // set quota - if (this.options.zfs.datasetEnableQuotas) { + if (driverOptions.zfs.datasetEnableQuotas) { setProps = true; properties.refquota = capacity_bytes; } // set reserve - if (this.options.zfs.datasetEnableReservation) { + if (driverOptions.zfs.datasetEnableReservation) { setProps = true; properties.refreservation = capacity_bytes; } @@ -1133,37 +1194,37 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { driver.ctx.logger.debug("zfs props data: %j", properties); // set mode - if (this.options.zfs.datasetPermissionsMode) { + if (driverOptions.zfs.datasetPermissionsMode) { await driver.setFilesystemMode( properties.mountpoint.value, - this.options.zfs.datasetPermissionsMode + driverOptions.zfs.datasetPermissionsMode ); } // set ownership if ( - String(_.get(this.options, "zfs.datasetPermissionsUser", "")).length > - 0 || - String(_.get(this.options, "zfs.datasetPermissionsGroup", "")) + String(_.get(driverOptions, "zfs.datasetPermissionsUser", "")) + .length > 0 || + String(_.get(driverOptions, "zfs.datasetPermissionsGroup", "")) .length > 0 ) { await driver.setFilesystemOwnership( properties.mountpoint.value, - this.options.zfs.datasetPermissionsUser, - this.options.zfs.datasetPermissionsGroup + driverOptions.zfs.datasetPermissionsUser, + driverOptions.zfs.datasetPermissionsGroup ); } // set acls // TODO: this is unsfafe approach, make it better // probably could see if ^-.*\s and split and then shell escape - if (this.options.zfs.datasetPermissionsAcls) { + if (driverOptions.zfs.datasetPermissionsAcls) { let aclBinary = _.get( - driver.options, + driverOptions, "zfs.datasetPermissionsAclsBinary", "setfacl" ); - for (const acl of this.options.zfs.datasetPermissionsAcls) { + for (const acl of driverOptions.zfs.datasetPermissionsAcls) { command = execClient.buildCommand(aclBinary, [ acl, properties.mountpoint.value, @@ -1198,21 +1259,21 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { // restore default must use the below // zfs inherit [-rS] property filesystem|volume|snapshot… if ( - (typeof this.options.zfs.zvolDedup === "string" || - this.options.zfs.zvolDedup instanceof String) && - this.options.zfs.zvolDedup.length > 0 + (typeof driverOptions.zfs.zvolDedup === "string" || + driverOptions.zfs.zvolDedup instanceof String) && + driverOptions.zfs.zvolDedup.length > 0 ) { - properties.dedup = this.options.zfs.zvolDedup; + properties.dedup = driverOptions.zfs.zvolDedup; } // compression // lz4, gzip-9, etc if ( - (typeof this.options.zfs.zvolCompression === "string" || - this.options.zfs.zvolCompression instanceof String) && - this.options.zfs.zvolCompression > 0 + (typeof driverOptions.zfs.zvolCompression === "string" || + driverOptions.zfs.zvolCompression instanceof String) && + driverOptions.zfs.zvolCompression > 0 ) { - properties.compression = this.options.zfs.zvolCompression; + properties.compression = driverOptions.zfs.zvolCompression; } if (setProps) { @@ -1227,10 +1288,10 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { [SHARE_VOLUME_CONTEXT_PROPERTY_NAME]: JSON.stringify(volume_context), }); - volume_context["provisioner_driver"] = driver.options.driver; - if (driver.options.instance_id) { + volume_context["provisioner_driver"] = driverOptions.driver; + if (driverOptions.instance_id) { volume_context["provisioner_driver_instance_id"] = - driver.options.instance_id; + driverOptions.instance_id; } // set this just before sending out response so we know if volume completed @@ -1247,7 +1308,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { volume_id, //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0 capacity_bytes: - this.options.zfs.datasetEnableQuotas || + driverOptions.zfs.datasetEnableQuotas || driverZfsResourceType == "volume" ? capacity_bytes : 0, @@ -1272,6 +1333,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { async DeleteVolume(call) { const driver = this; const zb = await this.getZetabyte(); + const driverOptions = driver.getMergedDriverOptions([]); let datasetParentName = this.getVolumeParentDatasetName(); let name = call.request.volume_id; @@ -1318,7 +1380,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { // deleteStrategy const delete_strategy = _.get( - driver.options, + driverOptions, "_private.csi.volume.deleteStrategy", "" ); @@ -1405,6 +1467,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const zb = await this.getZetabyte(); + const driverOptions = driver.getMergedDriverOptions([]); let datasetParentName = this.getVolumeParentDatasetName(); let name = call.request.volume_id; @@ -1477,13 +1540,13 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { switch (driverZfsResourceType) { case "filesystem": // set quota - if (this.options.zfs.datasetEnableQuotas) { + if (driverOptions.zfs.datasetEnableQuotas) { setProps = true; properties.refquota = capacity_bytes; } // set reserve - if (this.options.zfs.datasetEnableReservation) { + if (driverOptions.zfs.datasetEnableReservation) { setProps = true; properties.refreservation = capacity_bytes; } @@ -1493,7 +1556,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { setProps = true; // managed automatically for zvols - //if (this.options.zfs.zvolEnableReservation) { + //if (driverOptions.zfs.zvolEnableReservation) { // properties.refreservation = capacity_bytes; //} break; @@ -1507,7 +1570,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { return { capacity_bytes: - this.options.zfs.datasetEnableQuotas || + driverOptions.zfs.datasetEnableQuotas || driverZfsResourceType == "volume" ? capacity_bytes : 0, @@ -1523,6 +1586,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { async GetCapacity(call) { const driver = this; const zb = await this.getZetabyte(); + const driverOptions = driver.getMergedDriverOptions([]); let datasetParentName = this.getVolumeParentDatasetName(); @@ -1573,6 +1637,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const zb = await this.getZetabyte(); + const driverOptions = driver.getMergedDriverOptions([]); let datasetParentName = this.getVolumeParentDatasetName(); let response; @@ -1654,6 +1719,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const zb = await this.getZetabyte(); + const driverOptions = driver.getMergedDriverOptions([]); let datasetParentName = this.getVolumeParentDatasetName(); let entries = []; @@ -1794,6 +1860,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const zb = await this.getZetabyte(); + const driverOptions = driver.getMergedDriverOptions([]); let entries = []; let entries_length = 0; @@ -2048,6 +2115,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { const driver = this; const driverZfsResourceType = this.getDriverZfsResourceType(); const zb = await this.getZetabyte(); + const driverOptions = driver.getMergedDriverOptions([]); let size_bytes = 0; let detachedSnapshot = false; @@ -2108,9 +2176,9 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { // user-supplied properties // put early to prevent stupid (user-supplied values overwriting system values) - if (driver.options.zfs.snapshotProperties) { - for (let property in driver.options.zfs.snapshotProperties) { - let value = driver.options.zfs.snapshotProperties[property]; + if (driverOptions.zfs.snapshotProperties) { + for (let property in driverOptions.zfs.snapshotProperties) { + let value = driverOptions.zfs.snapshotProperties[property]; const template = Handlebars.compile(value); snapshotProperties[property] = template({ @@ -2368,6 +2436,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { async DeleteSnapshot(call) { const driver = this; const zb = await this.getZetabyte(); + const driverOptions = driver.getMergedDriverOptions([]); const snapshot_id = call.request.snapshot_id; @@ -2439,6 +2508,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { async ValidateVolumeCapabilities(call) { const driver = this; const zb = await this.getZetabyte(); + const driverOptions = driver.getMergedDriverOptions([]); const volume_id = call.request.volume_id; if (!volume_id) { diff --git a/src/driver/index.js b/src/driver/index.js index 406111c..0a5cc50 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -56,7 +56,7 @@ class CsiBaseDriver { * in order of preference: * - democratic-csi.org/{instance_id}/{key} * - democratic-csi.org/{driver}/{key} - * - {key} + * - democratic-csi.org/{key} * * @param {*} parameters * @param {*} key @@ -104,6 +104,32 @@ class CsiBaseDriver { return normalized; } + getMergedDriverOptions(optionOverlays = []) { + const driver = this; + let driverOptions = Object.assign({}, driver.options); + + const allowedOptionsOverrides = ["zfs.zvolBlocksize"]; + + optionOverlays.forEach((optionOverlay) => { + allowedOptionsOverrides.forEach((prop) => { + if (_.has(optionOverlay, prop)) { + switch (prop) { + // TODO: specific cases can be added here to do merge/replace logic etc + default: + driverOptions = _.set( + driverOptions, + prop, + _.get(optionOverlay, prop) + ); + break; + } + } + }); + }); + + return driverOptions; + } + /** * Get an instance of the Filesystem class * @@ -234,6 +260,15 @@ class CsiBaseDriver { ); } + async getPersistentVolumeClaim(name, namespace) { + const driver = this; + const kc = driver.getDefaultKubernetsConfigInstance(); + const k8sApi = kc.makeApiClient(k8s.CoreV1Api); + + let res = await k8sApi.readNamespacedPersistentVolumeClaim(name, namespace); + return res.body; + } + getCsiProxyEnabled() { const defaultValue = process.platform == "win32"; return _.get(this.options, "node.csiProxy.enabled", defaultValue); diff --git a/src/utils/iscsi.js b/src/utils/iscsi.js index 281324e..4bd8822 100644 --- a/src/utils/iscsi.js +++ b/src/utils/iscsi.js @@ -624,7 +624,7 @@ class ISCSI { return value.trim() == "node.session.auth.password"; }); - if (argIndex >= 0 && cleansedArgs[argIndex + 1].trim() == "--value") { + if (argIndex >= 0 && cleansedArgs[argIndex + 1]?.trim() == "--value") { cleansedArgs[argIndex + 2] = "redacted"; } @@ -633,7 +633,7 @@ class ISCSI { return value.trim() == "node.session.auth.password_in"; }); - if (argIndex >= 0 && cleansedArgs[argIndex + 1].trim() == "--value") { + if (argIndex >= 0 && cleansedArgs[argIndex + 1]?.trim() == "--value") { cleansedArgs[argIndex + 2] = "redacted"; } From 19b1cf2805ab07f2fca31eb77134ac468012ae9a Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 3 Nov 2025 16:45:36 -0700 Subject: [PATCH 31/55] use pre-built ctr binaries Signed-off-by: Travis Glenn Hansen --- Dockerfile | 4 ++++ Dockerfile.Windows | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 16817ac..901cbec 100644 --- a/Dockerfile +++ b/Dockerfile @@ -137,6 +137,10 @@ ARG YQ_VERSION=v4.48.1 ADD docker/yq-installer.sh /usr/local/sbin RUN chmod +x /usr/local/sbin/yq-installer.sh && yq-installer.sh +ARG CTR_VERSION=v2.0.4 +ADD docker/ctr-installer.sh /usr/local/sbin +RUN chmod +x /usr/local/sbin/ctr-installer.sh && ctr-installer.sh + # controller requirements #RUN apt-get update && \ # apt-get install -y ansible && \ diff --git a/Dockerfile.Windows b/Dockerfile.Windows index 0c0c2a0..0fca523 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -99,7 +99,7 @@ COPY --from=build /PowerShell /PowerShell COPY --from=build /app /app WORKDIR /app -ADD https://github.com/democratic-csi/democratic-csi/releases/download/v1.0.0/ctr.exe ./bin +ADD https://github.com/democratic-csi/democratic-csi/releases/download/v1.0.0/ctr-v2.0.4-windows-amd64.exe ./bin/ctr.exe COPY --from=build /nodejs/node.exe ./bin COPY --from=build /usr/local/bin/ ./bin From 277a2db6744e8e9e5708bd30ebe7b3ae6043f287 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 3 Nov 2025 17:43:29 -0700 Subject: [PATCH 32/55] include missing installer script Signed-off-by: Travis Glenn Hansen --- docker/ctr-installer.sh | 42 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100755 docker/ctr-installer.sh diff --git a/docker/ctr-installer.sh b/docker/ctr-installer.sh new file mode 100755 index 0000000..45c8898 --- /dev/null +++ b/docker/ctr-installer.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -e +set -x + +PLATFORM_TYPE=${1} + +if [[ "${PLATFORM_TYPE}" == "build" ]]; then + PLATFORM=$BUILDPLATFORM +else + PLATFORM=$TARGETPLATFORM +fi + +if [[ "x${PLATFORM}" == "x" ]]; then + PLATFORM="linux/amd64" +fi + +# these come from the --platform option of buildx, indirectly from DOCKER_BUILD_PLATFORM in main.yaml +# linux/amd64,linux/arm64,linux/arm/v7,linux/s390x,linux/ppc64le +if [ "$PLATFORM" = "linux/amd64" ]; then + export PLATFORM_ARCH="amd64" +elif [ "$PLATFORM" = "linux/arm64" ]; then + export PLATFORM_ARCH="arm64" +elif [ "$PLATFORM" = "linux/arm/v7" ]; then + export PLATFORM_ARCH="arm" +elif [ "$PLATFORM" = "linux/s390x" ]; then + export PLATFORM_ARCH="s390x" +elif [ "$PLATFORM" = "linux/ppc64le" ]; then + export PLATFORM_ARCH="ppc64le" +else + echo "unsupported/unknown ctr PLATFORM ${PLATFORM}" + exit 0 +fi + +echo "I am installing ctr $CTR_VERSION" + +export CTR_FILE="ctr-${CTR_VERSION}-linux-arm64" +wget -O "${CTR_FILE}" "https://github.com/democratic-csi/democratic-csi/releases/download/v1.0.0/${CTR_FILE}" + +mv ${CTR_FILE} /usr/local/bin/ctr +chown root:root /usr/local/bin/ctr +chmod +x /usr/local/bin/ctr From 3969a2fe405c62431897e90280a7c1ef69287faf Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 3 Nov 2025 17:44:30 -0700 Subject: [PATCH 33/55] fix ctr path Signed-off-by: Travis Glenn Hansen --- docker/ctr-installer.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/ctr-installer.sh b/docker/ctr-installer.sh index 45c8898..2acd09b 100755 --- a/docker/ctr-installer.sh +++ b/docker/ctr-installer.sh @@ -34,7 +34,7 @@ fi echo "I am installing ctr $CTR_VERSION" -export CTR_FILE="ctr-${CTR_VERSION}-linux-arm64" +export CTR_FILE="ctr-${CTR_VERSION}-linux-${PLATFORM_ARCH}" wget -O "${CTR_FILE}" "https://github.com/democratic-csi/democratic-csi/releases/download/v1.0.0/${CTR_FILE}" mv ${CTR_FILE} /usr/local/bin/ctr From 54912bf90ba3f656d6b162a8f9cbd026b0a67ade Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Mon, 3 Nov 2025 19:47:26 -0700 Subject: [PATCH 34/55] remove ctrbuilder Signed-off-by: Travis Glenn Hansen --- Dockerfile | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Dockerfile b/Dockerfile index 901cbec..6819fa0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,16 +5,16 @@ ###################### # golang builder ###################### -FROM golang:1.25.3-bookworm AS ctrbuilder - -# /go/containerd/ctr -ADD docker/ctr-mount-labels.diff /tmp -RUN \ - git clone https://github.com/containerd/containerd.git; \ - cd containerd && \ - git checkout v2.0.4 && \ - git apply /tmp/ctr-mount-labels.diff && \ - CGO_ENABLED=0 go build ./cmd/ctr/; +# FROM golang:1.25.3-bookworm AS ctrbuilder +# +# # /go/containerd/ctr +# ADD docker/ctr-mount-labels.diff /tmp +# RUN \ +# git clone https://github.com/containerd/containerd.git; \ +# cd containerd && \ +# git checkout v2.0.4 && \ +# git apply /tmp/ctr-mount-labels.diff && \ +# CGO_ENABLED=0 go build ./cmd/ctr/; ###################### @@ -103,7 +103,7 @@ RUN test $(uname -m) != armv7l || ( \ ) # install ctr -COPY --from=ctrbuilder /go/containerd/ctr /usr/local/bin/ctr +#COPY --from=ctrbuilder /go/containerd/ctr /usr/local/bin/ctr # install node #ENV PATH=/usr/local/lib/nodejs/bin:$PATH From 6b937e645a8c35d6c1b844e014974ba2ce493824 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Thu, 6 Nov 2025 13:53:18 -0700 Subject: [PATCH 35/55] New vhd-ephemeral-inline driver, windows updates --- src/driver/ephemeral-inline-vhd/index.js | 406 ++++++++++++++++++++ src/driver/factory.js | 5 + src/driver/index.js | 10 +- src/utils/windows.js | 462 ++++++++++++++++++++++- 4 files changed, 881 insertions(+), 2 deletions(-) create mode 100644 src/driver/ephemeral-inline-vhd/index.js diff --git a/src/driver/ephemeral-inline-vhd/index.js b/src/driver/ephemeral-inline-vhd/index.js new file mode 100644 index 0000000..30265a1 --- /dev/null +++ b/src/driver/ephemeral-inline-vhd/index.js @@ -0,0 +1,406 @@ +const _ = require("lodash"); +const fs = require("fs"); +const { CsiBaseDriver } = require("../index"); +const { GrpcError, grpc } = require("../../utils/grpc"); +const Handlebars = require("handlebars"); +const path = require("path"); +const semver = require("semver"); +const WindowsUtils = require("../../utils/windows").Windows; +const wutils = new WindowsUtils(); + +/** + * https://github.com/kubernetes/enhancements/blob/master/keps/sig-storage/20190122-csi-inline-volumes.md + * https://kubernetes-csi.github.io/docs/ephemeral-local-volumes.html + * + * Sample calls: + * - https://gcsweb.k8s.io/gcs/kubernetes-jenkins/pr-logs/pull/92387/pull-kubernetes-e2e-gce/1280784994997899264/artifacts/_sig-storage_CSI_Volumes/_Driver_csi-hostpath_/_Testpattern_inline_ephemeral_CSI_volume_ephemeral/should_create_read_write_inline_ephemeral_volume/ + * - https://storage.googleapis.com/kubernetes-jenkins/pr-logs/pull/92387/pull-kubernetes-e2e-gce/1280784994997899264/artifacts/_sig-storage_CSI_Volumes/_Driver_csi-hostpath_/_Testpattern_inline_ephemeral_CSI_volume_ephemeral/should_create_read-only_inline_ephemeral_volume/csi-hostpathplugin-0-hostpath.log + * + * inline drivers are assumed to be mount only (no block support) + * purposely there is no native support for size contraints + * + */ +class EphemeralInlineVHDDriver extends CsiBaseDriver { + constructor(ctx, options) { + super(...arguments); + + options = options || {}; + options.service = options.service || {}; + options.service.identity = options.service.identity || {}; + options.service.controller = options.service.controller || {}; + options.service.node = options.service.node || {}; + + options.service.identity.capabilities = + options.service.identity.capabilities || {}; + + options.service.controller.capabilities = + options.service.controller.capabilities || {}; + + options.service.node.capabilities = options.service.node.capabilities || {}; + + if (!("service" in options.service.identity.capabilities)) { + this.ctx.logger.debug("setting default identity service caps"); + + options.service.identity.capabilities.service = [ + "UNKNOWN", + //"CONTROLLER_SERVICE" + //"VOLUME_ACCESSIBILITY_CONSTRAINTS" + ]; + } + + if (!("volume_expansion" in options.service.identity.capabilities)) { + this.ctx.logger.debug("setting default identity volume_expansion caps"); + + options.service.identity.capabilities.volume_expansion = [ + "UNKNOWN", + //"ONLINE", + //"OFFLINE" + ]; + } + + if (!("rpc" in options.service.controller.capabilities)) { + this.ctx.logger.debug("setting default controller caps"); + + options.service.controller.capabilities.rpc = [ + //"UNKNOWN", + //"CREATE_DELETE_VOLUME", + //"PUBLISH_UNPUBLISH_VOLUME", + //"LIST_VOLUMES", + //"GET_CAPACITY", + //"CREATE_DELETE_SNAPSHOT", + //"LIST_SNAPSHOTS", + //"CLONE_VOLUME", + //"PUBLISH_READONLY", + //"EXPAND_VOLUME" + ]; + + if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { + options.service.controller.capabilities.rpc + .push + //"VOLUME_CONDITION", + //"GET_VOLUME" + (); + } + + if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { + options.service.controller.capabilities.rpc + .push + //"SINGLE_NODE_MULTI_WRITER" + (); + } + } + + if (!("rpc" in options.service.node.capabilities)) { + this.ctx.logger.debug("setting default node caps"); + options.service.node.capabilities.rpc = [ + //"UNKNOWN", + //"STAGE_UNSTAGE_VOLUME", + "GET_VOLUME_STATS", + //"EXPAND_VOLUME", + ]; + + if (semver.satisfies(this.ctx.csiVersion, ">=1.3.0")) { + //options.service.node.capabilities.rpc.push("VOLUME_CONDITION"); + } + + if (semver.satisfies(this.ctx.csiVersion, ">=1.5.0")) { + options.service.node.capabilities.rpc.push("SINGLE_NODE_MULTI_WRITER"); + /** + * This is for volumes that support a mount time gid such as smb or fat + */ + //options.service.node.capabilities.rpc.push("VOLUME_MOUNT_GROUP"); + } + } + } + + assertCapabilities(capabilities) { + this.ctx.logger.verbose("validating capabilities: %j", capabilities); + + let message = null; + //[{"access_mode":{"mode":"SINGLE_NODE_WRITER"},"mount":{"mount_flags":["noatime","_netdev"],"fs_type":"nfs"},"access_type":"mount"}] + const valid = capabilities.every((capability) => { + if (capability.access_type != "mount") { + message = `invalid access_type ${capability.access_type}`; + return false; + } + + if (capability.mount.fs_type) { + message = `invalid fs_type ${capability.mount.fs_type}`; + return false; + } + + if ( + capability.mount.mount_flags && + capability.mount.mount_flags.length > 0 + ) { + message = `invalid mount_flags ${capability.mount.mount_flags}`; + return false; + } + + if ( + ![ + "UNKNOWN", + "SINGLE_NODE_WRITER", + "SINGLE_NODE_SINGLE_WRITER", // added in v1.5.0 + "SINGLE_NODE_MULTI_WRITER", // added in v1.5.0 + "SINGLE_NODE_READER_ONLY", + ].includes(capability.access_mode.mode) + ) { + message = `invalid access_mode, ${capability.access_mode.mode}`; + return false; + } + + return true; + }); + + return { valid, message }; + } + + async Probe(call) { + if (process.platform != "win32") { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `vhd-ephemeral-inline is only available on the windows platform` + ); + } + + return super.Probe(...arguments); + } + + /** + * + * @param {*} call + */ + async NodePublishVolume(call) { + const driver = this; + + const volume_id = call.request.volume_id; + const staging_target_path = call.request.staging_target_path || ""; + const target_path = call.request.target_path; + const capability = call.request.volume_capability; + const access_type = capability.access_type || "mount"; + const readonly = call.request.readonly; + const volume_context = call.request.volume_context; + + let result; + + let vhdParentPath; + Object.keys(volume_context).forEach(function (key) { + switch (key) { + case "vhd.parentPath": + vhdParentPath = volume_context[key]; + break; + } + }); + + if (!vhdParentPath) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `vhd.parentPath is required` + ); + } + + if (!volume_id) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `volume_id is required` + ); + } + + if (!target_path) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `target_path is required` + ); + } + + if (capability) { + const result = driver.assertCapabilities([capability]); + + if (result.valid !== true) { + throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); + } + } + + // sanity check the parent + if (!fs.existsSync(vhdParentPath)) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `vhd.parentPath (${vhdParentPath}) file does not exist` + ); + } + + // create publish directory + // if (!fs.existsSync(target_path)) { + // await fs.mkdirSync(target_path, { recursive: true }); + // } + + // get child path name + let vhdParentPathDir = path.dirname(vhdParentPath); + let vhdChildDiskName = volume_id; + if (driver.options.vhd.nameTemplate) { + vhdChildDiskName = Handlebars.compile(driver.options.vhd.nameTemplate)({ + // parameters: call.request.parameters, + volume_id, + }); + } + + let vhdChildPath = `${vhdParentPathDir}${ + path.sep + }${vhdChildDiskName}${path.extname(vhdParentPath)}`; + + // create vhd + if (!fs.existsSync(vhdChildPath)) { + await wutils.NewVHDDifferencing(vhdParentPath, vhdChildPath); + } + + // mount vhd if needed + let disks; + disks = await wutils.GetDisksByLocation(vhdChildPath); + if (disks.length == 0) { + await wutils.MountVHD(vhdChildPath); + } + + // ensure disk is mounted + disks = await wutils.GetDisksByLocation(vhdChildPath); + let disk = disks[0]; + if (!disk) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `failed to mount vhd ${vhdParentPath}` + ); + } + + // ensure the disk is online + if (disk.OperationalStatus != "Online") { + await wutils.OnlineDisk(disk.DiskNumber); + } + + // get partition + let partition = await wutils.GetLastPartitionByDiskNumber(disk.DiskNumber); + + // get volume + let volume = await wutils.GetVolumeByDiskNumberPartitionNumber( + disk.DiskNumber, + partition.PartitionNumber + ); + + if (!volume) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `failed to discover volume for vhd ${vhdParentPath}` + ); + } + + result = await wutils.GetItem(target_path); + if (!result) { + fs.mkdirSync(target_path, { + recursive: true, + mode: "755", + }); + result = await wutils.GetItem(target_path); + } + + let targets = result.Target; + if (!Array.isArray(targets)) { + if (targets) { + targets[targets]; + } else { + targets = []; + } + } + + if ( + !targets.some((target) => { + return volume.UniqueId.includes(target); + }) + ) { + await wutils.MountVolume(volume.UniqueId, target_path); + } + + return {}; + } + + /** + * + * @param {*} call + */ + async NodeUnpublishVolume(call) { + const driver = this; + + const volume_id = call.request.volume_id; + const target_path = call.request.target_path; + + if (!volume_id) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `volume_id is required` + ); + } + + if (!target_path) { + throw new GrpcError( + grpc.status.INVALID_ARGUMENT, + `target_path is required` + ); + } + + let result; + + result = await wutils.GetItem(target_path); + if (result) { + if (result.LinkType == "Junction") { + let volumeId = (await wutils.GetRealTarget(target_path)) || ""; + if (volumeId) { + // should only ever have 1 + let disks = await wutils.GetDisksByVolumeId(volumeId); + for (const disk of disks) { + if (disk.Location) { + // unmount + await wutils.DismountVHD(disk.Location); + // remove the vhd + fs.rmSync(disk.Location); + } + } + } + } + } + + // remove publish folder + await wutils.DeleteItem(target_path); + + return {}; + } + + /** + * TODO: consider volume_capabilities? + * + * @param {*} call + */ + async GetCapacity(call) { + return { available_capacity: 0 }; + } + + /** + * + * @param {*} call + */ + async ValidateVolumeCapabilities(call) { + const driver = this; + const result = this.assertCapabilities(call.request.volume_capabilities); + + if (result.valid !== true) { + return { message: result.message }; + } + + return { + confirmed: { + volume_context: call.request.volume_context, + volume_capabilities: call.request.volume_capabilities, // TODO: this is a bit crude, should return *ALL* capabilities, not just what was requested + parameters: call.request.parameters, + }, + }; + } +} + +module.exports.EphemeralInlineVHDDriver = EphemeralInlineVHDDriver; diff --git a/src/driver/factory.js b/src/driver/factory.js index da6874e..e82460d 100644 --- a/src/driver/factory.js +++ b/src/driver/factory.js @@ -17,6 +17,9 @@ const { ControllerSynologyDriver } = require("./controller-synology"); const { EphemeralInlineContainerDOciDriver, } = require("./ephemeral-inline-containerd-oci"); +const { + EphemeralInlineVHDDriver, +} = require("./ephemeral-inline-vhd"); const { NodeManualDriver } = require("./node-manual"); function factory(ctx, options) { @@ -65,6 +68,8 @@ function factory(ctx, options) { return new ControllerObjectiveFSDriver(ctx, options); case "containerd-oci-ephemeral-inline": return new EphemeralInlineContainerDOciDriver(ctx, options); + case "vhd-ephemeral-inline": + return new EphemeralInlineVHDDriver(ctx, options); case "node-manual": return new NodeManualDriver(ctx, options); default: diff --git a/src/driver/index.js b/src/driver/index.js index 0a5cc50..cfd385d 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -2118,7 +2118,11 @@ class CsiBaseDriver { result = await wutils.GetItem(win_staging_target_path); } - if (!volume.UniqueId.includes(result.Target[0])) { + if ( + !result.Target.some((target) => { + return volume.UniqueId.includes(target); + }) + ) { // mount up! await wutils.MountVolume( volume.UniqueId, @@ -3663,6 +3667,9 @@ class CsiBaseDriver { if (await wutils.VolumeIsIscsi(target)) { node_attach_driver = "iscsi"; } + if (await wutils.VolumeIsVHD(target)) { + node_attach_driver = "vhd"; + } } if (!node_attach_driver) { @@ -3675,6 +3682,7 @@ class CsiBaseDriver { res.usage = [{ total: 0, unit: "BYTES" }]; break; case "iscsi": + case "vhd": let node_volume = await wutils.GetVolumeByVolumeId(target); res.usage = [ { diff --git a/src/utils/windows.js b/src/utils/windows.js index 3993c29..0880b5d 100644 --- a/src/utils/windows.js +++ b/src/utils/windows.js @@ -89,6 +89,24 @@ class Windows { } catch (err) {} } + async DeleteItem(localPath) { + let command; + let result; + command = '(Get-Item "$Env:localpath").Delete() | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: { + localpath: localPath, + }, + }); + } catch (err) { + let details = _.get(err, "stderr", ""); + if (!details.includes("does not exist")) { + throw err; + } + } + } + async GetSmbGlobalMapping(remotePath) { let command; // cannot have trailing slash nor a path @@ -423,13 +441,30 @@ class Windows { let command; let result; - command = "Get-WmiObject Win32_DiskDrive | ConvertTo-Json"; + //command = "Get-WmiObject Win32_DiskDrive | ConvertTo-Json"; + command = "Get-CimInstance Win32_DiskDrive | ConvertTo-Json"; result = await this.ps.exec(command); this.resultToArray(result); return result.parsed; } + async GetWin32DiskDriveByDiskNumber(diskNumber) { + let result; + result = await this.GetWin32DiskDrives(); + for (let drive of result) { + if (drive.Index == diskNumber) { + return drive; + } + } + } + + async GetWin32DiskDriveByUniqueId(uniqueId) { + let result; + result = await this.GetDiskByUniqueId(uniqueId); + return this.GetWin32DiskDriveByDiskNumber(result.DiskNumber); + } + async GetDiskLunByDiskNumber(diskNumber) { let result; result = await this.GetWin32DiskDrives(); @@ -514,6 +549,75 @@ class Windows { return result.parsed; } + async GetDiskByUniqueId(uniqueId) { + let command; + let result; + command = 'Get-Disk -UniqueId "$Env:uniqueid" | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: { + uniqueid: uniqueId, + }, + }); + return result.parsed; + } catch (err) { + throw err; + } + } + + async GetDisksByFriendlyName(friendlyName) { + let command; + let result; + command = 'Get-Disk -FriendlyName "$Env:friendlyname" | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: { + friendlyname: friendlyName, + }, + }); + this.resultToArray(result); + return result.parsed; + } catch (err) { + throw err; + } + } + + async GetDisksByBusType(busTpe) { + let command; + let result; + command = + 'Get-Disk | Where-Object { $_.BusType -eq "$Env:bustype" } | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: { + bustype: busTpe, + }, + }); + this.resultToArray(result); + return result.parsed; + } catch (err) { + throw err; + } + } + + async GetDisksByLocation(location) { + let command; + let result; + command = + 'Get-Disk | Where-Object { $_.Location -eq "$Env:location" } | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: { + location, + }, + }); + this.resultToArray(result); + return result.parsed; + } catch (err) { + throw err; + } + } + async GetDisks() { let command; let result; @@ -560,6 +664,20 @@ class Windows { await this.ps.exec(command); } + async OnlineDisk(diskNumber) { + let command; + + command = `Set-Disk -Number ${diskNumber} -IsOffline $false`; + await this.ps.exec(command); + } + + async OfflineDisk(diskNumber) { + let command; + + command = `Set-Disk -Number ${diskNumber} -IsOffline $true`; + await this.ps.exec(command); + } + async DiskHasBasicPartition(diskNumber) { let command; let result; @@ -632,6 +750,8 @@ class Windows { let command; let result; + // NOTE: this syntax is more forgiving + // Get-Volume | Where-Object { $_.UniqueId -match "Volume{74798398-bb39-11f0-af08-00155dab0c98}\\" } command = `Get-Volume -UniqueId \"${volumeId}\" -ErrorAction Stop | ConvertTo-Json`; result = await this.ps.exec(command); @@ -697,6 +817,20 @@ class Windows { return false; } + async VolumeIsVHD(volumeId) { + let disks = await this.GetDisksByVolumeId(volumeId); + for (let disk of disks) { + if ( + _.get(disk, "BusType", "").toLowerCase() == + "File Backed Virtual".toLowerCase() + ) { + return true; + } + } + + return false; + } + async FormatVolume(volumeId) { let command; command = `Get-Volume -UniqueId \"${volumeId}\" | Format-Volume -FileSystem ntfs -Confirm:$false`; @@ -781,6 +915,332 @@ class Windows { await this.ps.exec(command); } + + async GetStoragePoolByFriendlyName(friendlyName) { + let command; + let result; + command = + 'Get-StoragePool -FriendlyName "$Env:friendlyname" | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: { + friendlyname: friendlyName, + }, + }); + this.resultToArray(result); + return result.parsed; + } catch (err) { + throw err; + } + } + + async GetVirtualDisksByFriendlyName(friendlyName) { + let command; + let result; + command = + 'Get-VirtualDisk -FriendlyName "$Env:friendlyname" | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: { + friendlyname: friendlyName, + }, + }); + this.resultToArray(result); + return result.parsed; + } catch (err) { + throw err; + } + } + + async GetVirtualDiskByUniqueId(uniqueId) { + let command; + let result; + command = 'Get-VirtualDisk -UniqueId "$Env:uniqueid" | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: { + uniqueid: uniqueId, + }, + }); + return result.parsed; + } catch (err) { + throw err; + } + } + + async RemoveVirtualDisksByFriendlyName(friendlyName) { + let command; + command = + 'Remove-VirtualDisk -Confirm:$false -FriendlyName "$Env:friendlyname"'; + try { + await this.ps.exec(command, { + env: { + friendlyname: friendlyName, + }, + }); + } catch (err) { + let details = _.get(err, "stderr", ""); + if (details.includes("No MSFT_VirtualDisk objects found")) { + return; + } + throw err; + } + } + + async RemoveVirtualDiskByUniqueId(uniqueId) { + let command; + command = 'Remove-VirtualDisk -Confirm:$false -UniqueId "$Env:uniqueid"'; + try { + await this.ps.exec(command, { + env: { + uniqueid: uniqueId, + }, + }); + } catch (err) { + let details = _.get(err, "stderr", ""); + if (details.includes("No MSFT_VirtualDisk objects found")) { + return; + } + throw err; + } + } + + async ResizeVirtualDisksByFriendlyName(friendlyName, size) { + let command; + command = `Resize-VirtualDisk -Confirm:$false -FriendlyName "$Env:friendlyname" -Size ${size}`; + try { + await this.ps.exec(command, { + env: { + friendlyname: friendlyName, + }, + }); + } catch (err) { + throw err; + } + } + + async ResizeVirtualDiskByUniqueId(uniqueId, size) { + let command; + command = `Remove-VirtualDisk -Confirm:$false -UniqueId "$Env:uniqueid" -Size ${size}`; + try { + await this.ps.exec(command, { + env: { + uniqueid: uniqueId, + }, + }); + } catch (err) { + throw err; + } + } + + async NewVirtualDisk( + storagePoolFriendlyName, + friendlyName, + size, + extraArgs = [] + ) { + /** + * -ProvisioningType Thin|Fixed + * -ResiliencySettingName Simple|Mirror|Parity + * -Usage Data + */ + let command; + let result; + + extraArgs.push("-ResiliencySettingName", '"Simple"'); + extraArgs.push("-ProvisioningType", '"Thin"'); + + command = `New-VirtualDisk -StoragePoolFriendlyName "$Env:storagepoolfriendlyname" -FriendlyName "$Env:friendlyname" -Size ${size} ${extraArgs.join( + " " + )} | ConvertTo-Json`; + try { + result = await this.ps.exec(command, { + env: { + storagepoolfriendlyname: storagePoolFriendlyName, + friendlyname: friendlyName, + size, + }, + }); + return result.parsed; + } catch (err) { + throw err; + } + } + + async NewVirtualDiskCloneByFriendlyName( + storagePoolFriendlyName, + virutalDiskFriendlyName, + friendlyName + ) { + let command; + let result; + + command = `New-VirtualDiskClone -FriendlyName "$Env:friendlyname" -VirtualDiskFriendlyName "$Env:virutaldiskfriendlyname" -TargetStoragePoolName "$Env:storagepoolfriendlyname" | ConvertTo-Json`; + try { + result = await this.ps.exec(command, { + env: { + storagepoolfriendlyname: storagePoolFriendlyName, + friendlyname: friendlyName, + virutaldiskfriendlyname: virutalDiskFriendlyName, + }, + }); + return result.parsed; + } catch (err) { + throw err; + } + } + + async NewVirtualDiskCloneByUniqueId( + storagePoolFriendlyName, + uniqueId, + friendlyName + ) { + let command; + let result; + + command = `New-VirtualDiskClone -FriendlyName "$Env:friendlyname" -VirtualDiskUniqueId "$Env:uniqueid" -TargetStoragePoolName "$Env:storagepoolfriendlyname" | ConvertTo-Json`; + try { + result = await this.ps.exec(command, { + env: { + storagepoolfriendlyname: storagePoolFriendlyName, + friendlyname: friendlyName, + uniqueid: uniqueId, + }, + }); + return result.parsed; + } catch (err) { + throw err; + } + } + + async NewVirtualDiskSnapshotByFriendlyName( + storagePoolFriendlyName, + virutalDiskFriendlyName, + friendlyName + ) { + let command; + let result; + + command = `New-VirtualDiskSnapshot -FriendlyName "$Env:friendlyname" -VirtualDiskFriendlyName "$Env:virutaldiskfriendlyname" -TargetStoragePoolName "$Env:storagepoolfriendlyname" | ConvertTo-Json`; + try { + result = await this.ps.exec(command, { + env: { + storagepoolfriendlyname: storagePoolFriendlyName, + friendlyname: friendlyName, + virutaldiskfriendlyname: virutalDiskFriendlyName, + }, + }); + return result.parsed; + } catch (err) { + throw err; + } + } + + async NewVirtualDiskCloneByUniqueId( + storagePoolFriendlyName, + uniqueId, + friendlyName + ) { + let command; + let result; + + command = `New-VirtualDiskSnapshot -FriendlyName "$Env:friendlyname" -VirtualDiskUniqueId "$Env:uniqueid" -TargetStoragePoolName "$Env:storagepoolfriendlyname" | ConvertTo-Json`; + try { + result = await this.ps.exec(command, { + env: { + storagepoolfriendlyname: storagePoolFriendlyName, + friendlyname: friendlyName, + uniqueid: uniqueId, + }, + }); + return result.parsed; + } catch (err) { + throw err; + } + } + + // $volumeinfo = GWMI -namespace root\cimv2 -class win32_volume + // $volumeid = $volumeinfo[1].deviceid + // $taskname = "ShadowCopyVolume" + $volumeid.replace("\","").replace("?Volume","") + // $taskrun = "C:\Windows\system32\vssadmin.exe Create Shadow /AutoRetry=15 /For=$volumeid" + // Get-CimInstance Win32_ShadowCopy | ConvertTo-Json + + async VssCreateShadowByVolumeId( + storagePoolFriendlyName, + uniqueId, + friendlyName + ) { + let command; + let result; + + command = `New-VirtualDiskSnapshot -FriendlyName \"$Env:friendlyname" -VirtualDiskUniqueId "$Env:uniqueid" -TargetStoragePoolName "$Env:storagepoolfriendlyname" | ConvertTo-Json`; + try { + result = await this.ps.exec(command, { + env: { + storagepoolfriendlyname: storagePoolFriendlyName, + friendlyname: friendlyName, + uniqueid: uniqueId, + }, + }); + return result.parsed; + } catch (err) { + throw err; + } + } + + async NewVHDDifferencing(parentPath, childPath) { + let command; + let result; + + command = + 'New-VHD -ParentPath "${Env:parentpath}" -Path "${Env:childpath}" -Differencing | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: Object.assign({}, process.env, { + parentpath: parentPath, + childpath: childPath, + }), + }); + return result.parsed; + } catch (err) { + throw err; + } + } + + async MountVHD(path) { + let command; + let result; + + command = + 'Mount-VHD -NoDriveLetter -Path "${Env:vhdpath}" | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: Object.assign({}, process.env, { + vhdpath: path, + }), + }); + return result.parsed; + } catch (err) { + throw err; + } + } + + async DismountVHD(path) { + let command; + let result; + + command = 'Dismount-VHD -Path "${Env:vhdpath}" | ConvertTo-Json'; + try { + result = await this.ps.exec(command, { + env: Object.assign({}, process.env, { + vhdpath: path, + }), + }); + return result.parsed; + } catch (err) { + throw err; + } + } } module.exports.Windows = Windows; From 9ed7f8ad88591d7ac4cca05a3a5c1f80a921c65b Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 12 Nov 2025 18:45:04 -0700 Subject: [PATCH 36/55] more robust logic around volume deletion with snapshots Signed-off-by: Travis Glenn Hansen --- src/driver/controller-zfs/index.js | 36 ++++++++++++++++ src/driver/freenas/api.js | 66 +++++++++++++++++++++++++----- 2 files changed, 92 insertions(+), 10 deletions(-) diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index e65e14d..0092a9c 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -1423,6 +1423,42 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { } } + // Explicitly check if we have any managed snapshots + // If a clone has been created from a snapshot, it will fail anyway but if no clones + // have been created the destroy will succeed undesirably + let hasManagedSnapshot = false; + try { + let snapshots = await zb.zfs.list( + datasetName, + [ + "name", + // "democratic-csi:csi_snapshot_name", + // "democratic-csi:csi_snapshot_source_volume_id", + MANAGED_PROPERTY_NAME, + ], + { types: ["snapshot"] } + ); + + hasManagedSnapshot = snapshots.indexed.some((snapshot) => { + return snapshot[MANAGED_PROPERTY_NAME].toLowerCase() == "true"; + }); + } catch (err) { + // ignore errors when the dataset is already deleted + if (!err.toString().includes("dataset does not exist")) { + throw new GrpcError( + grpc.status.UNKNOWN, + `failed to test for snapshots: ${err.toString()}` + ); + } + } + + if (hasManagedSnapshot) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + "filesystem has dependent snapshots" + ); + } + // NOTE: -f does NOT allow deletes if dependent filesets exist // NOTE: -R will recursively delete items + dependent filesets // delete dataset diff --git a/src/driver/freenas/api.js b/src/driver/freenas/api.js index 3cf4616..7eb94a3 100644 --- a/src/driver/freenas/api.js +++ b/src/driver/freenas/api.js @@ -3429,13 +3429,22 @@ class FreeNASApiDriver extends CsiBaseDriver { // get properties needed for remaining calls try { - properties = await httpApiClient.DatasetGet(datasetName, [ - "mountpoint", - "origin", - "refquota", - "compression", - VOLUME_CSI_NAME_PROPERTY_NAME, - ]); + properties = await httpApiClient.DatasetGet( + datasetName, + [ + "id", + "mountpoint", + "origin", + "refquota", + "compression", + VOLUME_CSI_NAME_PROPERTY_NAME, + "snapshots", + ], + { + "extra.snapshots": "true", + "extra.retrieve_children": "false", + } + ); } catch (err) { let ignore = false; if (err.toString().includes("dataset does not exist")) { @@ -3469,16 +3478,16 @@ class FreeNASApiDriver extends CsiBaseDriver { properties.origin && properties.origin.value != "-" && zb.helpers - .extractSnapshotName(properties.origin.value) + .extractSnapshotName(properties.origin.parsed) .startsWith(VOLUME_SOURCE_CLONE_SNAPSHOT_PREFIX) ) { driver.ctx.logger.debug( "removing with defer source snapshot: %s", - properties.origin.value + properties.origin.parsed ); try { - await httpApiClient.SnapshotDelete(properties.origin.value, { + await httpApiClient.SnapshotDelete(properties.origin.parsed, { defer: true, }); } catch (err) { @@ -3492,6 +3501,43 @@ class FreeNASApiDriver extends CsiBaseDriver { } } + // Explicitly check if we have any managed snapshots + // If a clone has been created from a snapshot, it will fail anyway but if no clones + // have been created the destroy will succeed undesirably + let hasManagedSnapshot = false; + try { + for (const snapshot of _.get(properties, "snapshots", [])) { + let snapshotData = await httpApiClient.SnapshotGet(snapshot.name, [ + MANAGED_PROPERTY_NAME, + // "democratic-csi:csi_snapshot_name", + // "democratic-csi:csi_snapshot_source_volume_id", + ]); + + if ( + snapshotData[MANAGED_PROPERTY_NAME] && + snapshotData[MANAGED_PROPERTY_NAME].value.toLowerCase() == "true" + ) { + hasManagedSnapshot = true; + break; + } + } + } catch (err) { + // ignore errors when the dataset is already deleted + if (!err.toString().includes("dataset does not exist")) { + throw new GrpcError( + grpc.status.UNKNOWN, + `failed to test for snapshots: ${err.toString()}` + ); + } + } + + if (hasManagedSnapshot) { + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + "filesystem has dependent snapshots" + ); + } + // NOTE: -f does NOT allow deletes if dependent filesets exist // NOTE: -R will recursively delete items + dependent filesets // delete dataset From 31dcf845fc419d19bf9cae84b825f9be41108010 Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Sat, 22 Mar 2025 19:36:00 +0200 Subject: [PATCH 37/55] Add iSCSI targets/LUNs through Pacemaker clusters Support pcs for zfs-generic to configure iSCSI targets and LUNs. It uses the targetcli implementation since it's the default pcs resources and also to keep compatibility with the rest of the code. closes #462 --- examples/zfs-generic-iscsi.yaml | 12 ++ src/driver/controller-zfs-generic/index.js | 165 ++++++++++++++++++++- 2 files changed, 175 insertions(+), 2 deletions(-) diff --git a/examples/zfs-generic-iscsi.yaml b/examples/zfs-generic-iscsi.yaml index 8e38e42..e62e2c6 100644 --- a/examples/zfs-generic-iscsi.yaml +++ b/examples/zfs-generic-iscsi.yaml @@ -46,6 +46,7 @@ zfs: iscsi: shareStrategy: "targetCli" + #shareStrategy: "pcs" # https://kifarunix.com/how-to-install-and-configure-iscsi-storage-server-on-ubuntu-18-04/ # https://kifarunix.com/how-install-and-configure-iscsi-storage-server-on-centos-7/ @@ -77,6 +78,17 @@ iscsi: attributes: # set to 1 to enable Thin Provisioning Unmap emulate_tpu: 0 + + shareStrategyPcs: + #sudoEnabled: true + pcs_group: "group-nas" + basename: "iqn.2003-01.org.linux-iscsi.ubuntu-19.x8664" + auth: + enabled: 0 + # CHAP – incoming only, mutual not supported by the Pacemaker resource agent + incoming_username: "foo" + incoming_password: "bar" + targetPortal: "server[:port]" # for multipath targetPortals: [] # [ "server[:port]", "server[:port]", ... ] diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 1c7c32f..c2e15e8 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -303,8 +303,70 @@ create /backstores/block/${assetName} } ); break; + case "pcs": + basename = this.options.iscsi.shareStrategyPcs.basename; + pcs_group = this.options.iscsi.shareStrategyPcs.pcs_group; + + let groupText = `group ${pcs_group}`; + let createTargetText = [ + `resource create --future target-${assetName} iSCSITarget`, + 'implementation="lio-t"', + `iqn="${basename}:${assetName}"` + ]; + + if (this.options.iscsi.shareStrategyPcs.auth.enabled) { + createTargetText.push(`incoming_username="${this.options.iscsi.shareStrategyPcs.auth.incoming_username}"`); + createTargetText.push(`incoming_password="${this.options.iscsi.shareStrategyPcs.auth.incoming_password}"`); + } + + createTargetText.push(groupText); + + await GeneralUtils.retry( + 3, + 2000, + async () => { + await this.pcsCommand(createTargetText); + }, + { + retryCondition: (err) => { + if (err.stdout && err.stdout.includes("Timed Out")) { + return true; + } + return false; + }, + } + ); + + let createLunText = [ + `resource create --future lun-${assetName} iSCSILogicalUnit`, + 'implementation="lio-t"', + `target_iqn="${basename}:${assetName}" lun="1"`, + `path="/dev/${extentDiskName}"`, + groupText + ]; + + await GeneralUtils.retry( + 3, + 2000, + async () => { + await this.pcsCommand(createLunText); + }, + { + retryCondition: (err) => { + if (err.stdout && err.stdout.includes("Timed Out")) { + return true; + } + return false; + }, + } + ); + break; + default: - break; + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown shareStrategy ${this.options.iscsi.shareStrategy}` + ); } // iqn = target @@ -695,8 +757,54 @@ delete ${assetName} ); break; - default: + case "pcs": + let deleteLunText = [ + `resource delete lun-${assetName}` + ]; + + await GeneralUtils.retry( + 3, + 2000, + async () => { + await this.pcsCommand(deleteLunText); + }, + { + retryCondition: (err) => { + if (err.stdout && err.stdout.includes("Timed Out")) { + return true; + } + return false; + }, + } + ); + + let deleteTargetText = [ + `resource delete target-${assetName}` + ]; + + await GeneralUtils.retry( + 3, + 2000, + async () => { + await this.pcsCommand(deleteTargetText); + }, + { + retryCondition: (err) => { + if (err.stdout && err.stdout.includes("Timed Out")) { + return true; + } + return false; + }, + } + ); + break; + + default: + throw new GrpcError( + grpc.status.FAILED_PRECONDITION, + `invalid configuration: unknown shareStrategy ${this.options.iscsi.shareStrategy}` + ); } break; } @@ -846,6 +954,9 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} case "targetCli": // nothing required, just need to rescan on the node break; + case "pcs": + // nothing required, just need to rescan on the node + break; default: break; } @@ -856,6 +967,56 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} } } + async pcsCommand(commandLines) { + const execClient = this.getExecClient(); + const driver = this; + + let command = "sh"; + let args = ["-c"]; + + let cliArgs = ["pcs"]; + if ( + _.get(this.options, "iscsi.shareStrategyPcs.sudoEnabled", false) + ) { + cliArgs.unshift("sudo"); + } + + let cliCommand = []; + cliCommand.push(cliArgs.join(" ")); + cliCommand.push(commandLines.join(" ")); + args.push("'" + cliCommand.join(" ") + "'"); + + let logCommandTmp = command + " " + args.join(" "); + let logCommand = ""; + + logCommandTmp.split(" ").forEach((term) => { + logCommand += " "; + + if (term.startsWith("incoming_password=")) { + logCommand += "incoming_password="; + } else { + logCommand += term; + } + }); + + driver.ctx.logger.verbose("Pcs command:" + logCommand); + + let options = { + pty: true, + }; + let response = await execClient.exec( + execClient.buildCommand(command, args), + options + ); + driver.ctx.logger.verbose( + "Pcs response: " + JSON.stringify(response) + ); + if (response.code != 0) { + throw response; + } + return response; + } + async targetCliCommand(data) { const execClient = this.getExecClient(); const driver = this; From 373a7c7f79909a1fe17a732eef321fd8146152a7 Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Sun, 23 Mar 2025 10:48:49 +0200 Subject: [PATCH 38/55] Small CR and preparatory work for CI --- .github/workflows/main.yml | 3 +- ci/configs/zfs-generic/iscsi-pcs.yaml | 28 +++++++++++++ .../{iscsi.yaml => iscsi-targetcli.yaml} | 0 src/driver/controller-zfs-generic/index.js | 41 ++++++++----------- 4 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 ci/configs/zfs-generic/iscsi-pcs.yaml rename ci/configs/zfs-generic/{iscsi.yaml => iscsi-targetcli.yaml} (100%) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 16cdb78..81567cb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -192,7 +192,8 @@ jobs: max-parallel: 1 matrix: config: - - zfs-generic/iscsi.yaml + - zfs-generic/iscsi-pcs.yaml + - zfs-generic/iscsi-targetcli.yaml - zfs-generic/nfs.yaml - zfs-generic/smb.yaml - zfs-generic/nvmeof.yaml diff --git a/ci/configs/zfs-generic/iscsi-pcs.yaml b/ci/configs/zfs-generic/iscsi-pcs.yaml new file mode 100644 index 0000000..baabb69 --- /dev/null +++ b/ci/configs/zfs-generic/iscsi-pcs.yaml @@ -0,0 +1,28 @@ +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: "pcs" + shareStrategyPcs: + pcs_group: "group-nas" + basename: "iqn.2003-01.org.linux-iscsi.ubuntu-19.x8664" + auth: + enabled: 0 diff --git a/ci/configs/zfs-generic/iscsi.yaml b/ci/configs/zfs-generic/iscsi-targetcli.yaml similarity index 100% rename from ci/configs/zfs-generic/iscsi.yaml rename to ci/configs/zfs-generic/iscsi-targetcli.yaml diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index c2e15e8..cf86920 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -307,25 +307,22 @@ create /backstores/block/${assetName} basename = this.options.iscsi.shareStrategyPcs.basename; pcs_group = this.options.iscsi.shareStrategyPcs.pcs_group; - let groupText = `group ${pcs_group}`; - let createTargetText = [ - `resource create --future target-${assetName} iSCSITarget`, - 'implementation="lio-t"', - `iqn="${basename}:${assetName}"` + let groupTerms = ['group', `${pcs_group}`]; + let createTargetTerms = [ + 'resource', 'create', '--future', `target-${assetName}`, 'iSCSITarget', + 'implementation="lio-t"', `iqn="${basename}:${assetName}"` ]; if (this.options.iscsi.shareStrategyPcs.auth.enabled) { - createTargetText.push(`incoming_username="${this.options.iscsi.shareStrategyPcs.auth.incoming_username}"`); - createTargetText.push(`incoming_password="${this.options.iscsi.shareStrategyPcs.auth.incoming_password}"`); + createTargetTerms.push(`incoming_username="${this.options.iscsi.shareStrategyPcs.auth.incoming_username}"`); + createTargetTerms.push(`incoming_password="${this.options.iscsi.shareStrategyPcs.auth.incoming_password}"`); } - createTargetText.push(groupText); - await GeneralUtils.retry( 3, 2000, async () => { - await this.pcsCommand(createTargetText); + await this.pcsCommand(createTargetTerms.concat(groupTerms)); }, { retryCondition: (err) => { @@ -337,19 +334,17 @@ create /backstores/block/${assetName} } ); - let createLunText = [ - `resource create --future lun-${assetName} iSCSILogicalUnit`, - 'implementation="lio-t"', - `target_iqn="${basename}:${assetName}" lun="1"`, - `path="/dev/${extentDiskName}"`, - groupText + let createLunTerms = [ + 'resource', 'create', '--future', `lun-${assetName}`, 'iSCSILogicalUnit', + 'implementation="lio-t"', `target_iqn="${basename}:${assetName}" lun="1"`, + `path="/dev/${extentDiskName}"` ]; await GeneralUtils.retry( 3, 2000, async () => { - await this.pcsCommand(createLunText); + await this.pcsCommand(createLunTerms.concat(groupTerms)); }, { retryCondition: (err) => { @@ -759,7 +754,7 @@ delete ${assetName} break; case "pcs": let deleteLunText = [ - `resource delete lun-${assetName}` + 'resource', 'delete', `lun-${assetName}` ]; await GeneralUtils.retry( @@ -779,7 +774,7 @@ delete ${assetName} ); let deleteTargetText = [ - `resource delete target-${assetName}` + 'resource', 'delete', `target-${assetName}` ]; await GeneralUtils.retry( @@ -967,7 +962,7 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} } } - async pcsCommand(commandLines) { + async pcsCommand(commandTerms) { const execClient = this.getExecClient(); const driver = this; @@ -983,7 +978,7 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} let cliCommand = []; cliCommand.push(cliArgs.join(" ")); - cliCommand.push(commandLines.join(" ")); + cliCommand.push(commandTerms.join(" ")); args.push("'" + cliCommand.join(" ") + "'"); let logCommandTmp = command + " " + args.join(" "); @@ -999,7 +994,7 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} } }); - driver.ctx.logger.verbose("Pcs command:" + logCommand); + driver.ctx.logger.verbose("pcs command:" + logCommand); let options = { pty: true, @@ -1009,7 +1004,7 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} options ); driver.ctx.logger.verbose( - "Pcs response: " + JSON.stringify(response) + "pcs response: " + JSON.stringify(response) ); if (response.code != 0) { throw response; From 3931bd9ef0305c03ea9d364303e41ccc95e75e4e Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Sun, 23 Mar 2025 11:29:53 +0200 Subject: [PATCH 39/55] Adding some sleeps to allow things to settle down --- src/driver/controller-zfs-generic/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index cf86920..3899792 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -355,6 +355,9 @@ create /backstores/block/${assetName} }, } ); + + await GeneralUtils.sleep(2000); // let things settle + break; default: @@ -793,6 +796,8 @@ delete ${assetName} } ); + await GeneralUtils.sleep(2000); // let things settle + break; default: From 53560cd8b1e9f847877336cba368a5a6dbf95b12 Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Sun, 23 Mar 2025 11:32:32 +0200 Subject: [PATCH 40/55] small fix --- src/driver/controller-zfs-generic/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 3899792..98a7145 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -336,7 +336,7 @@ create /backstores/block/${assetName} let createLunTerms = [ 'resource', 'create', '--future', `lun-${assetName}`, 'iSCSILogicalUnit', - 'implementation="lio-t"', `target_iqn="${basename}:${assetName}" lun="1"`, + 'implementation="lio-t"', `target_iqn="${basename}:${assetName}"`, 'lun="1"', `path="/dev/${extentDiskName}"` ]; From 34452b2f3a72200582d11f1242f58aacce9569d2 Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Mon, 24 Mar 2025 23:11:00 +0000 Subject: [PATCH 41/55] Added vagrant and devcontainers + fixed all tests --- .devcontainer/devcontainer.json | 38 ++++++++ .github/dependabot.yml | 12 +++ .gitignore | 4 +- CONTRIBUTING.md | 108 +++++++++++++++++++++ Vagrantfile | 71 ++++++++++++++ ci/bin/run.sh | 2 +- dev/run.sh | 34 +++++++ src/driver/controller-zfs-generic/index.js | 27 +++--- 8 files changed, 282 insertions(+), 14 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .github/dependabot.yml create mode 100644 CONTRIBUTING.md create mode 100644 Vagrantfile create mode 100755 dev/run.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..2835179 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,38 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "democratic-csi", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers/features/go:1": {} // To compile csi-sanity during postCreateCommand + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "npm install", + + // Configure tool-specific properties. + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.shell.linux": "/bin/bash" + }, + "extensions": [ + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "waderyan.nodejs-extension-pack", + "ms-vscode.node-debug2", + "christian-kohler.npm-intellisense", + "christian-kohler.path-intellisense", + "HashiCorp.terraform" + ] + } + } + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..f33a02c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: "devcontainers" + directory: "/" + schedule: + interval: weekly diff --git a/.gitignore b/.gitignore index 2855d28..b945e95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ **~ node_modules -dev +dev/* /ci/bin/*dev* +.vagrant +!dev/run.sh \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a770271 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,108 @@ +# Contributing to democratic-csi + +## Development Environment Setup + +This project uses a hybrid development approach with devcontainers for IDE configuration and Vagrant for system-level testing. + +### Prerequisites + +Before you begin, ensure you have the following installed: +- [Visual Studio Code](https://code.visualstudio.com/) +- [Docker](https://www.docker.com/get-started) +- [Vagrant](https://www.vagrantup.com/downloads) +- Virtualization Provider: + - For Intel/AMD Machines: VirtualBox + - For Apple Silicon: Qemu (`brew install qemu vagrant` and `vagrant plugin install vagrant-qemu`) + +### Development Workflow + +#### 1. Local Development with Devcontainers + +Devcontainers provide a consistent development environment with: +- Configured VSCode extensions +- Necessary development tools +- Integrated development experience + +To use the devcontainer: +1. Open the project in VSCode +2. Install the "Dev Containers" extension +3. Click "Reopen in Container" when prompted +4. Start coding with pre-configured environment + +#### 2. System Testing with Vagrant + +Vagrant provides a full virtual machine environment for: +- System-level testing +- Running code with kernel dependencies +- Simulating production-like environments + +Workflow: +```bash +# Navigate to project directory +cd ~/democratic-csi + +# Start the Vagrant VM +vagrant up + +# Connect to the VM +vagrant ssh + +# Inside the VM, navigate to the project +cd ~/democratic-csi + +# Run project tests, the config.yaml can be any from the examples folders +# just configured for your own environment. +# You can also create a file `dev/secrets.env` that has `export VARIABLE=VALUE` +# and reference those in your `config.yaml` +./dev/run.sh -c ./dev/config.yaml +``` + +##### Keeping Files in Sync + +Use these methods to keep your local files synchronized with the Vagrant VM: + +###### Manual Sync +```bash +# Sync files from local to Vagrant VM +vagrant rsync +``` + +###### Continuous Sync +```bash +# Automatically sync files as they change +vagrant rsync-auto +``` + +### Best Practices + +- Use devcontainer for day-to-day development and coding +- Use Vagrant for comprehensive system testing +- Always run `vagrant rsync` before running tests in the VM +- Commit and push changes frequently +- If encountering issues, try: + 1. Recreating the devcontainer + 2. Reprovisioning the Vagrant VM with `vagrant reload --provision` or `vagrant destroy -f && vagrant up` + +### Troubleshooting + +#### Devcontainer Issues +- Ensure Docker is running +- Rebuild the container if extensions fail to load +- Check VSCode Dev Containers extension logs + +#### Vagrant Issues +- Verify virtualization is enabled in your BIOS +- Ensure you have the latest Vagrant and virtualization provider +- For Apple Silicon, use Parallels or Lima + +### Contribution Guidelines + +1. Create a new branch for your feature targetting `next` +2. Write clear, concise commit messages +3. Include coverage for tests of csi-sanity for new functionality +4. Run tests in Vagrant VM +5. Submit a pull request with a clear description of changes + +### Contact + +For any questions or issues, please [open an issue](https://github.com/democratic-csi/democratic-csi/issues) on the project repository. \ No newline at end of file diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 0000000..294ceab --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,71 @@ +Vagrant.configure("2") do |config| + # Check the host's architecture + host_arch = `uname -m`.strip + + # Use a different box for ARM vs x86_64 + if host_arch == "arm64" + # requires qemu, install qemu and then: + # vagrant plugin install vagrant-qemu + config.vm.box = "perk/ubuntu-24.04-arm64" + else + # Use the x86_64 compatible Ubuntu box + config.vm.box = "ubuntu/jammy64" + end + + config.vm.provider "virtualbox" do |vb| + vb.memory = "2048" + vb.cpus = 2 + end + + config.vm.provision "shell", inline: <<-SHELL + sudo apt-get update -y + sudo apt-get install -y open-iscsi nodejs git make + + # Enable and start iscsid service + sudo systemctl enable --now iscsid + + # Verify installation + systemctl status iscsid --no-pager + + #### + # Install golang + #### + GO_VERSION="1.24.1" + ARCH=$(uname -m) + GO_TAR_URL="" + + if [[ "$ARCH" == "aarch64" ]]; then + GO_TAR_URL="https://go.dev/dl/go${GO_VERSION}.linux-arm64.tar.gz" + elif [[ "$ARCH" == "x86_64" ]]; then + GO_TAR_URL="https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz" + else + echo "Unsupported architecture: $ARCH" + exit 1 + fi + + echo "Downloading Go version $GO_VERSION for $ARCH..." + wget -q "$GO_TAR_URL" -O go.tar.gz + tar -C /usr/local -xzf go.tar.gz + rm go.tar.gz + echo "export PATH=\$PATH:/usr/local/go/bin" >> /etc/profile + source /etc/profile + + #### + # Install csi-test + #### + echo "Installing csi-test" + git clone https://github.com/kubernetes-csi/csi-test /tmp/csi-test + pushd /tmp/csi-test/cmd/csi-sanity + make csi-sanity + sudo cp csi-sanity /usr/local/bin + popd + #rm -rf /tmp/csi-test + SHELL + + # Sync project directory for seamless workflow + config.vm.synced_folder ".", "/home/vagrant/democratic-csi", type: "rsync", + rsync__exclude: ".git/" + + # Allow SSH access with default key + config.ssh.insert_key = false + end \ No newline at end of file diff --git a/ci/bin/run.sh b/ci/bin/run.sh index 1d4eaf8..fd929f9 100755 --- a/ci/bin/run.sh +++ b/ci/bin/run.sh @@ -20,7 +20,7 @@ if [[ -f "node_modules-linux-amd64.tar.gz" && ! -d "node_modules" ]];then fi # generate key for paths etc -export CI_BUILD_KEY=$(uuidgen | cut -d "-" -f 1) +export CI_BUILD_KEY=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8) # launch the server sudo -E ci/bin/launch-server.sh & diff --git a/dev/run.sh b/dev/run.sh new file mode 100755 index 0000000..c38e310 --- /dev/null +++ b/dev/run.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +set -e +set -x +TEMPLATE_CONFIG="" + +ROOT_DIR="$(dirname "$(realpath "$0")")" + +while [[ "$#" -gt 0 ]]; do + case $1 in + -c|--config) TEMPLATE_CONFIG="$(realpath "$2")"; shift ;; + *) echo "Unknown parameter passed: $1"; exit 1 ;; + esac + shift +done + +if [ -z "${TEMPLATE_CONFIG}" ]; then + echo "Error: --config or -c parameter is required." + exit 1 +fi + +if [ ! -f $ROOT_DIR/secrets.env ]; then + echo "Error: secrets.env file not found." + exit 1 +fi + +source $ROOT_DIR/secrets.env # needs to have exported variables + +# generate key for paths etc +export CI_BUILD_KEY=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8) + +export TEMPLATE_CONFIG_FILE=${TEMPLATE_CONFIG} + +$ROOT_DIR/../ci/bin/run.sh \ No newline at end of file diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 98a7145..9e8d8c2 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -305,12 +305,12 @@ create /backstores/block/${assetName} break; case "pcs": basename = this.options.iscsi.shareStrategyPcs.basename; - pcs_group = this.options.iscsi.shareStrategyPcs.pcs_group; + let pcs_group = this.options.iscsi.shareStrategyPcs.pcs_group; - let groupTerms = ['group', `${pcs_group}`]; + let extraTerms = ['group', `${pcs_group}`, '--wait']; // The wait is important to avoid race conditions let createTargetTerms = [ - 'resource', 'create', '--future', `target-${assetName}`, 'iSCSITarget', - 'implementation="lio-t"', `iqn="${basename}:${assetName}"` + 'resource', 'create', '--future', `target-${assetName}`, 'ocf:heartbeat:iSCSITarget', + 'implementation="lio-t"', 'portals=":::3260"', `iqn="${basename}:${assetName}"` ]; if (this.options.iscsi.shareStrategyPcs.auth.enabled) { @@ -322,7 +322,7 @@ create /backstores/block/${assetName} 3, 2000, async () => { - await this.pcsCommand(createTargetTerms.concat(groupTerms)); + await this.pcsCommand(createTargetTerms.concat(extraTerms)); }, { retryCondition: (err) => { @@ -335,8 +335,8 @@ create /backstores/block/${assetName} ); let createLunTerms = [ - 'resource', 'create', '--future', `lun-${assetName}`, 'iSCSILogicalUnit', - 'implementation="lio-t"', `target_iqn="${basename}:${assetName}"`, 'lun="1"', + 'resource', 'create', '--future', `lun-${assetName}`, 'ocf:heartbeat:iSCSILogicalUnit', + 'implementation="lio-t"', `target_iqn="${basename}:${assetName}"`, 'lun="0"', `path="/dev/${extentDiskName}"` ]; @@ -344,7 +344,7 @@ create /backstores/block/${assetName} 3, 2000, async () => { - await this.pcsCommand(createLunTerms.concat(groupTerms)); + await this.pcsCommand(createLunTerms.concat(extraTerms)); }, { retryCondition: (err) => { @@ -356,8 +356,6 @@ create /backstores/block/${assetName} } ); - await GeneralUtils.sleep(2000); // let things settle - break; default: @@ -796,8 +794,6 @@ delete ${assetName} } ); - await GeneralUtils.sleep(2000); // let things settle - break; default: @@ -1011,6 +1007,13 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} driver.ctx.logger.verbose( "pcs response: " + JSON.stringify(response) ); + + // Handle idempotence for create commands + if (response.code == 1 && response.stdout.includes("already exists")) { + driver.ctx.logger.verbose("pcs resource already exists, ignoring error (setting response.code=0)"); + response.code = 0; + } + if (response.code != 0) { throw response; } From 7867bb467271925a50a9085f147544f53fa669a2 Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Mon, 24 Mar 2025 23:31:17 +0000 Subject: [PATCH 42/55] Minor changes --- .devcontainer/devcontainer.json | 2 +- .devcontainer/postCreate.sh | 12 ++++++++++++ .github/workflows/main.yml | 2 +- CONTRIBUTING.md | 5 +++++ 4 files changed, 19 insertions(+), 2 deletions(-) create mode 100755 .devcontainer/postCreate.sh diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2835179..22e0c6b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,7 +14,7 @@ // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "npm install", + "postCreateCommand": "/bin/bash .devcontainer/postCreate.sh", // Configure tool-specific properties. "customizations": { diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh new file mode 100755 index 0000000..1f1cee7 --- /dev/null +++ b/.devcontainer/postCreate.sh @@ -0,0 +1,12 @@ +#!/bin/env bash + +npm install + +git clone https://github.com/kubernetes-csi/csi-test /tmp/csi-test +pushd /tmp/csi-test +make +sudo cp /tmp/csi-test/cmd/csi-sanity/csi-sanity /usr/local/bin +popd +rm -rf /tmp/csi-test + +sudo apt update \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 81567cb..ecf7467 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -192,7 +192,7 @@ jobs: max-parallel: 1 matrix: config: - - zfs-generic/iscsi-pcs.yaml + #- zfs-generic/iscsi-pcs.yaml # TODO: enable this once the server is setup - zfs-generic/iscsi-targetcli.yaml - zfs-generic/nfs.yaml - zfs-generic/smb.yaml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a770271..f9965b6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,6 +29,11 @@ To use the devcontainer: 3. Click "Reopen in Container" when prompted 4. Start coding with pre-configured environment +> [!Note] +> For `iSCSI` it's mandatory to use the Vagrant VM, due to the need of a kernel driver. +> However for other tests the container is probably enough. It's possible to run the `dev/run.sh` +> as explained below in the devcontainer and see if it's possible, before spinning up a full VM. + #### 2. System Testing with Vagrant Vagrant provides a full virtual machine environment for: From 536f0d487fe23f539e3dd0a24bf6d8583b0f0f2c Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Tue, 25 Mar 2025 07:52:43 +0000 Subject: [PATCH 43/55] CR fixes --- .gitignore | 5 +++-- CONTRIBUTING.md | 4 ++-- Vagrantfile | 20 +++++++++++++++++++- {dev => hack}/run.sh | 0 4 files changed, 24 insertions(+), 5 deletions(-) rename {dev => hack}/run.sh (100%) diff --git a/.gitignore b/.gitignore index b945e95..d19bbc7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ **~ node_modules -dev/* +dev /ci/bin/*dev* .vagrant -!dev/run.sh \ No newline at end of file +hack/* +!hack/run.sh \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f9965b6..1da1090 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ To use the devcontainer: > [!Note] > For `iSCSI` it's mandatory to use the Vagrant VM, due to the need of a kernel driver. -> However for other tests the container is probably enough. It's possible to run the `dev/run.sh` +> However for other tests the container is probably enough. It's possible to run the `hack/run.sh` > as explained below in the devcontainer and see if it's possible, before spinning up a full VM. #### 2. System Testing with Vagrant @@ -59,7 +59,7 @@ cd ~/democratic-csi # just configured for your own environment. # You can also create a file `dev/secrets.env` that has `export VARIABLE=VALUE` # and reference those in your `config.yaml` -./dev/run.sh -c ./dev/config.yaml +./hack/run.sh -c ./hack/config.yaml ``` ##### Keeping Files in Sync diff --git a/Vagrantfile b/Vagrantfile index 294ceab..e4bf1e4 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -19,7 +19,25 @@ Vagrant.configure("2") do |config| config.vm.provision "shell", inline: <<-SHELL sudo apt-get update -y - sudo apt-get install -y open-iscsi nodejs git make + + # for building dependecies and executing node + sudo apt-get install -y nodejs git make + + # for app functionality + sudo apt-get install -y netbase socat e2fsprogs xfsprogs fatresize dosfstools nfs-common cifs-utils + + # Install the following system packages + sudo apt-get install -y open-iscsi lsscsi sg3-utils multipath-tools scsitools nvme-cli + + # Enable multipathing + sudo tee /etc/multipath.conf << EOF + defaults { + user_friendly_names yes + find_multipaths yes + } + EOF + + sudo systemctl enable multipath-tools.service # Enable and start iscsid service sudo systemctl enable --now iscsid diff --git a/dev/run.sh b/hack/run.sh similarity index 100% rename from dev/run.sh rename to hack/run.sh From 37b1726460aff45cb08b63bd2edb9696f8e2cead Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Tue, 25 Mar 2025 07:57:21 +0000 Subject: [PATCH 44/55] remove unneeded rm --- Vagrantfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Vagrantfile b/Vagrantfile index e4bf1e4..b69d2cf 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -77,7 +77,6 @@ Vagrant.configure("2") do |config| make csi-sanity sudo cp csi-sanity /usr/local/bin popd - #rm -rf /tmp/csi-test SHELL # Sync project directory for seamless workflow From 490f93e665f2933e4a0ced289f571a3976e7dccc Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Tue, 25 Mar 2025 10:06:47 +0200 Subject: [PATCH 45/55] fix vagrant provisioning script --- Vagrantfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Vagrantfile b/Vagrantfile index b69d2cf..0d5c5d5 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -31,11 +31,11 @@ Vagrant.configure("2") do |config| # Enable multipathing sudo tee /etc/multipath.conf << EOF - defaults { - user_friendly_names yes - find_multipaths yes - } - EOF +defaults { + user_friendly_names yes + find_multipaths yes +} +EOF sudo systemctl enable multipath-tools.service From 0c74b90e01ff187647ba3f738010b7bfe993d4a4 Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Sat, 5 Apr 2025 09:12:47 +0000 Subject: [PATCH 46/55] Add build_push.sh --- .gitignore | 3 ++- CONTRIBUTING.md | 25 +++++++++++++++++++++++++ hack/build_push.sh | 18 ++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 hack/build_push.sh diff --git a/.gitignore b/.gitignore index d19bbc7..5bafd17 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ dev /ci/bin/*dev* .vagrant hack/* -!hack/run.sh \ No newline at end of file +!hack/run.sh +!hack/build_push.sh \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1da1090..a0fef36 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -78,6 +78,31 @@ vagrant rsync vagrant rsync-auto ``` +#### 3. Deploy development version to K8s cluster + +Deployment provides a good environment for: +- Final testing in a real world scenario +- Run the final version until included in a release + +> [!Note] +> Make sure to do the build on the architecture you will be running it. +> For example, don't build in Apple Silicon if your cluster runs in amd64. + + +1. Login to your github container registry +```bash +docker login ghcr.io +``` + +> [!Important] +> Login to the container registry is stored plain text, use a PAT instead of your Github password. [Create a PAT with write:packages](https://github.com/settings/tokens/new?scopes=write:packages). + +2. Compile and push to your github container registry. +```bash +./hack/build_push.sh +``` +2. + ### Best Practices - Use devcontainer for day-to-day development and coding diff --git a/hack/build_push.sh b/hack/build_push.sh new file mode 100644 index 0000000..609bd28 --- /dev/null +++ b/hack/build_push.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e +set -x + +ROOT_DIR="$(dirname "$(realpath "$0")")" + +GITHUB_USER=${GITHUB_USER:-$(jq -r '.auths."ghcr.io".auth' ~/.docker/config.json|base64 -d|cut -d':' -f1)} +GITHUB_REPO=${GITHUB_REPO:-$(basename -s ${ROOT_DIR}/../.git $(git remote get-url origin))} +DOCKER_TAG=${DOCKER_TAG:-$(git branch --show-current)-$(git rev-parse --short HEAD)} + +if [ -z "${GITHUB_USER}" ]; then + echo "Error: Need to login to ghcr.io ; execute docker login ghcr.io" + exit 1 +fi + +docker build $ROOT_DIR/.. --push -t ghcr.io/${GITHUB_USER}/${GITHUB_REPO}:${DOCKER_TAG} +echo "Image pushed to ghcr.io/${GITHUB_USER}/${GITHUB_REPO}:${DOCKER_TAG}" \ No newline at end of file From 0f04c7733010dc8dca2af48284ec1d1fdd1be60e Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Sat, 5 Apr 2025 10:45:38 +0000 Subject: [PATCH 47/55] small typo --- hack/build_push.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/build_push.sh b/hack/build_push.sh index 609bd28..2aa4e2e 100644 --- a/hack/build_push.sh +++ b/hack/build_push.sh @@ -6,7 +6,7 @@ set -x ROOT_DIR="$(dirname "$(realpath "$0")")" GITHUB_USER=${GITHUB_USER:-$(jq -r '.auths."ghcr.io".auth' ~/.docker/config.json|base64 -d|cut -d':' -f1)} -GITHUB_REPO=${GITHUB_REPO:-$(basename -s ${ROOT_DIR}/../.git $(git remote get-url origin))} +GITHUB_REPO=${GITHUB_REPO:-$(basename -s .git $(git remote get-url origin))} DOCKER_TAG=${DOCKER_TAG:-$(git branch --show-current)-$(git rev-parse --short HEAD)} if [ -z "${GITHUB_USER}" ]; then From b1adbcd854f8f03912a281193e4b1606ea482f78 Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Sat, 5 Apr 2025 11:27:31 +0000 Subject: [PATCH 48/55] fixed doc --- CONTRIBUTING.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0fef36..9eabd8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -101,7 +101,16 @@ docker login ghcr.io ```bash ./hack/build_push.sh ``` -2. + +3. When you deploy, in the `values.yaml` add the following, using the output from the script +```yaml +controller: + driver: + image: ghcr.io/your_user/democratic-csi:your_branch-fc02fc4 +node: + driver: + image: ghcr.io/your_user/democratic-csi:your_branch-fc02fc4 +``` ### Best Practices From 126ab5356c46a94f1569a8535596c55c32d10bfb Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Sat, 5 Apr 2025 11:43:52 +0000 Subject: [PATCH 49/55] explain to put visibility public to package --- CONTRIBUTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9eabd8a..0ae44c5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -112,6 +112,14 @@ node: image: ghcr.io/your_user/democratic-csi:your_branch-fc02fc4 ``` +4. Make the Image Public + + By default, images pushed to GHCR are private. To make it public: + 1. Go to GitHub → Your Repository → Packages (or directly github.com/USERNAME?tab=packages) + 2. Select the package + 3. Click Package Settings + 4. Change Visibility to Public + ### Best Practices - Use devcontainer for day-to-day development and coding From ff206518edd7e4cdb290e226846c0a5f00ce9969 Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Thu, 1 May 2025 10:09:02 +0000 Subject: [PATCH 50/55] add force to target creation to allow multiple non-unique usernames --- src/driver/controller-zfs-generic/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index 9e8d8c2..b234078 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -309,7 +309,7 @@ create /backstores/block/${assetName} let extraTerms = ['group', `${pcs_group}`, '--wait']; // The wait is important to avoid race conditions let createTargetTerms = [ - 'resource', 'create', '--future', `target-${assetName}`, 'ocf:heartbeat:iSCSITarget', + 'resource', 'create', '--future', '--force', `target-${assetName}`, 'ocf:heartbeat:iSCSITarget', 'implementation="lio-t"', 'portals=":::3260"', `iqn="${basename}:${assetName}"` ]; From 822889ed1d5491109e95e3d2355a99b51026cf86 Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Thu, 1 May 2025 15:09:19 +0300 Subject: [PATCH 51/55] make build_push.sh executable --- hack/build_push.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 hack/build_push.sh diff --git a/hack/build_push.sh b/hack/build_push.sh old mode 100644 new mode 100755 From 00c025df8e51b5c010e88b1e15eb875051db940c Mon Sep 17 00:00:00 2001 From: Michel Peterson Date: Sun, 4 Jan 2026 08:53:55 +0000 Subject: [PATCH 52/55] update devcontainer image, enhance contributing guide, and implement mutex for PCS commands --- .devcontainer/devcontainer.json | 4 ++- CONTRIBUTING.md | 4 +++ Vagrantfile | 3 ++ src/driver/controller-zfs-generic/index.js | 38 ++++++++++++---------- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 22e0c6b..fd97ce9 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,9 @@ { "name": "democratic-csi", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", + // regarding versioning the first number is the devcontainer image version, + // the second is the Node.js version, and the third is the OS version. + "image": "mcr.microsoft.com/devcontainers/typescript-node:4-20-bookworm", // Features to add to the dev container. More info: https://containers.dev/features. "features": { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0ae44c5..277b4f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,6 +62,10 @@ cd ~/democratic-csi ./hack/run.sh -c ./hack/config.yaml ``` +>![Note] +> For running tests with democratic-csi the authentication needs to be disabled, as +> it always initiates connections to the share without authentication. + ##### Keeping Files in Sync Use these methods to keep your local files synchronized with the Vagrant VM: diff --git a/Vagrantfile b/Vagrantfile index 0d5c5d5..0238103 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -18,6 +18,9 @@ Vagrant.configure("2") do |config| end config.vm.provision "shell", inline: <<-SHELL + # force version 20.x of nodejs + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get update -y # for building dependecies and executing node diff --git a/src/driver/controller-zfs-generic/index.js b/src/driver/controller-zfs-generic/index.js index b234078..084b440 100644 --- a/src/driver/controller-zfs-generic/index.js +++ b/src/driver/controller-zfs-generic/index.js @@ -20,6 +20,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { this.targetCliMutex = new Mutex(); this.nvmetCliMutex = new Mutex(); this.spdkCliMutex = new Mutex(); + this.pcsMutex = new Mutex(); } getExecClient() { @@ -1000,24 +1001,27 @@ save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} let options = { pty: true, }; - let response = await execClient.exec( - execClient.buildCommand(command, args), - options - ); - driver.ctx.logger.verbose( - "pcs response: " + JSON.stringify(response) - ); - - // Handle idempotence for create commands - if (response.code == 1 && response.stdout.includes("already exists")) { - driver.ctx.logger.verbose("pcs resource already exists, ignoring error (setting response.code=0)"); - response.code = 0; - } - if (response.code != 0) { - throw response; - } - return response; + return driver.pcsMutex.runExclusive(async () => { + let response = await execClient.exec( + execClient.buildCommand(command, args), + options + ); + driver.ctx.logger.verbose( + "pcs response: " + JSON.stringify(response) + ); + + // Handle idempotence for create commands + if (response.code == 1 && response.stdout.includes("already exists")) { + driver.ctx.logger.verbose("pcs resource already exists, ignoring error (setting response.code=0)"); + response.code = 0; + } + + if (response.code != 0) { + throw response; + } + return response; + }); } async targetCliCommand(data) { From 333ff2b30b24c6e7dc108b5224382093b938fcbe Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 6 Jan 2026 12:07:19 -0700 Subject: [PATCH 53/55] docs, prep for release Signed-off-by: Travis Glenn Hansen --- .github/workflows/main.yml | 40 ++++++- Dockerfile.Windows | 9 +- README.md | 11 +- ...nerd-oci-ephemeral-inline-pod-windows.yaml | 37 +++++++ .../containerd-oci-ephemeral-inline-pod.yaml | 29 +++++ examples/containerd-oci-ephemeral-inline.yaml | 2 + examples/freenas-api-nvmeof.yaml | 94 ++++++++++++++++ examples/freenas-nvmeof.yaml | 104 ++++++++++++++++++ .../vhd-ephemeral-inline-pod-windows.yaml | 30 +++++ examples/vhd-ephemeral-inline.yaml | 3 + src/driver/controller-zfs/index.js | 21 +++- .../ephemeral-inline-containerd-oci/index.js | 6 +- src/driver/index.js | 18 ++- src/utils/mount.js | 31 ++++++ 14 files changed, 415 insertions(+), 20 deletions(-) create mode 100644 examples/containerd-oci-ephemeral-inline-pod-windows.yaml create mode 100644 examples/containerd-oci-ephemeral-inline-pod.yaml create mode 100644 examples/freenas-api-nvmeof.yaml create mode 100644 examples/freenas-nvmeof.yaml create mode 100644 examples/vhd-ephemeral-inline-pod-windows.yaml create mode 100644 examples/vhd-ephemeral-inline.yaml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ecf7467..4719e6f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -184,7 +184,7 @@ jobs: TRUENAS_PASSWORD: ${{ secrets.SANITY_TRUENAS_PASSWORD }} # ssh-based drivers - csi-sanity-zfs-generic: + csi-sanity-zfs-generic-targetcli: needs: - build-npm-linux-amd64 strategy: @@ -192,7 +192,6 @@ jobs: max-parallel: 1 matrix: config: - #- zfs-generic/iscsi-pcs.yaml # TODO: enable this once the server is setup - zfs-generic/iscsi-targetcli.yaml - zfs-generic/nfs.yaml - zfs-generic/smb.yaml @@ -213,7 +212,36 @@ jobs: ci/bin/run.sh env: TEMPLATE_CONFIG_FILE: "./ci/configs/${{ matrix.config }}" - SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_HOST }} + SERVER_HOST: ${{ secrets.SANITY_ZFS_GENERIC_TARGETCLI_HOST }} + SERVER_USERNAME: ${{ secrets.SANITY_ZFS_GENERIC_USERNAME }} + SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }} + + csi-sanity-zfs-generic-pcs: + needs: + - build-npm-linux-amd64 + strategy: + fail-fast: false + max-parallel: 1 + matrix: + config: + - zfs-generic/iscsi-pcs.yaml + runs-on: + - self-hosted + - Linux + - X64 + - csi-sanity-zfs-generic + steps: + - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + 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_PCS_HOST }} SERVER_USERNAME: ${{ secrets.SANITY_ZFS_GENERIC_USERNAME }} SERVER_PASSWORD: ${{ secrets.SANITY_ZFS_GENERIC_PASSWORD }} @@ -439,7 +467,8 @@ jobs: - csi-sanity-synology-dsm7 - csi-sanity-truenas-scale-25_10 - csi-sanity-truenas-core-13_0 - - csi-sanity-zfs-generic + - csi-sanity-zfs-generic-targetcli + - csi-sanity-zfs-generic-pcs - csi-sanity-objectivefs - csi-sanity-client - csi-sanity-client-windows @@ -480,7 +509,8 @@ jobs: - csi-sanity-synology-dsm7 - csi-sanity-truenas-scale-25_10 - csi-sanity-truenas-core-13_0 - - csi-sanity-zfs-generic + - csi-sanity-zfs-generic-targetcli + - csi-sanity-zfs-generic-pcs - csi-sanity-objectivefs - csi-sanity-client - csi-sanity-client-windows diff --git a/Dockerfile.Windows b/Dockerfile.Windows index 0fca523..8cf4eae 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -76,14 +76,15 @@ COPY . . ###################### # actual image ###################### -FROM mcr.microsoft.com/windows/nanoserver:${NANO_BASE_TAG} +#FROM mcr.microsoft.com/windows/nanoserver:${NANO_BASE_TAG} +FROM mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0 -SHELL ["cmd.exe", "/s" , "/c"] +#SHELL ["cmd.exe", "/s" , "/c"] #https://github.com/PowerShell/PowerShell-Docker/issues/236 # NOTE: this works for non-host process containers, but host process containers will have specials PATH requirements # C:\Windows\System32\WindowsPowerShell\v1.0\ -ENV PATH="C:\Windows\system32;C:\Windows;C:\PowerShell;C:\app\bin;" +#ENV PATH="C:\Windows\system32;C:\Windows;C:\PowerShell;C:\app\bin;" ENV DEMOCRATIC_CSI_IS_CONTAINER=true ENV NODE_ENV=production @@ -93,7 +94,7 @@ LABEL org.opencontainers.image.url https://github.com/democratic-csi/democratic- LABEL org.opencontainers.image.licenses MIT # install powershell -COPY --from=build /PowerShell /PowerShell +#COPY --from=build /PowerShell /PowerShell # install app COPY --from=build /app /app diff --git a/README.md b/README.md index e25be56..3e154a7 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,11 @@ have access to resizing, snapshots, clones, etc functionality. - `freenas-nfs` (manages zfs datasets to share over nfs) - `freenas-iscsi` (manages zfs zvols to share over iscsi) - `freenas-smb` (manages zfs datasets to share over smb) + - `freenas-nvmeof` (manages zfs zvols to share over nvmeof) - `freenas-api-nfs` experimental use with SCALE only (manages zfs datasets to share over nfs) - `freenas-api-iscsi` experimental use with SCALE only (manages zfs zvols to share over iscsi) - `freenas-api-smb` experimental use with SCALE only (manages zfs datasets to share over smb) + - `freenas-api-nvmeof` experimental use with SCALE only (manages zfs zvols to share over nvmeof) - `zfs-generic-nfs` (works with any ZoL installation...ie: Ubuntu) - `zfs-generic-iscsi` (works with any ZoL installation...ie: Ubuntu) - `zfs-generic-smb` (works with any ZoL installation...ie: Ubuntu) @@ -41,6 +43,8 @@ have access to resizing, snapshots, clones, etc functionality. - `node-manual` (allows connecting to manually created smb, nfs, lustre, oneclient, nvmeof, and iscsi volumes, see sample PVs in the `examples` directory) + - `containerd-oci-ephemeral-inline` (provisions ephemeral rw node-local storage using oci images as a base) + - `vhd-ephemeral-inline` (provisions ephemeral rw node-local storage using vhd images as a base) - framework for developing `csi` drivers If you have any interest in providing a `csi` driver, simply open an issue to @@ -58,6 +62,8 @@ Predominantly 3 things are needed: ## Community Guides +Join us in the Home Operations discord server in #democratic-csi + - https://jonathangazeley.com/2021/01/05/using-truenas-to-provide-persistent-storage-for-kubernetes/ - https://www.lisenet.com/2021/moving-to-truenas-and-democratic-csi-for-kubernetes-persistent-storage/ - https://gist.github.com/admun/4372899f20421a947b7544e5fc9f9117 (migrating @@ -65,6 +71,7 @@ Predominantly 3 things are needed: - https://gist.github.com/deefdragon/d58a4210622ff64088bd62a5d8a4e8cc (migrating between storage classes using `velero`) - https://github.com/fenio/k8s-truenas (NFS/iSCSI over API with TrueNAS Scale) +- https://wazaari.dev/blog/truenas-talos-democratic-csi ## Node Prep @@ -328,7 +335,7 @@ Set-MSDSMGlobalLoadBalancePolicy -Policy RR Server preparation depends slightly on which `driver` you are using. -### FreeNAS (freenas-nfs, freenas-iscsi, freenas-smb, freenas-api-nfs, freenas-api-iscsi, freenas-api-smb) +### FreeNAS (freenas-nfs, freenas-iscsi, freenas-smb, freenas-nvmeof, freenas-api-nfs, freenas-api-iscsi, freenas-api-smb, freenas-api-nvmeof) The recommended version of FreeNAS is 12.0-U2+, however the driver should work with much older versions as well. @@ -371,6 +378,8 @@ Ensure the following services are configurged and running: 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 +- nvmeof + - ensure you have at least 1 listener/port configured (typcially TCP port 4420) If you would prefer you can configure `democratic-csi` to use a non-`root` user when connecting to the FreeNAS server: diff --git a/examples/containerd-oci-ephemeral-inline-pod-windows.yaml b/examples/containerd-oci-ephemeral-inline-pod-windows.yaml new file mode 100644 index 0000000..38f63ff --- /dev/null +++ b/examples/containerd-oci-ephemeral-inline-pod-windows.yaml @@ -0,0 +1,37 @@ +kind: Pod +apiVersion: v1 +metadata: + name: some-oci-pod-windows +spec: + nodeSelector: + kubernetes.io/os: windows + containers: + - name: hello + image: mcr.microsoft.com/windows/servercore:ltsc2022 + command: + - powershell.exe + - -command + - while ($true) { Start-Sleep -Seconds 1 } + resources: + requests: + memory: "128Mi" + cpu: "500m" + limits: + memory: "128Mi" + cpu: "500m" + volumeMounts: + - name: oci + mountPath: /mnt/oci + volumes: + - name: oci + csi: + driver: org.democratic-csi.containerd-oci-inline-ephemeral + volumeAttributes: + # "image.reference": "ubuntu:24.04" + "image.reference": "democraticcsi/csi-grpc-proxy" + # NOTE: windows is incapable of using linux-based platform images + # windows/amd64 + # NOTE: linux seemingly can mount windows-based images however + # "image.platform": "linux/amd64" + # "image.pullPolicy": "Always", + "snapshot.label.containerd.io/snapshot/windows/rootfs.sizebytes": "107374182400" diff --git a/examples/containerd-oci-ephemeral-inline-pod.yaml b/examples/containerd-oci-ephemeral-inline-pod.yaml new file mode 100644 index 0000000..8d64c4d --- /dev/null +++ b/examples/containerd-oci-ephemeral-inline-pod.yaml @@ -0,0 +1,29 @@ +kind: Pod +apiVersion: v1 +metadata: + name: some-oci-pod +spec: + nodeName: node01 + containers: + - name: hello + image: busybox:1.37 + command: ["sh", "-c", 'echo "Hello, Kubernetes!" && sleep Infinity'] + resources: + requests: + memory: "128Mi" + cpu: "500m" + limits: + memory: "128Mi" + cpu: "500m" + volumeMounts: + - name: oci + mountPath: /mnt/oci + volumes: + - name: oci + csi: + driver: org.democratic-csi.containerd-oci-inline-ephemeral + volumeAttributes: + "image.reference": "ubuntu:24.04" + # "image.platform": "" + # "image.pullPolicy": "Always", + # "snapshot.label.containerd.io/snapshot/windows/rootfs.sizebytes": "107374182400" diff --git a/examples/containerd-oci-ephemeral-inline.yaml b/examples/containerd-oci-ephemeral-inline.yaml index feac678..ca227fc 100644 --- a/examples/containerd-oci-ephemeral-inline.yaml +++ b/examples/containerd-oci-ephemeral-inline.yaml @@ -2,5 +2,7 @@ driver: containerd-oci-ephemeral-inline containerd: #address: /run/containerd/containerd.sock #windowsAddress: \\\\.\\pipe\\containerd-containerd + + # use k8s.io to use the k8s ns #namespace: default #creds encryption key diff --git a/examples/freenas-api-nvmeof.yaml b/examples/freenas-api-nvmeof.yaml new file mode 100644 index 0000000..d8f7b6a --- /dev/null +++ b/examples/freenas-api-nvmeof.yaml @@ -0,0 +1,94 @@ +driver: freenas-api-nvmeof +instance_id: +httpConnection: + protocol: http + host: server address + port: 80 + # use only 1 of apiKey or username/password + # if both are present, apiKey is preferred + # apiKey is only available starting in TrueNAS-12 + #apiKey: + username: root + password: + allowInsecure: true + # use apiVersion 2 for TrueNAS-12 and up (will work on 11.x in some scenarios as well) + # leave unset for auto-detection + #apiVersion: 2 + +zfs: + # can be used to override defaults if necessary + # the example below is useful for TrueNAS 12 + #cli: + # sudoEnabled: true + # + # leave paths unset for auto-detection + # 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: + # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" + # "org.freenas:test": "{{ parameters.foo }}" + # "org.freenas:test2": "some value" + + # total volume name (zvol//) length cannot exceed 63 chars + # https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab + # standard volume naming overhead is 46 chars + # datasetParentName should therefore be 17 chars or less when using TrueNAS 12 or below + datasetParentName: tank/k8s/b/vols + # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap + # they may be siblings, but neither should be nested in the other + # do NOT comment this option out even if you don't plan to use snapshots, just leave it with dummy value + detachedSnapshotsDatasetParentName: tanks/k8s/b/snaps + # "" (inherit), lz4, gzip-9, etc + zvolCompression: + # "" (inherit), on, off, verify + zvolDedup: + zvolEnableReservation: false + # 512, 1K, 2K, 4K, 8K, 16K, 64K, 128K default is 16K + zvolBlocksize: + +nvmeof: + # these are for the node/client aspect + transports: + - tcp://server:port + #- "tcp://127.0.0.1:4420?host-iface=eth0" + #- "tcp://[2001:123:456::1]:4420" + #- "rdma://127.0.0.1:4420" + #- "fc://[nn-0x203b00a098cbcac6:pn-0x203d00a098cbcac6]" + + # MUST ensure uniqueness + # full iqn limit is 223 bytes, plan accordingly + # default is "{{ name }}" + #nameTemplate: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}" + namePrefix: csi- + nameSuffix: "-clustera" + + # port IDs to associate to the newly created subsystem + # the ports should be created as a pre-req to using the driver, democratic-csi does NOT manage the ports + # + # http:///api/docs/current/api_methods_nvmet.port.create.html + # + # curl -v 'http://username:password@IP/api/v2.0/nvmet/port' + # + # curl -v 'http://username:password@IP/api/v2.0/nvmet/port' \ + # --header "Content-Type: application/json" \ + # --request POST \ + # --data '{"addr_trtype": "TCP","addr_trsvcid": 4420,"addr_traddr": "","addr_adrfam": "IPV4"}' + ports: + - + + # http:///api/docs/current/api_methods_nvmet.subsys.create.html + subsystemTemplate: + pi_enable: true + qid_max: + ieee_oui: + ana: + + # http:///api/docs/current/api_methods_nvmet.namespace.create.html + # currently none of the fields can be tweaked so leave empty for now + namespaceTemplate: diff --git a/examples/freenas-nvmeof.yaml b/examples/freenas-nvmeof.yaml new file mode 100644 index 0000000..1af5c61 --- /dev/null +++ b/examples/freenas-nvmeof.yaml @@ -0,0 +1,104 @@ +driver: freenas-nvmeof +instance_id: +httpConnection: + protocol: http + host: server address + port: 80 + # use only 1 of apiKey or username/password + # if both are present, apiKey is preferred + # apiKey is only available starting in TrueNAS-12 + #apiKey: + username: root + password: + allowInsecure: true + # use apiVersion 2 for TrueNAS-12 and up (will work on 11.x in some scenarios as well) + # leave unset for auto-detection + #apiVersion: 2 +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 + # + # leave paths unset for auto-detection + # 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: + # "org.freenas:description": "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}/{{ parameters.[csi.storage.k8s.io/pvc/name] }}" + # "org.freenas:test": "{{ parameters.foo }}" + # "org.freenas:test2": "some value" + + # total volume name (zvol//) length cannot exceed 63 chars + # https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab + # standard volume naming overhead is 46 chars + # datasetParentName should therefore be 17 chars or less when using TrueNAS 12 or below + datasetParentName: tank/k8s/b/vols + # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap + # they may be siblings, but neither should be nested in the other + # do NOT comment this option out even if you don't plan to use snapshots, just leave it with dummy value + detachedSnapshotsDatasetParentName: tanks/k8s/b/snaps + # "" (inherit), lz4, gzip-9, etc + zvolCompression: + # "" (inherit), on, off, verify + zvolDedup: + zvolEnableReservation: false + # 512, 1K, 2K, 4K, 8K, 16K, 64K, 128K default is 16K + zvolBlocksize: + +nvmeof: + # these are for the node/client aspect + transports: + - tcp://server:port + #- "tcp://127.0.0.1:4420?host-iface=eth0" + #- "tcp://[2001:123:456::1]:4420" + #- "rdma://127.0.0.1:4420" + #- "fc://[nn-0x203b00a098cbcac6:pn-0x203d00a098cbcac6]" + + # MUST ensure uniqueness + # full iqn limit is 223 bytes, plan accordingly + # default is "{{ name }}" + #nameTemplate: "{{ parameters.[csi.storage.k8s.io/pvc/namespace] }}-{{ parameters.[csi.storage.k8s.io/pvc/name] }}" + namePrefix: csi- + nameSuffix: "-clustera" + + # port IDs to associate to the newly created subsystem + # the ports should be created as a pre-req to using the driver, democratic-csi does NOT manage the ports + # + # http:///api/docs/current/api_methods_nvmet.port.create.html + # + # curl -v 'http://username:password@IP/api/v2.0/nvmet/port' + # + # curl -v 'http://username:password@IP/api/v2.0/nvmet/port' \ + # --header "Content-Type: application/json" \ + # --request POST \ + # --data '{"addr_trtype": "TCP","addr_trsvcid": 4420,"addr_traddr": "","addr_adrfam": "IPV4"}' + ports: + - + + # http:///api/docs/current/api_methods_nvmet.subsys.create.html + subsystemTemplate: + pi_enable: true + qid_max: + ieee_oui: + ana: + + # http:///api/docs/current/api_methods_nvmet.namespace.create.html + # currently none of the fields can be tweaked so leave empty for now + namespaceTemplate: diff --git a/examples/vhd-ephemeral-inline-pod-windows.yaml b/examples/vhd-ephemeral-inline-pod-windows.yaml new file mode 100644 index 0000000..90d5148 --- /dev/null +++ b/examples/vhd-ephemeral-inline-pod-windows.yaml @@ -0,0 +1,30 @@ +kind: Pod +apiVersion: v1 +metadata: + name: some-vhd-pod-windows +spec: + nodeSelector: + kubernetes.io/os: windows + containers: + - name: hello + image: mcr.microsoft.com/windows/servercore:ltsc2022 + command: + - powershell.exe + - -command + - while ($true) { Start-Sleep -Seconds 1 } + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "500m" + volumeMounts: + - name: vhd + mountPath: /mnt/vhd + volumes: + - name: vhd + csi: + driver: org.democratic-csi.vhd-ephemeral-inline + volumeAttributes: + vhd.parentPath: "C:\\some\\host\\path\\to\\SampleDisk.vhdx" diff --git a/examples/vhd-ephemeral-inline.yaml b/examples/vhd-ephemeral-inline.yaml new file mode 100644 index 0000000..b946bdd --- /dev/null +++ b/examples/vhd-ephemeral-inline.yaml @@ -0,0 +1,3 @@ +driver: vhd-ephemeral-inline +vhd: + nameTemplate: "csi-ephemeral-inline-{{ volume_id }}" diff --git a/src/driver/controller-zfs/index.js b/src/driver/controller-zfs/index.js index 0092a9c..7017c12 100644 --- a/src/driver/controller-zfs/index.js +++ b/src/driver/controller-zfs/index.js @@ -3,6 +3,7 @@ const { CsiBaseDriver } = require("../index"); const { GrpcError, grpc } = require("../../utils/grpc"); const GeneralUtils = require("../../utils/general"); const getLargestNumber = require("../../utils/general").getLargestNumber; +const Mount = require("../../utils/mount").Mount; const Handlebars = require("handlebars"); const uuidv4 = require("uuid").v4; @@ -1193,10 +1194,21 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { properties = properties[datasetName]; driver.ctx.logger.debug("zfs props data: %j", properties); + // get mountpoint + let mountpoint = properties.mountpoint.value; + if (mountpoint == "legacy") { + let mount = new Mount(); + let mounts = await mount.getDeviceMounts(datasetName); + + if (mounts.filesystems[0]) { + mountpoint = mounts.filesystems[0].target; + } + } + // set mode if (driverOptions.zfs.datasetPermissionsMode) { await driver.setFilesystemMode( - properties.mountpoint.value, + mountpoint, driverOptions.zfs.datasetPermissionsMode ); } @@ -1209,7 +1221,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { .length > 0 ) { await driver.setFilesystemOwnership( - properties.mountpoint.value, + mountpoint, driverOptions.zfs.datasetPermissionsUser, driverOptions.zfs.datasetPermissionsGroup ); @@ -1225,10 +1237,7 @@ class ControllerZfsBaseDriver extends CsiBaseDriver { "setfacl" ); for (const acl of driverOptions.zfs.datasetPermissionsAcls) { - command = execClient.buildCommand(aclBinary, [ - acl, - properties.mountpoint.value, - ]); + command = execClient.buildCommand(aclBinary, [acl, mountpoint]); if ((await this.getWhoAmI()) != "root") { command = (await this.getSudoPath()) + " " + command; } diff --git a/src/driver/ephemeral-inline-containerd-oci/index.js b/src/driver/ephemeral-inline-containerd-oci/index.js index 4542f2d..c128972 100644 --- a/src/driver/ephemeral-inline-containerd-oci/index.js +++ b/src/driver/ephemeral-inline-containerd-oci/index.js @@ -115,6 +115,10 @@ class EphemeralInlineContainerDOciDriver extends CsiBaseDriver { } } + /** + * TODO: add Probe here with ctr check to ensure socket is alive + */ + /** * * @returns CTR @@ -273,7 +277,7 @@ class EphemeralInlineContainerDOciDriver extends CsiBaseDriver { // create publish directory if (!fs.existsSync(target_path)) { - await fs.mkdirSync(target_path, { recursive: true }); + fs.mkdirSync(target_path, { recursive: true }); } if (process.platform != "win32") { diff --git a/src/driver/index.js b/src/driver/index.js index cfd385d..664e34c 100644 --- a/src/driver/index.js +++ b/src/driver/index.js @@ -3800,10 +3800,14 @@ class CsiBaseDriver { if (!volume_path) { throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_path`); } + const block_path = volume_path + "/block_device"; const capacity_range = call.request.capacity_range; const volume_capability = call.request.volume_capability; + // placeholder + let capacity_bytes; + switch (driver.__getNodeOsDriver()) { case NODE_OS_DRIVER_POSIX: if ( @@ -3864,6 +3868,7 @@ class CsiBaseDriver { await GeneralUtils.sleep(2000); } + // is_formatted = false; if (is_formatted && access_type == "mount") { fs_info = await filesystem.getDeviceFilesystemInfo(device); fs_type = fs_info.type; @@ -3898,13 +3903,20 @@ class CsiBaseDriver { ); } } + + result = await mount.getMountDetails(device_path, ["size"]); + capacity_bytes = result.size; } else { //block device unformatted - return {}; + result = await filesystem.getBlockDevice(device); + capacity_bytes = result.size; + return { capacity_bytes }; } } else { // not block device - return {}; + result = await mount.getMountDetails(device_path, ["size"]); + capacity_bytes = result.size; + return { capacity_bytes }; } break; @@ -4069,7 +4081,7 @@ class CsiBaseDriver { ); } - return {}; + return { capacity_bytes }; } } diff --git a/src/utils/mount.js b/src/utils/mount.js index f6cd787..5729bbe 100644 --- a/src/utils/mount.js +++ b/src/utils/mount.js @@ -94,6 +94,37 @@ class Mount { return true; } + /** + * findmnt --source --output source,target,fstype,label,options,avail,size,used -b -J + * + * @param {*} device + */ + async getDeviceMounts(device) { + const mount = this; + const filesystem = await mount.getFilesystemInstance(); + if (device.startsWith("/")) { + device = await filesystem.realpath(device); + } + + let args = []; + args = args.concat(["--source", device]); + args = args.concat(FINDMNT_COMMON_OPTIONS); + let result; + + try { + result = await mount.exec(mount.options.paths.findmnt, args); + } catch (err) { + // no results + if (err.code == 1) { + return { filesystems: [] }; + } else { + throw err; + } + } + + return JSON.parse(result.stdout); + } + /** * findmnt --mountpoint / --output source,target,fstype,label,options,avail,size,used -b -J * From de9fab30373c562bf9388a9ecb63c86d6e0540d4 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Tue, 6 Jan 2026 13:57:55 -0700 Subject: [PATCH 54/55] use nanoserver still for windows Signed-off-by: Travis Glenn Hansen --- Dockerfile.Windows | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile.Windows b/Dockerfile.Windows index 8cf4eae..caf8ed6 100644 --- a/Dockerfile.Windows +++ b/Dockerfile.Windows @@ -76,8 +76,8 @@ COPY . . ###################### # actual image ###################### -#FROM mcr.microsoft.com/windows/nanoserver:${NANO_BASE_TAG} -FROM mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0 +FROM mcr.microsoft.com/windows/nanoserver:${NANO_BASE_TAG} +#FROM mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0 #SHELL ["cmd.exe", "/s" , "/c"] From b5be00c0748cbae251e661392cc7ab9fea335ac9 Mon Sep 17 00:00:00 2001 From: Travis Glenn Hansen Date: Wed, 7 Jan 2026 11:11:17 -0700 Subject: [PATCH 55/55] release v1.9.5 Signed-off-by: Travis Glenn Hansen --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8ce72b..5fbc853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# v1.9.5 + +Released 2026-01-07 + +- better support for nixos +- support SCALE-25.04 +- support SCALE-25.10 +- improved nvmeof support +- added `pcs` share strategy for `zfs-generic-iscsi` (see PR #464) +- added `freenas-nvmeof` and `freenas-api-nvmeof` driver to use the new nmveof features of TrueNAS 25.10+ +- support for ENV vars in the configuration yaml `${FOO}` will expand +- improved docker images +- bumped deps and bundled binaries +- support csi versions `v1.10.0` and `v1.11.0` +- new `containerd-oci-ephemeral-inline` driver +- new `vhd-ephemeral-inline` driver +- bump `objectivefs` binary to `7.3` +- possible to set `snapshotProperties` on zfs snapshot just like `datasetProperties` +- limit container images to amd64 and arm64 for now +- improve concurrency logic in the `zf-generic-foo` drivers (see #504) + # v1.9.4 Release 2024-07-06