Merge pull request #88 from democratic-csi/next
add smb-client driver, share code with nfs-client driver
This commit is contained in:
		
						commit
						72b29c860b
					
				|  | @ -1,11 +1,16 @@ | ||||||
| #!/bin/bash | #!/bin/bash | ||||||
| 
 | 
 | ||||||
| echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin | echo "$DOCKER_PASSWORD" | docker login         -u "$DOCKER_USERNAME" --password-stdin | ||||||
|  | echo "$GHCR_PASSWORD"   | docker login ghcr.io -u "$GHCR_USERNAME"   --password-stdin | ||||||
| 
 | 
 | ||||||
| export DOCKER_ORG="democraticcsi" | export DOCKER_ORG="democraticcsi" | ||||||
| export DOCKER_PROJECT="democratic-csi" | export DOCKER_PROJECT="democratic-csi" | ||||||
| export DOCKER_REPO="${DOCKER_ORG}/${DOCKER_PROJECT}" | export DOCKER_REPO="${DOCKER_ORG}/${DOCKER_PROJECT}" | ||||||
| 
 | 
 | ||||||
|  | export GHCR_ORG="democratic-csi" | ||||||
|  | export GHCR_PROJECT="democratic-csi" | ||||||
|  | export GHCR_REPO="ghcr.io/${GHCR_ORG}/${GHCR_PROJECT}" | ||||||
|  | 
 | ||||||
| if [[ $GITHUB_REF == refs/tags/* ]]; then | if [[ $GITHUB_REF == refs/tags/* ]]; then | ||||||
|   export GIT_TAG=${GITHUB_REF#refs/tags/} |   export GIT_TAG=${GITHUB_REF#refs/tags/} | ||||||
| else | else | ||||||
|  | @ -13,12 +18,12 @@ else | ||||||
| fi | fi | ||||||
| 
 | 
 | ||||||
| if [[ -n "${GIT_TAG}" ]]; then | if [[ -n "${GIT_TAG}" ]]; then | ||||||
|   docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${GIT_TAG} . |   docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${GIT_TAG} -t ${GHCR_REPO}:${GIT_TAG} . | ||||||
| elif [[ -n "${GIT_BRANCH}" ]]; then | elif [[ -n "${GIT_BRANCH}" ]]; then | ||||||
|   if [[ "${GIT_BRANCH}" == "master" ]]; then |   if [[ "${GIT_BRANCH}" == "master" ]]; then | ||||||
|     docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:latest . |     docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:latest -t ${GHCR_REPO}:latest . | ||||||
|   else |   else | ||||||
|     docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${GIT_BRANCH} . |     docker buildx build --progress plain --pull --push --platform "${DOCKER_BUILD_PLATFORM}" -t ${DOCKER_REPO}:${GIT_BRANCH} -t ${GHCR_REPO}:${GIT_BRANCH} . | ||||||
|   fi |   fi | ||||||
| else | else | ||||||
|   : |   : | ||||||
|  |  | ||||||
|  | @ -34,5 +34,7 @@ jobs: | ||||||
|         env: |         env: | ||||||
|           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} |           DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} | ||||||
|           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} |           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | ||||||
|  |           GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }} | ||||||
|  |           GHCR_PASSWORD: ${{ secrets.GHCR_PASSWORD }} | ||||||
|           DOCKER_CLI_EXPERIMENTAL: enabled |           DOCKER_CLI_EXPERIMENTAL: enabled | ||||||
|           DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64,linux/arm/v7 |           DOCKER_BUILD_PLATFORM: linux/amd64,linux/arm64,linux/arm/v7 | ||||||
|  |  | ||||||
							
								
								
									
										21
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										21
									
								
								CHANGELOG.md
								
								
								
								
							|  | @ -1,3 +1,24 @@ | ||||||
|  | # v1.3.0 | ||||||
|  | 
 | ||||||
|  | Released 2021-09-02 | ||||||
|  | 
 | ||||||
|  | - use `ghcr.io` for images as well as docker hub (#90) | ||||||
|  | - introduce api-only drivers for freenas (`freenas-api-*`) | ||||||
|  | - `smb-client` driver which creates folders on an smb share | ||||||
|  | - `lustre-client` driver which creates folders on a lustre share | ||||||
|  |   attaching to various volumes which have been pre-provisioned by the operator | ||||||
|  | - `synology-iscsi` driver | ||||||
|  | - various documentation improvements | ||||||
|  | - support for csi versions `1.4.0` and `1.5.0` | ||||||
|  | - reintroduce advanced options that allow control over `fsck` (#85) | ||||||
|  | - advanced options for customizing `mkfs` commands | ||||||
|  | - better handling of stale nfs connections | ||||||
|  | - do not log potentially sensitive data in mount commands | ||||||
|  | - timeouts on various commands to improve driver operations under adverse | ||||||
|  |   conditions | ||||||
|  | - various fixes and improvements throughout | ||||||
|  | - dependency bumps | ||||||
|  | 
 | ||||||
| # v1.2.0 | # v1.2.0 | ||||||
| 
 | 
 | ||||||
| Released 2021-05-12 | Released 2021-05-12 | ||||||
|  |  | ||||||
|  | @ -12,12 +12,12 @@ 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 |         && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8 | ||||||
| 
 | 
 | ||||||
| ENV LANG=en_US.utf8 | ENV LANG=en_US.utf8 | ||||||
| ENV NODE_VERSION=v12.20.0 | ENV NODE_VERSION=v12.22.6 | ||||||
| #ENV NODE_VERSION=v14.15.1 | #ENV NODE_VERSION=v14.15.1 | ||||||
| ENV NODE_ENV=production | ENV NODE_ENV=production | ||||||
| 
 | 
 | ||||||
| # install build deps | # install build deps | ||||||
| RUN apt-get update && apt-get install -y python make gcc g++ | RUN apt-get update && apt-get install -y python make cmake gcc g++ | ||||||
| 
 | 
 | ||||||
| # install node | # install node | ||||||
| RUN apt-get update && apt-get install -y wget xz-utils | RUN apt-get update && apt-get install -y wget xz-utils | ||||||
|  | @ -43,6 +43,8 @@ RUN rm -rf docker | ||||||
| ###################### | ###################### | ||||||
| FROM debian:10-slim | FROM debian:10-slim | ||||||
| 
 | 
 | ||||||
|  | LABEL org.opencontainers.image.source https://github.com/democratic-csi/democratic-csi | ||||||
|  | 
 | ||||||
| ENV DEBIAN_FRONTEND=noninteractive | ENV DEBIAN_FRONTEND=noninteractive | ||||||
| 
 | 
 | ||||||
| ARG TARGETPLATFORM | ARG TARGETPLATFORM | ||||||
|  |  | ||||||
							
								
								
									
										31
									
								
								README.md
								
								
								
								
							
							
						
						
									
										31
									
								
								README.md
								
								
								
								
							|  | @ -18,13 +18,21 @@ have access to resizing, snapshots, clones, etc functionality. | ||||||
|   - `freenas-nfs` (manages zfs datasets to share over nfs) |   - `freenas-nfs` (manages zfs datasets to share over nfs) | ||||||
|   - `freenas-iscsi` (manages zfs zvols to share over iscsi) |   - `freenas-iscsi` (manages zfs zvols to share over iscsi) | ||||||
|   - `freenas-smb` (manages zfs datasets to share over smb) |   - `freenas-smb` (manages zfs datasets to share over smb) | ||||||
|  |   - `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) | ||||||
|   - `zfs-generic-nfs` (works with any ZoL installation...ie: Ubuntu) |   - `zfs-generic-nfs` (works with any ZoL installation...ie: Ubuntu) | ||||||
|   - `zfs-generic-iscsi` (works with any ZoL installation...ie: Ubuntu) |   - `zfs-generic-iscsi` (works with any ZoL installation...ie: Ubuntu) | ||||||
|   - `zfs-local-ephemeral-inline` (provisions node-local zfs datasets) |   - `zfs-local-ephemeral-inline` (provisions node-local zfs datasets) | ||||||
|  |   - `synology-iscsi` experimental (manages volumes to share over iscsi) | ||||||
|  |   - `lustre-client` (crudely provisions storage using a shared lustre | ||||||
|  |     share/directory for all volumes) | ||||||
|   - `nfs-client` (crudely provisions storage using a shared nfs share/directory |   - `nfs-client` (crudely provisions storage using a shared nfs share/directory | ||||||
|     for all volumes) |     for all volumes) | ||||||
|   - `node-manual` (allows connecting to manually created smb, nfs, and iscsi |   - `smb-client` (crudely provisions storage using a shared smb share/directory | ||||||
|     volumes, see sample PVs in the `examples` directory) |     for all volumes) | ||||||
|  |   - `node-manual` (allows connecting to manually created smb, nfs, lustre, and | ||||||
|  |     iscsi volumes, see sample PVs in the `examples` directory) | ||||||
| - framework for developing `csi` drivers | - framework for developing `csi` drivers | ||||||
| 
 | 
 | ||||||
| If you have any interest in providing a `csi` driver, simply open an issue to | If you have any interest in providing a `csi` driver, simply open an issue to | ||||||
|  | @ -40,11 +48,13 @@ Predominantly 3 things are needed: | ||||||
| - deploy the driver into the cluster (`helm` chart provided with sample | - deploy the driver into the cluster (`helm` chart provided with sample | ||||||
|   `values.yaml`) |   `values.yaml`) | ||||||
| 
 | 
 | ||||||
| ## Guides | ## Community Guides | ||||||
| 
 | 
 | ||||||
| - https://jonathangazeley.com/2021/01/05/using-truenas-to-provide-persistent-storage-for-kubernetes/ | - https://jonathangazeley.com/2021/01/05/using-truenas-to-provide-persistent-storage-for-kubernetes/ | ||||||
| - https://gist.github.com/admun/4372899f20421a947b7544e5fc9f9117 (migrating | - https://gist.github.com/admun/4372899f20421a947b7544e5fc9f9117 (migrating | ||||||
|   from `nfs-client-provisioner` to `democratic-csi`) |   from `nfs-client-provisioner` to `democratic-csi`) | ||||||
|  | - https://gist.github.com/deefdragon/d58a4210622ff64088bd62a5d8a4e8cc | ||||||
|  |   (migrating between storage classes using `velero`) | ||||||
| 
 | 
 | ||||||
| ## Node Prep | ## Node Prep | ||||||
| 
 | 
 | ||||||
|  | @ -135,11 +145,16 @@ necessary. | ||||||
| 
 | 
 | ||||||
| Server preparation depends slightly on which `driver` you are using. | Server preparation depends slightly on which `driver` you are using. | ||||||
| 
 | 
 | ||||||
| ### FreeNAS (freenas-nfs, freenas-iscsi, freenas-smb) | ### FreeNAS (freenas-nfs, freenas-iscsi, freenas-smb, freenas-api-nfs, freenas-api-iscsi, freenas-api-smb) | ||||||
| 
 | 
 | ||||||
| The recommended version of FreeNAS is 12.0-U2+, however the driver should work | The recommended version of FreeNAS is 12.0-U2+, however the driver should work | ||||||
| with much older versions as well. | with much older versions as well. | ||||||
| 
 | 
 | ||||||
|  | The various `freenas-api-*` drivers are currently EXPERIMENTAL and can only be | ||||||
|  | used with SCALE 21.08+. Fundamentally these drivers remove the need for `ssh` | ||||||
|  | connections and do all operations entirely with the TrueNAS api. With that in | ||||||
|  | mind, any ssh/shell/etc requirements below can be safely ignored. | ||||||
|  | 
 | ||||||
| Ensure the following services are configurged and running: | Ensure the following services are configurged and running: | ||||||
| 
 | 
 | ||||||
| - ssh (if you use a password for authentication make sure it is allowed) | - ssh (if you use a password for authentication make sure it is allowed) | ||||||
|  | @ -172,6 +187,7 @@ non-`root` user when connecting to the FreeNAS server: | ||||||
|   ``` |   ``` | ||||||
|   csi ALL=(ALL) NOPASSWD:ALL |   csi ALL=(ALL) NOPASSWD:ALL | ||||||
|   ``` |   ``` | ||||||
|  | 
 | ||||||
|   (note this can get reset by FreeNAS if you alter the user via the |   (note this can get reset by FreeNAS if you alter the user via the | ||||||
|   GUI later) |   GUI later) | ||||||
| 
 | 
 | ||||||
|  | @ -203,6 +219,10 @@ Ensure ssh and zfs is installed on the nfs/iscsi server and that you have instal | ||||||
| - `sudo yum install targetcli -y` | - `sudo yum install targetcli -y` | ||||||
| - `sudo apt-get -y install targetcli-fb` | - `sudo apt-get -y install targetcli-fb` | ||||||
| 
 | 
 | ||||||
|  | ### Synology (synology-iscsi) | ||||||
|  | 
 | ||||||
|  | Ensure iscsi manager has been installed and is generally setup/configured. | ||||||
|  | 
 | ||||||
| ## Helm Installation | ## Helm Installation | ||||||
| 
 | 
 | ||||||
| ``` | ``` | ||||||
|  | @ -247,6 +267,9 @@ microk8s helm upgrade \ | ||||||
|   zfs-nfs democratic-csi/democratic-csi |   zfs-nfs democratic-csi/democratic-csi | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | - microk8s - `/var/snap/microk8s/common/var/lib/kubelet` | ||||||
|  | - pivotal - `/var/vcap/data/kubelet` | ||||||
|  | 
 | ||||||
| ### openshift | ### openshift | ||||||
| 
 | 
 | ||||||
| `democratic-csi` generally works fine with openshift. Some special parameters | `democratic-csi` generally works fine with openshift. Some special parameters | ||||||
|  |  | ||||||
|  | @ -32,7 +32,16 @@ const args = require("yargs") | ||||||
|   }) |   }) | ||||||
|   .option("csi-version", { |   .option("csi-version", { | ||||||
|     describe: "versin of the csi spec to load", |     describe: "versin of the csi spec to load", | ||||||
|     choices: ["0.2.0", "0.3.0", "1.0.0", "1.1.0", "1.2.0", "1.3.0"], |     choices: [ | ||||||
|  |       "0.2.0", | ||||||
|  |       "0.3.0", | ||||||
|  |       "1.0.0", | ||||||
|  |       "1.1.0", | ||||||
|  |       "1.2.0", | ||||||
|  |       "1.3.0", | ||||||
|  |       "1.4.0", | ||||||
|  |       "1.5.0", | ||||||
|  |     ], | ||||||
|   }) |   }) | ||||||
|   .demandOption(["csi-version"], "csi-version is required") |   .demandOption(["csi-version"], "csi-version is required") | ||||||
|   .option("csi-name", { |   .option("csi-name", { | ||||||
|  |  | ||||||
|  | @ -10,7 +10,16 @@ const args = require("yargs") | ||||||
|   .usage("$0 [options]") |   .usage("$0 [options]") | ||||||
|   .option("csi-version", { |   .option("csi-version", { | ||||||
|     describe: "versin of the csi spec to load", |     describe: "versin of the csi spec to load", | ||||||
|     choices: ["0.2.0", "0.3.0", "1.0.0", "1.1.0", "1.2.0", "1.3.0"], |     choices: [ | ||||||
|  |       "0.2.0", | ||||||
|  |       "0.3.0", | ||||||
|  |       "1.0.0", | ||||||
|  |       "1.1.0", | ||||||
|  |       "1.2.0", | ||||||
|  |       "1.3.0", | ||||||
|  |       "1.4.0", | ||||||
|  |       "1.5.0", | ||||||
|  |     ], | ||||||
|   }) |   }) | ||||||
|   .demandOption(["csi-version"], "csi-version is required") |   .demandOption(["csi-version"], "csi-version is required") | ||||||
|   .option("csi-address", { |   .option("csi-address", { | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,85 @@ | ||||||
|  | driver: freenas-api-iscsi | ||||||
|  | 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/<datasetParentName>/<pvc name>) 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 | ||||||
|  |   datasetParentName: tank/k8s/b/vols | ||||||
|  |   # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap | ||||||
|  |   # they may be siblings, but neither should be nested in the other  | ||||||
|  |   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: | ||||||
|  | iscsi: | ||||||
|  |   targetPortal: "server[:port]" | ||||||
|  |   # for multipath | ||||||
|  |   targetPortals: [] # [ "server[:port]", "server[:port]", ... ] | ||||||
|  |   # leave empty to omit usage of -I with iscsiadm | ||||||
|  |   interface: | ||||||
|  | 
 | ||||||
|  |   # 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" | ||||||
|  |   # add as many as needed | ||||||
|  |   targetGroups: | ||||||
|  |     # get the correct ID from the "portal" section in the UI | ||||||
|  |     - targetGroupPortalGroup: 1 | ||||||
|  |       # get the correct ID from the "initiators" section in the UI | ||||||
|  |       targetGroupInitiatorGroup: 1 | ||||||
|  |       # None, CHAP, or CHAP Mutual | ||||||
|  |       targetGroupAuthType: None | ||||||
|  |       # get the correct ID from the "Authorized Access" section of the UI | ||||||
|  |       # only required if using Chap | ||||||
|  |       targetGroupAuthGroup: | ||||||
|  | 
 | ||||||
|  |   extentInsecureTpc: true | ||||||
|  |   extentXenCompat: false | ||||||
|  |   extentDisablePhysicalBlocksize: true | ||||||
|  |   # 512, 1024, 2048, or 4096, | ||||||
|  |   extentBlocksize: 512 | ||||||
|  |   # "" (let FreeNAS decide, currently defaults to SSD), Unknown, SSD, 5400, 7200, 10000, 15000 | ||||||
|  |   extentRpm: "SSD" | ||||||
|  |   # 0-100 (0 == ignore) | ||||||
|  |   extentAvailThreshold: 0 | ||||||
|  | @ -0,0 +1,58 @@ | ||||||
|  | driver: freenas-api-nfs | ||||||
|  | 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" | ||||||
|  | 
 | ||||||
|  |   datasetParentName: tank/k8s/a/vols | ||||||
|  |   # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap | ||||||
|  |   # they may be siblings, but neither should be nested in the other | ||||||
|  |   detachedSnapshotsDatasetParentName: tank/k8s/a/snaps | ||||||
|  |   datasetEnableQuotas: true | ||||||
|  |   datasetEnableReservation: false | ||||||
|  |   datasetPermissionsMode: "0777" | ||||||
|  |   datasetPermissionsUser: 0 | ||||||
|  |   datasetPermissionsGroup: 0 | ||||||
|  |   #datasetPermissionsAcls: | ||||||
|  |   #- "-m everyone@:full_set:allow" | ||||||
|  |   #- "-m u:kube:full_set:allow" | ||||||
|  | 
 | ||||||
|  | nfs: | ||||||
|  |   shareHost: server address | ||||||
|  |   shareAlldirs: false | ||||||
|  |   shareAllowedHosts: [] | ||||||
|  |   shareAllowedNetworks: [] | ||||||
|  |   shareMaprootUser: root | ||||||
|  |   shareMaprootGroup: root | ||||||
|  |   shareMapallUser: "" | ||||||
|  |   shareMapallGroup: "" | ||||||
|  | @ -0,0 +1,77 @@ | ||||||
|  | driver: freenas-api-smb | ||||||
|  | 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" | ||||||
|  | 
 | ||||||
|  |   datasetProperties: | ||||||
|  |     aclmode: restricted | ||||||
|  |     casesensitivity: mixed | ||||||
|  | 
 | ||||||
|  |   datasetParentName: tank/k8s/a/vols | ||||||
|  |   # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap | ||||||
|  |   # they may be siblings, but neither should be nested in the other | ||||||
|  |   detachedSnapshotsDatasetParentName: tank/k8s/a/snaps | ||||||
|  |   datasetEnableQuotas: true | ||||||
|  |   datasetEnableReservation: false | ||||||
|  |   datasetPermissionsMode: "0777" | ||||||
|  |   datasetPermissionsUser: 0 | ||||||
|  |   datasetPermissionsGroup: 0 | ||||||
|  |   datasetPermissionsAcls: | ||||||
|  |   - "-m everyone@:full_set:allow" | ||||||
|  |   #- "-m u:kube:full_set:allow" | ||||||
|  | 
 | ||||||
|  | smb: | ||||||
|  |   shareHost: server address | ||||||
|  |   nameTemplate: "" | ||||||
|  |   namePrefix: "" | ||||||
|  |   nameSuffix: "" | ||||||
|  |    | ||||||
|  |   # if any of the shareFoo parameters do not work with your version of FreeNAS | ||||||
|  |   # simply comment the param (and use the configuration template if necessary) | ||||||
|  | 
 | ||||||
|  |   shareAuxiliaryConfigurationTemplate: | | ||||||
|  |     #guest ok = yes | ||||||
|  |     #guest only = yes | ||||||
|  |   shareHome: false | ||||||
|  |   shareAllowedHosts: [] | ||||||
|  |   shareDeniedHosts: [] | ||||||
|  |   #shareDefaultPermissions: true | ||||||
|  |   shareGuestOk: true | ||||||
|  |   #shareGuestOnly: true | ||||||
|  |   #shareShowHiddenFiles: true | ||||||
|  |   shareRecycleBin: true | ||||||
|  |   shareBrowsable: false | ||||||
|  |   shareAccessBasedEnumeration: true | ||||||
|  |   shareTimeMachine: false | ||||||
|  |   #shareStorageTask: | ||||||
|  | @ -60,8 +60,9 @@ zfs: | ||||||
|   # 512, 1K, 2K, 4K, 8K, 16K, 64K, 128K default is 16K |   # 512, 1K, 2K, 4K, 8K, 16K, 64K, 128K default is 16K | ||||||
|   zvolBlocksize: |   zvolBlocksize: | ||||||
| iscsi: | iscsi: | ||||||
|   targetPortal: "server:3261" |   targetPortal: "server[:port]" | ||||||
|   targetPortals: [] |   # for multipath | ||||||
|  |   targetPortals: [] # [ "server[:port]", "server[:port]", ... ] | ||||||
|   # leave empty to omit usage of -I with iscsiadm |   # leave empty to omit usage of -I with iscsiadm | ||||||
|   interface: |   interface: | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| driver: freenas-nfs | driver: freenas-smb | ||||||
| instance_id: | instance_id: | ||||||
| httpConnection: | httpConnection: | ||||||
|   protocol: http |   protocol: http | ||||||
|  |  | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | driver: lustre-client | ||||||
|  | instance_id: | ||||||
|  | lustre: | ||||||
|  |   shareHost: server address | ||||||
|  |   shareBasePath: "/some/path" | ||||||
|  |   # shareHost:shareBasePath should be mounted at this location in the controller container | ||||||
|  |   controllerBasePath: "/storage" | ||||||
|  |   dirPermissionsMode: "0777" | ||||||
|  |   dirPermissionsUser: root | ||||||
|  |   dirPermissionsGroup: wheel | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | # common options for the node service | ||||||
|  | 
 | ||||||
|  | node: | ||||||
|  |   mount: | ||||||
|  |     # should fsck be executed before mounting the fs | ||||||
|  |     checkFilesystem: | ||||||
|  |       xfs: | ||||||
|  |         enabled: false | ||||||
|  |         customOptions: [] | ||||||
|  |       ext4: | ||||||
|  |         enabled: false | ||||||
|  |         customOptions: [] | ||||||
|  |         customFilesystemOptions: [] | ||||||
|  |   format: | ||||||
|  |     xfs: | ||||||
|  |       customOptions: [] | ||||||
|  |       #- -K | ||||||
|  |       # ... | ||||||
|  |     ext4: | ||||||
|  |       customOptions: [] | ||||||
|  |       #- -E | ||||||
|  |       #- nodiscard | ||||||
|  |       # ... | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | driver: smb-client | ||||||
|  | instance_id: | ||||||
|  | smb: | ||||||
|  |   shareHost: server address | ||||||
|  |   shareBasePath: "someshare/path" | ||||||
|  |   # shareHost:shareBasePath should be mounted at this location in the controller container | ||||||
|  |   controllerBasePath: "/storage" | ||||||
|  |   dirPermissionsMode: "0777" | ||||||
|  |   dirPermissionsUser: root | ||||||
|  |   dirPermissionsGroup: wheel | ||||||
|  | @ -0,0 +1,89 @@ | ||||||
|  | driver: synology-iscsi | ||||||
|  | httpConnection: | ||||||
|  |   protocol: http | ||||||
|  |   host: server address | ||||||
|  |   port: 5000 | ||||||
|  |   username: admin | ||||||
|  |   password: password | ||||||
|  |   allowInsecure: true | ||||||
|  |   # should be uniqe across all installs to the same nas | ||||||
|  |   session: "democratic-csi" | ||||||
|  |   serialize: true | ||||||
|  | 
 | ||||||
|  | synology: | ||||||
|  |   # choose the proper volume for your system | ||||||
|  |   volume: /volume1 | ||||||
|  | 
 | ||||||
|  | iscsi: | ||||||
|  |   targetPortal: "server[:port]" | ||||||
|  |   # for multipath | ||||||
|  |   targetPortals: [] # [ "server[:port]", "server[:port]", ... ] | ||||||
|  |   # leave empty to omit usage of -I with iscsiadm | ||||||
|  |   interface: "" | ||||||
|  |   # can be whatever you would like | ||||||
|  |   baseiqn: "iqn.2000-01.com.synology:csi." | ||||||
|  | 
 | ||||||
|  |   # MUST ensure uniqueness | ||||||
|  |   # full iqn limit is 223 bytes, plan accordingly | ||||||
|  |   namePrefix: "" | ||||||
|  |   nameSuffix: "" | ||||||
|  | 
 | ||||||
|  |   # documented below are several blocks | ||||||
|  |   # pick the option appropriate for you based on what your backing fs is and desired features | ||||||
|  |   # you do not need to alter dev_attribs under normal circumstances but they may be altered in advanced use-cases | ||||||
|  |   lunTemplate: | ||||||
|  |     # btrfs thin provisioning | ||||||
|  |     type: "BLUN" | ||||||
|  |     # tpws = Hardware-assisted zeroing | ||||||
|  |     # caw = Hardware-assisted locking | ||||||
|  |     # 3pc = Hardware-assisted data transfer | ||||||
|  |     # tpu = Space reclamation | ||||||
|  |     # can_snapshot = Snapshot | ||||||
|  |     #dev_attribs: | ||||||
|  |     #- dev_attrib: emulate_tpws | ||||||
|  |     #  enable: 1 | ||||||
|  |     #- dev_attrib: emulate_caw | ||||||
|  |     #  enable: 1 | ||||||
|  |     #- dev_attrib: emulate_3pc | ||||||
|  |     #  enable: 1 | ||||||
|  |     #- dev_attrib: emulate_tpu | ||||||
|  |     #  enable: 0 | ||||||
|  |     #- dev_attrib: can_snapshot | ||||||
|  |     #  enable: 1 | ||||||
|  | 
 | ||||||
|  |     # btfs thick provisioning | ||||||
|  |     # only zeroing and locking supported | ||||||
|  |     #type: "BLUN_THICK" | ||||||
|  |     # tpws = Hardware-assisted zeroing | ||||||
|  |     # caw = Hardware-assisted locking | ||||||
|  |     #dev_attribs: | ||||||
|  |     #- dev_attrib: emulate_tpws | ||||||
|  |     #  enable: 1 | ||||||
|  |     #- dev_attrib: emulate_caw | ||||||
|  |     #  enable: 1 | ||||||
|  | 
 | ||||||
|  |     # ext4 thinn provisioning UI sends everything with enabled=0 | ||||||
|  |     #type: "THIN" | ||||||
|  | 
 | ||||||
|  |     # ext4 thin with advanced legacy features set | ||||||
|  |     # can only alter tpu (all others are set as enabled=1) | ||||||
|  |     #type: "ADV" | ||||||
|  |     #dev_attribs: | ||||||
|  |     #- dev_attrib: emulate_tpu | ||||||
|  |     #  enable: 1 | ||||||
|  | 
 | ||||||
|  |     # ext4 thick | ||||||
|  |     # can only alter caw | ||||||
|  |     #type: "FILE" | ||||||
|  |     #dev_attribs: | ||||||
|  |     #- dev_attrib: emulate_caw | ||||||
|  |     #  enable: 1 | ||||||
|  | 
 | ||||||
|  |   lunSnapshotTemplate: | ||||||
|  |     is_locked: true | ||||||
|  |     # https://kb.synology.com/en-me/DSM/tutorial/What_is_file_system_consistent_snapshot | ||||||
|  |     is_app_consistent: true | ||||||
|  | 
 | ||||||
|  |   targetTemplate: | ||||||
|  |     auth_type: 0 | ||||||
|  |     max_sessions: 0 | ||||||
|  | @ -73,8 +73,10 @@ iscsi: | ||||||
|         # mutual CHAP |         # mutual CHAP | ||||||
|         #mutual_userid: "baz" |         #mutual_userid: "baz" | ||||||
|         #mutual_password: "bar"   |         #mutual_password: "bar"   | ||||||
|   targetPortal: "server address" |   targetPortal: "server[:port]" | ||||||
|   targetPortals: [] |   # for multipath | ||||||
|  |   targetPortals: [] # [ "server[:port]", "server[:port]", ... ] | ||||||
|  |   # leave empty to omit usage of -I with iscsiadm | ||||||
|   interface: "" |   interface: "" | ||||||
| 
 | 
 | ||||||
|   # MUST ensure uniqueness |   # MUST ensure uniqueness | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|   "name": "democratic-csi", |   "name": "democratic-csi", | ||||||
|   "version": "1.2.0", |   "version": "1.3.0", | ||||||
|   "description": "kubernetes csi driver framework", |   "description": "kubernetes csi driver framework", | ||||||
|   "main": "bin/democratic-csi", |   "main": "bin/democratic-csi", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|  | @ -18,15 +18,18 @@ | ||||||
|     "url": "https://github.com/democratic-csi/democratic-csi.git" |     "url": "https://github.com/democratic-csi/democratic-csi.git" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |     "@grpc/grpc-js": "^1.3.6", | ||||||
|     "@grpc/proto-loader": "^0.6.0", |     "@grpc/proto-loader": "^0.6.0", | ||||||
|  |     "async-mutex": "^0.3.1", | ||||||
|     "bunyan": "^1.8.15", |     "bunyan": "^1.8.15", | ||||||
|     "grpc-uds": "^0.1.6", |     "grpc-uds": "^0.1.6", | ||||||
|     "handlebars": "^4.7.7", |     "handlebars": "^4.7.7", | ||||||
|     "js-yaml": "^4.0.0", |     "js-yaml": "^4.0.0", | ||||||
|  |     "lodash": "^4.17.21", | ||||||
|     "lru-cache": "^6.0.0", |     "lru-cache": "^6.0.0", | ||||||
|     "request": "^2.88.2", |     "request": "^2.88.2", | ||||||
|     "semver": "^7.3.4", |     "semver": "^7.3.4", | ||||||
|     "ssh2": "^0.8.9", |     "ssh2": "^1.1.0", | ||||||
|     "uri-js": "^4.4.1", |     "uri-js": "^4.4.1", | ||||||
|     "uuid": "^8.3.2", |     "uuid": "^8.3.2", | ||||||
|     "winston": "^3.3.3", |     "winston": "^3.3.3", | ||||||
|  |  | ||||||
|  | @ -0,0 +1,700 @@ | ||||||
|  | const { CsiBaseDriver } = require("../index"); | ||||||
|  | const { GrpcError, grpc } = require("../../utils/grpc"); | ||||||
|  | const cp = require("child_process"); | ||||||
|  | const semver = require("semver"); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Crude nfs-client driver which simply creates directories to be mounted | ||||||
|  |  * and uses rsync for cloning/snapshots | ||||||
|  |  */ | ||||||
|  | class ControllerClientCommonDriver 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) { | ||||||
|  |     const driver = this; | ||||||
|  |     this.ctx.logger.verbose("validating capabilities: %j", capabilities); | ||||||
|  | 
 | ||||||
|  |     let message = null; | ||||||
|  |     let fs_types = driver.getFsTypes(); | ||||||
|  |     //[{"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 && | ||||||
|  |         !fs_types.includes(capability.mount.fs_type) | ||||||
|  |       ) { | ||||||
|  |         message = `invalid fs_type ${capability.mount.fs_type}`; | ||||||
|  |         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", | ||||||
|  |           "MULTI_NODE_READER_ONLY", | ||||||
|  |           "MULTI_NODE_SINGLE_WRITER", | ||||||
|  |           "MULTI_NODE_MULTI_WRITER", | ||||||
|  |         ].includes(capability.access_mode.mode) | ||||||
|  |       ) { | ||||||
|  |         message = `invalid access_mode, ${capability.access_mode.mode}`; | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       return true; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return { valid, message }; | ||||||
|  |   } | ||||||
|  |   // share paths
 | ||||||
|  |   getShareBasePath() { | ||||||
|  |     let config_key = this.getConfigKey(); | ||||||
|  |     let path = this.options[config_key].shareBasePath; | ||||||
|  |     if (!path) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.FAILED_PRECONDITION, | ||||||
|  |         `invalid configuration: missing shareBasePath` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     path = path.replace(/\/$/, ""); | ||||||
|  |     if (!path) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.FAILED_PRECONDITION, | ||||||
|  |         `invalid configuration: missing shareBasePath` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return path; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // controller paths
 | ||||||
|  |   getControllerBasePath() { | ||||||
|  |     let config_key = this.getConfigKey(); | ||||||
|  |     let path = this.options[config_key].controllerBasePath; | ||||||
|  |     if (!path) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.FAILED_PRECONDITION, | ||||||
|  |         `invalid configuration: missing controllerBasePath` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     path = path.replace(/\/$/, ""); | ||||||
|  |     if (!path) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.FAILED_PRECONDITION, | ||||||
|  |         `invalid configuration: missing controllerBasePath` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return path; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // path helpers
 | ||||||
|  |   getVolumeExtraPath() { | ||||||
|  |     return "/v"; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getSnapshotExtraPath() { | ||||||
|  |     return "/s"; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getShareVolumeBasePath() { | ||||||
|  |     return this.getShareBasePath() + this.getVolumeExtraPath(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getShareSnapshotBasePath() { | ||||||
|  |     return this.getShareBasePath() + this.getSnapshotExtraPath(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getShareVolumePath(volume_id) { | ||||||
|  |     return this.getShareVolumeBasePath() + "/" + volume_id; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getShareSnapshotPath(snapshot_id) { | ||||||
|  |     return this.getShareSnapshotBasePath() + "/" + snapshot_id; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getControllerVolumeBasePath() { | ||||||
|  |     return this.getControllerBasePath() + this.getVolumeExtraPath(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getControllerSnapshotBasePath() { | ||||||
|  |     return this.getControllerBasePath() + this.getSnapshotExtraPath(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getControllerVolumePath(volume_id) { | ||||||
|  |     return this.getControllerVolumeBasePath() + "/" + volume_id; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getControllerSnapshotPath(snapshot_id) { | ||||||
|  |     return this.getControllerSnapshotBasePath() + "/" + snapshot_id; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   exec(command, args, options = {}) { | ||||||
|  |     args = args || []; | ||||||
|  | 
 | ||||||
|  |     let timeout; | ||||||
|  |     let stdout = ""; | ||||||
|  |     let stderr = ""; | ||||||
|  | 
 | ||||||
|  |     if (options.sudo) { | ||||||
|  |       args.unshift(command); | ||||||
|  |       command = "sudo"; | ||||||
|  |     } | ||||||
|  |     console.log("executing command: %s %s", command, args.join(" ")); | ||||||
|  |     const child = cp.spawn(command, args, options); | ||||||
|  | 
 | ||||||
|  |     let didTimeout = false; | ||||||
|  |     if (options && options.timeout) { | ||||||
|  |       timeout = setTimeout(() => { | ||||||
|  |         didTimeout = true; | ||||||
|  |         child.kill(options.killSignal || "SIGTERM"); | ||||||
|  |       }, options.timeout); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return new Promise((resolve, reject) => { | ||||||
|  |       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 }; | ||||||
|  |         if (timeout) { | ||||||
|  |           clearTimeout(timeout); | ||||||
|  |         } | ||||||
|  |         if (code) { | ||||||
|  |           reject(result); | ||||||
|  |         } else { | ||||||
|  |           resolve(result); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   stripTrailingSlash(s) { | ||||||
|  |     if (s.length > 1) { | ||||||
|  |       return s.replace(/\/$/, ""); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return s; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   stripLeadingSlash(s) { | ||||||
|  |     if (s.length > 1) { | ||||||
|  |       return s.replace(/^\/+/, ""); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return s; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async cloneDir(source_path, target_path) { | ||||||
|  |     await this.exec("mkdir", ["-p", target_path]); | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * trailing / is important | ||||||
|  |      * rsync -a /mnt/storage/s/foo/ /mnt/storage/v/PVC-111/ | ||||||
|  |      */ | ||||||
|  |     await this.exec("rsync", [ | ||||||
|  |       "-a", | ||||||
|  |       this.stripTrailingSlash(source_path) + "/", | ||||||
|  |       this.stripTrailingSlash(target_path) + "/", | ||||||
|  |     ]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getAvailableSpaceAtPath(path) { | ||||||
|  |     //df --output=avail /mnt/storage/
 | ||||||
|  |     //     Avail
 | ||||||
|  |     //1481334328
 | ||||||
|  | 
 | ||||||
|  |     const response = await this.exec("df", ["--output=avail", path]); | ||||||
|  | 
 | ||||||
|  |     return response.stdout.split("\n")[1].trim(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async deleteDir(path) { | ||||||
|  |     await this.exec("rm", ["-rf", path]); | ||||||
|  | 
 | ||||||
|  |     return; | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * trailing / is important | ||||||
|  |      * rsync -a /mnt/storage/s/foo/ /mnt/storage/v/PVC-111/ | ||||||
|  |      */ | ||||||
|  |     await this.exec("rsync", [ | ||||||
|  |       "-a", | ||||||
|  |       "--delete", | ||||||
|  |       this.stripTrailingSlash(empty_path) + "/", | ||||||
|  |       this.stripTrailingSlash(path) + "/", | ||||||
|  |     ]); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Create a volume doing in essence the following: | ||||||
|  |    * 1. create directory | ||||||
|  |    * | ||||||
|  |    * Should return 2 parameters | ||||||
|  |    * 1. `server` - host/ip of the nfs server | ||||||
|  |    * 2. `share` - path of the mount shared | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async CreateVolume(call) { | ||||||
|  |     const driver = this; | ||||||
|  | 
 | ||||||
|  |     let config_key = this.getConfigKey(); | ||||||
|  |     let name = call.request.name; | ||||||
|  |     let volume_content_source = call.request.volume_content_source; | ||||||
|  | 
 | ||||||
|  |     if (!name) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `volume name is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (call.request.volume_capabilities) { | ||||||
|  |       const result = this.assertCapabilities(call.request.volume_capabilities); | ||||||
|  |       if (result.valid !== true) { | ||||||
|  |         throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       call.request.capacity_range.required_bytes > 0 && | ||||||
|  |       call.request.capacity_range.limit_bytes > 0 && | ||||||
|  |       call.request.capacity_range.required_bytes > | ||||||
|  |         call.request.capacity_range.limit_bytes | ||||||
|  |     ) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.OUT_OF_RANGE, | ||||||
|  |         `required_bytes is greather than limit_bytes` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let capacity_bytes = | ||||||
|  |       call.request.capacity_range.required_bytes || | ||||||
|  |       call.request.capacity_range.limit_bytes; | ||||||
|  | 
 | ||||||
|  |     if (!capacity_bytes) { | ||||||
|  |       //should never happen, value must be set
 | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `volume capacity is required (either required_bytes or limit_bytes)` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // ensure *actual* capacity is not greater than limit
 | ||||||
|  |     if ( | ||||||
|  |       call.request.capacity_range.limit_bytes && | ||||||
|  |       call.request.capacity_range.limit_bytes > 0 && | ||||||
|  |       capacity_bytes > call.request.capacity_range.limit_bytes | ||||||
|  |     ) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.OUT_OF_RANGE, | ||||||
|  |         `required volume capacity is greater than limit` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const volume_path = driver.getControllerVolumePath(name); | ||||||
|  | 
 | ||||||
|  |     let response; | ||||||
|  |     let source_path; | ||||||
|  |     //let volume_content_source_snapshot_id;
 | ||||||
|  |     //let volume_content_source_volume_id;
 | ||||||
|  | 
 | ||||||
|  |     // create target dir
 | ||||||
|  |     response = await driver.exec("mkdir", ["-p", volume_path]); | ||||||
|  | 
 | ||||||
|  |     // create dataset
 | ||||||
|  |     if (volume_content_source) { | ||||||
|  |       switch (volume_content_source.type) { | ||||||
|  |         // must be available when adverstising CREATE_DELETE_SNAPSHOT
 | ||||||
|  |         // simply clone
 | ||||||
|  |         case "snapshot": | ||||||
|  |           source_path = driver.getControllerSnapshotPath( | ||||||
|  |             volume_content_source.snapshot.snapshot_id | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |         // must be available when adverstising CLONE_VOLUME
 | ||||||
|  |         // create snapshot first, then clone
 | ||||||
|  |         case "volume": | ||||||
|  |           source_path = driver.getControllerVolumePath( | ||||||
|  |             volume_content_source.volume.volume_id | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           throw new GrpcError( | ||||||
|  |             grpc.status.INVALID_ARGUMENT, | ||||||
|  |             `invalid volume_content_source type: ${volume_content_source.type}` | ||||||
|  |           ); | ||||||
|  |           break; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       driver.ctx.logger.debug("controller source path: %s", source_path); | ||||||
|  |       response = await driver.cloneDir(source_path, volume_path); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // set mode
 | ||||||
|  |     if (this.options[config_key].dirPermissionsMode) { | ||||||
|  |       driver.ctx.logger.verbose( | ||||||
|  |         "setting dir mode to: %s on dir: %s", | ||||||
|  |         this.options[config_key].dirPermissionsMode, | ||||||
|  |         volume_path | ||||||
|  |       ); | ||||||
|  |       response = await driver.exec("chmod", [ | ||||||
|  |         this.options[config_key].dirPermissionsMode, | ||||||
|  |         volume_path, | ||||||
|  |       ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // set ownership
 | ||||||
|  |     if ( | ||||||
|  |       this.options[config_key].dirPermissionsUser || | ||||||
|  |       this.options[config_key].dirPermissionsGroup | ||||||
|  |     ) { | ||||||
|  |       driver.ctx.logger.verbose( | ||||||
|  |         "setting ownership to: %s:%s on dir: %s", | ||||||
|  |         this.options[config_key].dirPermissionsUser, | ||||||
|  |         this.options[config_key].dirPermissionsGroup, | ||||||
|  |         volume_path | ||||||
|  |       ); | ||||||
|  |       response = await driver.exec("chown", [ | ||||||
|  |         (this.options[config_key].dirPermissionsUser | ||||||
|  |           ? this.options[config_key].dirPermissionsUser | ||||||
|  |           : "") + | ||||||
|  |           ":" + | ||||||
|  |           (this.options[config_key].dirPermissionsGroup | ||||||
|  |             ? this.options[config_key].dirPermissionsGroup | ||||||
|  |             : ""), | ||||||
|  |         volume_path, | ||||||
|  |       ]); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let volume_context = driver.getVolumeContext(name); | ||||||
|  | 
 | ||||||
|  |     volume_context["provisioner_driver"] = driver.options.driver; | ||||||
|  |     if (driver.options.instance_id) { | ||||||
|  |       volume_context["provisioner_driver_instance_id"] = | ||||||
|  |         driver.options.instance_id; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const res = { | ||||||
|  |       volume: { | ||||||
|  |         volume_id: name, | ||||||
|  |         //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0
 | ||||||
|  |         capacity_bytes: 0, | ||||||
|  |         content_source: volume_content_source, | ||||||
|  |         volume_context, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return res; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Delete a volume | ||||||
|  |    * | ||||||
|  |    * Deleting a volume consists of the following steps: | ||||||
|  |    * 1. delete directory | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async DeleteVolume(call) { | ||||||
|  |     const driver = this; | ||||||
|  | 
 | ||||||
|  |     let name = call.request.volume_id; | ||||||
|  | 
 | ||||||
|  |     if (!name) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `volume_id is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const volume_path = driver.getControllerVolumePath(name); | ||||||
|  |     await driver.deleteDir(volume_path); | ||||||
|  | 
 | ||||||
|  |     return {}; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async ControllerExpandVolume(call) { | ||||||
|  |     throw new GrpcError( | ||||||
|  |       grpc.status.UNIMPLEMENTED, | ||||||
|  |       `operation not supported by driver` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * TODO: consider volume_capabilities? | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async GetCapacity(call) { | ||||||
|  |     // really capacity is not used at all with nfs in this fashion, so no reason to enable
 | ||||||
|  |     // here even though it is technically feasible.
 | ||||||
|  |     throw new GrpcError( | ||||||
|  |       grpc.status.UNIMPLEMENTED, | ||||||
|  |       `operation not supported by driver` | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     const driver = this; | ||||||
|  | 
 | ||||||
|  |     if (call.request.volume_capabilities) { | ||||||
|  |       const result = this.assertCapabilities(call.request.volume_capabilities); | ||||||
|  | 
 | ||||||
|  |       if (result.valid !== true) { | ||||||
|  |         return { available_capacity: 0 }; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const available_capacity = await driver.getAvailableSpaceAtPath( | ||||||
|  |       driver.getControllerBasePath() | ||||||
|  |     ); | ||||||
|  |     return { available_capacity }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * TODO: check capability to ensure not asking about block volumes | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async ListVolumes(call) { | ||||||
|  |     throw new GrpcError( | ||||||
|  |       grpc.status.UNIMPLEMENTED, | ||||||
|  |       `operation not supported by driver` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async ListSnapshots(call) { | ||||||
|  |     throw new GrpcError( | ||||||
|  |       grpc.status.UNIMPLEMENTED, | ||||||
|  |       `operation not supported by driver` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async CreateSnapshot(call) { | ||||||
|  |     const driver = this; | ||||||
|  | 
 | ||||||
|  |     // both these are required
 | ||||||
|  |     let source_volume_id = call.request.source_volume_id; | ||||||
|  |     let name = call.request.name; | ||||||
|  | 
 | ||||||
|  |     if (!source_volume_id) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `snapshot source_volume_id is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!name) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `snapshot name is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     driver.ctx.logger.verbose("requested snapshot name: %s", name); | ||||||
|  | 
 | ||||||
|  |     let invalid_chars; | ||||||
|  |     invalid_chars = name.match(/[^a-z0-9_\-:.+]+/gi); | ||||||
|  |     if (invalid_chars) { | ||||||
|  |       invalid_chars = String.prototype.concat( | ||||||
|  |         ...new Set(invalid_chars.join("")) | ||||||
|  |       ); | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `snapshot name contains invalid characters: ${invalid_chars}` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // https://stackoverflow.com/questions/32106243/regex-to-remove-all-non-alpha-numeric-and-replace-spaces-with/32106277
 | ||||||
|  |     name = name.replace(/[^a-z0-9_\-:.+]+/gi, ""); | ||||||
|  | 
 | ||||||
|  |     driver.ctx.logger.verbose("cleansed snapshot name: %s", name); | ||||||
|  | 
 | ||||||
|  |     const snapshot_id = `${source_volume_id}-${name}`; | ||||||
|  |     const volume_path = driver.getControllerVolumePath(source_volume_id); | ||||||
|  |     const snapshot_path = driver.getControllerSnapshotPath(snapshot_id); | ||||||
|  | 
 | ||||||
|  |     await driver.cloneDir(volume_path, snapshot_path); | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       snapshot: { | ||||||
|  |         /** | ||||||
|  |          * The purpose of this field is to give CO guidance on how much space | ||||||
|  |          * is needed to create a volume from this snapshot. | ||||||
|  |          */ | ||||||
|  |         size_bytes: 0, | ||||||
|  |         snapshot_id, | ||||||
|  |         source_volume_id: source_volume_id, | ||||||
|  |         //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto
 | ||||||
|  |         creation_time: { | ||||||
|  |           seconds: Math.round(new Date().getTime() / 1000), | ||||||
|  |           nanos: 0, | ||||||
|  |         }, | ||||||
|  |         ready_to_use: true, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * In addition, if clones have been created from a snapshot, then they must | ||||||
|  |    * be destroyed before the snapshot can be destroyed. | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async DeleteSnapshot(call) { | ||||||
|  |     const driver = this; | ||||||
|  | 
 | ||||||
|  |     const snapshot_id = call.request.snapshot_id; | ||||||
|  | 
 | ||||||
|  |     if (!snapshot_id) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `snapshot_id is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const snapshot_path = driver.getControllerSnapshotPath(snapshot_id); | ||||||
|  |     await driver.deleteDir(snapshot_path); | ||||||
|  | 
 | ||||||
|  |     return {}; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @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.ControllerClientCommonDriver = ControllerClientCommonDriver; | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | const { ControllerClientCommonDriver } = require("../controller-client-common"); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Crude lustre-client driver which simply creates directories to be mounted | ||||||
|  |  * and uses rsync for cloning/snapshots | ||||||
|  |  */ | ||||||
|  | class ControllerLustreClientDriver extends ControllerClientCommonDriver { | ||||||
|  |   constructor(ctx, options) { | ||||||
|  |     super(...arguments); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getConfigKey() { | ||||||
|  |     return "lustre"; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getVolumeContext(name) { | ||||||
|  |     const driver = this; | ||||||
|  |     const config_key = driver.getConfigKey(); | ||||||
|  |     return { | ||||||
|  |       node_attach_driver: "lustre", | ||||||
|  |       server: this.options[config_key].shareHost, | ||||||
|  |       share: driver.getShareVolumePath(name), | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getFsTypes() { | ||||||
|  |     return ["lustre"]; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports.ControllerLustreClientDriver = ControllerLustreClientDriver; | ||||||
|  | @ -1,663 +1,30 @@ | ||||||
| const { CsiBaseDriver } = require("../index"); | const { ControllerClientCommonDriver } = require("../controller-client-common"); | ||||||
| const { GrpcError, grpc } = require("../../utils/grpc"); |  | ||||||
| const cp = require("child_process"); |  | ||||||
| const { Mount } = require("../../utils/mount"); |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Crude nfs-client driver which simply creates directories to be mounted |  * Crude nfs-client driver which simply creates directories to be mounted | ||||||
|  * and uses rsync for cloning/snapshots |  * and uses rsync for cloning/snapshots | ||||||
|  */ |  */ | ||||||
| class ControllerNfsClientDriver extends CsiBaseDriver { | class ControllerNfsClientDriver extends ControllerClientCommonDriver { | ||||||
|   constructor(ctx, options) { |   constructor(ctx, options) { | ||||||
|     super(...arguments); |     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 (!("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"
 |  | ||||||
|       ]; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   assertCapabilities(capabilities) { |   getConfigKey() { | ||||||
|     this.ctx.logger.verbose("validating capabilities: %j", capabilities); |     return "nfs"; | ||||||
| 
 |  | ||||||
|     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 && |  | ||||||
|         !["nfs"].includes(capability.mount.fs_type) |  | ||||||
|       ) { |  | ||||||
|         message = `invalid fs_type ${capability.mount.fs_type}`; |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       if ( |  | ||||||
|         ![ |  | ||||||
|           "UNKNOWN", |  | ||||||
|           "SINGLE_NODE_WRITER", |  | ||||||
|           "SINGLE_NODE_READER_ONLY", |  | ||||||
|           "MULTI_NODE_READER_ONLY", |  | ||||||
|           "MULTI_NODE_SINGLE_WRITER", |  | ||||||
|           "MULTI_NODE_MULTI_WRITER", |  | ||||||
|         ].includes(capability.access_mode.mode) |  | ||||||
|       ) { |  | ||||||
|         message = `invalid access_mode, ${capability.access_mode.mode}`; |  | ||||||
|         return false; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       return true; |  | ||||||
|     }); |  | ||||||
| 
 |  | ||||||
|     return { valid, message }; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // path helpers
 |   getVolumeContext(name) { | ||||||
|   getVolumeExtraPath() { |  | ||||||
|     return "/v"; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getSnapshotExtraPath() { |  | ||||||
|     return "/s"; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // share paths
 |  | ||||||
|   getShareBasePath() { |  | ||||||
|     let path = this.options.nfs.shareBasePath; |  | ||||||
|     if (!path) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.FAILED_PRECONDITION, |  | ||||||
|         `invalid configuration: missing shareBasePath` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     path = path.replace(/\/$/, ""); |  | ||||||
|     if (!path) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.FAILED_PRECONDITION, |  | ||||||
|         `invalid configuration: missing shareBasePath` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return path; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getShareVolumeBasePath() { |  | ||||||
|     return this.getShareBasePath() + this.getVolumeExtraPath(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getShareSnapshotBasePath() { |  | ||||||
|     return this.getShareBasePath() + this.getSnapshotExtraPath(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getShareVolumePath(volume_id) { |  | ||||||
|     return this.getShareVolumeBasePath() + "/" + volume_id; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getShareSnapshotPath(snapshot_id) { |  | ||||||
|     return this.getShareSnapshotBasePath() + "/" + snapshot_id; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   // controller paths
 |  | ||||||
|   getControllerBasePath() { |  | ||||||
|     let path = this.options.nfs.controllerBasePath; |  | ||||||
|     if (!path) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.FAILED_PRECONDITION, |  | ||||||
|         `invalid configuration: missing controllerBasePath` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     path = path.replace(/\/$/, ""); |  | ||||||
|     if (!path) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.FAILED_PRECONDITION, |  | ||||||
|         `invalid configuration: missing controllerBasePath` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return path; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getControllerVolumeBasePath() { |  | ||||||
|     return this.getControllerBasePath() + this.getVolumeExtraPath(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getControllerSnapshotBasePath() { |  | ||||||
|     return this.getControllerBasePath() + this.getSnapshotExtraPath(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getControllerVolumePath(volume_id) { |  | ||||||
|     return this.getControllerVolumeBasePath() + "/" + volume_id; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   getControllerSnapshotPath(snapshot_id) { |  | ||||||
|     return this.getControllerSnapshotBasePath() + "/" + snapshot_id; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   exec(command, args, options = {}) { |  | ||||||
|     args = args || []; |  | ||||||
| 
 |  | ||||||
|     let timeout; |  | ||||||
|     let stdout = ""; |  | ||||||
|     let stderr = ""; |  | ||||||
| 
 |  | ||||||
|     if (options.sudo) { |  | ||||||
|       args.unshift(command); |  | ||||||
|       command = "sudo"; |  | ||||||
|     } |  | ||||||
|     console.log("executing command: %s %s", command, args.join(" ")); |  | ||||||
|     const child = cp.spawn(command, args, options); |  | ||||||
| 
 |  | ||||||
|     let didTimeout = false; |  | ||||||
|     if (options && options.timeout) { |  | ||||||
|       timeout = setTimeout(() => { |  | ||||||
|         didTimeout = true; |  | ||||||
|         child.kill(options.killSignal || "SIGTERM"); |  | ||||||
|       }, options.timeout); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return new Promise((resolve, reject) => { |  | ||||||
|       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 }; |  | ||||||
|         if (timeout) { |  | ||||||
|           clearTimeout(timeout); |  | ||||||
|         } |  | ||||||
|         if (code) { |  | ||||||
|           reject(result); |  | ||||||
|         } else { |  | ||||||
|           resolve(result); |  | ||||||
|         } |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   stripTrailingSlash(s) { |  | ||||||
|     if (s.length > 1) { |  | ||||||
|       return s.replace(/\/$/, ""); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return s; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async cloneDir(source_path, target_path) { |  | ||||||
|     await this.exec("mkdir", ["-p", target_path]); |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * trailing / is important |  | ||||||
|      * rsync -a /mnt/storage/s/foo/ /mnt/storage/v/PVC-111/ |  | ||||||
|      */ |  | ||||||
|     await this.exec("rsync", [ |  | ||||||
|       "-a", |  | ||||||
|       this.stripTrailingSlash(source_path) + "/", |  | ||||||
|       this.stripTrailingSlash(target_path) + "/", |  | ||||||
|     ]); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async getAvailableSpaceAtPath(path) { |  | ||||||
|     //df --output=avail /mnt/storage/
 |  | ||||||
|     //     Avail
 |  | ||||||
|     //1481334328
 |  | ||||||
| 
 |  | ||||||
|     const response = await this.exec("df", ["--output=avail", path]); |  | ||||||
| 
 |  | ||||||
|     return response.stdout.split("\n")[1].trim(); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async deleteDir(path) { |  | ||||||
|     await this.exec("rm", ["-rf", path]); |  | ||||||
| 
 |  | ||||||
|     return; |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * trailing / is important |  | ||||||
|      * rsync -a /mnt/storage/s/foo/ /mnt/storage/v/PVC-111/ |  | ||||||
|      */ |  | ||||||
|     await this.exec("rsync", [ |  | ||||||
|       "-a", |  | ||||||
|       "--delete", |  | ||||||
|       this.stripTrailingSlash(empty_path) + "/", |  | ||||||
|       this.stripTrailingSlash(path) + "/", |  | ||||||
|     ]); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * Create a volume doing in essence the following: |  | ||||||
|    * 1. create directory |  | ||||||
|    * |  | ||||||
|    * Should return 2 parameters |  | ||||||
|    * 1. `server` - host/ip of the nfs server |  | ||||||
|    * 2. `share` - path of the mount shared |  | ||||||
|    * |  | ||||||
|    * @param {*} call |  | ||||||
|    */ |  | ||||||
|   async CreateVolume(call) { |  | ||||||
|     const driver = this; |     const driver = this; | ||||||
| 
 |     const config_key = driver.getConfigKey(); | ||||||
|     let name = call.request.name; |     return { | ||||||
|     let volume_content_source = call.request.volume_content_source; |  | ||||||
| 
 |  | ||||||
|     if (!name) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.INVALID_ARGUMENT, |  | ||||||
|         `volume name is required` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (call.request.volume_capabilities) { |  | ||||||
|       const result = this.assertCapabilities(call.request.volume_capabilities); |  | ||||||
|       if (result.valid !== true) { |  | ||||||
|         throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if ( |  | ||||||
|       call.request.capacity_range.required_bytes > 0 && |  | ||||||
|       call.request.capacity_range.limit_bytes > 0 && |  | ||||||
|       call.request.capacity_range.required_bytes > |  | ||||||
|         call.request.capacity_range.limit_bytes |  | ||||||
|     ) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.OUT_OF_RANGE, |  | ||||||
|         `required_bytes is greather than limit_bytes` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let capacity_bytes = |  | ||||||
|       call.request.capacity_range.required_bytes || |  | ||||||
|       call.request.capacity_range.limit_bytes; |  | ||||||
| 
 |  | ||||||
|     if (!capacity_bytes) { |  | ||||||
|       //should never happen, value must be set
 |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.INVALID_ARGUMENT, |  | ||||||
|         `volume capacity is required (either required_bytes or limit_bytes)` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // ensure *actual* capacity is not greater than limit
 |  | ||||||
|     if ( |  | ||||||
|       call.request.capacity_range.limit_bytes && |  | ||||||
|       call.request.capacity_range.limit_bytes > 0 && |  | ||||||
|       capacity_bytes > call.request.capacity_range.limit_bytes |  | ||||||
|     ) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.OUT_OF_RANGE, |  | ||||||
|         `required volume capacity is greater than limit` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const volume_path = driver.getControllerVolumePath(name); |  | ||||||
| 
 |  | ||||||
|     let response; |  | ||||||
|     let source_path; |  | ||||||
|     //let volume_content_source_snapshot_id;
 |  | ||||||
|     //let volume_content_source_volume_id;
 |  | ||||||
| 
 |  | ||||||
|     // create target dir
 |  | ||||||
|     response = await driver.exec("mkdir", ["-p", volume_path]); |  | ||||||
| 
 |  | ||||||
|     // create dataset
 |  | ||||||
|     if (volume_content_source) { |  | ||||||
|       switch (volume_content_source.type) { |  | ||||||
|         // must be available when adverstising CREATE_DELETE_SNAPSHOT
 |  | ||||||
|         // simply clone
 |  | ||||||
|         case "snapshot": |  | ||||||
|           source_path = driver.getControllerSnapshotPath( |  | ||||||
|             volume_content_source.snapshot.snapshot_id |  | ||||||
|           ); |  | ||||||
|           break; |  | ||||||
|         // must be available when adverstising CLONE_VOLUME
 |  | ||||||
|         // create snapshot first, then clone
 |  | ||||||
|         case "volume": |  | ||||||
|           source_path = driver.getControllerVolumePath( |  | ||||||
|             volume_content_source.volume.volume_id |  | ||||||
|           ); |  | ||||||
|           break; |  | ||||||
|         default: |  | ||||||
|           throw new GrpcError( |  | ||||||
|             grpc.status.INVALID_ARGUMENT, |  | ||||||
|             `invalid volume_content_source type: ${volume_content_source.type}` |  | ||||||
|           ); |  | ||||||
|           break; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       driver.ctx.logger.debug("controller source path: %s", source_path); |  | ||||||
|       response = await driver.cloneDir(source_path, volume_path); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // set mode
 |  | ||||||
|     if (this.options.nfs.dirPermissionsMode) { |  | ||||||
|       driver.ctx.logger.verbose( |  | ||||||
|         "setting dir mode to: %s on dir: %s", |  | ||||||
|         this.options.nfs.dirPermissionsMode, |  | ||||||
|         volume_path |  | ||||||
|       ); |  | ||||||
|       response = await driver.exec("chmod", [ |  | ||||||
|         this.options.nfs.dirPermissionsMode, |  | ||||||
|         volume_path, |  | ||||||
|       ]); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // set ownership
 |  | ||||||
|     if ( |  | ||||||
|       this.options.nfs.dirPermissionsUser || |  | ||||||
|       this.options.nfs.dirPermissionsGroup |  | ||||||
|     ) { |  | ||||||
|       driver.ctx.logger.verbose( |  | ||||||
|         "setting ownership to: %s:%s on dir: %s", |  | ||||||
|         this.options.nfs.dirPermissionsUser, |  | ||||||
|         this.options.nfs.dirPermissionsGroup, |  | ||||||
|         volume_path |  | ||||||
|       ); |  | ||||||
|       response = await driver.exec("chown", [ |  | ||||||
|         (this.options.nfs.dirPermissionsUser |  | ||||||
|           ? this.options.nfs.dirPermissionsUser |  | ||||||
|           : "") + |  | ||||||
|           ":" + |  | ||||||
|           (this.options.nfs.dirPermissionsGroup |  | ||||||
|             ? this.options.nfs.dirPermissionsGroup |  | ||||||
|             : ""), |  | ||||||
|         volume_path, |  | ||||||
|       ]); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let volume_context = { |  | ||||||
|       node_attach_driver: "nfs", |       node_attach_driver: "nfs", | ||||||
|       server: this.options.nfs.shareHost, |       server: this.options[config_key].shareHost, | ||||||
|       share: driver.getShareVolumePath(name), |       share: driver.getShareVolumePath(name), | ||||||
|     }; |     }; | ||||||
| 
 |  | ||||||
|     volume_context["provisioner_driver"] = driver.options.driver; |  | ||||||
|     if (driver.options.instance_id) { |  | ||||||
|       volume_context["provisioner_driver_instance_id"] = |  | ||||||
|         driver.options.instance_id; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const res = { |  | ||||||
|       volume: { |  | ||||||
|         volume_id: name, |  | ||||||
|         //capacity_bytes: capacity_bytes, // kubernetes currently pukes if capacity is returned as 0
 |  | ||||||
|         capacity_bytes: 0, |  | ||||||
|         content_source: volume_content_source, |  | ||||||
|         volume_context, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
|     return res; |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   getFsTypes() { | ||||||
|    * Delete a volume |     return ["nfs"]; | ||||||
|    * |  | ||||||
|    * Deleting a volume consists of the following steps: |  | ||||||
|    * 1. delete directory |  | ||||||
|    * |  | ||||||
|    * @param {*} call |  | ||||||
|    */ |  | ||||||
|   async DeleteVolume(call) { |  | ||||||
|     const driver = this; |  | ||||||
| 
 |  | ||||||
|     let name = call.request.volume_id; |  | ||||||
| 
 |  | ||||||
|     if (!name) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.INVALID_ARGUMENT, |  | ||||||
|         `volume_id is required` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const volume_path = driver.getControllerVolumePath(name); |  | ||||||
|     await driver.deleteDir(volume_path); |  | ||||||
| 
 |  | ||||||
|     return {}; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * |  | ||||||
|    * @param {*} call |  | ||||||
|    */ |  | ||||||
|   async ControllerExpandVolume(call) { |  | ||||||
|     throw new GrpcError( |  | ||||||
|       grpc.status.UNIMPLEMENTED, |  | ||||||
|       `operation not supported by driver` |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * TODO: consider volume_capabilities? |  | ||||||
|    * |  | ||||||
|    * @param {*} call |  | ||||||
|    */ |  | ||||||
|   async GetCapacity(call) { |  | ||||||
|     // really capacity is not used at all with nfs in this fashion, so no reason to enable
 |  | ||||||
|     // here even though it is technically feasible.
 |  | ||||||
|     throw new GrpcError( |  | ||||||
|       grpc.status.UNIMPLEMENTED, |  | ||||||
|       `operation not supported by driver` |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const driver = this; |  | ||||||
| 
 |  | ||||||
|     if (call.request.volume_capabilities) { |  | ||||||
|       const result = this.assertCapabilities(call.request.volume_capabilities); |  | ||||||
| 
 |  | ||||||
|       if (result.valid !== true) { |  | ||||||
|         return { available_capacity: 0 }; |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const available_capacity = await driver.getAvailableSpaceAtPath( |  | ||||||
|       driver.getControllerBasePath() |  | ||||||
|     ); |  | ||||||
|     return { available_capacity }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * |  | ||||||
|    * TODO: check capability to ensure not asking about block volumes |  | ||||||
|    * |  | ||||||
|    * @param {*} call |  | ||||||
|    */ |  | ||||||
|   async ListVolumes(call) { |  | ||||||
|     throw new GrpcError( |  | ||||||
|       grpc.status.UNIMPLEMENTED, |  | ||||||
|       `operation not supported by driver` |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * |  | ||||||
|    * @param {*} call |  | ||||||
|    */ |  | ||||||
|   async ListSnapshots(call) { |  | ||||||
|     throw new GrpcError( |  | ||||||
|       grpc.status.UNIMPLEMENTED, |  | ||||||
|       `operation not supported by driver` |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * |  | ||||||
|    * @param {*} call |  | ||||||
|    */ |  | ||||||
|   async CreateSnapshot(call) { |  | ||||||
|     const driver = this; |  | ||||||
| 
 |  | ||||||
|     // both these are required
 |  | ||||||
|     let source_volume_id = call.request.source_volume_id; |  | ||||||
|     let name = call.request.name; |  | ||||||
| 
 |  | ||||||
|     if (!source_volume_id) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.INVALID_ARGUMENT, |  | ||||||
|         `snapshot source_volume_id is required` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     if (!name) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.INVALID_ARGUMENT, |  | ||||||
|         `snapshot name is required` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     driver.ctx.logger.verbose("requested snapshot name: %s", name); |  | ||||||
| 
 |  | ||||||
|     let invalid_chars; |  | ||||||
|     invalid_chars = name.match(/[^a-z0-9_\-:.+]+/gi); |  | ||||||
|     if (invalid_chars) { |  | ||||||
|       invalid_chars = String.prototype.concat( |  | ||||||
|         ...new Set(invalid_chars.join("")) |  | ||||||
|       ); |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.INVALID_ARGUMENT, |  | ||||||
|         `snapshot name contains invalid characters: ${invalid_chars}` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     // https://stackoverflow.com/questions/32106243/regex-to-remove-all-non-alpha-numeric-and-replace-spaces-with/32106277
 |  | ||||||
|     name = name.replace(/[^a-z0-9_\-:.+]+/gi, ""); |  | ||||||
| 
 |  | ||||||
|     driver.ctx.logger.verbose("cleansed snapshot name: %s", name); |  | ||||||
| 
 |  | ||||||
|     const snapshot_id = `${source_volume_id}-${name}`; |  | ||||||
|     const volume_path = driver.getControllerVolumePath(source_volume_id); |  | ||||||
|     const snapshot_path = driver.getControllerSnapshotPath(snapshot_id); |  | ||||||
| 
 |  | ||||||
|     await driver.cloneDir(volume_path, snapshot_path); |  | ||||||
| 
 |  | ||||||
|     return { |  | ||||||
|       snapshot: { |  | ||||||
|         /** |  | ||||||
|          * The purpose of this field is to give CO guidance on how much space |  | ||||||
|          * is needed to create a volume from this snapshot. |  | ||||||
|          */ |  | ||||||
|         size_bytes: 0, |  | ||||||
|         snapshot_id, |  | ||||||
|         source_volume_id: source_volume_id, |  | ||||||
|         //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto
 |  | ||||||
|         creation_time: { |  | ||||||
|           seconds: Math.round(new Date().getTime() / 1000), |  | ||||||
|           nanos: 0, |  | ||||||
|         }, |  | ||||||
|         ready_to_use: true, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * In addition, if clones have been created from a snapshot, then they must |  | ||||||
|    * be destroyed before the snapshot can be destroyed. |  | ||||||
|    * |  | ||||||
|    * @param {*} call |  | ||||||
|    */ |  | ||||||
|   async DeleteSnapshot(call) { |  | ||||||
|     const driver = this; |  | ||||||
| 
 |  | ||||||
|     const snapshot_id = call.request.snapshot_id; |  | ||||||
| 
 |  | ||||||
|     if (!snapshot_id) { |  | ||||||
|       throw new GrpcError( |  | ||||||
|         grpc.status.INVALID_ARGUMENT, |  | ||||||
|         `snapshot_id is required` |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     const snapshot_path = driver.getControllerSnapshotPath(snapshot_id); |  | ||||||
|     await driver.deleteDir(snapshot_path); |  | ||||||
| 
 |  | ||||||
|     return {}; |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   /** |  | ||||||
|    * |  | ||||||
|    * @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, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | const { ControllerClientCommonDriver } = require("../controller-client-common"); | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Crude smb-client driver which simply creates directories to be mounted | ||||||
|  |  * and uses rsync for cloning/snapshots | ||||||
|  |  */ | ||||||
|  | class ControllerSmbClientDriver extends ControllerClientCommonDriver { | ||||||
|  |   constructor(ctx, options) { | ||||||
|  |     super(...arguments); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getConfigKey() { | ||||||
|  |     return "smb"; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getVolumeContext(name) { | ||||||
|  |     const driver = this; | ||||||
|  |     const config_key = driver.getConfigKey(); | ||||||
|  |     return { | ||||||
|  |       node_attach_driver: "smb", | ||||||
|  |       server: this.options[config_key].shareHost, | ||||||
|  |       share: driver.stripLeadingSlash(driver.getShareVolumePath(name)), | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getFsTypes() { | ||||||
|  |     return ["cifs"]; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports.ControllerSmbClientDriver = ControllerSmbClientDriver; | ||||||
|  | @ -0,0 +1,527 @@ | ||||||
|  | const request = require("request"); | ||||||
|  | const Mutex = require("async-mutex").Mutex; | ||||||
|  | 
 | ||||||
|  | const USER_AGENT = "democratic-csi"; | ||||||
|  | 
 | ||||||
|  | class SynologyHttpClient { | ||||||
|  |   constructor(options = {}) { | ||||||
|  |     this.options = JSON.parse(JSON.stringify(options)); | ||||||
|  |     this.logger = console; | ||||||
|  |     this.doLoginMutex = new Mutex(); | ||||||
|  |     this.apiSerializeMutex = new Mutex(); | ||||||
|  | 
 | ||||||
|  |     if (false) { | ||||||
|  |       setInterval(() => { | ||||||
|  |         console.log("WIPING OUT SYNOLOGY SID"); | ||||||
|  |         this.sid = null; | ||||||
|  |       }, 5 * 1000); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async login() { | ||||||
|  |     if (!this.sid) { | ||||||
|  |       const data = { | ||||||
|  |         api: "SYNO.API.Auth", | ||||||
|  |         version: "2", | ||||||
|  |         method: "login", | ||||||
|  |         account: this.options.username, | ||||||
|  |         passwd: this.options.password, | ||||||
|  |         session: this.options.session, | ||||||
|  |         format: "sid", | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       let response = await this.do_request("GET", "auth.cgi", data); | ||||||
|  |       this.sid = response.body.data.sid; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return this.sid; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   log_response(error, response, body, options) { | ||||||
|  |     this.logger.debug("SYNOLOGY HTTP REQUEST: " + JSON.stringify(options)); | ||||||
|  |     this.logger.debug("SYNOLOGY HTTP ERROR: " + error); | ||||||
|  |     this.logger.debug("SYNOLOGY HTTP STATUS: " + response.statusCode); | ||||||
|  |     this.logger.debug( | ||||||
|  |       "SYNOLOGY HTTP HEADERS: " + JSON.stringify(response.headers) | ||||||
|  |     ); | ||||||
|  |     this.logger.debug("SYNOLOGY HTTP BODY: " + JSON.stringify(body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async do_request(method, path, data = {}, options = {}) { | ||||||
|  |     const client = this; | ||||||
|  |     const isAuth = data.api == "SYNO.API.Auth" && data.method == "login"; | ||||||
|  |     let sid; | ||||||
|  |     let apiMutexRelease; | ||||||
|  |     if (!isAuth) { | ||||||
|  |       sid = await this.doLoginMutex.runExclusive(async () => { | ||||||
|  |         return await this.login(); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const invoke_options = options; | ||||||
|  | 
 | ||||||
|  |     if (!isAuth) { | ||||||
|  |       if (this.options.serialize) { | ||||||
|  |         apiMutexRelease = await this.apiSerializeMutex.acquire(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return new Promise((resolve, reject) => { | ||||||
|  |       if (!isAuth) { | ||||||
|  |         data._sid = sid; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const options = { | ||||||
|  |         method: method, | ||||||
|  |         url: `${this.options.protocol}://${this.options.host}:${this.options.port}/webapi/${path}`, | ||||||
|  |         headers: { | ||||||
|  |           Accept: "application/json", | ||||||
|  |           "User-Agent": USER_AGENT, | ||||||
|  |           "Content-Type": invoke_options.use_form_encoded | ||||||
|  |             ? "application/x-www-form-urlencoded" | ||||||
|  |             : "application/json", | ||||||
|  |         }, | ||||||
|  |         json: invoke_options.use_form_encoded ? false : true, | ||||||
|  |         agentOptions: { | ||||||
|  |           rejectUnauthorized: !!!client.options.allowInsecure, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  | 
 | ||||||
|  |       switch (method) { | ||||||
|  |         case "GET": | ||||||
|  |           let qsData = JSON.parse(JSON.stringify(data)); | ||||||
|  |           for (let p in qsData) { | ||||||
|  |             if (Array.isArray(qsData[p]) || typeof qsData[p] == "boolean") { | ||||||
|  |               qsData[p] = JSON.stringify(qsData[p]); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |           options.qs = qsData; | ||||||
|  |           break; | ||||||
|  |         default: | ||||||
|  |           if (invoke_options.use_form_encoded) { | ||||||
|  |             //options.body = URLSearchParams(data);
 | ||||||
|  |             options.form = data; | ||||||
|  |           } else { | ||||||
|  |             options.body = data; | ||||||
|  |           } | ||||||
|  |           break; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       try { | ||||||
|  |         request(options, function (error, response, body) { | ||||||
|  |           client.log_response(...arguments, options); | ||||||
|  | 
 | ||||||
|  |           if (error) { | ||||||
|  |             reject(error); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if ( | ||||||
|  |             typeof response.body !== "object" && | ||||||
|  |             response.body !== null && | ||||||
|  |             response.headers["content-type"] && | ||||||
|  |             response.headers["content-type"].includes("application/json") | ||||||
|  |           ) { | ||||||
|  |             response.body = JSON.parse(response.body); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if (response.statusCode > 299 || response.statusCode < 200) { | ||||||
|  |             reject(response); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if (response.body.success === false) { | ||||||
|  |             // remove invalid sid
 | ||||||
|  |             if (response.body.error.code == 119 && sid == client.sid) { | ||||||
|  |               client.sid = null; | ||||||
|  |             } | ||||||
|  |             reject(response); | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           resolve(response); | ||||||
|  |         }); | ||||||
|  |       } finally { | ||||||
|  |         if (typeof apiMutexRelease == "function") { | ||||||
|  |           apiMutexRelease(); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async GetLunUUIDByName(name) { | ||||||
|  |     const lun_list = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       version: "1", | ||||||
|  |       method: "list", | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let response = await this.do_request("GET", "entry.cgi", lun_list); | ||||||
|  |     let lun = response.body.data.luns.find((i) => { | ||||||
|  |       return i.name == name; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (lun) { | ||||||
|  |       return lun.uuid; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async GetLunIDByName(name) { | ||||||
|  |     const lun_list = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       version: "1", | ||||||
|  |       method: "list", | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let response = await this.do_request("GET", "entry.cgi", lun_list); | ||||||
|  |     let lun = response.body.data.luns.find((i) => { | ||||||
|  |       return i.name == name; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (lun) { | ||||||
|  |       return lun.lun_id; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async GetLunByID(lun_id) { | ||||||
|  |     const lun_list = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       version: "1", | ||||||
|  |       method: "list", | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let response = await this.do_request("GET", "entry.cgi", lun_list); | ||||||
|  |     let lun = response.body.data.luns.find((i) => { | ||||||
|  |       return i.lun_id == lun_id; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (lun) { | ||||||
|  |       return lun; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async GetLunByName(name) { | ||||||
|  |     const lun_list = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       version: "1", | ||||||
|  |       method: "list", | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let response = await this.do_request("GET", "entry.cgi", lun_list); | ||||||
|  |     let lun = response.body.data.luns.find((i) => { | ||||||
|  |       return i.name == name; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (lun) { | ||||||
|  |       return lun; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async GetSnapshotByLunIDAndName(lun_id, name) { | ||||||
|  |     const get_snapshot_info = { | ||||||
|  |       lid: lun_id, //check?
 | ||||||
|  |       api: "SYNO.Core.Storage.iSCSILUN", | ||||||
|  |       method: "load_snapshot", | ||||||
|  |       version: 1, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let response = await this.do_request("GET", "entry.cgi", get_snapshot_info); | ||||||
|  | 
 | ||||||
|  |     if (response.body.data) { | ||||||
|  |       let snapshot = response.body.data.find((i) => { | ||||||
|  |         return i.desc == name; | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       if (snapshot) { | ||||||
|  |         return snapshot; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async GetSnapshotByLunIDAndSnapshotUUID(lun_id, snapshot_uuid) { | ||||||
|  |     const get_snapshot_info = { | ||||||
|  |       lid: lun_id, //check?
 | ||||||
|  |       api: "SYNO.Core.Storage.iSCSILUN", | ||||||
|  |       method: "load_snapshot", | ||||||
|  |       version: 1, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let response = await this.do_request("GET", "entry.cgi", get_snapshot_info); | ||||||
|  | 
 | ||||||
|  |     if (response.body.data) { | ||||||
|  |       let snapshot = response.body.data.find((i) => { | ||||||
|  |         return i.uuid == snapshot_uuid; | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       if (snapshot) { | ||||||
|  |         return snapshot; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async DeleteSnapshot(snapshot_uuid) { | ||||||
|  |     const iscsi_snapshot_delete = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       method: "delete_snapshot", | ||||||
|  |       version: 1, | ||||||
|  |       snapshot_uuid: JSON.stringify(snapshot_uuid), // snapshot_id
 | ||||||
|  |       deleted_by: "democratic_csi", // ?
 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let response = await this.do_request( | ||||||
|  |       "GET", | ||||||
|  |       "entry.cgi", | ||||||
|  |       iscsi_snapshot_delete | ||||||
|  |     ); | ||||||
|  |     // return?
 | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async GetVolumeInfo(volume_path) { | ||||||
|  |     let data = { | ||||||
|  |       api: "SYNO.Core.Storage.Volume", | ||||||
|  |       method: "get", | ||||||
|  |       version: "1", | ||||||
|  |       //volume_path: "/volume1",
 | ||||||
|  |       volume_path, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return await this.do_request("GET", "entry.cgi", data); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async GetTargetByTargetID(target_id) { | ||||||
|  |     let targets = await this.ListTargets(); | ||||||
|  |     let target = targets.find((i) => { | ||||||
|  |       return i.target_id == target_id; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return target; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async GetTargetByIQN(iqn) { | ||||||
|  |     let targets = await this.ListTargets(); | ||||||
|  |     let target = targets.find((i) => { | ||||||
|  |       return i.iqn == iqn; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return target; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async ListTargets() { | ||||||
|  |     const iscsi_target_list = { | ||||||
|  |       api: "SYNO.Core.ISCSI.Target", | ||||||
|  |       version: "1", | ||||||
|  |       path: "entry.cgi", | ||||||
|  |       method: "list", | ||||||
|  |       additional: '["mapped_lun", "status", "acls", "connected_sessions"]', | ||||||
|  |     }; | ||||||
|  |     let response = await this.do_request("GET", "entry.cgi", iscsi_target_list); | ||||||
|  |     return response.body.data.targets; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async CreateLun(data = {}) { | ||||||
|  |     let response; | ||||||
|  |     let iscsi_lun_create = Object.assign({}, data, { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       version: "1", | ||||||
|  |       method: "create", | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const lun_list = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       version: "1", | ||||||
|  |       method: "list", | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       response = await this.do_request("GET", "entry.cgi", iscsi_lun_create); | ||||||
|  |       return response.body.data.uuid; | ||||||
|  |     } catch (err) { | ||||||
|  |       if ([18990538].includes(err.body.error.code)) { | ||||||
|  |         response = await this.do_request("GET", "entry.cgi", lun_list); | ||||||
|  |         let lun = response.body.data.luns.find((i) => { | ||||||
|  |           return i.name == iscsi_lun_create.name; | ||||||
|  |         }); | ||||||
|  |         return lun.uuid; | ||||||
|  |       } else { | ||||||
|  |         throw err; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async MapLun(data = {}) { | ||||||
|  |     // this is mapping from the perspective of the lun
 | ||||||
|  |     let iscsi_target_map = Object.assign({}, data, { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       method: "map_target", | ||||||
|  |       version: "1", | ||||||
|  |     }); | ||||||
|  |     iscsi_target_map.uuid = JSON.stringify(iscsi_target_map.uuid); | ||||||
|  |     iscsi_target_map.target_ids = JSON.stringify(iscsi_target_map.target_ids); | ||||||
|  | 
 | ||||||
|  |     // this is mapping from the perspective of the target
 | ||||||
|  |     /* | ||||||
|  |     iscsi_target_map = Object.assign(data, { | ||||||
|  |       api: "SYNO.Core.ISCSI.Target", | ||||||
|  |       method: "map_lun", | ||||||
|  |       version: "1", | ||||||
|  |     }); | ||||||
|  |     iscsi_target_map.lun_uuids = JSON.stringify(iscsi_target_map.lun_uuids); | ||||||
|  |     */ | ||||||
|  | 
 | ||||||
|  |     await this.do_request("GET", "entry.cgi", iscsi_target_map); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async DeleteLun(uuid) { | ||||||
|  |     uuid = uuid || ""; | ||||||
|  |     let iscsi_lun_delete = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       method: "delete", | ||||||
|  |       version: 1, | ||||||
|  |       //uuid: uuid,
 | ||||||
|  |       uuid: JSON.stringify(""), | ||||||
|  |       uuids: JSON.stringify([uuid]), | ||||||
|  |       //is_soft_feas_ignored: false,
 | ||||||
|  |       is_soft_feas_ignored: true, | ||||||
|  |       //feasibility_precheck: true,
 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     await this.do_request("GET", "entry.cgi", iscsi_lun_delete); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async DeleteAllLuns() { | ||||||
|  |     const lun_list = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       version: "1", | ||||||
|  |       method: "list", | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     let response = await this.do_request("GET", "entry.cgi", lun_list); | ||||||
|  |     for (let lun of response.body.data.luns) { | ||||||
|  |       await this.DeleteLun(lun.uuid); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async CreateSnapshot(data) { | ||||||
|  |     data = Object.assign({}, data, { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       method: "take_snapshot", | ||||||
|  |       version: 1, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     data.src_lun_uuid = JSON.stringify(data.src_lun_uuid); | ||||||
|  | 
 | ||||||
|  |     return await this.do_request("GET", "entry.cgi", data); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async CreateTarget(data = {}) { | ||||||
|  |     let iscsi_target_create = Object.assign({}, data, { | ||||||
|  |       api: "SYNO.Core.ISCSI.Target", | ||||||
|  |       version: "1", | ||||||
|  |       method: "create", | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     let response; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       response = await this.do_request("GET", "entry.cgi", iscsi_target_create); | ||||||
|  | 
 | ||||||
|  |       return response.body.data.target_id; | ||||||
|  |     } catch (err) { | ||||||
|  |       if ([18990744].includes(err.body.error.code)) { | ||||||
|  |         //do lookup
 | ||||||
|  |         const iscsi_target_list = { | ||||||
|  |           api: "SYNO.Core.ISCSI.Target", | ||||||
|  |           version: "1", | ||||||
|  |           path: "entry.cgi", | ||||||
|  |           method: "list", | ||||||
|  |           additional: '["mapped_lun", "status", "acls", "connected_sessions"]', | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         response = await this.do_request("GET", "entry.cgi", iscsi_target_list); | ||||||
|  |         let target = response.body.data.targets.find((i) => { | ||||||
|  |           return i.iqn == iscsi_target_create.iqn; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         if (target) { | ||||||
|  |           return target.target_id; | ||||||
|  |         } else { | ||||||
|  |           throw err; | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         throw err; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async DeleteTarget(target_id) { | ||||||
|  |     const iscsi_target_delete = { | ||||||
|  |       api: "SYNO.Core.ISCSI.Target", | ||||||
|  |       method: "delete", | ||||||
|  |       version: "1", | ||||||
|  |       path: "entry.cgi", | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       await this.do_request( | ||||||
|  |         "GET", | ||||||
|  |         "entry.cgi", | ||||||
|  |         Object.assign({}, iscsi_target_delete, { | ||||||
|  |           target_id: JSON.stringify(String(target_id || "")), | ||||||
|  |         }) | ||||||
|  |       ); | ||||||
|  |     } catch (err) { | ||||||
|  |       /** | ||||||
|  |        * 18990710 = non-existant | ||||||
|  |        */ | ||||||
|  |       //if (![18990710].includes(err.body.error.code)) {
 | ||||||
|  |       throw err; | ||||||
|  |       //}
 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async ExpandISCSILun(uuid, size) { | ||||||
|  |     const iscsi_lun_extend = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       method: "set", | ||||||
|  |       version: 1, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return await this.do_request( | ||||||
|  |       "GET", | ||||||
|  |       "entry.cgi", | ||||||
|  |       Object.assign({}, iscsi_lun_extend, { | ||||||
|  |         uuid: JSON.stringify(uuid), | ||||||
|  |         new_size: size, | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async CreateClonedVolume(src_lun_uuid, dst_lun_name) { | ||||||
|  |     const create_cloned_volume = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       version: 1, | ||||||
|  |       method: "clone", | ||||||
|  |       src_lun_uuid: JSON.stringify(src_lun_uuid), // src lun uuid
 | ||||||
|  |       dst_lun_name: dst_lun_name, // dst lun name
 | ||||||
|  |       is_same_pool: true, // always true? string?
 | ||||||
|  |       clone_type: "democratic-csi", // check
 | ||||||
|  |     }; | ||||||
|  |     return await this.do_request("GET", "entry.cgi", create_cloned_volume); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async CreateVolumeFromSnapshot(src_lun_uuid, snapshot_uuid, cloned_lun_name) { | ||||||
|  |     const create_volume_from_snapshot = { | ||||||
|  |       api: "SYNO.Core.ISCSI.LUN", | ||||||
|  |       version: 1, | ||||||
|  |       method: "clone_snapshot", | ||||||
|  |       src_lun_uuid: JSON.stringify(src_lun_uuid), // src lun uuid, snapshot id?
 | ||||||
|  |       snapshot_uuid: JSON.stringify(snapshot_uuid), // shaptop uuid
 | ||||||
|  |       cloned_lun_name: cloned_lun_name, // cloned lun name
 | ||||||
|  |       clone_type: "democratic-csi", // check
 | ||||||
|  |     }; | ||||||
|  |     return await this.do_request( | ||||||
|  |       "GET", | ||||||
|  |       "entry.cgi", | ||||||
|  |       create_volume_from_snapshot | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports.SynologyHttpClient = SynologyHttpClient; | ||||||
|  | @ -0,0 +1,867 @@ | ||||||
|  | const { CsiBaseDriver } = require("../index"); | ||||||
|  | const { GrpcError, grpc } = require("../../utils/grpc"); | ||||||
|  | const SynologyHttpClient = require("./http").SynologyHttpClient; | ||||||
|  | const semver = require("semver"); | ||||||
|  | const sleep = require("../../utils/general").sleep; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * | ||||||
|  |  * Driver to provision storage on a synology device | ||||||
|  |  * | ||||||
|  |  */ | ||||||
|  | class ControllerSynologyDriver 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 || {}; | ||||||
|  | 
 | ||||||
|  |     const driverResourceType = this.getDriverResourceType(); | ||||||
|  | 
 | ||||||
|  |     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" (would need to properly handle volume_content_source)
 | ||||||
|  |           (); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       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 (driverResourceType == "volume") { | ||||||
|  |         options.service.node.capabilities.rpc.push("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");
 | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getHttpClient() { | ||||||
|  |     if (!this.httpClient) { | ||||||
|  |       this.httpClient = new SynologyHttpClient(this.options.httpConnection); | ||||||
|  |     } | ||||||
|  |     return this.httpClient; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getDriverResourceType() { | ||||||
|  |     switch (this.options.driver) { | ||||||
|  |       case "synology-nfs": | ||||||
|  |       case "synology-smb": | ||||||
|  |         return "filesystem"; | ||||||
|  |       case "synology-iscsi": | ||||||
|  |         return "volume"; | ||||||
|  |       default: | ||||||
|  |         throw new Error("unknown driver: " + this.ctx.args.driver); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getDriverShareType() { | ||||||
|  |     switch (this.options.driver) { | ||||||
|  |       case "synology-nfs": | ||||||
|  |         return "nfs"; | ||||||
|  |       case "synology-smb": | ||||||
|  |         return "smb"; | ||||||
|  |       case "synology-iscsi": | ||||||
|  |         return "iscsi"; | ||||||
|  |       default: | ||||||
|  |         throw new Error("unknown driver: " + this.ctx.args.driver); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   buildIscsiName(name) { | ||||||
|  |     let iscsiName = name; | ||||||
|  |     if (this.options.iscsi.namePrefix) { | ||||||
|  |       iscsiName = this.options.iscsi.namePrefix + iscsiName; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (this.options.iscsi.nameSuffix) { | ||||||
|  |       iscsiName += this.options.iscsi.nameSuffix; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return iscsiName.toLowerCase(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   assertCapabilities(capabilities) { | ||||||
|  |     const driverResourceType = this.getDriverResourceType(); | ||||||
|  |     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) => { | ||||||
|  |       switch (driverResourceType) { | ||||||
|  |         case "filesystem": | ||||||
|  |           if (capability.access_type != "mount") { | ||||||
|  |             message = `invalid access_type ${capability.access_type}`; | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if ( | ||||||
|  |             capability.mount.fs_type && | ||||||
|  |             !["nfs", "cifs"].includes(capability.mount.fs_type) | ||||||
|  |           ) { | ||||||
|  |             message = `invalid fs_type ${capability.mount.fs_type}`; | ||||||
|  |             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", | ||||||
|  |               "MULTI_NODE_READER_ONLY", | ||||||
|  |               "MULTI_NODE_SINGLE_WRITER", | ||||||
|  |               "MULTI_NODE_MULTI_WRITER", | ||||||
|  |             ].includes(capability.access_mode.mode) | ||||||
|  |           ) { | ||||||
|  |             message = `invalid access_mode, ${capability.access_mode.mode}`; | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return true; | ||||||
|  |         case "volume": | ||||||
|  |           if (capability.access_type == "mount") { | ||||||
|  |             if ( | ||||||
|  |               capability.mount.fs_type && | ||||||
|  |               !["ext3", "ext4", "ext4dev", "xfs"].includes( | ||||||
|  |                 capability.mount.fs_type | ||||||
|  |               ) | ||||||
|  |             ) { | ||||||
|  |               message = `invalid fs_type ${capability.mount.fs_type}`; | ||||||
|  |               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", | ||||||
|  |               "MULTI_NODE_READER_ONLY", | ||||||
|  |               "MULTI_NODE_SINGLE_WRITER", | ||||||
|  |             ].includes(capability.access_mode.mode) | ||||||
|  |           ) { | ||||||
|  |             message = `invalid access_mode, ${capability.access_mode.mode}`; | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return true; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     return { valid, message }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * CreateVolume | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async CreateVolume(call) { | ||||||
|  |     const driver = this; | ||||||
|  |     const httpClient = await driver.getHttpClient(); | ||||||
|  | 
 | ||||||
|  |     let name = call.request.name; | ||||||
|  |     let volume_content_source = call.request.volume_content_source; | ||||||
|  | 
 | ||||||
|  |     if (!name) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `volume name is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (call.request.volume_capabilities) { | ||||||
|  |       const result = this.assertCapabilities(call.request.volume_capabilities); | ||||||
|  |       if (result.valid !== true) { | ||||||
|  |         throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       call.request.capacity_range.required_bytes > 0 && | ||||||
|  |       call.request.capacity_range.limit_bytes > 0 && | ||||||
|  |       call.request.capacity_range.required_bytes > | ||||||
|  |         call.request.capacity_range.limit_bytes | ||||||
|  |     ) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.OUT_OF_RANGE, | ||||||
|  |         `required_bytes is greather than limit_bytes` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let capacity_bytes = | ||||||
|  |       call.request.capacity_range.required_bytes || | ||||||
|  |       call.request.capacity_range.limit_bytes; | ||||||
|  | 
 | ||||||
|  |     if (!capacity_bytes) { | ||||||
|  |       //should never happen, value must be set
 | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `volume capacity is required (either required_bytes or limit_bytes)` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // ensure *actual* capacity is not greater than limit
 | ||||||
|  |     if ( | ||||||
|  |       call.request.capacity_range.limit_bytes && | ||||||
|  |       call.request.capacity_range.limit_bytes > 0 && | ||||||
|  |       capacity_bytes > call.request.capacity_range.limit_bytes | ||||||
|  |     ) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.OUT_OF_RANGE, | ||||||
|  |         `required volume capacity is greater than limit` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let volume_context = {}; | ||||||
|  |     switch (driver.getDriverShareType()) { | ||||||
|  |       case "nfs": | ||||||
|  |         // TODO: create volume here
 | ||||||
|  |         throw new GrpcError( | ||||||
|  |           grpc.status.UNIMPLEMENTED, | ||||||
|  |           `operation not supported by driver` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |       case "smb": | ||||||
|  |         // TODO: create volume here
 | ||||||
|  |         throw new GrpcError( | ||||||
|  |           grpc.status.UNIMPLEMENTED, | ||||||
|  |           `operation not supported by driver` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |       case "iscsi": | ||||||
|  |         let iscsiName = driver.buildIscsiName(name); | ||||||
|  |         let data; | ||||||
|  |         let target; | ||||||
|  |         let lun_mapping; | ||||||
|  |         let lun_uuid; | ||||||
|  |         let existingLun; | ||||||
|  | 
 | ||||||
|  |         if (volume_content_source) { | ||||||
|  |           let src_lun_uuid; | ||||||
|  |           let src_lun_id; | ||||||
|  |           switch (volume_content_source.type) { | ||||||
|  |             case "snapshot": | ||||||
|  |               let parts = volume_content_source.snapshot.snapshot_id.split("/"); | ||||||
|  |               src_lun_id = parts[2]; | ||||||
|  |               let snapshot_uuid = parts[3]; | ||||||
|  |               let src_lun = await httpClient.GetLunByID(src_lun_id); | ||||||
|  |               src_lun_uuid = src_lun.uuid; | ||||||
|  | 
 | ||||||
|  |               existingLun = await httpClient.GetLunByName(iscsiName); | ||||||
|  |               if (!existingLun) { | ||||||
|  |                 await httpClient.CreateVolumeFromSnapshot( | ||||||
|  |                   src_lun_uuid, | ||||||
|  |                   snapshot_uuid, | ||||||
|  |                   iscsiName | ||||||
|  |                 ); | ||||||
|  |               } | ||||||
|  |               break; | ||||||
|  |             case "volume": | ||||||
|  |               existingLun = await httpClient.GetLunByName(iscsiName); | ||||||
|  |               if (!existingLun) { | ||||||
|  |                 let srcLunName = driver.buildIscsiName( | ||||||
|  |                   volume_content_source.volume.volume_id | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 src_lun_uuid = await httpClient.GetLunUUIDByName(srcLunName); | ||||||
|  |                 await httpClient.CreateClonedVolume(src_lun_uuid, iscsiName); | ||||||
|  |               } | ||||||
|  |               break; | ||||||
|  |             default: | ||||||
|  |               throw new GrpcError( | ||||||
|  |                 grpc.status.INVALID_ARGUMENT, | ||||||
|  |                 `invalid volume_content_source type: ${volume_content_source.type}` | ||||||
|  |               ); | ||||||
|  |               break; | ||||||
|  |           } | ||||||
|  |           // resize to requested amount
 | ||||||
|  | 
 | ||||||
|  |           let lun = await httpClient.GetLunByName(iscsiName); | ||||||
|  |           lun_uuid = lun.uuid; | ||||||
|  |           if (lun.size < capacity_bytes) { | ||||||
|  |             await httpClient.ExpandISCSILun(lun_uuid, capacity_bytes); | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           // create lun
 | ||||||
|  |           data = Object.assign({}, driver.options.iscsi.lunTemplate, { | ||||||
|  |             name: iscsiName, | ||||||
|  |             location: driver.options.synology.volume, | ||||||
|  |             size: capacity_bytes, | ||||||
|  |           }); | ||||||
|  |           lun_uuid = await httpClient.CreateLun(data); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // create target
 | ||||||
|  |         let iqn = driver.options.iscsi.baseiqn + iscsiName; | ||||||
|  |         data = Object.assign({}, driver.options.iscsi.targetTemplate, { | ||||||
|  |           name: iscsiName, | ||||||
|  |           iqn, | ||||||
|  |         }); | ||||||
|  |         let target_id = await httpClient.CreateTarget(data); | ||||||
|  |         //target = await httpClient.GetTargetByTargetID(target_id);
 | ||||||
|  |         target = await httpClient.GetTargetByIQN(iqn); | ||||||
|  |         if (!target) { | ||||||
|  |           throw new GrpcError( | ||||||
|  |             grpc.status.UNKNOWN, | ||||||
|  |             `failed to lookup target: ${iqn}` | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         target_id = target.target_id; | ||||||
|  | 
 | ||||||
|  |         // check if mapping of lun <-> target already exists
 | ||||||
|  |         lun_mapping = target.mapped_luns.find((lun) => { | ||||||
|  |           return lun.lun_uuid == lun_uuid; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // create mapping if not present already
 | ||||||
|  |         if (!lun_mapping) { | ||||||
|  |           data = { | ||||||
|  |             uuid: lun_uuid, | ||||||
|  |             target_ids: [target_id], | ||||||
|  |           }; | ||||||
|  |           /* | ||||||
|  |           data = { | ||||||
|  |             lun_uuids: [lun_uuid], | ||||||
|  |             target_id: target_id, | ||||||
|  |           }; | ||||||
|  |           */ | ||||||
|  |           await httpClient.MapLun(data); | ||||||
|  | 
 | ||||||
|  |           // re-retrieve target to ensure proper lun (mapping_index) value is returned
 | ||||||
|  |           target = await httpClient.GetTargetByTargetID(target_id); | ||||||
|  |           lun_mapping = target.mapped_luns.find((lun) => { | ||||||
|  |             return lun.lun_uuid == lun_uuid; | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!lun_mapping) { | ||||||
|  |           throw new GrpcError( | ||||||
|  |             grpc.status.UNKNOWN, | ||||||
|  |             `failed to lookup lun_mapping_id` | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         volume_context = { | ||||||
|  |           node_attach_driver: "iscsi", | ||||||
|  |           portal: driver.options.iscsi.targetPortal || "", | ||||||
|  |           portals: driver.options.iscsi.targetPortals | ||||||
|  |             ? driver.options.iscsi.targetPortals.join(",") | ||||||
|  |             : "", | ||||||
|  |           interface: driver.options.iscsi.interface || "", | ||||||
|  |           iqn, | ||||||
|  |           lun: lun_mapping.mapping_index, | ||||||
|  |         }; | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         throw new GrpcError( | ||||||
|  |           grpc.status.UNIMPLEMENTED, | ||||||
|  |           `operation not supported by driver` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     volume_context["provisioner_driver"] = driver.options.driver; | ||||||
|  |     if (driver.options.instance_id) { | ||||||
|  |       volume_context["provisioner_driver_instance_id"] = | ||||||
|  |         driver.options.instance_id; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const res = { | ||||||
|  |       volume: { | ||||||
|  |         volume_id: name, | ||||||
|  |         capacity_bytes, // kubernetes currently pukes if capacity is returned as 0
 | ||||||
|  |         content_source: volume_content_source, | ||||||
|  |         volume_context, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return res; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * DeleteVolume | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async DeleteVolume(call) { | ||||||
|  |     const driver = this; | ||||||
|  |     const httpClient = await driver.getHttpClient(); | ||||||
|  | 
 | ||||||
|  |     let name = call.request.volume_id; | ||||||
|  | 
 | ||||||
|  |     if (!name) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `volume_id is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let response; | ||||||
|  | 
 | ||||||
|  |     switch (driver.getDriverShareType()) { | ||||||
|  |       case "nfs": | ||||||
|  |         // TODO: delete volume here
 | ||||||
|  |         throw new GrpcError( | ||||||
|  |           grpc.status.UNIMPLEMENTED, | ||||||
|  |           `operation not supported by driver` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |       case "smb": | ||||||
|  |         // TODO: delete volume here
 | ||||||
|  |         throw new GrpcError( | ||||||
|  |           grpc.status.UNIMPLEMENTED, | ||||||
|  |           `operation not supported by driver` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |       case "iscsi": | ||||||
|  |         //await httpClient.DeleteAllLuns();
 | ||||||
|  | 
 | ||||||
|  |         let iscsiName = driver.buildIscsiName(name); | ||||||
|  |         let iqn = driver.options.iscsi.baseiqn + iscsiName; | ||||||
|  | 
 | ||||||
|  |         let target = await httpClient.GetTargetByIQN(iqn); | ||||||
|  |         if (target) { | ||||||
|  |           await httpClient.DeleteTarget(target.target_id); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); | ||||||
|  |         if (lun_uuid) { | ||||||
|  |           // this is an async process where a success is returned but delete is happening still behind the scenes
 | ||||||
|  |           // therefore we continue to search for the lun after delete success call to ensure full deletion
 | ||||||
|  |           await httpClient.DeleteLun(lun_uuid); | ||||||
|  | 
 | ||||||
|  |           //let settleEnabled = driver.options.api.lunDelete.settleEnabled;
 | ||||||
|  |           let settleEnabled = true; | ||||||
|  | 
 | ||||||
|  |           if (settleEnabled) { | ||||||
|  |             let currentCheck = 0; | ||||||
|  | 
 | ||||||
|  |             /* | ||||||
|  |             let settleMaxRetries = | ||||||
|  |               driver.options.api.lunDelete.settleMaxRetries || 6; | ||||||
|  |             let settleSeconds = driver.options.api.lunDelete.settleSeconds || 5; | ||||||
|  |             */ | ||||||
|  | 
 | ||||||
|  |             let settleMaxRetries = 6; | ||||||
|  |             let settleSeconds = 5; | ||||||
|  | 
 | ||||||
|  |             let waitTimeBetweenChecks = settleSeconds * 1000; | ||||||
|  | 
 | ||||||
|  |             await sleep(waitTimeBetweenChecks); | ||||||
|  |             lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); | ||||||
|  | 
 | ||||||
|  |             while (currentCheck <= settleMaxRetries && lun_uuid) { | ||||||
|  |               currentCheck++; | ||||||
|  |               await sleep(waitTimeBetweenChecks); | ||||||
|  |               lun_uuid = await httpClient.GetLunUUIDByName(iscsiName); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (lun_uuid) { | ||||||
|  |               throw new GrpcError( | ||||||
|  |                 grpc.status.UNKNOWN, | ||||||
|  |                 `failed to remove lun: ${lun_uuid}` | ||||||
|  |               ); | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         throw new GrpcError( | ||||||
|  |           grpc.status.UNIMPLEMENTED, | ||||||
|  |           `operation not supported by driver` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return {}; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async ControllerExpandVolume(call) { | ||||||
|  |     const driver = this; | ||||||
|  |     const httpClient = await driver.getHttpClient(); | ||||||
|  | 
 | ||||||
|  |     let name = call.request.volume_id; | ||||||
|  | 
 | ||||||
|  |     if (!name) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `volume_id is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let capacity_bytes = | ||||||
|  |       call.request.capacity_range.required_bytes || | ||||||
|  |       call.request.capacity_range.limit_bytes; | ||||||
|  |     if (!capacity_bytes) { | ||||||
|  |       //should never happen, value must be set
 | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `volume capacity is required (either required_bytes or limit_bytes)` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       call.request.capacity_range.required_bytes > 0 && | ||||||
|  |       call.request.capacity_range.limit_bytes > 0 && | ||||||
|  |       call.request.capacity_range.required_bytes > | ||||||
|  |         call.request.capacity_range.limit_bytes | ||||||
|  |     ) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `required_bytes is greather than limit_bytes` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // ensure *actual* capacity is not greater than limit
 | ||||||
|  |     if ( | ||||||
|  |       call.request.capacity_range.limit_bytes && | ||||||
|  |       call.request.capacity_range.limit_bytes > 0 && | ||||||
|  |       capacity_bytes > call.request.capacity_range.limit_bytes | ||||||
|  |     ) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.OUT_OF_RANGE, | ||||||
|  |         `required volume capacity is greater than limit` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let node_expansion_required = false; | ||||||
|  |     let response; | ||||||
|  | 
 | ||||||
|  |     switch (driver.getDriverShareType()) { | ||||||
|  |       case "nfs": | ||||||
|  |         // TODO: expand volume here
 | ||||||
|  |         throw new GrpcError( | ||||||
|  |           grpc.status.UNIMPLEMENTED, | ||||||
|  |           `operation not supported by driver` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |       case "smb": | ||||||
|  |         // TODO: expand volume here
 | ||||||
|  |         throw new GrpcError( | ||||||
|  |           grpc.status.UNIMPLEMENTED, | ||||||
|  |           `operation not supported by driver` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |       case "iscsi": | ||||||
|  |         node_expansion_required = true; | ||||||
|  |         let iscsiName = driver.buildIscsiName(name); | ||||||
|  | 
 | ||||||
|  |         response = await httpClient.GetLunUUIDByName(iscsiName); | ||||||
|  |         await httpClient.ExpandISCSILun(response, capacity_bytes); | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         throw new GrpcError( | ||||||
|  |           grpc.status.UNIMPLEMENTED, | ||||||
|  |           `operation not supported by driver` | ||||||
|  |         ); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       capacity_bytes, | ||||||
|  |       node_expansion_required, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * TODO: consider volume_capabilities? | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async GetCapacity(call) { | ||||||
|  |     const driver = this; | ||||||
|  |     const httpClient = await driver.getHttpClient(); | ||||||
|  | 
 | ||||||
|  |     if (!driver.options.synology.volume) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.FAILED_PRECONDITION, | ||||||
|  |         `invalid configuration: missing volume` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (call.request.volume_capabilities) { | ||||||
|  |       const result = this.assertCapabilities(call.request.volume_capabilities); | ||||||
|  | 
 | ||||||
|  |       if (result.valid !== true) { | ||||||
|  |         return { available_capacity: 0 }; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let response = await httpClient.GetVolumeInfo( | ||||||
|  |       driver.options.synology.volume | ||||||
|  |     ); | ||||||
|  |     return { available_capacity: response.body.data.volume.size_free_byte }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * TODO: check capability to ensure not asking about block volumes | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async ListVolumes(call) { | ||||||
|  |     throw new GrpcError( | ||||||
|  |       grpc.status.UNIMPLEMENTED, | ||||||
|  |       `operation not supported by driver` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async ListSnapshots(call) { | ||||||
|  |     throw new GrpcError( | ||||||
|  |       grpc.status.UNIMPLEMENTED, | ||||||
|  |       `operation not supported by driver` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async CreateSnapshot(call) { | ||||||
|  |     const driver = this; | ||||||
|  |     const httpClient = await driver.getHttpClient(); | ||||||
|  | 
 | ||||||
|  |     // both these are required
 | ||||||
|  |     let source_volume_id = call.request.source_volume_id; | ||||||
|  |     let name = call.request.name; | ||||||
|  | 
 | ||||||
|  |     if (!source_volume_id) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `snapshot source_volume_id is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!name) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `snapshot name is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     driver.ctx.logger.verbose("requested snapshot name: %s", name); | ||||||
|  | 
 | ||||||
|  |     let invalid_chars; | ||||||
|  |     invalid_chars = name.match(/[^a-z0-9_\-:.+]+/gi); | ||||||
|  |     if (invalid_chars) { | ||||||
|  |       invalid_chars = String.prototype.concat( | ||||||
|  |         ...new Set(invalid_chars.join("")) | ||||||
|  |       ); | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `snapshot name contains invalid characters: ${invalid_chars}` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // create snapshot here
 | ||||||
|  | 
 | ||||||
|  |     let iscsiName = driver.buildIscsiName(source_volume_id); | ||||||
|  |     let lun = await httpClient.GetLunByName(iscsiName); | ||||||
|  | 
 | ||||||
|  |     if (!lun) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `invalid source_volume_id: ${source_volume_id}` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // check for already exists
 | ||||||
|  |     let snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name); | ||||||
|  |     if (snapshot) { | ||||||
|  |       return { | ||||||
|  |         snapshot: { | ||||||
|  |           /** | ||||||
|  |            * The purpose of this field is to give CO guidance on how much space | ||||||
|  |            * is needed to create a volume from this snapshot. | ||||||
|  |            */ | ||||||
|  |           size_bytes: 0, | ||||||
|  |           snapshot_id: `/lun/${lun.lun_id}/${snapshot.uuid}`, // add shanpshot_uuid //fixme
 | ||||||
|  |           source_volume_id: source_volume_id, | ||||||
|  |           //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto
 | ||||||
|  |           creation_time: { | ||||||
|  |             seconds: snapshot.time, | ||||||
|  |             nanos: 0, | ||||||
|  |           }, | ||||||
|  |           ready_to_use: true, | ||||||
|  |         }, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let data = Object.assign({}, driver.options.iscsi.lunSnapshotTemplate, { | ||||||
|  |       src_lun_uuid: lun.uuid, | ||||||
|  |       taken_by: "democratic-csi", | ||||||
|  |       description: name, //check
 | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     let response = await httpClient.CreateSnapshot(data); | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       snapshot: { | ||||||
|  |         /** | ||||||
|  |          * The purpose of this field is to give CO guidance on how much space | ||||||
|  |          * is needed to create a volume from this snapshot. | ||||||
|  |          */ | ||||||
|  |         size_bytes: 0, | ||||||
|  |         snapshot_id: `/lun/${lun.lun_id}/${response.body.data.snapshot_uuid}`, | ||||||
|  |         source_volume_id: source_volume_id, | ||||||
|  |         //https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/timestamp.proto
 | ||||||
|  |         creation_time: { | ||||||
|  |           seconds: Math.round(new Date().getTime() / 1000), | ||||||
|  |           nanos: 0, | ||||||
|  |         }, | ||||||
|  |         ready_to_use: true, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * In addition, if clones have been created from a snapshot, then they must | ||||||
|  |    * be destroyed before the snapshot can be destroyed. | ||||||
|  |    * | ||||||
|  |    * @param {*} call | ||||||
|  |    */ | ||||||
|  |   async DeleteSnapshot(call) { | ||||||
|  |     // throw new GrpcError(
 | ||||||
|  |     //   grpc.status.UNIMPLEMENTED,
 | ||||||
|  |     //   `operation not supported by driver`
 | ||||||
|  |     // );
 | ||||||
|  | 
 | ||||||
|  |     const driver = this; | ||||||
|  |     const httpClient = await driver.getHttpClient(); | ||||||
|  | 
 | ||||||
|  |     const snapshot_id = call.request.snapshot_id; | ||||||
|  | 
 | ||||||
|  |     if (!snapshot_id) { | ||||||
|  |       throw new GrpcError( | ||||||
|  |         grpc.status.INVALID_ARGUMENT, | ||||||
|  |         `snapshot_id is required` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let parts = snapshot_id.split("/"); | ||||||
|  |     let lun_id = parts[2]; | ||||||
|  |     let snapshot_uuid = parts[3]; | ||||||
|  | 
 | ||||||
|  |     // TODO: delete snapshot
 | ||||||
|  |     let snapshot = await httpClient.GetSnapshotByLunIDAndSnapshotUUID( | ||||||
|  |       lun_id, | ||||||
|  |       snapshot_uuid | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     if (snapshot) { | ||||||
|  |       await httpClient.DeleteSnapshot(snapshot.uuid); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return {}; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @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.ControllerSynologyDriver = ControllerSynologyDriver; | ||||||
|  | @ -47,8 +47,9 @@ class ControllerZfsGenericDriver extends ControllerZfsSshBaseDriver { | ||||||
|                 ] |                 ] | ||||||
|               ) { |               ) { | ||||||
|                 await zb.zfs.set(datasetName, { |                 await zb.zfs.set(datasetName, { | ||||||
|                   [key]: this.options.nfs.shareStrategySetDatasetProperties |                   [key]: | ||||||
|                     .properties[key], |                     this.options.nfs.shareStrategySetDatasetProperties | ||||||
|  |                       .properties[key], | ||||||
|                 }); |                 }); | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|  | @ -114,8 +115,10 @@ class ControllerZfsGenericDriver extends ControllerZfsSshBaseDriver { | ||||||
|               if (this.options.iscsi.shareStrategyTargetCli.tpg.attributes) { |               if (this.options.iscsi.shareStrategyTargetCli.tpg.attributes) { | ||||||
|                 for (const attributeName in this.options.iscsi |                 for (const attributeName in this.options.iscsi | ||||||
|                   .shareStrategyTargetCli.tpg.attributes) { |                   .shareStrategyTargetCli.tpg.attributes) { | ||||||
|                   const attributeValue = this.options.iscsi |                   const attributeValue = | ||||||
|                     .shareStrategyTargetCli.tpg.attributes[attributeName]; |                     this.options.iscsi.shareStrategyTargetCli.tpg.attributes[ | ||||||
|  |                       attributeName | ||||||
|  |                     ]; | ||||||
|                   setAttributesText += "\n"; |                   setAttributesText += "\n"; | ||||||
|                   setAttributesText += `set attribute ${attributeName}=${attributeValue}`; |                   setAttributesText += `set attribute ${attributeName}=${attributeValue}`; | ||||||
|                 } |                 } | ||||||
|  | @ -124,8 +127,10 @@ class ControllerZfsGenericDriver extends ControllerZfsSshBaseDriver { | ||||||
|               if (this.options.iscsi.shareStrategyTargetCli.tpg.auth) { |               if (this.options.iscsi.shareStrategyTargetCli.tpg.auth) { | ||||||
|                 for (const attributeName in this.options.iscsi |                 for (const attributeName in this.options.iscsi | ||||||
|                   .shareStrategyTargetCli.tpg.auth) { |                   .shareStrategyTargetCli.tpg.auth) { | ||||||
|                   const attributeValue = this.options.iscsi |                   const attributeValue = | ||||||
|                     .shareStrategyTargetCli.tpg.auth[attributeName]; |                     this.options.iscsi.shareStrategyTargetCli.tpg.auth[ | ||||||
|  |                       attributeName | ||||||
|  |                     ]; | ||||||
|                   setAttributesText += "\n"; |                   setAttributesText += "\n"; | ||||||
|                   setAttributesText += `set auth ${attributeName}=${attributeValue}`; |                   setAttributesText += `set auth ${attributeName}=${attributeValue}`; | ||||||
|                 } |                 } | ||||||
|  | @ -168,9 +173,11 @@ create /backstores/block/${iscsiName} | ||||||
| 
 | 
 | ||||||
|         volume_context = { |         volume_context = { | ||||||
|           node_attach_driver: "iscsi", |           node_attach_driver: "iscsi", | ||||||
|           portal: this.options.iscsi.targetPortal, |           portal: this.options.iscsi.targetPortal || "", | ||||||
|           portals: this.options.iscsi.targetPortals.join(","), |           portals: this.options.iscsi.targetPortals | ||||||
|           interface: this.options.iscsi.interface, |             ? this.options.iscsi.targetPortals.join(",") | ||||||
|  |             : "", | ||||||
|  |           interface: this.options.iscsi.interface || "", | ||||||
|           iqn: iqn, |           iqn: iqn, | ||||||
|           lun: 0, |           lun: 0, | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  | @ -90,9 +90,20 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | ||||||
|         "CLONE_VOLUME", |         "CLONE_VOLUME", | ||||||
|         //"PUBLISH_READONLY",
 |         //"PUBLISH_READONLY",
 | ||||||
|         "EXPAND_VOLUME", |         "EXPAND_VOLUME", | ||||||
|         //"VOLUME_CONDITION", // added in v1.3.0
 |  | ||||||
|         //"GET_VOLUME", // added in v1.3.0
 |  | ||||||
|       ]; |       ]; | ||||||
|  | 
 | ||||||
|  |       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)) { |     if (!("rpc" in options.service.node.capabilities)) { | ||||||
|  | @ -118,6 +129,18 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | ||||||
|           ]; |           ]; | ||||||
|           break; |           break; | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|  |       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"); // in k8s is sent in as the security context fsgroup
 | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -219,6 +242,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | ||||||
|             ![ |             ![ | ||||||
|               "UNKNOWN", |               "UNKNOWN", | ||||||
|               "SINGLE_NODE_WRITER", |               "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", |               "SINGLE_NODE_READER_ONLY", | ||||||
|               "MULTI_NODE_READER_ONLY", |               "MULTI_NODE_READER_ONLY", | ||||||
|               "MULTI_NODE_SINGLE_WRITER", |               "MULTI_NODE_SINGLE_WRITER", | ||||||
|  | @ -247,6 +272,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | ||||||
|             ![ |             ![ | ||||||
|               "UNKNOWN", |               "UNKNOWN", | ||||||
|               "SINGLE_NODE_WRITER", |               "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", |               "SINGLE_NODE_READER_ONLY", | ||||||
|               "MULTI_NODE_READER_ONLY", |               "MULTI_NODE_READER_ONLY", | ||||||
|               "MULTI_NODE_SINGLE_WRITER", |               "MULTI_NODE_SINGLE_WRITER", | ||||||
|  | @ -1492,7 +1519,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | ||||||
|     let types = []; |     let types = []; | ||||||
| 
 | 
 | ||||||
|     const volumeParentDatasetName = this.getVolumeParentDatasetName(); |     const volumeParentDatasetName = this.getVolumeParentDatasetName(); | ||||||
|     const snapshotParentDatasetName = this.getDetachedSnapshotParentDatasetName(); |     const snapshotParentDatasetName = | ||||||
|  |       this.getDetachedSnapshotParentDatasetName(); | ||||||
| 
 | 
 | ||||||
|     // get data from cache and return immediately
 |     // get data from cache and return immediately
 | ||||||
|     if (starting_token) { |     if (starting_token) { | ||||||
|  | @ -1618,7 +1646,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | ||||||
|           } |           } | ||||||
|           throw new GrpcError(grpc.status.NOT_FOUND, message); |           throw new GrpcError(grpc.status.NOT_FOUND, message); | ||||||
|         } |         } | ||||||
|         throw new GrpcError(grpc.status.FAILED_PRECONDITION, e.toString()); |         throw new GrpcError(grpc.status.FAILED_PRECONDITION, err.toString()); | ||||||
|       } |       } | ||||||
| 
 | 
 | ||||||
|       response.indexed.forEach((row) => { |       response.indexed.forEach((row) => { | ||||||
|  | @ -1771,9 +1799,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | ||||||
| 
 | 
 | ||||||
|     const datasetName = datasetParentName + "/" + source_volume_id; |     const datasetName = datasetParentName + "/" + source_volume_id; | ||||||
|     snapshotProperties[SNAPSHOT_CSI_NAME_PROPERTY_NAME] = name; |     snapshotProperties[SNAPSHOT_CSI_NAME_PROPERTY_NAME] = name; | ||||||
|     snapshotProperties[ |     snapshotProperties[SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME] = | ||||||
|       SNAPSHOT_CSI_SOURCE_VOLUME_ID_PROPERTY_NAME |       source_volume_id; | ||||||
|     ] = source_volume_id; |  | ||||||
|     snapshotProperties[MANAGED_PROPERTY_NAME] = "true"; |     snapshotProperties[MANAGED_PROPERTY_NAME] = "true"; | ||||||
| 
 | 
 | ||||||
|     driver.ctx.logger.verbose("requested snapshot name: %s", name); |     driver.ctx.logger.verbose("requested snapshot name: %s", name); | ||||||
|  | @ -1995,9 +2022,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | ||||||
| 
 | 
 | ||||||
|     // cleanup parent dataset if possible
 |     // cleanup parent dataset if possible
 | ||||||
|     if (detachedSnapshot) { |     if (detachedSnapshot) { | ||||||
|       let containerDataset = zb.helpers.extractParentDatasetName( |       let containerDataset = | ||||||
|         fullSnapshotName |         zb.helpers.extractParentDatasetName(fullSnapshotName); | ||||||
|       ); |  | ||||||
|       try { |       try { | ||||||
|         await this.removeSnapshotsFromDatatset(containerDataset); |         await this.removeSnapshotsFromDatatset(containerDataset); | ||||||
|         await zb.zfs.destroy(containerDataset); |         await zb.zfs.destroy(containerDataset); | ||||||
|  |  | ||||||
|  | @ -1,10 +1,14 @@ | ||||||
| const { FreeNASDriver } = require("./freenas"); | const { FreeNASSshDriver } = require("./freenas/ssh"); | ||||||
|  | const { FreeNASApiDriver } = require("./freenas/api"); | ||||||
| const { ControllerZfsGenericDriver } = require("./controller-zfs-generic"); | const { ControllerZfsGenericDriver } = require("./controller-zfs-generic"); | ||||||
| const { | const { | ||||||
|   ZfsLocalEphemeralInlineDriver, |   ZfsLocalEphemeralInlineDriver, | ||||||
| } = require("./zfs-local-ephemeral-inline"); | } = require("./zfs-local-ephemeral-inline"); | ||||||
| 
 | 
 | ||||||
| const { ControllerNfsClientDriver } = require("./controller-nfs-client"); | const { ControllerNfsClientDriver } = require("./controller-nfs-client"); | ||||||
|  | const { ControllerSmbClientDriver } = require("./controller-smb-client"); | ||||||
|  | const { ControllerLustreClientDriver } = require("./controller-lustre-client"); | ||||||
|  | const { ControllerSynologyDriver } = require("./controller-synology"); | ||||||
| const { NodeManualDriver } = require("./node-manual"); | const { NodeManualDriver } = require("./node-manual"); | ||||||
| 
 | 
 | ||||||
| function factory(ctx, options) { | function factory(ctx, options) { | ||||||
|  | @ -15,14 +19,26 @@ function factory(ctx, options) { | ||||||
|     case "truenas-nfs": |     case "truenas-nfs": | ||||||
|     case "truenas-smb": |     case "truenas-smb": | ||||||
|     case "truenas-iscsi": |     case "truenas-iscsi": | ||||||
|       return new FreeNASDriver(ctx, options); |       return new FreeNASSshDriver(ctx, options); | ||||||
|  |     case "freenas-api-iscsi": | ||||||
|  |     case "freenas-api-nfs": | ||||||
|  |     case "freenas-api-smb": | ||||||
|  |       return new FreeNASApiDriver(ctx, options); | ||||||
|  |     case "synology-nfs": | ||||||
|  |     case "synology-smb": | ||||||
|  |     case "synology-iscsi": | ||||||
|  |       return new ControllerSynologyDriver(ctx, options); | ||||||
|     case "zfs-generic-nfs": |     case "zfs-generic-nfs": | ||||||
|     case "zfs-generic-iscsi": |     case "zfs-generic-iscsi": | ||||||
|       return new ControllerZfsGenericDriver(ctx, options); |       return new ControllerZfsGenericDriver(ctx, options); | ||||||
|     case "zfs-local-ephemeral-inline": |     case "zfs-local-ephemeral-inline": | ||||||
|       return new ZfsLocalEphemeralInlineDriver(ctx, options); |       return new ZfsLocalEphemeralInlineDriver(ctx, options); | ||||||
|  |     case "smb-client": | ||||||
|  |       return new ControllerSmbClientDriver(ctx, options); | ||||||
|     case "nfs-client": |     case "nfs-client": | ||||||
|       return new ControllerNfsClientDriver(ctx, options); |       return new ControllerNfsClientDriver(ctx, options); | ||||||
|  |     case "lustre-client": | ||||||
|  |       return new ControllerLustreClientDriver(ctx, options); | ||||||
|     case "node-manual": |     case "node-manual": | ||||||
|       return new NodeManualDriver(ctx, options); |       return new NodeManualDriver(ctx, options); | ||||||
|     default: |     default: | ||||||
|  |  | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,771 @@ | ||||||
|  | const { sleep } = require("../../../utils/general"); | ||||||
|  | const { Zetabyte } = require("../../../utils/zfs"); | ||||||
|  | 
 | ||||||
|  | // used for in-memory cache of the version info
 | ||||||
|  | const FREENAS_SYSTEM_VERSION_CACHE_KEY = "freenas:system_version"; | ||||||
|  | 
 | ||||||
|  | class Api { | ||||||
|  |   constructor(client, cache, options = {}) { | ||||||
|  |     this.client = client; | ||||||
|  |     this.cache = cache; | ||||||
|  |     this.options = options; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getHttpClient() { | ||||||
|  |     return this.client; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * only here for the helpers | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async getZetabyte() { | ||||||
|  |     return new Zetabyte({ | ||||||
|  |       executor: { | ||||||
|  |         spawn: function () { | ||||||
|  |           throw new Error( | ||||||
|  |             "cannot use the zb implementation to execute zfs commands, must use the http api" | ||||||
|  |           ); | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async findResourceByProperties(endpoint, match) { | ||||||
|  |     if (!match) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (typeof match === "object" && Object.keys(match).length < 1) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const httpClient = await this.getHttpClient(); | ||||||
|  |     let target; | ||||||
|  |     let page = 0; | ||||||
|  |     let lastReponse; | ||||||
|  | 
 | ||||||
|  |     // loop and find target
 | ||||||
|  |     let queryParams = {}; | ||||||
|  |     queryParams.limit = 100; | ||||||
|  |     queryParams.offset = 0; | ||||||
|  | 
 | ||||||
|  |     while (!target) { | ||||||
|  |       //Content-Range: items 0-2/3 (full set)
 | ||||||
|  |       //Content-Range: items 0--1/3 (invalid offset)
 | ||||||
|  |       if (queryParams.hasOwnProperty("offset")) { | ||||||
|  |         queryParams.offset = queryParams.limit * page; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // crude stoppage attempt
 | ||||||
|  |       let response = await httpClient.get(endpoint, queryParams); | ||||||
|  |       if (lastReponse) { | ||||||
|  |         if (JSON.stringify(lastReponse) == JSON.stringify(response)) { | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       lastReponse = response; | ||||||
|  | 
 | ||||||
|  |       if (response.statusCode == 200) { | ||||||
|  |         if (response.body.length < 1) { | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |         response.body.some((i) => { | ||||||
|  |           let isMatch = true; | ||||||
|  | 
 | ||||||
|  |           if (typeof match === "function") { | ||||||
|  |             isMatch = match(i); | ||||||
|  |           } else { | ||||||
|  |             for (let property in match) { | ||||||
|  |               if (match[property] != i[property]) { | ||||||
|  |                 isMatch = false; | ||||||
|  |                 break; | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if (isMatch) { | ||||||
|  |             target = i; | ||||||
|  |             return true; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return false; | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         throw new Error( | ||||||
|  |           "FreeNAS http error - code: " + | ||||||
|  |             response.statusCode + | ||||||
|  |             " body: " + | ||||||
|  |             JSON.stringify(response.body) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       page++; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return target; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getApiVersion() { | ||||||
|  |     const systemVersion = await this.getSystemVersion(); | ||||||
|  | 
 | ||||||
|  |     if (systemVersion.v2) { | ||||||
|  |       if ((await this.getSystemVersionMajorMinor()) == 11.2) { | ||||||
|  |         return 1; | ||||||
|  |       } | ||||||
|  |       return 2; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return 1; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   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; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getIsScale() { | ||||||
|  |     const systemVersion = await this.getSystemVersion(); | ||||||
|  | 
 | ||||||
|  |     if (systemVersion.v2 && systemVersion.v2.toLowerCase().includes("scale")) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getSystemVersionMajorMinor() { | ||||||
|  |     const systemVersion = await this.getSystemVersion(); | ||||||
|  |     let parts; | ||||||
|  |     let parts_i; | ||||||
|  |     let version; | ||||||
|  | 
 | ||||||
|  |     /* | ||||||
|  |     systemVersion.v2 = "FreeNAS-11.2-U5"; | ||||||
|  |     systemVersion.v2 = "TrueNAS-SCALE-20.11-MASTER-20201127-092915"; | ||||||
|  |     systemVersion.v1 = { | ||||||
|  |       fullversion: "FreeNAS-9.3-STABLE-201503200528", | ||||||
|  |       fullversion: "FreeNAS-11.2-U5 (c129415c52)", | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     systemVersion.v2 = null; | ||||||
|  |     */ | ||||||
|  | 
 | ||||||
|  |     if (systemVersion.v2) { | ||||||
|  |       version = systemVersion.v2; | ||||||
|  |     } else { | ||||||
|  |       version = systemVersion.v1.fullversion; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (version) { | ||||||
|  |       parts = version.split("-"); | ||||||
|  |       parts_i = []; | ||||||
|  |       parts.forEach((value) => { | ||||||
|  |         let i = value.replace(/[^\d.]/g, ""); | ||||||
|  |         if (i.length > 0) { | ||||||
|  |           parts_i.push(i); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       // join and resplit to deal with single elements which contain a decimal
 | ||||||
|  |       parts_i = parts_i.join(".").split("."); | ||||||
|  |       parts_i.splice(2); | ||||||
|  |       return parts_i.join("."); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getSystemVersionMajor() { | ||||||
|  |     const majorMinor = await this.getSystemVersionMajorMinor(); | ||||||
|  |     return majorMinor.split(".")[0]; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async setVersionInfoCache(versionInfo) { | ||||||
|  |     await this.cache.set( | ||||||
|  |       FREENAS_SYSTEM_VERSION_CACHE_KEY, | ||||||
|  |       versionInfo, | ||||||
|  |       60 * 1000 | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async getSystemVersion() { | ||||||
|  |     let cacheData = await this.cache.get(FREENAS_SYSTEM_VERSION_CACHE_KEY); | ||||||
|  | 
 | ||||||
|  |     if (cacheData) { | ||||||
|  |       return cacheData; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     const endpoint = "/system/version/"; | ||||||
|  |     let response; | ||||||
|  |     const startApiVersion = httpClient.getApiVersion(); | ||||||
|  |     const versionInfo = {}; | ||||||
|  |     const versionErrors = {}; | ||||||
|  |     const versionResponses = {}; | ||||||
|  | 
 | ||||||
|  |     httpClient.setApiVersion(2); | ||||||
|  |     /** | ||||||
|  |      * FreeNAS-11.2-U5 | ||||||
|  |      * TrueNAS-12.0-RELEASE | ||||||
|  |      * TrueNAS-SCALE-20.11-MASTER-20201127-092915 | ||||||
|  |      */ | ||||||
|  |     try { | ||||||
|  |       response = await httpClient.get(endpoint); | ||||||
|  |       versionResponses.v2 = response; | ||||||
|  |       if (response.statusCode == 200) { | ||||||
|  |         versionInfo.v2 = response.body; | ||||||
|  | 
 | ||||||
|  |         // return immediately to save on resources and silly requests
 | ||||||
|  |         await this.setVersionInfoCache(versionInfo); | ||||||
|  | 
 | ||||||
|  |         // reset apiVersion
 | ||||||
|  |         httpClient.setApiVersion(startApiVersion); | ||||||
|  | 
 | ||||||
|  |         return versionInfo; | ||||||
|  |       } | ||||||
|  |     } catch (e) { | ||||||
|  |       // if more info is needed use e.stack
 | ||||||
|  |       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); | ||||||
|  |       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 GrpcError( | ||||||
|  |       grpc.status.UNKNOWN, | ||||||
|  |       `FreeNAS error getting system version info: ${JSON.stringify({ | ||||||
|  |         errors: versionErrors, | ||||||
|  |         responses: versionResponses, | ||||||
|  |       })}` | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getIsUserProperty(property) { | ||||||
|  |     if (property.includes(":")) { | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getUserProperties(properties) { | ||||||
|  |     let user_properties = {}; | ||||||
|  |     for (const property in properties) { | ||||||
|  |       if (this.getIsUserProperty(property)) { | ||||||
|  |         user_properties[property] = properties[property]; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return user_properties; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getSystemProperties(properties) { | ||||||
|  |     let system_properties = {}; | ||||||
|  |     for (const property in properties) { | ||||||
|  |       if (!this.getIsUserProperty(property)) { | ||||||
|  |         system_properties[property] = properties[property]; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return system_properties; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getPropertiesKeyValueArray(properties) { | ||||||
|  |     let arr = []; | ||||||
|  |     for (const property in properties) { | ||||||
|  |       arr.push({ key: property, value: properties[property] }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return arr; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   normalizeProperties(dataset, properties) { | ||||||
|  |     let res = {}; | ||||||
|  |     for (const property of properties) { | ||||||
|  |       let p; | ||||||
|  |       if (dataset.hasOwnProperty(property)) { | ||||||
|  |         p = dataset[property]; | ||||||
|  |       } else if ( | ||||||
|  |         dataset.properties && | ||||||
|  |         dataset.properties.hasOwnProperty(property) | ||||||
|  |       ) { | ||||||
|  |         p = dataset.properties[property]; | ||||||
|  |       } else if ( | ||||||
|  |         dataset.user_properties && | ||||||
|  |         dataset.user_properties.hasOwnProperty(property) | ||||||
|  |       ) { | ||||||
|  |         p = dataset.user_properties[property]; | ||||||
|  |       } else { | ||||||
|  |         p = { | ||||||
|  |           value: "-", | ||||||
|  |           rawvalue: "-", | ||||||
|  |           source: "-", | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (typeof p === "object" && p !== null) { | ||||||
|  |         // nothing, leave as is
 | ||||||
|  |       } else { | ||||||
|  |         p = { | ||||||
|  |           value: p, | ||||||
|  |           rawvalue: p, | ||||||
|  |           source: "-", | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       res[property] = p; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return res; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async DatasetCreate(datasetName, data) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     data.name = datasetName; | ||||||
|  | 
 | ||||||
|  |     endpoint = "/pool/dataset"; | ||||||
|  |     response = await httpClient.post(endpoint, data); | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       response.statusCode == 422 && | ||||||
|  |       JSON.stringify(response.body).includes("already exists") | ||||||
|  |     ) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @param {*} datasetName | ||||||
|  |    * @param {*} data | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async DatasetDelete(datasetName, data) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; | ||||||
|  |     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 DatasetSet(datasetName, properties) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; | ||||||
|  |     response = await httpClient.put(endpoint, { | ||||||
|  |       ...this.getSystemProperties(properties), | ||||||
|  |       user_properties_update: this.getPropertiesKeyValueArray( | ||||||
|  |         this.getUserProperties(properties) | ||||||
|  |       ), | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async DatasetInherit(datasetName, property) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  |     let system_properties = {}; | ||||||
|  |     let user_properties_update = []; | ||||||
|  | 
 | ||||||
|  |     const isUserProperty = this.getIsUserProperty(property); | ||||||
|  |     if (isUserProperty) { | ||||||
|  |       user_properties_update = [{ key: property, remove: true }]; | ||||||
|  |     } else { | ||||||
|  |       system_properties[property] = "INHERIT"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; | ||||||
|  |     response = await httpClient.put(endpoint, { | ||||||
|  |       ...system_properties, | ||||||
|  |       user_properties_update, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * zfs get -Hp all tank/k8s/test/PVC-111 | ||||||
|  |    * | ||||||
|  |    * @param {*} datasetName | ||||||
|  |    * @param {*} properties | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async DatasetGet(datasetName, properties) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     endpoint = `/pool/dataset/id/${encodeURIComponent(datasetName)}`; | ||||||
|  |     response = await httpClient.get(endpoint); | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return this.normalizeProperties(response.body, properties); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 404) { | ||||||
|  |       throw new Error("dataset does not exist"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async DatasetDestroySnapshots(datasetName, data = {}) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     data.name = datasetName; | ||||||
|  | 
 | ||||||
|  |     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)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async SnapshotSet(snapshotName, properties) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; | ||||||
|  |     response = await httpClient.put(endpoint, { | ||||||
|  |       //...this.getSystemProperties(properties),
 | ||||||
|  |       user_properties_update: this.getPropertiesKeyValueArray( | ||||||
|  |         this.getUserProperties(properties) | ||||||
|  |       ), | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * zfs get -Hp all tank/k8s/test/PVC-111 | ||||||
|  |    * | ||||||
|  |    * @param {*} snapshotName | ||||||
|  |    * @param {*} properties | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async SnapshotGet(snapshotName, properties) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; | ||||||
|  |     response = await httpClient.get(endpoint); | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return this.normalizeProperties(response.body, properties); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 404) { | ||||||
|  |       throw new Error("dataset does not exist"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async SnapshotCreate(snapshotName, data = {}) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     const zb = await this.getZetabyte(); | ||||||
|  | 
 | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     const dataset = zb.helpers.extractDatasetName(snapshotName); | ||||||
|  |     const snapshot = zb.helpers.extractSnapshotName(snapshotName); | ||||||
|  | 
 | ||||||
|  |     data.dataset = dataset; | ||||||
|  |     data.name = snapshot; | ||||||
|  | 
 | ||||||
|  |     endpoint = "/zfs/snapshot"; | ||||||
|  |     response = await httpClient.post(endpoint, data); | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       response.statusCode == 422 && | ||||||
|  |       JSON.stringify(response.body).includes("already exists") | ||||||
|  |     ) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async SnapshotDelete(snapshotName, data = {}) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     const zb = await this.getZetabyte(); | ||||||
|  | 
 | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     endpoint = `/zfs/snapshot/id/${encodeURIComponent(snapshotName)}`; | ||||||
|  |     response = await httpClient.delete(endpoint, data); | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 404) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       response.statusCode == 422 && | ||||||
|  |       JSON.stringify(response.body).includes("not found") | ||||||
|  |     ) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async CloneCreate(snapshotName, datasetName, data = {}) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     const zb = await this.getZetabyte(); | ||||||
|  | 
 | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     data.snapshot = snapshotName; | ||||||
|  |     data.dataset_dst = datasetName; | ||||||
|  | 
 | ||||||
|  |     endpoint = "/zfs/snapshot/clone"; | ||||||
|  |     response = await httpClient.post(endpoint, data); | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if ( | ||||||
|  |       response.statusCode == 422 && | ||||||
|  |       JSON.stringify(response.body).includes("already exists") | ||||||
|  |     ) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // get all dataset snapshots
 | ||||||
|  |   // https://github.com/truenas/middleware/pull/6934
 | ||||||
|  |   // then use core.bulk to delete all
 | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * /usr/lib/python3/dist-packages/middlewared/plugins/replication.py | ||||||
|  |    * readonly enum=["SET", "REQUIRE", "IGNORE"] | ||||||
|  |    * | ||||||
|  |    * @param {*} data | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async ReplicationRunOnetime(data) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  | 
 | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     endpoint = "/replication/run_onetime"; | ||||||
|  |     response = await httpClient.post(endpoint, data); | ||||||
|  | 
 | ||||||
|  |     // 200 means the 'job' was accepted only
 | ||||||
|  |     // must continue to check the status of the job to know when it has finished and if it was successful
 | ||||||
|  |     // /core/get_jobs [["id", "=", jobidhere]]
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return response.body; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async CoreWaitForJob(job_id, timeout = 0) { | ||||||
|  |     if (!job_id) { | ||||||
|  |       throw new Error("invalid job_id"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const startTime = Date.now() / 1000; | ||||||
|  |     let currentTime; | ||||||
|  | 
 | ||||||
|  |     let job; | ||||||
|  | 
 | ||||||
|  |     // wait for job to finish
 | ||||||
|  |     while (!job || !["SUCCESS", "ABORTED", "FAILED"].includes(job.state)) { | ||||||
|  |       job = await this.CoreGetJobs({ id: job_id }); | ||||||
|  |       job = job[0]; | ||||||
|  |       await sleep(3000); | ||||||
|  | 
 | ||||||
|  |       currentTime = Date.now() / 1000; | ||||||
|  |       if (timeout > 0 && currentTime > startTime + timeout) { | ||||||
|  |         throw new Error("timeout waiting for job to complete"); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return job; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async CoreGetJobs(data) { | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  | 
 | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     endpoint = "/core/get_jobs"; | ||||||
|  |     response = await httpClient.get(endpoint, data); | ||||||
|  | 
 | ||||||
|  |     // 200 means the 'job' was accepted only
 | ||||||
|  |     // must continue to check the status of the job to know when it has finished and if it was successful
 | ||||||
|  |     // /core/get_jobs [["id", "=", jobidhere]]
 | ||||||
|  |     // state = SUCCESS/ABORTED/FAILED means finality has been reached
 | ||||||
|  |     // state = RUNNING
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return response.body; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * | ||||||
|  |    * @param {*} data | ||||||
|  |    */ | ||||||
|  |   async FilesystemSetperm(data) { | ||||||
|  |     /* | ||||||
|  |       { | ||||||
|  |         "path": "string", | ||||||
|  |         "mode": "string", | ||||||
|  |         "uid": 0, | ||||||
|  |         "gid": 0, | ||||||
|  |         "options": { | ||||||
|  |           "stripacl": false, | ||||||
|  |           "recursive": false, | ||||||
|  |           "traverse": false | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     */ | ||||||
|  | 
 | ||||||
|  |     const httpClient = await this.getHttpClient(false); | ||||||
|  |     let response; | ||||||
|  |     let endpoint; | ||||||
|  | 
 | ||||||
|  |     endpoint = `/filesystem/setperm`; | ||||||
|  |     response = await httpClient.post(endpoint, data); | ||||||
|  | 
 | ||||||
|  |     if (response.statusCode == 200) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     throw new Error(JSON.stringify(response.body)); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function IsJsonString(str) { | ||||||
|  |   try { | ||||||
|  |     JSON.parse(str); | ||||||
|  |   } catch (e) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports.Api = Api; | ||||||
|  | @ -18,7 +18,7 @@ const FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME = | ||||||
| 
 | 
 | ||||||
| // used for in-memory cache of the version info
 | // used for in-memory cache of the version info
 | ||||||
| const FREENAS_SYSTEM_VERSION_CACHE_KEY = "freenas:system_version"; | const FREENAS_SYSTEM_VERSION_CACHE_KEY = "freenas:system_version"; | ||||||
| class FreeNASDriver extends ControllerZfsSshBaseDriver { | class FreeNASSshDriver extends ControllerZfsSshBaseDriver { | ||||||
|   /** |   /** | ||||||
|    * cannot make this a storage class parameter as storage class/etc context is *not* sent |    * cannot make this a storage class parameter as storage class/etc context is *not* sent | ||||||
|    * into various calls such as GetControllerCapabilities etc |    * into various calls such as GetControllerCapabilities etc | ||||||
|  | @ -201,9 +201,8 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { | ||||||
|                   share = { |                   share = { | ||||||
|                     nfs_paths: [properties.mountpoint.value], |                     nfs_paths: [properties.mountpoint.value], | ||||||
|                     nfs_comment: `democratic-csi (${this.ctx.args.csiName}): ${datasetName}`, |                     nfs_comment: `democratic-csi (${this.ctx.args.csiName}): ${datasetName}`, | ||||||
|                     nfs_network: this.options.nfs.shareAllowedNetworks.join( |                     nfs_network: | ||||||
|                       "," |                       this.options.nfs.shareAllowedNetworks.join(","), | ||||||
|                     ), |  | ||||||
|                     nfs_hosts: this.options.nfs.shareAllowedHosts.join(","), |                     nfs_hosts: this.options.nfs.shareAllowedHosts.join(","), | ||||||
|                     nfs_alldirs: this.options.nfs.shareAlldirs, |                     nfs_alldirs: this.options.nfs.shareAlldirs, | ||||||
|                     nfs_ro: false, |                     nfs_ro: false, | ||||||
|  | @ -633,11 +632,10 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { | ||||||
|           ? this.options.iscsi.extentBlocksize |           ? this.options.iscsi.extentBlocksize | ||||||
|           : 512; |           : 512; | ||||||
| 
 | 
 | ||||||
|         const extentDisablePhysicalBlocksize = this.options.iscsi.hasOwnProperty( |         const extentDisablePhysicalBlocksize = | ||||||
|           "extentDisablePhysicalBlocksize" |           this.options.iscsi.hasOwnProperty("extentDisablePhysicalBlocksize") | ||||||
|         ) |             ? this.options.iscsi.extentDisablePhysicalBlocksize | ||||||
|           ? this.options.iscsi.extentDisablePhysicalBlocksize |             : true; | ||||||
|           : true; |  | ||||||
| 
 | 
 | ||||||
|         const extentRpm = this.options.iscsi.hasOwnProperty("extentRpm") |         const extentRpm = this.options.iscsi.hasOwnProperty("extentRpm") | ||||||
|           ? this.options.iscsi.extentRpm |           ? this.options.iscsi.extentRpm | ||||||
|  | @ -1232,27 +1230,13 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { | ||||||
|           [FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME]: iscsiName, |           [FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME]: iscsiName, | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         // iscsiadm -m discovery -t st -p 172.21.26.81
 |  | ||||||
|         // iscsiadm -m node -T iqn.2011-03.lan.bitness.istgt:test -p bitness.lan -l
 |  | ||||||
| 
 |  | ||||||
|         // FROM driver config? no, node attachment should have everything required to remain independent
 |  | ||||||
|         // portal
 |  | ||||||
|         // portals
 |  | ||||||
|         // interface
 |  | ||||||
|         // chap discovery
 |  | ||||||
|         // chap session
 |  | ||||||
| 
 |  | ||||||
|         // FROM context
 |  | ||||||
|         // iqn
 |  | ||||||
|         // lun
 |  | ||||||
| 
 |  | ||||||
|         volume_context = { |         volume_context = { | ||||||
|           node_attach_driver: "iscsi", |           node_attach_driver: "iscsi", | ||||||
|           portal: this.options.iscsi.targetPortal, |           portal: this.options.iscsi.targetPortal || "", | ||||||
|           portals: this.options.iscsi.targetPortals.join(","), |           portals: this.options.iscsi.targetPortals | ||||||
|  |             ? this.options.iscsi.targetPortals.join(",") | ||||||
|  |             : "", | ||||||
|           interface: this.options.iscsi.interface || "", |           interface: this.options.iscsi.interface || "", | ||||||
|           //chapDiscoveryEnabled: this.options.iscsi.chapDiscoveryEnabled,
 |  | ||||||
|           //chapSessionEnabled: this.options.iscsi.chapSessionEnabled,
 |  | ||||||
|           iqn: iqn, |           iqn: iqn, | ||||||
|           lun: 0, |           lun: 0, | ||||||
|         }; |         }; | ||||||
|  | @ -1619,6 +1603,7 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { | ||||||
|   async expandVolume(call, datasetName) { |   async expandVolume(call, datasetName) { | ||||||
|     const driverShareType = this.getDriverShareType(); |     const driverShareType = this.getDriverShareType(); | ||||||
|     const sshClient = this.getSshClient(); |     const sshClient = this.getSshClient(); | ||||||
|  |     const zb = await this.getZetabyte(); | ||||||
| 
 | 
 | ||||||
|     switch (driverShareType) { |     switch (driverShareType) { | ||||||
|       case "iscsi": |       case "iscsi": | ||||||
|  | @ -1626,7 +1611,29 @@ class FreeNASDriver extends ControllerZfsSshBaseDriver { | ||||||
|         let command; |         let command; | ||||||
|         let reload = false; |         let reload = false; | ||||||
|         if (isScale) { |         if (isScale) { | ||||||
|           command = sshClient.buildCommand("systemctl", ["reload", "scst"]); |           let properties; | ||||||
|  |           properties = await zb.zfs.get(datasetName, [ | ||||||
|  |             FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME, | ||||||
|  |           ]); | ||||||
|  |           properties = properties[datasetName]; | ||||||
|  |           this.ctx.logger.debug("zfs props data: %j", properties); | ||||||
|  |           let iscsiName = | ||||||
|  |             properties[FREENAS_ISCSI_ASSETS_NAME_PROPERTY_NAME].value; | ||||||
|  | 
 | ||||||
|  |           /** | ||||||
|  |            * command = sshClient.buildCommand("systemctl", ["reload", "scst"]); | ||||||
|  |            * does not help ^ | ||||||
|  |            * | ||||||
|  |            * echo 1 > /sys/kernel/scst_tgt/devices/${iscsiName}/resync_size | ||||||
|  |            * works ^ | ||||||
|  |            * | ||||||
|  |            * scstadmin -resync_dev ${iscsiName} | ||||||
|  |            * works but always give a exit code of 1 ^ | ||||||
|  |            */ | ||||||
|  |           command = sshClient.buildCommand("sh", [ | ||||||
|  |             "-c", | ||||||
|  |             `echo 1 > /sys/kernel/scst_tgt/devices/${iscsiName}/resync_size`, | ||||||
|  |           ]); | ||||||
|           reload = true; |           reload = true; | ||||||
|         } else { |         } else { | ||||||
|           command = sshClient.buildCommand("/etc/rc.d/ctld", ["reload"]); |           command = sshClient.buildCommand("/etc/rc.d/ctld", ["reload"]); | ||||||
|  | @ -1853,4 +1860,4 @@ function IsJsonString(str) { | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| module.exports.FreeNASDriver = FreeNASDriver; | module.exports.FreeNASSshDriver = FreeNASSshDriver; | ||||||
|  | @ -1,3 +1,4 @@ | ||||||
|  | const _ = require("lodash"); | ||||||
| const os = require("os"); | const os = require("os"); | ||||||
| const fs = require("fs"); | const fs = require("fs"); | ||||||
| const { GrpcError, grpc } = require("../utils/grpc"); | const { GrpcError, grpc } = require("../utils/grpc"); | ||||||
|  | @ -17,7 +18,23 @@ const sleep = require("../utils/general").sleep; | ||||||
| class CsiBaseDriver { | class CsiBaseDriver { | ||||||
|   constructor(ctx, options) { |   constructor(ctx, options) { | ||||||
|     this.ctx = ctx; |     this.ctx = ctx; | ||||||
|     this.options = options; |     this.options = options || {}; | ||||||
|  | 
 | ||||||
|  |     if (!this.options.hasOwnProperty("node")) { | ||||||
|  |       this.options.node = {}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!this.options.node.hasOwnProperty("format")) { | ||||||
|  |       this.options.node.format = {}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!this.options.node.hasOwnProperty("mount")) { | ||||||
|  |       this.options.node.mount = {}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!this.options.node.mount.hasOwnProperty("checkFilesystem")) { | ||||||
|  |       this.options.node.mount.checkFilesystem = {}; | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|  | @ -269,6 +286,7 @@ class CsiBaseDriver { | ||||||
|     const volume_context = call.request.volume_context; |     const volume_context = call.request.volume_context; | ||||||
|     let fs_type; |     let fs_type; | ||||||
|     let mount_flags; |     let mount_flags; | ||||||
|  |     let volume_mount_group; | ||||||
|     const node_attach_driver = volume_context.node_attach_driver; |     const node_attach_driver = volume_context.node_attach_driver; | ||||||
|     const block_path = staging_target_path + "/block_device"; |     const block_path = staging_target_path + "/block_device"; | ||||||
|     const bind_mount_flags = []; |     const bind_mount_flags = []; | ||||||
|  | @ -280,6 +298,14 @@ class CsiBaseDriver { | ||||||
|       call.request.volume_context.provisioner_driver_instance_id |       call.request.volume_context.provisioner_driver_instance_id | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|  |     /* | ||||||
|  |     let mount_options = await mount.getMountOptions(staging_target_path); | ||||||
|  |     console.log(mount_options); | ||||||
|  |     console.log(await mount.getMountOptionValue(mount_options, "stripe")); | ||||||
|  |     console.log(await mount.getMountOptionPresent(mount_options, "stripee")); | ||||||
|  |     throw new Error("foobar"); | ||||||
|  |     */ | ||||||
|  | 
 | ||||||
|     if (access_type == "mount") { |     if (access_type == "mount") { | ||||||
|       fs_type = capability.mount.fs_type; |       fs_type = capability.mount.fs_type; | ||||||
|       mount_flags = capability.mount.mount_flags || []; |       mount_flags = capability.mount.mount_flags || []; | ||||||
|  | @ -288,6 +314,19 @@ class CsiBaseDriver { | ||||||
|         mount_flags.push(normalizedSecrets.mount_flags); |         mount_flags.push(normalizedSecrets.mount_flags); | ||||||
|       } |       } | ||||||
|       mount_flags.push("defaults"); |       mount_flags.push("defaults"); | ||||||
|  | 
 | ||||||
|  |       // https://github.com/karelzak/util-linux/issues/1429
 | ||||||
|  |       //mount_flags.push("x-democratic-csi.managed");
 | ||||||
|  |       //mount_flags.push("x-democratic-csi.staged");
 | ||||||
|  | 
 | ||||||
|  |       if ( | ||||||
|  |         semver.satisfies(driver.ctx.csiVersion, ">=1.5.0") && | ||||||
|  |         driver.options.service.node.capabilities.rpc.includes( | ||||||
|  |           "VOLUME_MOUNT_GROUP" | ||||||
|  |         ) | ||||||
|  |       ) { | ||||||
|  |         volume_mount_group = capability.mount.volume_mount_group; // in k8s this is derrived from the fsgroup in the pod security context
 | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (call.request.volume_context.provisioner_driver == "node-manual") { |     if (call.request.volume_context.provisioner_driver == "node-manual") { | ||||||
|  | @ -316,6 +355,7 @@ class CsiBaseDriver { | ||||||
| 
 | 
 | ||||||
|     switch (node_attach_driver) { |     switch (node_attach_driver) { | ||||||
|       case "nfs": |       case "nfs": | ||||||
|  |       case "lustre": | ||||||
|         device = `${volume_context.server}:${volume_context.share}`; |         device = `${volume_context.server}:${volume_context.share}`; | ||||||
|         break; |         break; | ||||||
|       case "smb": |       case "smb": | ||||||
|  | @ -345,9 +385,27 @@ class CsiBaseDriver { | ||||||
|         // ensure unique entries only
 |         // ensure unique entries only
 | ||||||
|         portals = [...new Set(portals)]; |         portals = [...new Set(portals)]; | ||||||
| 
 | 
 | ||||||
|  |         // stores actual device paths after iscsi login
 | ||||||
|         let iscsiDevices = []; |         let iscsiDevices = []; | ||||||
| 
 | 
 | ||||||
|  |         // stores configuration of targets/iqn/luns to connect to
 | ||||||
|  |         let iscsiConnections = []; | ||||||
|         for (let portal of portals) { |         for (let portal of portals) { | ||||||
|  |           iscsiConnections.push({ | ||||||
|  |             portal, | ||||||
|  |             iqn: volume_context.iqn, | ||||||
|  |             lun: volume_context.lun, | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * TODO: allow sending in iscsiConnection in a raw/manual format | ||||||
|  |          * TODO: allow option to determine if send_targets should be invoked | ||||||
|  |          * TODO: allow option to control whether nodedb entry should be created by driver | ||||||
|  |          * TODO: allow option to control whether nodedb entry should be deleted by driver | ||||||
|  |          */ | ||||||
|  | 
 | ||||||
|  |         for (let iscsiConnection of iscsiConnections) { | ||||||
|           // create DB entry
 |           // create DB entry
 | ||||||
|           // https://library.netapp.com/ecmdocs/ECMP1654943/html/GUID-8EC685B4-8CB6-40D8-A8D5-031A3899BCDC.html
 |           // https://library.netapp.com/ecmdocs/ECMP1654943/html/GUID-8EC685B4-8CB6-40D8-A8D5-031A3899BCDC.html
 | ||||||
|           // put these options in place to force targets managed by csi to be explicitly attached (in the case of unclearn shutdown etc)
 |           // put these options in place to force targets managed by csi to be explicitly attached (in the case of unclearn shutdown etc)
 | ||||||
|  | @ -363,24 +421,27 @@ class CsiBaseDriver { | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|           await iscsi.iscsiadm.createNodeDBEntry( |           await iscsi.iscsiadm.createNodeDBEntry( | ||||||
|             volume_context.iqn, |             iscsiConnection.iqn, | ||||||
|             portal, |             iscsiConnection.portal, | ||||||
|             nodeDB |             nodeDB | ||||||
|           ); |           ); | ||||||
|           // login
 |           // login
 | ||||||
|           await iscsi.iscsiadm.login(volume_context.iqn, portal); |           await iscsi.iscsiadm.login( | ||||||
|  |             iscsiConnection.iqn, | ||||||
|  |             iscsiConnection.portal | ||||||
|  |           ); | ||||||
| 
 | 
 | ||||||
|           // get associated session
 |           // get associated session
 | ||||||
|           let session = await iscsi.iscsiadm.getSession( |           let session = await iscsi.iscsiadm.getSession( | ||||||
|             volume_context.iqn, |             iscsiConnection.iqn, | ||||||
|             portal |             iscsiConnection.portal | ||||||
|           ); |           ); | ||||||
| 
 | 
 | ||||||
|           // rescan in scenarios when login previously occurred but volumes never appeared
 |           // rescan in scenarios when login previously occurred but volumes never appeared
 | ||||||
|           await iscsi.iscsiadm.rescanSession(session); |           await iscsi.iscsiadm.rescanSession(session); | ||||||
| 
 | 
 | ||||||
|           // find device name
 |           // find device name
 | ||||||
|           device = `/dev/disk/by-path/ip-${portal}-iscsi-${volume_context.iqn}-lun-${volume_context.lun}`; |           device = `/dev/disk/by-path/ip-${iscsiConnection.portal}-iscsi-${iscsiConnection.iqn}-lun-${iscsiConnection.lun}`; | ||||||
|           let deviceByPath = device; |           let deviceByPath = device; | ||||||
| 
 | 
 | ||||||
|           // can take some time for device to show up, loop for some period
 |           // can take some time for device to show up, loop for some period
 | ||||||
|  | @ -411,7 +472,7 @@ class CsiBaseDriver { | ||||||
|             iscsiDevices.push(device); |             iscsiDevices.push(device); | ||||||
| 
 | 
 | ||||||
|             driver.ctx.logger.info( |             driver.ctx.logger.info( | ||||||
|               `successfully logged into portal ${portal} and created device ${deviceByPath} with realpath ${device}` |               `successfully logged into portal ${iscsiConnection.portal} and created device ${deviceByPath} with realpath ${device}` | ||||||
|             ); |             ); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|  | @ -433,7 +494,7 @@ class CsiBaseDriver { | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (iscsiDevices.length != portals.length) { |         if (iscsiDevices.length != iscsiConnections.length) { | ||||||
|           driver.ctx.logger.warn( |           driver.ctx.logger.warn( | ||||||
|             `failed to attach all iscsi devices/targets/portals` |             `failed to attach all iscsi devices/targets/portals` | ||||||
|           ); |           ); | ||||||
|  | @ -450,12 +511,14 @@ class CsiBaseDriver { | ||||||
|         // compare all device-mapper slaves with the newly created devices
 |         // compare all device-mapper slaves with the newly created devices
 | ||||||
|         // if any of the new devices are device-mapper slaves treat this as a
 |         // if any of the new devices are device-mapper slaves treat this as a
 | ||||||
|         // multipath scenario
 |         // multipath scenario
 | ||||||
|         let allDeviceMapperSlaves = await filesystem.getAllDeviceMapperSlaveDevices(); |         let allDeviceMapperSlaves = | ||||||
|  |           await filesystem.getAllDeviceMapperSlaveDevices(); | ||||||
|         let commonDevices = allDeviceMapperSlaves.filter((value) => |         let commonDevices = allDeviceMapperSlaves.filter((value) => | ||||||
|           iscsiDevices.includes(value) |           iscsiDevices.includes(value) | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         const useMultipath = portals.length > 1 || commonDevices.length > 0; |         const useMultipath = | ||||||
|  |           iscsiConnections.length > 1 || commonDevices.length > 0; | ||||||
| 
 | 
 | ||||||
|         // discover multipath device to use
 |         // discover multipath device to use
 | ||||||
|         if (useMultipath) { |         if (useMultipath) { | ||||||
|  | @ -488,7 +551,15 @@ class CsiBaseDriver { | ||||||
|               // format
 |               // format
 | ||||||
|               result = await filesystem.deviceIsFormatted(device); |               result = await filesystem.deviceIsFormatted(device); | ||||||
|               if (!result) { |               if (!result) { | ||||||
|                 await filesystem.formatDevice(device, fs_type); |                 let formatOptions = _.get( | ||||||
|  |                   driver.options.node.format, | ||||||
|  |                   [fs_type, "customOptions"], | ||||||
|  |                   [] | ||||||
|  |                 ); | ||||||
|  |                 if (!Array.isArray(formatOptions)) { | ||||||
|  |                   formatOptions = []; | ||||||
|  |                 } | ||||||
|  |                 await filesystem.formatDevice(device, fs_type, formatOptions); | ||||||
|               } |               } | ||||||
| 
 | 
 | ||||||
|               let fs_info = await filesystem.getDeviceFilesystemInfo(device); |               let fs_info = await filesystem.getDeviceFilesystemInfo(device); | ||||||
|  | @ -500,9 +571,17 @@ class CsiBaseDriver { | ||||||
|                 staging_target_path |                 staging_target_path | ||||||
|               ); |               ); | ||||||
|               if (!result) { |               if (!result) { | ||||||
|                 // TODO: add a parameter to control this behavior
 |  | ||||||
|                 // https://github.com/democratic-csi/democratic-csi/issues/52#issuecomment-768463401
 |                 // https://github.com/democratic-csi/democratic-csi/issues/52#issuecomment-768463401
 | ||||||
|                 //await filesystem.checkFilesystem(device, fs_type);
 |                 let checkFilesystem = | ||||||
|  |                   driver.options.node.mount.checkFilesystem[fs_type] || {}; | ||||||
|  |                 if (checkFilesystem.enabled) { | ||||||
|  |                   await filesystem.checkFilesystem( | ||||||
|  |                     device, | ||||||
|  |                     fs_type, | ||||||
|  |                     checkFilesystem.customOptions || [], | ||||||
|  |                     checkFilesystem.customFilesystemOptions || [] | ||||||
|  |                   ); | ||||||
|  |                 } | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|             break; |             break; | ||||||
|  | @ -526,7 +605,33 @@ class CsiBaseDriver { | ||||||
|             case "ext3": |             case "ext3": | ||||||
|             case "ext4dev": |             case "ext4dev": | ||||||
|               //await filesystem.checkFilesystem(device, fs_info.type);
 |               //await filesystem.checkFilesystem(device, fs_info.type);
 | ||||||
|               await filesystem.expandFilesystem(device, fs_type); |               try { | ||||||
|  |                 await filesystem.expandFilesystem(device, fs_type); | ||||||
|  |               } catch (err) { | ||||||
|  |                 // mount is clean and rw, but it will not expand until clean umount has been done
 | ||||||
|  |                 // failed to execute filesystem command: resize2fs /dev/sda, response: {"code":1,"stdout":"Couldn't find valid filesystem superblock.\n","stderr":"resize2fs 1.44.5 (15-Dec-2018)\nresize2fs: Superblock checksum does not match superblock while trying to open /dev/sda\n"}
 | ||||||
|  |                 // /dev/sda on /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-4a80757e-5e87-475d-826f-44fcc4719348/globalmount type ext4 (rw,relatime,stripe=256)
 | ||||||
|  |                 if ( | ||||||
|  |                   err.code == 1 && | ||||||
|  |                   err.stdout.includes("find valid filesystem superblock") && | ||||||
|  |                   err.stderr.includes("checksum does not match superblock") | ||||||
|  |                 ) { | ||||||
|  |                   driver.ctx.logger.warn( | ||||||
|  |                     `successful mount, unsuccessful fs resize: attempting abnormal umount/mount/resize2fs to clear things up ${staging_target_path} (${device})` | ||||||
|  |                   ); | ||||||
|  | 
 | ||||||
|  |                   // try an unmount/mount/fsck cycle again just to clean things up
 | ||||||
|  |                   await mount.umount(staging_target_path, []); | ||||||
|  |                   await mount.mount( | ||||||
|  |                     device, | ||||||
|  |                     staging_target_path, | ||||||
|  |                     ["-t", fs_type].concat(["-o", mount_flags.join(",")]) | ||||||
|  |                   ); | ||||||
|  |                   await filesystem.expandFilesystem(device, fs_type); | ||||||
|  |                 } else { | ||||||
|  |                   throw err; | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|               break; |               break; | ||||||
|             case "xfs": |             case "xfs": | ||||||
|               //await filesystem.checkFilesystem(device, fs_info.type);
 |               //await filesystem.checkFilesystem(device, fs_info.type);
 | ||||||
|  | @ -581,6 +686,7 @@ class CsiBaseDriver { | ||||||
|    * @param {*} call |    * @param {*} call | ||||||
|    */ |    */ | ||||||
|   async NodeUnstageVolume(call) { |   async NodeUnstageVolume(call) { | ||||||
|  |     const driver = this; | ||||||
|     const mount = new Mount(); |     const mount = new Mount(); | ||||||
|     const filesystem = new Filesystem(); |     const filesystem = new Filesystem(); | ||||||
|     const iscsi = new ISCSI(); |     const iscsi = new ISCSI(); | ||||||
|  | @ -594,7 +700,8 @@ class CsiBaseDriver { | ||||||
|     const staging_target_path = call.request.staging_target_path; |     const staging_target_path = call.request.staging_target_path; | ||||||
|     const block_path = staging_target_path + "/block_device"; |     const block_path = staging_target_path + "/block_device"; | ||||||
|     let normalized_staging_path = staging_target_path; |     let normalized_staging_path = staging_target_path; | ||||||
|     const umount_args = []; // --force
 |     const umount_args = []; | ||||||
|  |     const umount_force_extra_args = ["--force", "--lazy"]; | ||||||
| 
 | 
 | ||||||
|     if (!staging_target_path) { |     if (!staging_target_path) { | ||||||
|       throw new GrpcError( |       throw new GrpcError( | ||||||
|  | @ -606,7 +713,30 @@ class CsiBaseDriver { | ||||||
|     //result = await mount.pathIsMounted(block_path);
 |     //result = await mount.pathIsMounted(block_path);
 | ||||||
|     //result = await mount.pathIsMounted(staging_target_path)
 |     //result = await mount.pathIsMounted(staging_target_path)
 | ||||||
| 
 | 
 | ||||||
|     result = await mount.pathIsMounted(block_path); |     // TODO: use the x-* mount options to detect if we should delete target
 | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       result = await mount.pathIsMounted(block_path); | ||||||
|  |     } catch (err) { | ||||||
|  |       /** | ||||||
|  |        * on stalled fs such as nfs, even findmnt will return immediately for the base mount point | ||||||
|  |        * so in the case of timeout here (base mount point and then a file/folder beneath it) we almost certainly are not a block device | ||||||
|  |        * AND the fs is probably stalled | ||||||
|  |        */ | ||||||
|  |       if (err.timeout) { | ||||||
|  |         driver.ctx.logger.warn( | ||||||
|  |           `detected stale mount, attempting to force unmount: ${normalized_staging_path}` | ||||||
|  |         ); | ||||||
|  |         await mount.umount( | ||||||
|  |           normalized_staging_path, | ||||||
|  |           umount_args.concat(umount_force_extra_args) | ||||||
|  |         ); | ||||||
|  |         result = false; // assume we are *NOT* a block device at this point
 | ||||||
|  |       } else { | ||||||
|  |         throw err; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     if (result) { |     if (result) { | ||||||
|       is_block = true; |       is_block = true; | ||||||
|       access_type = "block"; |       access_type = "block"; | ||||||
|  | @ -626,7 +756,33 @@ class CsiBaseDriver { | ||||||
| 
 | 
 | ||||||
|     result = await mount.pathIsMounted(normalized_staging_path); |     result = await mount.pathIsMounted(normalized_staging_path); | ||||||
|     if (result) { |     if (result) { | ||||||
|       result = await mount.umount(normalized_staging_path, umount_args); |       try { | ||||||
|  |         result = await mount.umount(normalized_staging_path, umount_args); | ||||||
|  |       } catch (err) { | ||||||
|  |         if (err.timeout) { | ||||||
|  |           driver.ctx.logger.warn( | ||||||
|  |             `hit timeout waiting to unmount path: ${normalized_staging_path}` | ||||||
|  |           ); | ||||||
|  |           result = await mount.getMountDetails(normalized_staging_path); | ||||||
|  |           switch (result.fstype) { | ||||||
|  |             case "nfs": | ||||||
|  |             case "nfs4": | ||||||
|  |               driver.ctx.logger.warn( | ||||||
|  |                 `detected stale nfs filesystem, attempting to force unmount: ${normalized_staging_path}` | ||||||
|  |               ); | ||||||
|  |               result = await mount.umount( | ||||||
|  |                 normalized_staging_path, | ||||||
|  |                 umount_args.concat(umount_force_extra_args) | ||||||
|  |               ); | ||||||
|  |               break; | ||||||
|  |             default: | ||||||
|  |               throw err; | ||||||
|  |               break; | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           throw err; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if (is_block) { |     if (is_block) { | ||||||
|  | @ -666,14 +822,13 @@ class CsiBaseDriver { | ||||||
|               session.attached_scsi_devices.host && |               session.attached_scsi_devices.host && | ||||||
|               session.attached_scsi_devices.host.devices |               session.attached_scsi_devices.host.devices | ||||||
|             ) { |             ) { | ||||||
|               is_attached_to_session = session.attached_scsi_devices.host.devices.some( |               is_attached_to_session = | ||||||
|                 (device) => { |                 session.attached_scsi_devices.host.devices.some((device) => { | ||||||
|                   if (device.attached_scsi_disk == block_device_info_i.name) { |                   if (device.attached_scsi_disk == block_device_info_i.name) { | ||||||
|                     return true; |                     return true; | ||||||
|                   } |                   } | ||||||
|                   return false; |                   return false; | ||||||
|                 } |                 }); | ||||||
|               ); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (is_attached_to_session) { |             if (is_attached_to_session) { | ||||||
|  | @ -749,6 +904,7 @@ class CsiBaseDriver { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async NodePublishVolume(call) { |   async NodePublishVolume(call) { | ||||||
|  |     const driver = this; | ||||||
|     const mount = new Mount(); |     const mount = new Mount(); | ||||||
|     const filesystem = new Filesystem(); |     const filesystem = new Filesystem(); | ||||||
|     let result; |     let result; | ||||||
|  | @ -758,22 +914,40 @@ class CsiBaseDriver { | ||||||
|     const target_path = call.request.target_path; |     const target_path = call.request.target_path; | ||||||
|     const capability = call.request.volume_capability; |     const capability = call.request.volume_capability; | ||||||
|     const access_type = capability.access_type || "mount"; |     const access_type = capability.access_type || "mount"; | ||||||
|  |     let mount_flags; | ||||||
|  |     let volume_mount_group; | ||||||
|     const readonly = call.request.readonly; |     const readonly = call.request.readonly; | ||||||
|     const volume_context = call.request.volume_context; |     const volume_context = call.request.volume_context; | ||||||
|     const bind_mount_flags = []; |     const bind_mount_flags = []; | ||||||
|     const node_attach_driver = volume_context.node_attach_driver; |     const node_attach_driver = volume_context.node_attach_driver; | ||||||
| 
 | 
 | ||||||
|     if (access_type == "mount") { |     if (access_type == "mount") { | ||||||
|       let mount_flags = capability.mount.mount_flags || []; |       mount_flags = capability.mount.mount_flags || []; | ||||||
|       bind_mount_flags.push(...mount_flags); |       bind_mount_flags.push(...mount_flags); | ||||||
|  | 
 | ||||||
|  |       if ( | ||||||
|  |         semver.satisfies(driver.ctx.csiVersion, ">=1.5.0") && | ||||||
|  |         driver.options.service.node.capabilities.rpc.includes( | ||||||
|  |           "VOLUME_MOUNT_GROUP" | ||||||
|  |         ) | ||||||
|  |       ) { | ||||||
|  |         volume_mount_group = capability.mount.volume_mount_group; // in k8s this is derrived from the fsgroup in the pod security context
 | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     bind_mount_flags.push("defaults"); |     bind_mount_flags.push("defaults"); | ||||||
|  | 
 | ||||||
|  |     // https://github.com/karelzak/util-linux/issues/1429
 | ||||||
|  |     //bind_mount_flags.push("x-democratic-csi.managed");
 | ||||||
|  |     //bind_mount_flags.push("x-democratic-csi.published");
 | ||||||
|  | 
 | ||||||
|     if (readonly) bind_mount_flags.push("ro"); |     if (readonly) bind_mount_flags.push("ro"); | ||||||
|  |     // , "x-democratic-csi.ro"
 | ||||||
| 
 | 
 | ||||||
|     switch (node_attach_driver) { |     switch (node_attach_driver) { | ||||||
|       case "nfs": |       case "nfs": | ||||||
|       case "smb": |       case "smb": | ||||||
|  |       case "lustre": | ||||||
|       case "iscsi": |       case "iscsi": | ||||||
|         // ensure appropriate directories/files
 |         // ensure appropriate directories/files
 | ||||||
|         switch (access_type) { |         switch (access_type) { | ||||||
|  | @ -864,17 +1038,65 @@ class CsiBaseDriver { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async NodeUnpublishVolume(call) { |   async NodeUnpublishVolume(call) { | ||||||
|  |     const driver = this; | ||||||
|     const mount = new Mount(); |     const mount = new Mount(); | ||||||
|     const filesystem = new Filesystem(); |     const filesystem = new Filesystem(); | ||||||
|     let result; |     let result; | ||||||
| 
 | 
 | ||||||
|     const volume_id = call.request.volume_id; |     const volume_id = call.request.volume_id; | ||||||
|     const target_path = call.request.target_path; |     const target_path = call.request.target_path; | ||||||
|     const umount_args = []; // --force
 |     const umount_args = []; | ||||||
|  |     const umount_force_extra_args = ["--force", "--lazy"]; | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       result = await mount.pathIsMounted(target_path); | ||||||
|  |     } catch (err) { | ||||||
|  |       // running findmnt on non-existant paths return immediately
 | ||||||
|  |       // the only time this should timeout is on a stale fs
 | ||||||
|  |       // so if timeout is hit we should be near certain it is indeed mounted
 | ||||||
|  |       if (err.timeout) { | ||||||
|  |         driver.ctx.logger.warn( | ||||||
|  |           `detected stale mount, attempting to force unmount: ${target_path}` | ||||||
|  |         ); | ||||||
|  |         await mount.umount( | ||||||
|  |           target_path, | ||||||
|  |           umount_args.concat(umount_force_extra_args) | ||||||
|  |         ); | ||||||
|  |         result = false; // assume we have fully unmounted
 | ||||||
|  |       } else { | ||||||
|  |         throw err; | ||||||
|  |       } | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     result = await mount.pathIsMounted(target_path); |  | ||||||
|     if (result) { |     if (result) { | ||||||
|       result = await mount.umount(target_path, umount_args); |       try { | ||||||
|  |         result = await mount.umount(target_path, umount_args); | ||||||
|  |       } catch (err) { | ||||||
|  |         if (err.timeout) { | ||||||
|  |           driver.ctx.logger.warn( | ||||||
|  |             `hit timeout waiting to unmount path: ${target_path}` | ||||||
|  |           ); | ||||||
|  |           // bind mounts do show the 'real' fs details
 | ||||||
|  |           result = await mount.getMountDetails(target_path); | ||||||
|  |           switch (result.fstype) { | ||||||
|  |             case "nfs": | ||||||
|  |             case "nfs4": | ||||||
|  |               driver.ctx.logger.warn( | ||||||
|  |                 `detected stale nfs filesystem, attempting to force unmount: ${target_path}` | ||||||
|  |               ); | ||||||
|  |               result = await mount.umount( | ||||||
|  |                 target_path, | ||||||
|  |                 umount_args.concat(umount_force_extra_args) | ||||||
|  |               ); | ||||||
|  |               break; | ||||||
|  |             default: | ||||||
|  |               throw err; | ||||||
|  |               break; | ||||||
|  |           } | ||||||
|  |         } else { | ||||||
|  |           throw err; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     result = await filesystem.pathExists(target_path); |     result = await filesystem.pathExists(target_path); | ||||||
|  | @ -909,7 +1131,7 @@ class CsiBaseDriver { | ||||||
|     //VOLUME_CONDITION
 |     //VOLUME_CONDITION
 | ||||||
|     if ( |     if ( | ||||||
|       semver.satisfies(driver.ctx.csiVersion, ">=1.3.0") && |       semver.satisfies(driver.ctx.csiVersion, ">=1.3.0") && | ||||||
|       options.service.node.capabilities.rpc.includes("VOLUME_CONDITION") |       driver.options.service.node.capabilities.rpc.includes("VOLUME_CONDITION") | ||||||
|     ) { |     ) { | ||||||
|       // TODO: let drivers fill ths in
 |       // TODO: let drivers fill ths in
 | ||||||
|       let abnormal = false; |       let abnormal = false; | ||||||
|  | @ -930,7 +1152,11 @@ class CsiBaseDriver { | ||||||
| 
 | 
 | ||||||
|     switch (access_type) { |     switch (access_type) { | ||||||
|       case "mount": |       case "mount": | ||||||
|         result = await mount.getMountDetails(device_path); |         result = await mount.getMountDetails(device_path, [ | ||||||
|  |           "avail", | ||||||
|  |           "size", | ||||||
|  |           "used", | ||||||
|  |         ]); | ||||||
| 
 | 
 | ||||||
|         res.usage = [ |         res.usage = [ | ||||||
|           { |           { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| const { CsiBaseDriver } = require("../index"); | const { CsiBaseDriver } = require("../index"); | ||||||
| const { GrpcError, grpc } = require("../../utils/grpc"); | const { GrpcError, grpc } = require("../../utils/grpc"); | ||||||
|  | const semver = require("semver"); | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Driver which only runs the node portion and is meant to be used entirely |  * Driver which only runs the node portion and is meant to be used entirely | ||||||
|  | @ -58,6 +59,21 @@ class NodeManualDriver extends CsiBaseDriver { | ||||||
|         //"PUBLISH_READONLY",
 |         //"PUBLISH_READONLY",
 | ||||||
|         //"EXPAND_VOLUME",
 |         //"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)) { |     if (!("rpc" in options.service.node.capabilities)) { | ||||||
|  | @ -69,6 +85,18 @@ class NodeManualDriver extends CsiBaseDriver { | ||||||
|         "GET_VOLUME_STATS", |         "GET_VOLUME_STATS", | ||||||
|         //"EXPAND_VOLUME"
 |         //"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");
 | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -87,6 +115,9 @@ class NodeManualDriver extends CsiBaseDriver { | ||||||
|       case "smb": |       case "smb": | ||||||
|         driverResourceType = "filesystem"; |         driverResourceType = "filesystem"; | ||||||
|         fs_types = ["cifs"]; |         fs_types = ["cifs"]; | ||||||
|  |       case "lustre": | ||||||
|  |         driverResourceType = "filesystem"; | ||||||
|  |         fs_types = ["lustre"]; | ||||||
|         break; |         break; | ||||||
|       case "iscsi": |       case "iscsi": | ||||||
|         driverResourceType = "volume"; |         driverResourceType = "volume"; | ||||||
|  | @ -119,6 +150,8 @@ class NodeManualDriver extends CsiBaseDriver { | ||||||
|             ![ |             ![ | ||||||
|               "UNKNOWN", |               "UNKNOWN", | ||||||
|               "SINGLE_NODE_WRITER", |               "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", |               "SINGLE_NODE_READER_ONLY", | ||||||
|               "MULTI_NODE_READER_ONLY", |               "MULTI_NODE_READER_ONLY", | ||||||
|               "MULTI_NODE_SINGLE_WRITER", |               "MULTI_NODE_SINGLE_WRITER", | ||||||
|  | @ -145,6 +178,8 @@ class NodeManualDriver extends CsiBaseDriver { | ||||||
|             ![ |             ![ | ||||||
|               "UNKNOWN", |               "UNKNOWN", | ||||||
|               "SINGLE_NODE_WRITER", |               "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", |               "SINGLE_NODE_READER_ONLY", | ||||||
|               "MULTI_NODE_READER_ONLY", |               "MULTI_NODE_READER_ONLY", | ||||||
|               "MULTI_NODE_SINGLE_WRITER", |               "MULTI_NODE_SINGLE_WRITER", | ||||||
|  |  | ||||||
|  | @ -2,6 +2,7 @@ const fs = require("fs"); | ||||||
| const { CsiBaseDriver } = require("../index"); | const { CsiBaseDriver } = require("../index"); | ||||||
| const { GrpcError, grpc } = require("../../utils/grpc"); | const { GrpcError, grpc } = require("../../utils/grpc"); | ||||||
| const { Filesystem } = require("../../utils/filesystem"); | const { Filesystem } = require("../../utils/filesystem"); | ||||||
|  | const semver = require("semver"); | ||||||
| const SshClient = require("../../utils/ssh").SshClient; | const SshClient = require("../../utils/ssh").SshClient; | ||||||
| const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); | const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); | ||||||
| 
 | 
 | ||||||
|  | @ -81,6 +82,21 @@ class ZfsLocalEphemeralInlineDriver extends CsiBaseDriver { | ||||||
|         //"PUBLISH_READONLY",
 |         //"PUBLISH_READONLY",
 | ||||||
|         //"EXPAND_VOLUME"
 |         //"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)) { |     if (!("rpc" in options.service.node.capabilities)) { | ||||||
|  | @ -91,6 +107,18 @@ class ZfsLocalEphemeralInlineDriver extends CsiBaseDriver { | ||||||
|         "GET_VOLUME_STATS", |         "GET_VOLUME_STATS", | ||||||
|         //"EXPAND_VOLUME",
 |         //"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");
 | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -167,6 +195,8 @@ class ZfsLocalEphemeralInlineDriver extends CsiBaseDriver { | ||||||
|             ![ |             ![ | ||||||
|               "UNKNOWN", |               "UNKNOWN", | ||||||
|               "SINGLE_NODE_WRITER", |               "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", |               "SINGLE_NODE_READER_ONLY", | ||||||
|             ].includes(capability.access_mode.mode) |             ].includes(capability.access_mode.mode) | ||||||
|           ) { |           ) { | ||||||
|  | @ -192,6 +222,8 @@ class ZfsLocalEphemeralInlineDriver extends CsiBaseDriver { | ||||||
|             ![ |             ![ | ||||||
|               "UNKNOWN", |               "UNKNOWN", | ||||||
|               "SINGLE_NODE_WRITER", |               "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", |               "SINGLE_NODE_READER_ONLY", | ||||||
|             ].includes(capability.access_mode.mode) |             ].includes(capability.access_mode.mode) | ||||||
|           ) { |           ) { | ||||||
|  |  | ||||||
|  | @ -326,9 +326,14 @@ class Filesystem { | ||||||
|     try { |     try { | ||||||
|       result = await filesystem.exec("blkid", args); |       result = await filesystem.exec("blkid", args); | ||||||
|     } catch (err) { |     } catch (err) { | ||||||
|  |       if (err.code == 2 && err.stderr.includes("No such device or address")) { | ||||||
|  |         throw err; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       if (err.code == 2) { |       if (err.code == 2) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|  | 
 | ||||||
|       throw err; |       throw err; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -426,12 +431,13 @@ class Filesystem { | ||||||
| 
 | 
 | ||||||
|       // echo 1 > /sys/block/sdb/device/rescan
 |       // echo 1 > /sys/block/sdb/device/rescan
 | ||||||
|       const sys_file = `/sys/block/${device_name}/device/rescan`; |       const sys_file = `/sys/block/${device_name}/device/rescan`; | ||||||
|  |       console.log(`executing filesystem command: echo 1 > ${sys_file}`); | ||||||
|       fs.writeFileSync(sys_file, "1"); |       fs.writeFileSync(sys_file, "1"); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * expand a give filesystem |    * expand a given filesystem | ||||||
|    * |    * | ||||||
|    * @param {*} device |    * @param {*} device | ||||||
|    * @param {*} fstype |    * @param {*} fstype | ||||||
|  | @ -474,7 +480,7 @@ class Filesystem { | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|    * expand a give filesystem |    * check a given filesystem | ||||||
|    * |    * | ||||||
|    * fsck [options] -- [fs-options] [<filesystem> ...] |    * fsck [options] -- [fs-options] [<filesystem> ...] | ||||||
|    * |    * | ||||||
|  | @ -593,7 +599,7 @@ class Filesystem { | ||||||
|       args.unshift(command); |       args.unshift(command); | ||||||
|       command = filesystem.options.paths.sudo; |       command = filesystem.options.paths.sudo; | ||||||
|     } |     } | ||||||
|     console.log("executing fileystem command: %s %s", command, args.join(" ")); |     console.log("executing filesystem command: %s %s", command, args.join(" ")); | ||||||
|     const child = filesystem.options.executor.spawn(command, args, options); |     const child = filesystem.options.executor.spawn(command, args, options); | ||||||
| 
 | 
 | ||||||
|     let didTimeout = false; |     let didTimeout = false; | ||||||
|  | @ -614,10 +620,16 @@ class Filesystem { | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       child.on("close", function (code) { |       child.on("close", function (code) { | ||||||
|         const result = { code, stdout, stderr }; |         const result = { code, stdout, stderr, timeout: false }; | ||||||
|         if (timeout) { |         if (timeout) { | ||||||
|           clearTimeout(timeout); |           clearTimeout(timeout); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         if (code === null) { | ||||||
|  |           result.timeout = true; | ||||||
|  |           reject(result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if (code) { |         if (code) { | ||||||
|           console.log( |           console.log( | ||||||
|             "failed to execute filesystem command: %s, response: %j", |             "failed to execute filesystem command: %s, response: %j", | ||||||
|  |  | ||||||
|  | @ -1,13 +1,17 @@ | ||||||
| const cp = require("child_process"); | const cp = require("child_process"); | ||||||
| const { Filesystem } = require("../utils/filesystem"); | const { Filesystem } = require("../utils/filesystem"); | ||||||
| 
 | 
 | ||||||
|  | // avoid using avail,size,used as it causes hangs when the fs is stale
 | ||||||
| FINDMNT_COMMON_OPTIONS = [ | FINDMNT_COMMON_OPTIONS = [ | ||||||
|   "--output", |   "--output", | ||||||
|   "source,target,fstype,label,options,avail,size,used", |   "source,target,fstype,label,options", | ||||||
|   "-b", |   "-b", | ||||||
|   "-J" |   "-J", | ||||||
|  |   "--nofsroot", // prevents unwanted behavior with cifs volumes
 | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  | DEFAUT_TIMEOUT = 30000; | ||||||
|  | 
 | ||||||
| class Mount { | class Mount { | ||||||
|   constructor(options = {}) { |   constructor(options = {}) { | ||||||
|     const mount = this; |     const mount = this; | ||||||
|  | @ -36,7 +40,7 @@ class Mount { | ||||||
| 
 | 
 | ||||||
|     if (!options.executor) { |     if (!options.executor) { | ||||||
|       options.executor = { |       options.executor = { | ||||||
|         spawn: cp.spawn |         spawn: cp.spawn, | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | @ -141,11 +145,18 @@ class Mount { | ||||||
|    * |    * | ||||||
|    * @param {*} path |    * @param {*} path | ||||||
|    */ |    */ | ||||||
|   async getMountDetails(path) { |   async getMountDetails(path, extraOutputProperties = [], extraArgs = []) { | ||||||
|     const mount = this; |     const mount = this; | ||||||
|     let args = []; |     let args = []; | ||||||
|  |     const common_options = JSON.parse(JSON.stringify(FINDMNT_COMMON_OPTIONS)); | ||||||
|  |     if (extraOutputProperties.length > 0) { | ||||||
|  |       common_options[1] = | ||||||
|  |         common_options[1] + "," + extraOutputProperties.join(","); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     args = args.concat(["--mountpoint", path]); |     args = args.concat(["--mountpoint", path]); | ||||||
|     args = args.concat(FINDMNT_COMMON_OPTIONS); |     args = args.concat(common_options); | ||||||
|  |     args = args.concat(extraArgs); | ||||||
|     let result; |     let result; | ||||||
| 
 | 
 | ||||||
|     try { |     try { | ||||||
|  | @ -157,6 +168,94 @@ class Mount { | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * parse a mount options string into an array | ||||||
|  |    * | ||||||
|  |    * @param {*} options | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async parseMountOptions(options) { | ||||||
|  |     if (!options) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (Array.isArray(options)) { | ||||||
|  |       return options; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     options = options.split(","); | ||||||
|  |     return options; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Given the set of mount options and sought after option, return true if the option is present | ||||||
|  |    * | ||||||
|  |    * @param {*} options | ||||||
|  |    * @param {*} option | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async getMountOptionPresent(options, option) { | ||||||
|  |     const mount = this; | ||||||
|  | 
 | ||||||
|  |     if (!Array.isArray(options)) { | ||||||
|  |       options = await mount.parseMountOptions(options); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (let i of options) { | ||||||
|  |       let parts = i.split("=", 2); | ||||||
|  |       if (parts[0] == option) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get the value of the given mount option | ||||||
|  |    * | ||||||
|  |    * if the mount option is present by has no value null is returned | ||||||
|  |    * if the mount option is NOT present undefined is returned | ||||||
|  |    * is the mount option has a value that value is returned | ||||||
|  |    * | ||||||
|  |    * @param {*} options | ||||||
|  |    * @param {*} option | ||||||
|  |    * @returns | ||||||
|  |    */ | ||||||
|  |   async getMountOptionValue(options, option) { | ||||||
|  |     const mount = this; | ||||||
|  | 
 | ||||||
|  |     if (!Array.isArray(options)) { | ||||||
|  |       options = await mount.parseMountOptions(options); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (let i of options) { | ||||||
|  |       let parts = i.split("=", 2); | ||||||
|  |       if (parts[0] == option) { | ||||||
|  |         if (typeof parts[1] === "undefined") { | ||||||
|  |           return null; | ||||||
|  |         } else { | ||||||
|  |           return parts[1]; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return undefined; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get mount optsion for a given path | ||||||
|  |    * | ||||||
|  |    * @param {*} path | ||||||
|  |    * @returns Array | ||||||
|  |    */ | ||||||
|  |   async getMountOptions(path) { | ||||||
|  |     const mount = this; | ||||||
|  |     let details = await mount.getMountDetails(path, [], ["-m"]); | ||||||
|  | 
 | ||||||
|  |     return await mount.parseMountOptions(details.options); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   /** |   /** | ||||||
|    * Get the device (source) at the given mount point |    * Get the device (source) at the given mount point | ||||||
|    * |    * | ||||||
|  | @ -278,7 +377,11 @@ class Mount { | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   exec(command, args, options) { |   exec(command, args, options = {}) { | ||||||
|  |     if (!options.hasOwnProperty("timeout")) { | ||||||
|  |       options.timeout = DEFAUT_TIMEOUT; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     const mount = this; |     const mount = this; | ||||||
|     args = args || []; |     args = args || []; | ||||||
| 
 | 
 | ||||||
|  | @ -290,9 +393,22 @@ class Mount { | ||||||
|       args.unshift(command); |       args.unshift(command); | ||||||
|       command = mount.options.paths.sudo; |       command = mount.options.paths.sudo; | ||||||
|     } |     } | ||||||
|     console.log("executing mount command: %s %s", command, args.join(" ")); |     // https://regex101.com/r/FHIbcw/3
 | ||||||
|  |     // replace password=foo with password=redacted
 | ||||||
|  |     // (?<=password=)(?:([\"'])(?:\\\1|.)*?\1|[^,\s]+)
 | ||||||
|  |     const regex = /(?<=password=)(?:([\"'])(?:\\\1|.)*?\1|[^,\s]+)/gi; | ||||||
|  |     const cleansedLog = `${command} ${args.join(" ")}`.replace( | ||||||
|  |       regex, | ||||||
|  |       "redacted" | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     console.log("executing mount command: %s", cleansedLog); | ||||||
|     const child = mount.options.executor.spawn(command, args, options); |     const child = mount.options.executor.spawn(command, args, options); | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * timeout option natively supported since v16 | ||||||
|  |      * TODO: properly handle this based on nodejs version | ||||||
|  |      */ | ||||||
|     let didTimeout = false; |     let didTimeout = false; | ||||||
|     if (options && options.timeout) { |     if (options && options.timeout) { | ||||||
|       timeout = setTimeout(() => { |       timeout = setTimeout(() => { | ||||||
|  | @ -302,19 +418,27 @@ class Mount { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return new Promise((resolve, reject) => { |     return new Promise((resolve, reject) => { | ||||||
|       child.stdout.on("data", function(data) { |       child.stdout.on("data", function (data) { | ||||||
|         stdout = stdout + data; |         stdout = stdout + data; | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       child.stderr.on("data", function(data) { |       child.stderr.on("data", function (data) { | ||||||
|         stderr = stderr + data; |         stderr = stderr + data; | ||||||
|       }); |       }); | ||||||
| 
 | 
 | ||||||
|       child.on("close", function(code) { |       child.on("close", function (code) { | ||||||
|         const result = { code, stdout, stderr }; |         const result = { code, stdout, stderr, timeout: false }; | ||||||
|  | 
 | ||||||
|         if (timeout) { |         if (timeout) { | ||||||
|           clearTimeout(timeout); |           clearTimeout(timeout); | ||||||
|         } |         } | ||||||
|  | 
 | ||||||
|  |         // timeout scenario
 | ||||||
|  |         if (code === null) { | ||||||
|  |           result.timeout = true; | ||||||
|  |           reject(result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if (code) { |         if (code) { | ||||||
|           reject(result); |           reject(result); | ||||||
|         } else { |         } else { | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue