csi-test conformance, relax iscsi name length
Signed-off-by: Travis Glenn Hansen <travisghansen@yahoo.com>
This commit is contained in:
		
							parent
							
								
									109d493cfc
								
							
						
					
					
						commit
						855f48d3af
					
				
							
								
								
									
										13
									
								
								CHANGELOG.md
								
								
								
								
							
							
						
						
									
										13
									
								
								CHANGELOG.md
								
								
								
								
							|  | @ -1,3 +1,16 @@ | |||
| # v1.4.0 | ||||
| 
 | ||||
| Released 2021-09-21 | ||||
| 
 | ||||
| - more advanced logic for iscsi naming limits (allowing > 63 chars in certain | ||||
|   circumstances, SCALE, linux, FreeBSD 13+) | ||||
| - various updates to support running the csi-test tool and conform to expected | ||||
|   responses/behaviors (full conformance for several drivers!) | ||||
| - default `fs_type` during `NodeStageVolume` when omitted by `CO` | ||||
| - automatcally add `guest` mount option to `cifs` shares when creds are absent | ||||
| - fix `ListVolumes` and `ListSnapshot` behavior on various `zfs-generic-*` and | ||||
|   `freenas-*` drivers | ||||
| 
 | ||||
| # v1.3.2 | ||||
| 
 | ||||
| Released 2021-09-09 | ||||
|  |  | |||
|  | @ -201,6 +201,7 @@ non-`root` user when connecting to the FreeNAS server: | |||
|   account user query select=id,username,uid,sudo_nopasswd | ||||
| 
 | ||||
|   # find the `id` of the user you want to update (note, this is distinct from the `uid`) | ||||
|   account user update id=<id> sudo=true | ||||
|   account user update id=<id> sudo_nopasswd=true | ||||
|   # optional if you want to disable password | ||||
|   #account user update id=<id> password_disabled=true | ||||
|  |  | |||
|  | @ -37,7 +37,7 @@ zfs: | |||
|   # 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 should therefore be 17 chars or less when using TrueNAS 12 or below | ||||
|   datasetParentName: tank/k8s/b/vols | ||||
|   # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap | ||||
|   # they may be siblings, but neither should be nested in the other  | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ zfs: | |||
|   # 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 should therefore be 17 chars or less when using TrueNAS 12 or below | ||||
|   datasetParentName: tank/k8s/b/vols | ||||
|   # do NOT make datasetParentName and detachedSnapshotsDatasetParentName overlap | ||||
|   # they may be siblings, but neither should be nested in the other  | ||||
|  |  | |||
|  | @ -1,12 +1,12 @@ | |||
| { | ||||
|   "name": "democratic-csi", | ||||
|   "version": "1.3.2", | ||||
|   "version": "1.4.0", | ||||
|   "lockfileVersion": 2, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "democratic-csi", | ||||
|       "version": "1.3.2", | ||||
|       "version": "1.4.0", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@grpc/grpc-js": "^1.3.6", | ||||
|  | @ -40,9 +40,9 @@ | |||
|       } | ||||
|     }, | ||||
|     "node_modules/@babel/helper-validator-identifier": { | ||||
|       "version": "7.14.9", | ||||
|       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", | ||||
|       "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", | ||||
|       "version": "7.15.7", | ||||
|       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", | ||||
|       "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", | ||||
|       "dev": true, | ||||
|       "engines": { | ||||
|         "node": ">=6.9.0" | ||||
|  | @ -197,9 +197,9 @@ | |||
|       } | ||||
|     }, | ||||
|     "node_modules/@grpc/proto-loader": { | ||||
|       "version": "0.6.4", | ||||
|       "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.4.tgz", | ||||
|       "integrity": "sha512-7xvDvW/vJEcmLUltCUGOgWRPM8Oofv0eCFSVMuKqaqWJaXSzmB+m9hiyqe34QofAl4WAzIKUZZlinIF9FOHyTQ==", | ||||
|       "version": "0.6.5", | ||||
|       "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.5.tgz", | ||||
|       "integrity": "sha512-GZdzyVQI1Bln/kCzIYgTKu+rQJ5dno0gVrfmLe4jqQu7T2e7svSwJzpCBqVU5hhBSJP3peuPjOMWsj5GR61YmQ==", | ||||
|       "dependencies": { | ||||
|         "@types/long": "^4.0.1", | ||||
|         "lodash.camelcase": "^4.3.0", | ||||
|  | @ -311,9 +311,9 @@ | |||
|       "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" | ||||
|     }, | ||||
|     "node_modules/@types/node": { | ||||
|       "version": "16.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz", | ||||
|       "integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==" | ||||
|       "version": "16.9.6", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz", | ||||
|       "integrity": "sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==" | ||||
|     }, | ||||
|     "node_modules/acorn": { | ||||
|       "version": "7.4.1", | ||||
|  | @ -361,9 +361,9 @@ | |||
|       } | ||||
|     }, | ||||
|     "node_modules/ansi-regex": { | ||||
|       "version": "5.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", | ||||
|       "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", | ||||
|       "version": "5.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", | ||||
|       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", | ||||
|       "engines": { | ||||
|         "node": ">=8" | ||||
|       } | ||||
|  | @ -2793,9 +2793,9 @@ | |||
|       } | ||||
|     }, | ||||
|     "node_modules/table/node_modules/ajv": { | ||||
|       "version": "8.6.2", | ||||
|       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", | ||||
|       "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", | ||||
|       "version": "8.6.3", | ||||
|       "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", | ||||
|       "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", | ||||
|       "dev": true, | ||||
|       "dependencies": { | ||||
|         "fast-deep-equal": "^3.1.1", | ||||
|  | @ -3108,9 +3108,9 @@ | |||
|       } | ||||
|     }, | ||||
|     "@babel/helper-validator-identifier": { | ||||
|       "version": "7.14.9", | ||||
|       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", | ||||
|       "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==", | ||||
|       "version": "7.15.7", | ||||
|       "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", | ||||
|       "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "@babel/highlight": { | ||||
|  | @ -3239,9 +3239,9 @@ | |||
|       } | ||||
|     }, | ||||
|     "@grpc/proto-loader": { | ||||
|       "version": "0.6.4", | ||||
|       "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.4.tgz", | ||||
|       "integrity": "sha512-7xvDvW/vJEcmLUltCUGOgWRPM8Oofv0eCFSVMuKqaqWJaXSzmB+m9hiyqe34QofAl4WAzIKUZZlinIF9FOHyTQ==", | ||||
|       "version": "0.6.5", | ||||
|       "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.5.tgz", | ||||
|       "integrity": "sha512-GZdzyVQI1Bln/kCzIYgTKu+rQJ5dno0gVrfmLe4jqQu7T2e7svSwJzpCBqVU5hhBSJP3peuPjOMWsj5GR61YmQ==", | ||||
|       "requires": { | ||||
|         "@types/long": "^4.0.1", | ||||
|         "lodash.camelcase": "^4.3.0", | ||||
|  | @ -3343,9 +3343,9 @@ | |||
|       "integrity": "sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w==" | ||||
|     }, | ||||
|     "@types/node": { | ||||
|       "version": "16.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.0.tgz", | ||||
|       "integrity": "sha512-nmP+VR4oT0pJUPFbKE4SXj3Yb4Q/kz3M9dSAO1GGMebRKWHQxLfDNmU/yh3xxCJha3N60nQ/JwXWwOE/ZSEVag==" | ||||
|       "version": "16.9.6", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.6.tgz", | ||||
|       "integrity": "sha512-YHUZhBOMTM3mjFkXVcK+WwAcYmyhe1wL4lfqNtzI0b3qAy7yuSetnM7QJazgE5PFmgVTNGiLOgRFfJMqW7XpSQ==" | ||||
|     }, | ||||
|     "acorn": { | ||||
|       "version": "7.4.1", | ||||
|  | @ -3378,9 +3378,9 @@ | |||
|       "dev": true | ||||
|     }, | ||||
|     "ansi-regex": { | ||||
|       "version": "5.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", | ||||
|       "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" | ||||
|       "version": "5.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", | ||||
|       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" | ||||
|     }, | ||||
|     "ansi-styles": { | ||||
|       "version": "4.3.0", | ||||
|  | @ -5240,9 +5240,9 @@ | |||
|       }, | ||||
|       "dependencies": { | ||||
|         "ajv": { | ||||
|           "version": "8.6.2", | ||||
|           "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", | ||||
|           "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", | ||||
|           "version": "8.6.3", | ||||
|           "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", | ||||
|           "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "fast-deep-equal": "^3.1.1", | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| { | ||||
|   "name": "democratic-csi", | ||||
|   "version": "1.3.2", | ||||
|   "version": "1.4.0", | ||||
|   "description": "kubernetes csi driver framework", | ||||
|   "main": "bin/democratic-csi", | ||||
|   "scripts": { | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| const { CsiBaseDriver } = require("../index"); | ||||
| const { GrpcError, grpc } = require("../../utils/grpc"); | ||||
| const cp = require("child_process"); | ||||
| const fs = require("fs"); | ||||
| const semver = require("semver"); | ||||
| 
 | ||||
| /** | ||||
|  | @ -329,6 +330,10 @@ class ControllerClientCommonDriver extends CsiBaseDriver { | |||
|     ]); | ||||
|   } | ||||
| 
 | ||||
|   async directoryExists(path) { | ||||
|     return fs.existsSync(path); | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Create a volume doing in essence the following: | ||||
|    * 1. create directory | ||||
|  | @ -353,11 +358,28 @@ class ControllerClientCommonDriver extends CsiBaseDriver { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (call.request.volume_capabilities) { | ||||
|     if ( | ||||
|       call.request.volume_capabilities && | ||||
|       call.request.volume_capabilities.length > 0 | ||||
|     ) { | ||||
|       const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
|       if (result.valid !== true) { | ||||
|         throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); | ||||
|       } | ||||
|     } else { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         "missing volume_capabilities" | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       !call.request.capacity_range || | ||||
|       Object.keys(call.request.capacity_range).length === 0 | ||||
|     ) { | ||||
|       call.request.capacity_range = { | ||||
|         required_bytes: 1073741824, // meaningless
 | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|  | @ -431,6 +453,13 @@ class ControllerClientCommonDriver extends CsiBaseDriver { | |||
|           break; | ||||
|       } | ||||
| 
 | ||||
|       if (!(await driver.directoryExists(source_path))) { | ||||
|         throw new GrpcError( | ||||
|           grpc.status.NOT_FOUND, | ||||
|           `invalid volume_content_source path: ${source_path}` | ||||
|         ); | ||||
|       } | ||||
| 
 | ||||
|       driver.ctx.logger.debug("controller source path: %s", source_path); | ||||
|       response = await driver.cloneDir(source_path, volume_path); | ||||
|     } | ||||
|  | @ -630,7 +659,10 @@ class ControllerClientCommonDriver extends CsiBaseDriver { | |||
|     const volume_path = driver.getControllerVolumePath(source_volume_id); | ||||
|     const snapshot_path = driver.getControllerSnapshotPath(snapshot_id); | ||||
| 
 | ||||
|     // do NOT overwrite existing snapshot
 | ||||
|     if (!(await driver.directoryExists(snapshot_path))) { | ||||
|       await driver.cloneDir(volume_path, snapshot_path); | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|       snapshot: { | ||||
|  | @ -681,6 +713,25 @@ class ControllerClientCommonDriver extends CsiBaseDriver { | |||
|    */ | ||||
|   async ValidateVolumeCapabilities(call) { | ||||
|     const driver = this; | ||||
| 
 | ||||
|     const volume_id = call.request.volume_id; | ||||
|     if (!volume_id) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); | ||||
|     } | ||||
| 
 | ||||
|     const capabilities = call.request.volume_capabilities; | ||||
|     if (!capabilities || capabilities.length === 0) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); | ||||
|     } | ||||
| 
 | ||||
|     const volume_path = driver.getControllerVolumePath(volume_id); | ||||
|     if (!(await driver.directoryExists(volume_path))) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.NOT_FOUND, | ||||
|         `invalid volume_id: ${volume_id}` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
| 
 | ||||
|     if (result.valid !== true) { | ||||
|  |  | |||
|  | @ -146,6 +146,17 @@ class SynologyHttpClient { | |||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   async GetLuns() { | ||||
|     const lun_list = { | ||||
|       api: "SYNO.Core.ISCSI.LUN", | ||||
|       version: "1", | ||||
|       method: "list", | ||||
|     }; | ||||
| 
 | ||||
|     let response = await this.do_request("GET", "entry.cgi", lun_list); | ||||
|     return response.body.data.luns; | ||||
|   } | ||||
| 
 | ||||
|   async GetLunUUIDByName(name) { | ||||
|     const lun_list = { | ||||
|       api: "SYNO.Core.ISCSI.LUN", | ||||
|  | @ -214,6 +225,30 @@ class SynologyHttpClient { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   async GetSnapshots() { | ||||
|     let luns = await this.GetLuns(); | ||||
|     let snapshots = []; | ||||
| 
 | ||||
|     for (let lun of luns) { | ||||
|       const get_snapshot_info = { | ||||
|         api: "SYNO.Core.ISCSI.LUN", | ||||
|         method: "list_snapshot", | ||||
|         version: 1, | ||||
|         src_lun_uuid: JSON.stringify(lun.uuid), | ||||
|       }; | ||||
| 
 | ||||
|       let response = await this.do_request( | ||||
|         "GET", | ||||
|         "entry.cgi", | ||||
|         get_snapshot_info | ||||
|       ); | ||||
| 
 | ||||
|       snapshots = snapshots.concat(response.body.data.snapshots); | ||||
|     } | ||||
| 
 | ||||
|     return snapshots; | ||||
|   } | ||||
| 
 | ||||
|   async GetSnapshotByLunIDAndName(lun_id, name) { | ||||
|     const get_snapshot_info = { | ||||
|       lid: lun_id, //check?
 | ||||
|  |  | |||
|  | @ -247,11 +247,28 @@ class ControllerSynologyDriver extends CsiBaseDriver { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (call.request.volume_capabilities) { | ||||
|     if ( | ||||
|       call.request.volume_capabilities && | ||||
|       call.request.volume_capabilities.length > 0 | ||||
|     ) { | ||||
|       const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
|       if (result.valid !== true) { | ||||
|         throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); | ||||
|       } | ||||
|     } else { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         "missing volume_capabilities" | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       !call.request.capacity_range || | ||||
|       Object.keys(call.request.capacity_range).length === 0 | ||||
|     ) { | ||||
|       call.request.capacity_range = { | ||||
|         required_bytes: 1073741824, | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|  | @ -314,17 +331,69 @@ class ControllerSynologyDriver extends CsiBaseDriver { | |||
|         let lun_uuid; | ||||
|         let existingLun; | ||||
| 
 | ||||
|         // ensure volumes with the same name being requested a 2nd time but with a different size fails
 | ||||
|         try { | ||||
|           let lun = await httpClient.GetLunByName(iscsiName); | ||||
|           if (lun) { | ||||
|             let size = lun.size; | ||||
|             let check = true; | ||||
|             if (check) { | ||||
|               if ( | ||||
|                 (call.request.capacity_range.required_bytes && | ||||
|                   call.request.capacity_range.required_bytes > 0 && | ||||
|                   size < call.request.capacity_range.required_bytes) || | ||||
|                 (call.request.capacity_range.limit_bytes && | ||||
|                   call.request.capacity_range.limit_bytes > 0 && | ||||
|                   size > call.request.capacity_range.limit_bytes) | ||||
|               ) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.ALREADY_EXISTS, | ||||
|                   `volume has already been created with a different size, existing size: ${size}, required_bytes: ${call.request.capacity_range.required_bytes}, limit_bytes: ${call.request.capacity_range.limit_bytes}` | ||||
|                 ); | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } catch (err) { | ||||
|           throw err; | ||||
|         } | ||||
| 
 | ||||
|         if (volume_content_source) { | ||||
|           let src_lun_uuid; | ||||
|           let src_lun_id; | ||||
|           switch (volume_content_source.type) { | ||||
|             case "snapshot": | ||||
|               let parts = volume_content_source.snapshot.snapshot_id.split("/"); | ||||
| 
 | ||||
|               src_lun_id = parts[2]; | ||||
|               if (!src_lun_id) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` | ||||
|                 ); | ||||
|               } | ||||
| 
 | ||||
|               let snapshot_uuid = parts[3]; | ||||
|               if (!snapshot_uuid) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` | ||||
|                 ); | ||||
|               } | ||||
| 
 | ||||
|               let src_lun = await httpClient.GetLunByID(src_lun_id); | ||||
|               src_lun_uuid = src_lun.uuid; | ||||
| 
 | ||||
|               let snapshot = await httpClient.GetSnapshotByLunIDAndSnapshotUUID( | ||||
|                 src_lun_id, | ||||
|                 snapshot_uuid | ||||
|               ); | ||||
|               if (!snapshot) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   `invalid snapshot_id: ${volume_content_source.snapshot.snapshot_id}` | ||||
|                 ); | ||||
|               } | ||||
| 
 | ||||
|               existingLun = await httpClient.GetLunByName(iscsiName); | ||||
|               if (!existingLun) { | ||||
|                 await httpClient.CreateVolumeFromSnapshot( | ||||
|  | @ -340,8 +409,20 @@ class ControllerSynologyDriver extends CsiBaseDriver { | |||
|                 let srcLunName = driver.buildIscsiName( | ||||
|                   volume_content_source.volume.volume_id | ||||
|                 ); | ||||
|                 if (!srcLunName) { | ||||
|                   throw new GrpcError( | ||||
|                     grpc.status.NOT_FOUND, | ||||
|                     `invalid volume_id: ${volume_content_source.volume.volume_id}` | ||||
|                   ); | ||||
|                 } | ||||
| 
 | ||||
|                 src_lun_uuid = await httpClient.GetLunUUIDByName(srcLunName); | ||||
|                 if (!src_lun_uuid) { | ||||
|                   throw new GrpcError( | ||||
|                     grpc.status.NOT_FOUND, | ||||
|                     `invalid volume_id: ${volume_content_source.volume.volume_id}` | ||||
|                   ); | ||||
|                 } | ||||
|                 await httpClient.CreateClonedVolume(src_lun_uuid, iscsiName); | ||||
|               } | ||||
|               break; | ||||
|  | @ -740,8 +821,6 @@ class ControllerSynologyDriver extends CsiBaseDriver { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     // create snapshot here
 | ||||
| 
 | ||||
|     let iscsiName = driver.buildIscsiName(source_volume_id); | ||||
|     let lun = await httpClient.GetLunByName(iscsiName); | ||||
| 
 | ||||
|  | @ -752,6 +831,19 @@ class ControllerSynologyDriver extends CsiBaseDriver { | |||
|       ); | ||||
|     } | ||||
|      | ||||
|     // check for other snapshopts with the same name on other volumes and fail as appropriate
 | ||||
|     // TODO: technically this should only be checking lun/snapshots relevant to this specific install of the driver
 | ||||
|     // but alas an isolation/namespacing mechanism does not exist in synology
 | ||||
|     let snapshots = await httpClient.GetSnapshots(); | ||||
|     for (let snapshot of snapshots) { | ||||
|       if (snapshot.description == name && snapshot.parent_uuid != lun.uuid) { | ||||
|         throw new GrpcError( | ||||
|           grpc.status.ALREADY_EXISTS, | ||||
|           `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // check for already exists
 | ||||
|     let snapshot = await httpClient.GetSnapshotByLunIDAndName(lun.lun_id, name); | ||||
|     if (snapshot) { | ||||
|  | @ -827,7 +919,14 @@ class ControllerSynologyDriver extends CsiBaseDriver { | |||
| 
 | ||||
|     let parts = snapshot_id.split("/"); | ||||
|     let lun_id = parts[2]; | ||||
|     if (!lun_id) { | ||||
|       return {}; | ||||
|     } | ||||
| 
 | ||||
|     let snapshot_uuid = parts[3]; | ||||
|     if (!snapshot_uuid) { | ||||
|       return {}; | ||||
|     } | ||||
| 
 | ||||
|     // TODO: delete snapshot
 | ||||
|     let snapshot = await httpClient.GetSnapshotByLunIDAndSnapshotUUID( | ||||
|  | @ -848,8 +947,55 @@ class ControllerSynologyDriver extends CsiBaseDriver { | |||
|    */ | ||||
|   async ValidateVolumeCapabilities(call) { | ||||
|     const driver = this; | ||||
|     const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
|     const httpClient = await driver.getHttpClient(); | ||||
| 
 | ||||
|     let response; | ||||
| 
 | ||||
|     const volume_id = call.request.volume_id; | ||||
|     if (!volume_id) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); | ||||
|     } | ||||
| 
 | ||||
|     const capabilities = call.request.volume_capabilities; | ||||
|     if (!capabilities || capabilities.length === 0) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); | ||||
|     } | ||||
| 
 | ||||
|     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": | ||||
|         let iscsiName = driver.buildIscsiName(volume_id); | ||||
| 
 | ||||
|         response = await httpClient.GetLunUUIDByName(iscsiName); | ||||
|         if (!response) { | ||||
|           throw new GrpcError( | ||||
|             grpc.status.NOT_FOUND, | ||||
|             `invalid volume_id: ${volume_id}` | ||||
|           ); | ||||
|         } | ||||
|         break; | ||||
|       default: | ||||
|         throw new GrpcError( | ||||
|           grpc.status.UNIMPLEMENTED, | ||||
|           `operation not supported by driver` | ||||
|         ); | ||||
|         break; | ||||
|     } | ||||
| 
 | ||||
|     const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
|     if (result.valid !== true) { | ||||
|       return { message: result.message }; | ||||
|     } | ||||
|  |  | |||
|  | @ -392,6 +392,62 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|     return volume; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get the max size a zvol name can be | ||||
|    * | ||||
|    * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238112
 | ||||
|    * https://svnweb.freebsd.org/base?view=revision&revision=343485
 | ||||
|    */ | ||||
|   async getMaxZvolNameLength() { | ||||
|     const driver = this; | ||||
|     const sshClient = driver.getSshClient(); | ||||
| 
 | ||||
|     let response; | ||||
|     let command; | ||||
|     let kernel; | ||||
|     let kernel_release; | ||||
| 
 | ||||
|     // get kernel
 | ||||
|     command = "uname -s"; | ||||
|     driver.ctx.logger.verbose("uname command: %s", command); | ||||
|     response = await sshClient.exec(command); | ||||
|     if (response.code !== 0) { | ||||
|       throw new Error("failed to run uname to determine max zvol name length"); | ||||
|     } else { | ||||
|       kernel = response.stdout.trim(); | ||||
|     } | ||||
| 
 | ||||
|     switch (kernel.toLowerCase().trim()) { | ||||
|       // Linux is 255 (probably larger 4096) but scst may have a 255 limit
 | ||||
|       // https://ngelinux.com/what-is-the-maximum-file-name-length-in-linux-and-how-to-see-this-is-this-really-255-characters-answer-is-no/
 | ||||
|       // https://github.com/dmeister/scst/blob/master/iscsi-scst/include/iscsi_scst.h#L28
 | ||||
|       case "linux": | ||||
|         return 255; | ||||
|       case "freebsd": | ||||
|         // get kernel_release
 | ||||
|         command = "uname -r"; | ||||
|         driver.ctx.logger.verbose("uname command: %s", command); | ||||
|         response = await sshClient.exec(command); | ||||
|         if (response.code !== 0) { | ||||
|           throw new Error( | ||||
|             "failed to run uname to determine max zvol name length" | ||||
|           ); | ||||
|         } else { | ||||
|           kernel_release = response.stdout; | ||||
|           let parts = kernel_release.split("."); | ||||
|           let kernel_release_major = parts[0]; | ||||
| 
 | ||||
|           if (kernel_release_major >= 13) { | ||||
|             return 255; | ||||
|           } else { | ||||
|             return 63; | ||||
|           } | ||||
|         } | ||||
|       default: | ||||
|         throw new Error(`unknown kernel: ${kernel}`); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Ensure sane options are used etc | ||||
|    * true = ready | ||||
|  | @ -498,11 +554,28 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (call.request.volume_capabilities) { | ||||
|     if ( | ||||
|       call.request.volume_capabilities && | ||||
|       call.request.volume_capabilities.length > 0 | ||||
|     ) { | ||||
|       const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
|       if (result.valid !== true) { | ||||
|         throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); | ||||
|       } | ||||
|     } else { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         "missing volume_capabilities" | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       !call.request.capacity_range || | ||||
|       Object.keys(call.request.capacity_range).length === 0 | ||||
|     ) { | ||||
|       call.request.capacity_range = { | ||||
|         required_bytes: 1073741824, | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|  | @ -554,18 +627,75 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     // ensure volumes with the same name being requested a 2nd time but with a different size fails
 | ||||
|     try { | ||||
|       let properties = await zb.zfs.get(datasetName, ["volsize", "refquota"]); | ||||
|       properties = properties[datasetName]; | ||||
|       let size; | ||||
|       switch (driverZfsResourceType) { | ||||
|         case "volume": | ||||
|           size = properties["volsize"].value; | ||||
|           break; | ||||
|         case "filesystem": | ||||
|           size = properties["refquota"].value; | ||||
|           break; | ||||
|         default: | ||||
|           throw new Error( | ||||
|             `unknown zfs resource type: ${driverZfsResourceType}` | ||||
|           ); | ||||
|       } | ||||
| 
 | ||||
|       let check = false; | ||||
|       if (driverZfsResourceType == "volume") { | ||||
|         check = true; | ||||
|       } | ||||
| 
 | ||||
|       if ( | ||||
|         driverZfsResourceType == "filesystem" && | ||||
|         this.options.zfs.datasetEnableQuotas | ||||
|       ) { | ||||
|         check = true; | ||||
|       } | ||||
| 
 | ||||
|       if (check) { | ||||
|         if ( | ||||
|           (call.request.capacity_range.required_bytes && | ||||
|             call.request.capacity_range.required_bytes > 0 && | ||||
|             size < call.request.capacity_range.required_bytes) || | ||||
|           (call.request.capacity_range.limit_bytes && | ||||
|             call.request.capacity_range.limit_bytes > 0 && | ||||
|             size > call.request.capacity_range.limit_bytes) | ||||
|         ) { | ||||
|           throw new GrpcError( | ||||
|             grpc.status.ALREADY_EXISTS, | ||||
|             `volume has already been created with a different size, existing size: ${size}, required_bytes: ${call.request.capacity_range.required_bytes}, limit_bytes: ${call.request.capacity_range.limit_bytes}` | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } catch (err) { | ||||
|       if (err.toString().includes("dataset does not exist")) { | ||||
|         // does NOT already exist
 | ||||
|       } else { | ||||
|         throw err; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This is specifically a FreeBSD limitation, not sure what linux limit is | ||||
|      * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab
 | ||||
|      * https://www.ixsystems.com/documentation/freenas/11.3-BETA1/intro.html#path-and-name-lengths
 | ||||
|      * https://www.freebsd.org/cgi/man.cgi?query=devfs
 | ||||
|      * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238112
 | ||||
|      */ | ||||
|     if (driverZfsResourceType == "volume") { | ||||
|       let extentDiskName = "zvol/" + datasetName; | ||||
|       if (extentDiskName.length > 63) { | ||||
|       let maxZvolNameLength = await driver.getMaxZvolNameLength(); | ||||
|       driver.ctx.logger.debug("max zvol name length: %s", maxZvolNameLength); | ||||
| 
 | ||||
|       if (extentDiskName.length > maxZvolNameLength) { | ||||
|         throw new GrpcError( | ||||
|           grpc.status.FAILED_PRECONDITION, | ||||
|           `extent disk name cannot exceed 63 characters:  ${extentDiskName}` | ||||
|           `extent disk name cannot exceed ${maxZvolNameLength} characters:  ${extentDiskName}` | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | @ -658,7 +788,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|             } catch (err) { | ||||
|               if (err.toString().includes("dataset does not exist")) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.FAILED_PRECONDITION, | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` | ||||
|                 ); | ||||
|               } | ||||
|  | @ -700,7 +830,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|             } catch (err) { | ||||
|               if (err.toString().includes("dataset does not exist")) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.FAILED_PRECONDITION, | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   "dataset does not exists" | ||||
|                 ); | ||||
|               } | ||||
|  | @ -720,7 +850,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|             } catch (err) { | ||||
|               if (err.toString().includes("dataset does not exist")) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.FAILED_PRECONDITION, | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` | ||||
|                 ); | ||||
|               } | ||||
|  | @ -766,7 +896,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|           } catch (err) { | ||||
|             if (err.toString().includes("dataset does not exist")) { | ||||
|               throw new GrpcError( | ||||
|                 grpc.status.FAILED_PRECONDITION, | ||||
|                 grpc.status.NOT_FOUND, | ||||
|                 "dataset does not exists" | ||||
|               ); | ||||
|             } | ||||
|  | @ -816,7 +946,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|             } catch (err) { | ||||
|               if (err.toString().includes("dataset does not exist")) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.FAILED_PRECONDITION, | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   "dataset does not exists" | ||||
|                 ); | ||||
|               } | ||||
|  | @ -1274,10 +1404,20 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|     const datasetName = datasetParentName; | ||||
| 
 | ||||
|     let properties; | ||||
|     try { | ||||
|       properties = await zb.zfs.get(datasetName, ["avail"]); | ||||
|       properties = properties[datasetName]; | ||||
| 
 | ||||
|       return { available_capacity: properties.available.value }; | ||||
|     } catch (err) { | ||||
|       throw err; | ||||
|       // gracefully handle csi-test suite when parent dataset does not yet exist
 | ||||
|       if (err.toString().includes("dataset does not exist")) { | ||||
|         return { available_capacity: 0 }; | ||||
|       } else { | ||||
|         throw err; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|  | @ -1375,7 +1515,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|     let entries = []; | ||||
|     let entries_length = 0; | ||||
|     let next_token; | ||||
|     let uuid, page, next_page; | ||||
|     let uuid; | ||||
|     let response; | ||||
| 
 | ||||
|     const max_entries = call.request.max_entries; | ||||
|  | @ -1385,15 +1525,18 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|     if (starting_token) { | ||||
|       let parts = starting_token.split(":"); | ||||
|       uuid = parts[0]; | ||||
|       page = parseInt(parts[1]); | ||||
|       let start_position = parseInt(parts[1]); | ||||
|       let end_position; | ||||
|       if (max_entries > 0) { | ||||
|         end_position = start_position + max_entries; | ||||
|       } | ||||
|       entries = this.ctx.cache.get(`ListVolumes:result:${uuid}`); | ||||
|       if (entries) { | ||||
|         entries = JSON.parse(JSON.stringify(entries)); | ||||
|         entries_length = entries.length; | ||||
|         entries = entries.splice((page - 1) * max_entries, max_entries); | ||||
|         if (page * max_entries < entries_length) { | ||||
|           next_page = page + 1; | ||||
|           next_token = `${uuid}:${next_page}`; | ||||
|         entries = entries.slice(start_position, end_position); | ||||
|         if (max_entries > 0 && end_position > entries_length) { | ||||
|           next_token = `${uuid}:${end_position}`; | ||||
|         } else { | ||||
|           next_token = null; | ||||
|         } | ||||
|  | @ -1404,7 +1547,10 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
| 
 | ||||
|         return data; | ||||
|       } else { | ||||
|         // TODO: throw error / cache expired
 | ||||
|         throw new GrpcError( | ||||
|           grpc.status.ABORTED, | ||||
|           `invalid starting_token: ${starting_token}` | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|  | @ -1469,7 +1615,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|     for (let row of response.indexed) { | ||||
|       // ignore rows were csi_name is empty
 | ||||
|       if (row[MANAGED_PROPERTY_NAME] != "true") { | ||||
|         return; | ||||
|         continue; | ||||
|       } | ||||
| 
 | ||||
|       let volume = await driver.populateCsiVolumeFromData(row); | ||||
|  | @ -1487,8 +1633,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|         `ListVolumes:result:${uuid}`, | ||||
|         JSON.parse(JSON.stringify(entries)) | ||||
|       ); | ||||
|       next_token = `${uuid}:2`; | ||||
|       entries = entries.splice(0, max_entries); | ||||
|       next_token = `${uuid}:${max_entries}`; | ||||
|       entries = entries.slice(0, max_entries); | ||||
|     } | ||||
| 
 | ||||
|     const data = { | ||||
|  | @ -1511,7 +1657,7 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|     let entries = []; | ||||
|     let entries_length = 0; | ||||
|     let next_token; | ||||
|     let uuid, page, next_page; | ||||
|     let uuid; | ||||
| 
 | ||||
|     const max_entries = call.request.max_entries; | ||||
|     const starting_token = call.request.starting_token; | ||||
|  | @ -1526,15 +1672,18 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|     if (starting_token) { | ||||
|       let parts = starting_token.split(":"); | ||||
|       uuid = parts[0]; | ||||
|       page = parseInt(parts[1]); | ||||
|       let start_position = parseInt(parts[1]); | ||||
|       let end_position; | ||||
|       if (max_entries > 0) { | ||||
|         end_position = start_position + max_entries; | ||||
|       } | ||||
|       entries = this.ctx.cache.get(`ListSnapshots:result:${uuid}`); | ||||
|       if (entries) { | ||||
|         entries = JSON.parse(JSON.stringify(entries)); | ||||
|         entries_length = entries.length; | ||||
|         entries = entries.splice((page - 1) * max_entries, max_entries); | ||||
|         if (page * max_entries < entries_length) { | ||||
|           next_page = page + 1; | ||||
|           next_token = `${uuid}:${next_page}`; | ||||
|         entries = entries.slice(start_position, end_position); | ||||
|         if (max_entries > 0 && end_position > entries_length) { | ||||
|           next_token = `${uuid}:${end_position}`; | ||||
|         } else { | ||||
|           next_token = null; | ||||
|         } | ||||
|  | @ -1545,7 +1694,10 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
| 
 | ||||
|         return data; | ||||
|       } else { | ||||
|         // TODO: throw error / cache expired
 | ||||
|         throw new GrpcError( | ||||
|           grpc.status.ABORTED, | ||||
|           `invalid starting_token: ${starting_token}` | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|  | @ -1639,9 +1791,11 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|               break; | ||||
|             case 2: | ||||
|               message = `source_volume_id ${source_volume_id} does not exist`; | ||||
|               continue; | ||||
|               break; | ||||
|             case 3: | ||||
|               message = `snapshot_id ${snapshot_id} does not exist`; | ||||
|               continue; | ||||
|               break; | ||||
|           } | ||||
|           throw new GrpcError(grpc.status.NOT_FOUND, message); | ||||
|  | @ -1720,8 +1874,8 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|         `ListSnapshots:result:${uuid}`, | ||||
|         JSON.parse(JSON.stringify(entries)) | ||||
|       ); | ||||
|       next_token = `${uuid}:2`; | ||||
|       entries = entries.splice(0, max_entries); | ||||
|       next_token = `${uuid}:${max_entries}`; | ||||
|       entries = entries.slice(0, max_entries); | ||||
|     } | ||||
| 
 | ||||
|     const data = { | ||||
|  | @ -1822,6 +1976,54 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
| 
 | ||||
|     driver.ctx.logger.verbose("cleansed snapshot name: %s", name); | ||||
| 
 | ||||
|     // check for other snapshopts with the same name on other volumes and fail as appropriate
 | ||||
|     { | ||||
|       try { | ||||
|         let datasets = []; | ||||
|         datasets = await zb.zfs.list( | ||||
|           this.getDetachedSnapshotParentDatasetName(), | ||||
|           [], | ||||
|           { recurse: true, types } | ||||
|         ); | ||||
|         for (let dataset of datasets.indexed) { | ||||
|           let parts = dataset.name.split("/").slice(-2); | ||||
|           if (parts[1] != name) { | ||||
|             continue; | ||||
|           } | ||||
| 
 | ||||
|           if (parts[0] != source_volume_id) { | ||||
|             throw new GrpcError( | ||||
|               grpc.status.ALREADY_EXISTS, | ||||
|               `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` | ||||
|             ); | ||||
|           } | ||||
|         } | ||||
|       } catch (err) { | ||||
|         if (!err.toString().includes("dataset does not exist")) { | ||||
|           throw err; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       let snapshots = []; | ||||
|       snapshots = await zb.zfs.list(this.getVolumeParentDatasetName(), [], { | ||||
|         recurse: true, | ||||
|         types, | ||||
|       }); | ||||
|       for (let snapshot of snapshots.indexed) { | ||||
|         let parts = zb.helpers.extractLeafName(snapshot.name).split("@"); | ||||
|         if (parts[1] != name) { | ||||
|           continue; | ||||
|         } | ||||
| 
 | ||||
|         if (parts[0] != source_volume_id) { | ||||
|           throw new GrpcError( | ||||
|             grpc.status.ALREADY_EXISTS, | ||||
|             `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     let fullSnapshotName; | ||||
|     let snapshotDatasetName; | ||||
|     let tmpSnapshotName; | ||||
|  | @ -2043,6 +2245,42 @@ class ControllerZfsSshBaseDriver extends CsiBaseDriver { | |||
|    */ | ||||
|   async ValidateVolumeCapabilities(call) { | ||||
|     const driver = this; | ||||
|     const zb = await this.getZetabyte(); | ||||
| 
 | ||||
|     const volume_id = call.request.volume_id; | ||||
|     if (!volume_id) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); | ||||
|     } | ||||
| 
 | ||||
|     const capabilities = call.request.volume_capabilities; | ||||
|     if (!capabilities || capabilities.length === 0) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); | ||||
|     } | ||||
| 
 | ||||
|     let datasetParentName = this.getVolumeParentDatasetName(); | ||||
|     let name = volume_id; | ||||
| 
 | ||||
|     if (!datasetParentName) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.FAILED_PRECONDITION, | ||||
|         `invalid configuration: missing datasetParentName` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     const datasetName = datasetParentName + "/" + name; | ||||
|     try { | ||||
|       await zb.zfs.get(datasetName, []); | ||||
|     } catch (err) { | ||||
|       if (err.toString().includes("dataset does not exist")) { | ||||
|         throw new GrpcError( | ||||
|           grpc.status.NOT_FOUND, | ||||
|           `invalid volume_id: ${volume_id}` | ||||
|         ); | ||||
|       } else { | ||||
|         throw err; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
| 
 | ||||
|     if (result.valid !== true) { | ||||
|  |  | |||
|  | @ -170,6 +170,7 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|    * @param {*} datasetName | ||||
|    */ | ||||
|   async createShare(call, datasetName) { | ||||
|     const driver = this; | ||||
|     const driverShareType = this.getDriverShareType(); | ||||
|     const httpClient = await this.getHttpClient(); | ||||
|     const httpApiClient = await this.getTrueNASHttpApiClient(); | ||||
|  | @ -515,7 +516,7 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|                 } | ||||
| 
 | ||||
|                 //set zfs property
 | ||||
|                 await zb.zfs.set(datasetName, { | ||||
|                 await httpApiClient.DatasetSet(datasetName, { | ||||
|                   [FREENAS_SMB_SHARE_PROPERTY_NAME]: response.body.id, | ||||
|                 }); | ||||
|               } else { | ||||
|  | @ -620,15 +621,17 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|         iscsiName = iscsiName.toLowerCase(); | ||||
| 
 | ||||
|         let extentDiskName = "zvol/" + datasetName; | ||||
|         let maxZvolNameLength = await driver.getMaxZvolNameLength(); | ||||
|         driver.ctx.logger.debug("max zvol name length: %s", maxZvolNameLength); | ||||
| 
 | ||||
|         /** | ||||
|          * limit is a FreeBSD limitation | ||||
|          * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab
 | ||||
|          */ | ||||
|         if (extentDiskName.length > 63) { | ||||
|         if (extentDiskName.length > maxZvolNameLength) { | ||||
|           throw new GrpcError( | ||||
|             grpc.status.FAILED_PRECONDITION, | ||||
|             `extent disk name cannot exceed 63 characters:  ${extentDiskName}` | ||||
|             `extent disk name cannot exceed ${maxZvolNameLength} characters:  ${extentDiskName}` | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|  | @ -1431,7 +1434,7 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
| 
 | ||||
|                   // remove property to prevent delete race conditions
 | ||||
|                   // due to id re-use by FreeNAS/TrueNAS
 | ||||
|                   await zb.zfs.inherit( | ||||
|                   await httpApiClient.DatasetInherit( | ||||
|                     datasetName, | ||||
|                     FREENAS_SMB_SHARE_PROPERTY_NAME | ||||
|                   ); | ||||
|  | @ -1922,6 +1925,32 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|     return { valid, message }; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Get the max size a zvol name can be | ||||
|    * | ||||
|    * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=238112
 | ||||
|    * https://svnweb.freebsd.org/base?view=revision&revision=343485
 | ||||
|    * https://www.ixsystems.com/documentation/freenas/11.3-BETA1/intro.html#path-and-name-lengths
 | ||||
|    */ | ||||
|   async getMaxZvolNameLength() { | ||||
|     const driver = this; | ||||
|     const httpApiClient = await driver.getTrueNASHttpApiClient(); | ||||
| 
 | ||||
|     // Linux is 255 (probably larger 4096) but scst may have a 255 limit
 | ||||
|     // https://ngelinux.com/what-is-the-maximum-file-name-length-in-linux-and-how-to-see-this-is-this-really-255-characters-answer-is-no/
 | ||||
|     // https://github.com/dmeister/scst/blob/master/iscsi-scst/include/iscsi_scst.h#L28
 | ||||
|     if (await httpApiClient.getIsScale()) { | ||||
|       return 255; | ||||
|     } | ||||
| 
 | ||||
|     let major = await httpApiClient.getSystemVersionMajor(); | ||||
|     if (parseInt(major) >= 13) { | ||||
|       return 255; | ||||
|     } else { | ||||
|       return 63; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Ensure sane options are used etc | ||||
|    * true = ready | ||||
|  | @ -1989,11 +2018,28 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (call.request.volume_capabilities) { | ||||
|     if ( | ||||
|       call.request.volume_capabilities && | ||||
|       call.request.volume_capabilities.length > 0 | ||||
|     ) { | ||||
|       const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
|       if (result.valid !== true) { | ||||
|         throw new GrpcError(grpc.status.INVALID_ARGUMENT, result.message); | ||||
|       } | ||||
|     } else { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         "missing volume_capabilities" | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       !call.request.capacity_range || | ||||
|       Object.keys(call.request.capacity_range).length === 0 | ||||
|     ) { | ||||
|       call.request.capacity_range = { | ||||
|         required_bytes: 1073741824, | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|  | @ -2045,6 +2091,61 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     // ensure volumes with the same name being requested a 2nd time but with a different size fails
 | ||||
|     try { | ||||
|       let properties = await httpApiClient.DatasetGet(datasetName, [ | ||||
|         "volsize", | ||||
|         "refquota", | ||||
|       ]); | ||||
|       let size; | ||||
|       switch (driverZfsResourceType) { | ||||
|         case "volume": | ||||
|           size = properties["volsize"].value; | ||||
|           break; | ||||
|         case "filesystem": | ||||
|           size = properties["refquota"].value; | ||||
|           break; | ||||
|         default: | ||||
|           throw new Error( | ||||
|             `unknown zfs resource type: ${driverZfsResourceType}` | ||||
|           ); | ||||
|       } | ||||
| 
 | ||||
|       let check = false; | ||||
|       if (driverZfsResourceType == "volume") { | ||||
|         check = true; | ||||
|       } | ||||
| 
 | ||||
|       if ( | ||||
|         driverZfsResourceType == "filesystem" && | ||||
|         this.options.zfs.datasetEnableQuotas | ||||
|       ) { | ||||
|         check = true; | ||||
|       } | ||||
| 
 | ||||
|       if (check) { | ||||
|         if ( | ||||
|           (call.request.capacity_range.required_bytes && | ||||
|             call.request.capacity_range.required_bytes > 0 && | ||||
|             size < call.request.capacity_range.required_bytes) || | ||||
|           (call.request.capacity_range.limit_bytes && | ||||
|             call.request.capacity_range.limit_bytes > 0 && | ||||
|             size > call.request.capacity_range.limit_bytes) | ||||
|         ) { | ||||
|           throw new GrpcError( | ||||
|             grpc.status.ALREADY_EXISTS, | ||||
|             `volume has already been created with a different size, existing size: ${size}, required_bytes: ${call.request.capacity_range.required_bytes}, limit_bytes: ${call.request.capacity_range.limit_bytes}` | ||||
|           ); | ||||
|         } | ||||
|       } | ||||
|     } catch (err) { | ||||
|       if (err.toString().includes("dataset does not exist")) { | ||||
|         // does NOT already exist
 | ||||
|       } else { | ||||
|         throw err; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This is specifically a FreeBSD limitation, not sure what linux limit is | ||||
|      * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab
 | ||||
|  | @ -2053,10 +2154,12 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|      */ | ||||
|     if (driverZfsResourceType == "volume") { | ||||
|       let extentDiskName = "zvol/" + datasetName; | ||||
|       if (extentDiskName.length > 63) { | ||||
|       let maxZvolNameLength = await driver.getMaxZvolNameLength(); | ||||
|       driver.ctx.logger.debug("max zvol name length: %s", maxZvolNameLength); | ||||
|       if (extentDiskName.length > maxZvolNameLength) { | ||||
|         throw new GrpcError( | ||||
|           grpc.status.FAILED_PRECONDITION, | ||||
|           `extent disk name cannot exceed 63 characters:  ${extentDiskName}` | ||||
|           `extent disk name cannot exceed ${maxZvolNameLength} characters:  ${extentDiskName}` | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|  | @ -2147,9 +2250,12 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|             try { | ||||
|               await httpApiClient.SnapshotCreate(fullSnapshotName); | ||||
|             } catch (err) { | ||||
|               if (err.toString().includes("dataset does not exist")) { | ||||
|               if ( | ||||
|                 err.toString().includes("dataset does not exist") || | ||||
|                 err.toString().includes("not found") | ||||
|               ) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.FAILED_PRECONDITION, | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` | ||||
|                 ); | ||||
|               } | ||||
|  | @ -2235,9 +2341,12 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|                 } | ||||
|               ); | ||||
|             } catch (err) { | ||||
|               if (err.toString().includes("dataset does not exist")) { | ||||
|               if ( | ||||
|                 err.toString().includes("dataset does not exist") || | ||||
|                 err.toString().includes("not found") | ||||
|               ) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.FAILED_PRECONDITION, | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   "dataset does not exists" | ||||
|                 ); | ||||
|               } | ||||
|  | @ -2253,9 +2362,12 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|                 defer: true, | ||||
|               }); | ||||
|             } catch (err) { | ||||
|               if (err.toString().includes("dataset does not exist")) { | ||||
|               if ( | ||||
|                 err.toString().includes("dataset does not exist") || | ||||
|                 err.toString().includes("not found") | ||||
|               ) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.FAILED_PRECONDITION, | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   `snapshot source_snapshot_id ${volume_content_source_snapshot_id} does not exist` | ||||
|                 ); | ||||
|               } | ||||
|  | @ -2299,9 +2411,12 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|           try { | ||||
|             response = await httpApiClient.SnapshotCreate(fullSnapshotName); | ||||
|           } catch (err) { | ||||
|             if (err.toString().includes("dataset does not exist")) { | ||||
|             if ( | ||||
|               err.toString().includes("dataset does not exist") || | ||||
|               err.toString().includes("not found") | ||||
|             ) { | ||||
|               throw new GrpcError( | ||||
|                 grpc.status.FAILED_PRECONDITION, | ||||
|                 grpc.status.NOT_FOUND, | ||||
|                 "dataset does not exists" | ||||
|               ); | ||||
|             } | ||||
|  | @ -2393,9 +2508,12 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|                 } | ||||
|               ); | ||||
|             } catch (err) { | ||||
|               if (err.toString().includes("dataset does not exist")) { | ||||
|               if ( | ||||
|                 err.toString().includes("dataset does not exist") || | ||||
|                 err.toString().includes("not found") | ||||
|               ) { | ||||
|                 throw new GrpcError( | ||||
|                   grpc.status.FAILED_PRECONDITION, | ||||
|                   grpc.status.NOT_FOUND, | ||||
|                   "dataset does not exists" | ||||
|                 ); | ||||
|               } | ||||
|  | @ -2942,7 +3060,7 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|     let entries = []; | ||||
|     let entries_length = 0; | ||||
|     let next_token; | ||||
|     let uuid, page, next_page; | ||||
|     let uuid; | ||||
|     let response; | ||||
|     let endpoint; | ||||
| 
 | ||||
|  | @ -2953,15 +3071,18 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|     if (starting_token) { | ||||
|       let parts = starting_token.split(":"); | ||||
|       uuid = parts[0]; | ||||
|       page = parseInt(parts[1]); | ||||
|       let start_position = parseInt(parts[1]); | ||||
|       let end_position; | ||||
|       if (max_entries > 0) { | ||||
|         end_position = start_position + max_entries; | ||||
|       } | ||||
|       entries = this.ctx.cache.get(`ListVolumes:result:${uuid}`); | ||||
|       if (entries) { | ||||
|         entries = JSON.parse(JSON.stringify(entries)); | ||||
|         entries_length = entries.length; | ||||
|         entries = entries.splice((page - 1) * max_entries, max_entries); | ||||
|         if (page * max_entries < entries_length) { | ||||
|           next_page = page + 1; | ||||
|           next_token = `${uuid}:${next_page}`; | ||||
|         entries = entries.slice(start_position, end_position); | ||||
|         if (max_entries > 0 && end_position > entries_length) { | ||||
|           next_token = `${uuid}:${end_position}`; | ||||
|         } else { | ||||
|           next_token = null; | ||||
|         } | ||||
|  | @ -2972,7 +3093,10 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
| 
 | ||||
|         return data; | ||||
|       } else { | ||||
|         // TODO: throw error / cache expired
 | ||||
|         throw new GrpcError( | ||||
|           grpc.status.ABORTED, | ||||
|           `invalid starting_token: ${starting_token}` | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|  | @ -3031,7 +3155,7 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|     for (let row of rows) { | ||||
|       // ignore rows were csi_name is empty
 | ||||
|       if (row[MANAGED_PROPERTY_NAME] != "true") { | ||||
|         return; | ||||
|         continue; | ||||
|       } | ||||
| 
 | ||||
|       let volume_id = row["name"].replace( | ||||
|  | @ -3054,8 +3178,8 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|         `ListVolumes:result:${uuid}`, | ||||
|         JSON.parse(JSON.stringify(entries)) | ||||
|       ); | ||||
|       next_token = `${uuid}:2`; | ||||
|       entries = entries.splice(0, max_entries); | ||||
|       next_token = `${uuid}:${max_entries}`; | ||||
|       entries = entries.slice(0, max_entries); | ||||
|     } | ||||
| 
 | ||||
|     const data = { | ||||
|  | @ -3080,7 +3204,7 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|     let entries = []; | ||||
|     let entries_length = 0; | ||||
|     let next_token; | ||||
|     let uuid, page, next_page; | ||||
|     let uuid; | ||||
| 
 | ||||
|     const max_entries = call.request.max_entries; | ||||
|     const starting_token = call.request.starting_token; | ||||
|  | @ -3095,15 +3219,18 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|     if (starting_token) { | ||||
|       let parts = starting_token.split(":"); | ||||
|       uuid = parts[0]; | ||||
|       page = parseInt(parts[1]); | ||||
|       let start_position = parseInt(parts[1]); | ||||
|       let end_position; | ||||
|       if (max_entries > 0) { | ||||
|         end_position = start_position + max_entries; | ||||
|       } | ||||
|       entries = this.ctx.cache.get(`ListSnapshots:result:${uuid}`); | ||||
|       if (entries) { | ||||
|         entries = JSON.parse(JSON.stringify(entries)); | ||||
|         entries_length = entries.length; | ||||
|         entries = entries.splice((page - 1) * max_entries, max_entries); | ||||
|         if (page * max_entries < entries_length) { | ||||
|           next_page = page + 1; | ||||
|           next_token = `${uuid}:${next_page}`; | ||||
|         entries = entries.slice(start_position, end_position); | ||||
|         if (max_entries > 0 && end_position > entries_length) { | ||||
|           next_token = `${uuid}:${end_position}`; | ||||
|         } else { | ||||
|           next_token = null; | ||||
|         } | ||||
|  | @ -3114,7 +3241,10 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
| 
 | ||||
|         return data; | ||||
|       } else { | ||||
|         // TODO: throw error / cache expired
 | ||||
|         throw new GrpcError( | ||||
|           grpc.status.ABORTED, | ||||
|           `invalid starting_token: ${starting_token}` | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|  | @ -3364,9 +3494,11 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|               break; | ||||
|             case 2: | ||||
|               message = `source_volume_id ${source_volume_id} does not exist`; | ||||
|               continue; | ||||
|               break; | ||||
|             case 3: | ||||
|               message = `snapshot_id ${snapshot_id} does not exist`; | ||||
|               continue; | ||||
|               break; | ||||
|           } | ||||
|           throw new GrpcError(grpc.status.NOT_FOUND, message); | ||||
|  | @ -3447,8 +3579,8 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|         `ListSnapshots:result:${uuid}`, | ||||
|         JSON.parse(JSON.stringify(entries)) | ||||
|       ); | ||||
|       next_token = `${uuid}:2`; | ||||
|       entries = entries.splice(0, max_entries); | ||||
|       next_token = `${uuid}:${max_entries}`; | ||||
|       entries = entries.slice(0, max_entries); | ||||
|     } | ||||
| 
 | ||||
|     const data = { | ||||
|  | @ -3466,6 +3598,7 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|   async CreateSnapshot(call) { | ||||
|     const driver = this; | ||||
|     const driverZfsResourceType = this.getDriverZfsResourceType(); | ||||
|     const httpClient = await this.getHttpClient(); | ||||
|     const httpApiClient = await this.getTrueNASHttpApiClient(); | ||||
|     const zb = await this.getZetabyte(); | ||||
| 
 | ||||
|  | @ -3550,6 +3683,80 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
| 
 | ||||
|     driver.ctx.logger.verbose("cleansed snapshot name: %s", name); | ||||
| 
 | ||||
|     // check for other snapshopts with the same name on other volumes and fail as appropriate
 | ||||
|     { | ||||
|       let endpoint; | ||||
|       let response; | ||||
| 
 | ||||
|       let datasets = []; | ||||
|       endpoint = `/pool/dataset/id/${encodeURIComponent( | ||||
|         this.getDetachedSnapshotParentDatasetName() | ||||
|       )}`;
 | ||||
|       response = await httpClient.get(endpoint); | ||||
| 
 | ||||
|       switch (response.statusCode) { | ||||
|         case 200: | ||||
|           for (let child of response.body.children) { | ||||
|             datasets = datasets.concat(child.children); | ||||
|           } | ||||
|           //console.log(datasets);
 | ||||
|           for (let dataset of datasets) { | ||||
|             let parts = dataset.name.split("/").slice(-2); | ||||
|             if (parts[1] != name) { | ||||
|               continue; | ||||
|             } | ||||
| 
 | ||||
|             if (parts[0] != source_volume_id) { | ||||
|               throw new GrpcError( | ||||
|                 grpc.status.ALREADY_EXISTS, | ||||
|                 `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` | ||||
|               ); | ||||
|             } | ||||
|           } | ||||
|           break; | ||||
|         case 404: | ||||
|           break; | ||||
|         default: | ||||
|           throw new Error(JSON.stringify(response.body)); | ||||
|       } | ||||
| 
 | ||||
|       // get all snapshot recursively from the parent dataset
 | ||||
|       let snapshots = []; | ||||
|       endpoint = `/pool/dataset/id/${encodeURIComponent( | ||||
|         this.getVolumeParentDatasetName() | ||||
|       )}`;
 | ||||
|       response = await httpClient.get(endpoint, { | ||||
|         "extra.snapshots": 1, | ||||
|         //"extra.snapshots_properties": JSON.stringify(zfsProperties),
 | ||||
|       }); | ||||
| 
 | ||||
|       switch (response.statusCode) { | ||||
|         case 200: | ||||
|           for (let child of response.body.children) { | ||||
|             snapshots = snapshots.concat(child.snapshots); | ||||
|           } | ||||
|           //console.log(snapshots);
 | ||||
|           for (let snapshot of snapshots) { | ||||
|             let parts = zb.helpers.extractLeafName(snapshot.name).split("@"); | ||||
|             if (parts[1] != name) { | ||||
|               continue; | ||||
|             } | ||||
| 
 | ||||
|             if (parts[0] != source_volume_id) { | ||||
|               throw new GrpcError( | ||||
|                 grpc.status.ALREADY_EXISTS, | ||||
|                 `snapshot name: ${name} is incompatible with source_volume_id: ${source_volume_id} due to being used with another source_volume_id` | ||||
|               ); | ||||
|             } | ||||
|           } | ||||
|           break; | ||||
|         case 404: | ||||
|           break; | ||||
|         default: | ||||
|           throw new Error(JSON.stringify(response.body)); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     let fullSnapshotName; | ||||
|     let snapshotDatasetName; | ||||
|     let tmpSnapshotName; | ||||
|  | @ -3671,7 +3878,10 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|           properties: snapshotProperties, | ||||
|         }); | ||||
|       } catch (err) { | ||||
|         if (err.toString().includes("dataset does not exist")) { | ||||
|         if ( | ||||
|           err.toString().includes("dataset does not exist") || | ||||
|           err.toString().includes("not found") | ||||
|         ) { | ||||
|           throw new GrpcError( | ||||
|             grpc.status.FAILED_PRECONDITION, | ||||
|             `snapshot source_volume_id ${source_volume_id} does not exist` | ||||
|  | @ -3840,7 +4050,42 @@ class FreeNASApiDriver extends CsiBaseDriver { | |||
|    */ | ||||
|   async ValidateVolumeCapabilities(call) { | ||||
|     const driver = this; | ||||
|     const result = this.assertCapabilities(call.request.volume_capabilities); | ||||
|     const httpApiClient = await this.getTrueNASHttpApiClient(); | ||||
| 
 | ||||
|     const volume_id = call.request.volume_id; | ||||
|     if (!volume_id) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); | ||||
|     } | ||||
|     const capabilities = call.request.volume_capabilities; | ||||
|     if (!capabilities || capabilities.length === 0) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capabilities`); | ||||
|     } | ||||
| 
 | ||||
|     let datasetParentName = this.getVolumeParentDatasetName(); | ||||
|     let name = volume_id; | ||||
| 
 | ||||
|     if (!datasetParentName) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.FAILED_PRECONDITION, | ||||
|         `invalid configuration: missing datasetParentName` | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     const datasetName = datasetParentName + "/" + name; | ||||
|     try { | ||||
|       await httpApiClient.DatasetGet(datasetName, []); | ||||
|     } catch (err) { | ||||
|       if (err.toString().includes("dataset does not exist")) { | ||||
|         throw new GrpcError( | ||||
|           grpc.status.NOT_FOUND, | ||||
|           `invalid volume_id: ${volume_id}` | ||||
|         ); | ||||
|       } else { | ||||
|         throw err; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const result = this.assertCapabilities(capabilities); | ||||
| 
 | ||||
|     if (result.valid !== true) { | ||||
|       return { message: result.message }; | ||||
|  |  | |||
|  | @ -167,7 +167,9 @@ class FreeNASSshDriver extends ControllerZfsSshBaseDriver { | |||
|    * @param {*} datasetName | ||||
|    */ | ||||
|   async createShare(call, datasetName) { | ||||
|     const driver = this; | ||||
|     const driverShareType = this.getDriverShareType(); | ||||
|     const sshClient = this.getSshClient(); | ||||
|     const httpClient = await this.getHttpClient(); | ||||
|     const apiVersion = httpClient.getApiVersion(); | ||||
|     const zb = await this.getZetabyte(); | ||||
|  | @ -617,15 +619,18 @@ class FreeNASSshDriver extends ControllerZfsSshBaseDriver { | |||
|         iscsiName = iscsiName.toLowerCase(); | ||||
| 
 | ||||
|         let extentDiskName = "zvol/" + datasetName; | ||||
|         let maxZvolNameLength = await driver.getMaxZvolNameLength(); | ||||
|         driver.ctx.logger.debug("max zvol name length: %s", maxZvolNameLength); | ||||
| 
 | ||||
|         /** | ||||
|          * limit is a FreeBSD limitation | ||||
|          * https://www.ixsystems.com/documentation/freenas/11.2-U5/storage.html#zfs-zvol-config-opts-tab
 | ||||
|          */ | ||||
|         if (extentDiskName.length > 63) { | ||||
| 
 | ||||
|         if (extentDiskName.length > maxZvolNameLength) { | ||||
|           throw new GrpcError( | ||||
|             grpc.status.FAILED_PRECONDITION, | ||||
|             `extent disk name cannot exceed 63 characters:  ${extentDiskName}` | ||||
|             `extent disk name cannot exceed ${maxZvolNameLength} characters:  ${extentDiskName}` | ||||
|           ); | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -280,8 +280,20 @@ class CsiBaseDriver { | |||
|     let device; | ||||
| 
 | ||||
|     const volume_id = call.request.volume_id; | ||||
|     if (!volume_id) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); | ||||
|     } | ||||
|     const staging_target_path = call.request.staging_target_path; | ||||
|     if (!staging_target_path) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         `missing staging_target_path` | ||||
|       ); | ||||
|     } | ||||
|     const capability = call.request.volume_capability; | ||||
|     if (!capability || Object.keys(capability).length === 0) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capability`); | ||||
|     } | ||||
|     const access_type = capability.access_type || "mount"; | ||||
|     const volume_context = call.request.volume_context; | ||||
|     let fs_type; | ||||
|  | @ -360,6 +372,24 @@ class CsiBaseDriver { | |||
|         break; | ||||
|       case "smb": | ||||
|         device = `//${volume_context.server}/${volume_context.share}`; | ||||
| 
 | ||||
|         // if not present add guest
 | ||||
|         let has_username = mount_flags.some((element) => { | ||||
|           element = element.trim().toLowerCase(); | ||||
|           return element.startsWith("username="); | ||||
|         }); | ||||
| 
 | ||||
|         // prevents driver from hanging on stdin waiting for a password to be entered at the cli
 | ||||
|         if (!has_username) { | ||||
|           let has_guest = mount_flags.some((element) => { | ||||
|             element = element.trim().toLowerCase(); | ||||
|             return element === "guest"; | ||||
|           }); | ||||
| 
 | ||||
|           if (!has_guest) { | ||||
|             mount_flags.push("guest"); | ||||
|           } | ||||
|         } | ||||
|         break; | ||||
|       case "iscsi": | ||||
|         let portals = []; | ||||
|  | @ -547,6 +577,10 @@ class CsiBaseDriver { | |||
|         switch (node_attach_driver) { | ||||
|           // block specific logic
 | ||||
|           case "iscsi": | ||||
|             if (!fs_type) { | ||||
|               fs_type = "ext4"; | ||||
|             } | ||||
| 
 | ||||
|             if (await filesystem.isBlockDevice(device)) { | ||||
|               // format
 | ||||
|               result = await filesystem.deviceIsFormatted(device); | ||||
|  | @ -591,6 +625,24 @@ class CsiBaseDriver { | |||
| 
 | ||||
|         result = await mount.deviceIsMountedAtPath(device, staging_target_path); | ||||
|         if (!result) { | ||||
|           if (!fs_type) { | ||||
|             switch (node_attach_driver) { | ||||
|               case "nfs": | ||||
|                 fs_type = "nfs"; | ||||
|                 break; | ||||
|               case "lustre": | ||||
|                 fs_type = "lustre"; | ||||
|                 break; | ||||
|               case "smb": | ||||
|                 fs_type = "cifs"; | ||||
|                 break; | ||||
|               case "iscsi": | ||||
|                 fs_type = "ext4"; | ||||
|                 break; | ||||
|               default: | ||||
|                 break; | ||||
|             } | ||||
|           } | ||||
|           await mount.mount( | ||||
|             device, | ||||
|             staging_target_path, | ||||
|  | @ -697,18 +749,20 @@ class CsiBaseDriver { | |||
|     let access_type = "mount"; | ||||
| 
 | ||||
|     const volume_id = call.request.volume_id; | ||||
|     if (!volume_id) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); | ||||
|     } | ||||
|     const staging_target_path = call.request.staging_target_path; | ||||
|     const block_path = staging_target_path + "/block_device"; | ||||
|     let normalized_staging_path = staging_target_path; | ||||
|     const umount_args = []; | ||||
|     const umount_force_extra_args = ["--force", "--lazy"]; | ||||
| 
 | ||||
|     if (!staging_target_path) { | ||||
|       throw new GrpcError( | ||||
|         grpc.status.INVALID_ARGUMENT, | ||||
|         `missing staging_target_path` | ||||
|       ); | ||||
|     } | ||||
|     const block_path = staging_target_path + "/block_device"; | ||||
|     let normalized_staging_path = staging_target_path; | ||||
|     const umount_args = []; | ||||
|     const umount_force_extra_args = ["--force", "--lazy"]; | ||||
| 
 | ||||
|     //result = await mount.pathIsMounted(block_path);
 | ||||
|     //result = await mount.pathIsMounted(staging_target_path)
 | ||||
|  | @ -910,9 +964,18 @@ class CsiBaseDriver { | |||
|     let result; | ||||
| 
 | ||||
|     const volume_id = call.request.volume_id; | ||||
|     if (!volume_id) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); | ||||
|     } | ||||
|     const staging_target_path = call.request.staging_target_path || ""; | ||||
|     const target_path = call.request.target_path; | ||||
|     if (!target_path) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing target_path`); | ||||
|     } | ||||
|     const capability = call.request.volume_capability; | ||||
|     if (!capability || Object.keys(capability).length === 0) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing capability`); | ||||
|     } | ||||
|     const access_type = capability.access_type || "mount"; | ||||
|     let mount_flags; | ||||
|     let volume_mount_group; | ||||
|  | @ -1044,7 +1107,13 @@ class CsiBaseDriver { | |||
|     let result; | ||||
| 
 | ||||
|     const volume_id = call.request.volume_id; | ||||
|     if (!volume_id) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); | ||||
|     } | ||||
|     const target_path = call.request.target_path; | ||||
|     if (!target_path) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing target_path`); | ||||
|     } | ||||
|     const umount_args = []; | ||||
|     const umount_force_extra_args = ["--force", "--lazy"]; | ||||
| 
 | ||||
|  | @ -1119,6 +1188,9 @@ class CsiBaseDriver { | |||
|     let device_path; | ||||
|     let access_type; | ||||
|     const volume_id = call.request.volume_id; | ||||
|     if (!volume_id) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); | ||||
|     } | ||||
|     const volume_path = call.request.volume_path; | ||||
|     const block_path = volume_path + "/block_device"; | ||||
| 
 | ||||
|  | @ -1152,6 +1224,12 @@ class CsiBaseDriver { | |||
| 
 | ||||
|     switch (access_type) { | ||||
|       case "mount": | ||||
|         if (!(await mount.pathIsMounted(device_path))) { | ||||
|           throw new GrpcError( | ||||
|             grpc.status.NOT_FOUND, | ||||
|             `nothing mounted at path: ${device_path}` | ||||
|           ); | ||||
|         } | ||||
|         result = await mount.getMountDetails(device_path, [ | ||||
|           "avail", | ||||
|           "size", | ||||
|  | @ -1168,6 +1246,12 @@ class CsiBaseDriver { | |||
|         ]; | ||||
|         break; | ||||
|       case "block": | ||||
|         if (!(await filesystem.pathExists(device_path))) { | ||||
|           throw new GrpcError( | ||||
|             grpc.status.NOT_FOUND, | ||||
|             `nothing mounted at path: ${device_path}` | ||||
|           ); | ||||
|         } | ||||
|         result = await filesystem.getBlockDevice(device_path); | ||||
| 
 | ||||
|         res.usage = [ | ||||
|  | @ -1208,14 +1292,16 @@ class CsiBaseDriver { | |||
|     let is_device_mapper = false; | ||||
| 
 | ||||
|     const volume_id = call.request.volume_id; | ||||
|     if (!volume_id) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_id`); | ||||
|     } | ||||
|     const volume_path = call.request.volume_path; | ||||
|     const block_path = volume_path + "/block_device"; | ||||
|     const capacity_range = call.request.capacity_range; | ||||
|     const volume_capability = call.request.volume_capability; | ||||
| 
 | ||||
|     if (!volume_path) { | ||||
|       throw new GrpcError(grpc.status.INVALID_ARGUMENT, `missing volume_path`); | ||||
|     } | ||||
|     const block_path = volume_path + "/block_device"; | ||||
|     const capacity_range = call.request.capacity_range; | ||||
|     const volume_capability = call.request.volume_capability; | ||||
| 
 | ||||
|     if ( | ||||
|       (await mount.isBindMountedBlockDevice(volume_path)) || | ||||
|  | @ -1235,7 +1321,7 @@ class CsiBaseDriver { | |||
|     } catch (err) { | ||||
|       if (err.code == 1) { | ||||
|         throw new GrpcError( | ||||
|           grpc.status.FAILED_PRECONDITION, | ||||
|           grpc.status.NOT_FOUND, | ||||
|           `volume_path ${volume_path} is not currently mounted` | ||||
|         ); | ||||
|       } | ||||
|  |  | |||
|  | @ -1153,14 +1153,15 @@ class Zetabyte { | |||
| 
 | ||||
|           args.push("'" + command.join(" ") + "'"); | ||||
| 
 | ||||
|           zb.exec("/bin/sh", args, { timeout: zb.options.timeout }, function ( | ||||
|             error, | ||||
|             stdout, | ||||
|             stderr | ||||
|           ) { | ||||
|           zb.exec( | ||||
|             "/bin/sh", | ||||
|             args, | ||||
|             { timeout: zb.options.timeout }, | ||||
|             function (error, stdout, stderr) { | ||||
|               if (error) return reject(zb.helpers.zfsError(error, stderr)); | ||||
|               return resolve(stdout); | ||||
|           }); | ||||
|             } | ||||
|           ); | ||||
|         }); | ||||
|       }, | ||||
| 
 | ||||
|  | @ -1230,6 +1231,7 @@ class Zetabyte { | |||
|        *  filesystem|volume|snapshot... | ||||
|        * | ||||
|        * @param {*} dataset | ||||
|        * @param {*} properties | ||||
|        * @param {*} options | ||||
|        */ | ||||
|       list: function (dataset, properties, options = {}) { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue