initial support for nvmeof
Signed-off-by: Travis Glenn Hansen <travisghansen@yahoo.com>
This commit is contained in:
		
							parent
							
								
									c8b13450d2
								
							
						
					
					
						commit
						9d2943b62d
					
				|  | @ -256,6 +256,7 @@ jobs: | ||||||
|           - zfs-generic/iscsi.yaml |           - zfs-generic/iscsi.yaml | ||||||
|           - zfs-generic/nfs.yaml |           - zfs-generic/nfs.yaml | ||||||
|           - zfs-generic/smb.yaml |           - zfs-generic/smb.yaml | ||||||
|  |           - zfs-generic/nvmeof.yaml | ||||||
|     runs-on: |     runs-on: | ||||||
|       - self-hosted |       - self-hosted | ||||||
|       - Linux |       - Linux | ||||||
|  |  | ||||||
|  | @ -0,0 +1,30 @@ | ||||||
|  | driver: zfs-generic-nvmeof | ||||||
|  | 
 | ||||||
|  | sshConnection: | ||||||
|  |   host: ${SERVER_HOST} | ||||||
|  |   port: 22 | ||||||
|  |   username: ${SERVER_USERNAME} | ||||||
|  |   password: ${SERVER_PASSWORD} | ||||||
|  | 
 | ||||||
|  | zfs: | ||||||
|  |   datasetParentName: tank/ci/${CI_BUILD_KEY}/v | ||||||
|  |   detachedSnapshotsDatasetParentName: tank/ci/${CI_BUILD_KEY}/s | ||||||
|  | 
 | ||||||
|  |   zvolCompression: | ||||||
|  |   zvolDedup: | ||||||
|  |   zvolEnableReservation: false | ||||||
|  |   zvolBlocksize: | ||||||
|  | 
 | ||||||
|  | nvmeof: | ||||||
|  |   transports: | ||||||
|  |     - "tcp://${SERVER_HOST}:4420" | ||||||
|  |   namePrefix: "csi-ci-${CI_BUILD_KEY}-" | ||||||
|  |   nameSuffix: "" | ||||||
|  |   shareStrategy: "nvmetCli" | ||||||
|  |   shareStrategyNvmetCli: | ||||||
|  |     basename: "nqn.2003-01.org.linux-nvmeof.ubuntu-19.x8664" | ||||||
|  |     ports: | ||||||
|  |       - "1" | ||||||
|  |     subsystem: | ||||||
|  |       attributes: | ||||||
|  |         allow_any_host: 1 | ||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -1,6 +1,6 @@ | ||||||
| { | { | ||||||
|   "name": "democratic-csi", |   "name": "democratic-csi", | ||||||
|   "version": "1.7.7", |   "version": "1.8.0", | ||||||
|   "description": "kubernetes csi driver framework", |   "description": "kubernetes csi driver framework", | ||||||
|   "main": "bin/democratic-csi", |   "main": "bin/democratic-csi", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|  | @ -20,11 +20,11 @@ | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@grpc/grpc-js": "^1.5.7", |     "@grpc/grpc-js": "^1.5.7", | ||||||
|     "@grpc/proto-loader": "^0.7.0", |     "@grpc/proto-loader": "^0.7.0", | ||||||
|     "@kubernetes/client-node": "^0.17.0", |     "@kubernetes/client-node": "^0.18.0", | ||||||
|     "async-mutex": "^0.4.0", |     "async-mutex": "^0.4.0", | ||||||
|     "axios": "^1.1.3", |     "axios": "^1.1.3", | ||||||
|     "bunyan": "^1.8.15", |     "bunyan": "^1.8.15", | ||||||
|     "fs-extra": "^10.1.0", |     "fs-extra": "^11.1.0", | ||||||
|     "handlebars": "^4.7.7", |     "handlebars": "^4.7.7", | ||||||
|     "js-yaml": "^4.0.0", |     "js-yaml": "^4.0.0", | ||||||
|     "lodash": "^4.17.21", |     "lodash": "^4.17.21", | ||||||
|  |  | ||||||
|  | @ -3,20 +3,29 @@ const { ControllerZfsBaseDriver } = require("../controller-zfs"); | ||||||
| const { GrpcError, grpc } = require("../../utils/grpc"); | const { GrpcError, grpc } = require("../../utils/grpc"); | ||||||
| const GeneralUtils = require("../../utils/general"); | const GeneralUtils = require("../../utils/general"); | ||||||
| const registry = require("../../utils/registry"); | const registry = require("../../utils/registry"); | ||||||
| const SshClient = require("../../utils/ssh").SshClient; | const LocalCliExecClient = | ||||||
|  |   require("../../utils/zfs_local_exec_client").LocalCliClient; | ||||||
|  | const SshClient = require("../../utils/zfs_ssh_exec_client").SshClient; | ||||||
| const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); | const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); | ||||||
| 
 | 
 | ||||||
| const Handlebars = require("handlebars"); | const Handlebars = require("handlebars"); | ||||||
| 
 | 
 | ||||||
| const ISCSI_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:iscsi_assets_name"; | const ISCSI_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:iscsi_assets_name"; | ||||||
|  | const NVMEOF_ASSETS_NAME_PROPERTY_NAME = "democratic-csi:nvmeof_assets_name"; | ||||||
| const __REGISTRY_NS__ = "ControllerZfsGenericDriver"; | const __REGISTRY_NS__ = "ControllerZfsGenericDriver"; | ||||||
| class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { | class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { | ||||||
|   getExecClient() { |   getExecClient() { | ||||||
|     return registry.get(`${__REGISTRY_NS__}:exec_client`, () => { |     return registry.get(`${__REGISTRY_NS__}:exec_client`, () => { | ||||||
|       return new SshClient({ |       if (this.options.sshConnection) { | ||||||
|         logger: this.ctx.logger, |         return new SshClient({ | ||||||
|         connection: this.options.sshConnection, |           logger: this.ctx.logger, | ||||||
|       }); |           connection: this.options.sshConnection, | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         return new LocalCliExecClient({ | ||||||
|  |           logger: this.ctx.logger, | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | @ -24,7 +33,11 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { | ||||||
|     return registry.getAsync(`${__REGISTRY_NS__}:zb`, async () => { |     return registry.getAsync(`${__REGISTRY_NS__}:zb`, async () => { | ||||||
|       const execClient = this.getExecClient(); |       const execClient = this.getExecClient(); | ||||||
|       const options = {}; |       const options = {}; | ||||||
|       options.executor = new ZfsSshProcessManager(execClient); |       if (this.options.sshConnection) { | ||||||
|  |         options.executor = new ZfsSshProcessManager(execClient); | ||||||
|  |       } else { | ||||||
|  |         options.executor = execClient; | ||||||
|  |       } | ||||||
|       options.idempotent = true; |       options.idempotent = true; | ||||||
| 
 | 
 | ||||||
|       if ( |       if ( | ||||||
|  | @ -55,6 +68,7 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { | ||||||
|       case "zfs-generic-smb": |       case "zfs-generic-smb": | ||||||
|         return "filesystem"; |         return "filesystem"; | ||||||
|       case "zfs-generic-iscsi": |       case "zfs-generic-iscsi": | ||||||
|  |       case "zfs-generic-nvmeof": | ||||||
|         return "volume"; |         return "volume"; | ||||||
|       default: |       default: | ||||||
|         throw new Error("unknown driver: " + this.ctx.args.driver); |         throw new Error("unknown driver: " + this.ctx.args.driver); | ||||||
|  | @ -164,28 +178,28 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { | ||||||
|         }; |         }; | ||||||
|         return volume_context; |         return volume_context; | ||||||
| 
 | 
 | ||||||
|       case "zfs-generic-iscsi": |       case "zfs-generic-iscsi": { | ||||||
|         let basename; |         let basename; | ||||||
|         let iscsiName; |         let assetName; | ||||||
| 
 | 
 | ||||||
|         if (this.options.iscsi.nameTemplate) { |         if (this.options.iscsi.nameTemplate) { | ||||||
|           iscsiName = Handlebars.compile(this.options.iscsi.nameTemplate)({ |           assetName = Handlebars.compile(this.options.iscsi.nameTemplate)({ | ||||||
|             name: call.request.name, |             name: call.request.name, | ||||||
|             parameters: call.request.parameters, |             parameters: call.request.parameters, | ||||||
|           }); |           }); | ||||||
|         } else { |         } else { | ||||||
|           iscsiName = zb.helpers.extractLeafName(datasetName); |           assetName = zb.helpers.extractLeafName(datasetName); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.options.iscsi.namePrefix) { |         if (this.options.iscsi.namePrefix) { | ||||||
|           iscsiName = this.options.iscsi.namePrefix + iscsiName; |           assetName = this.options.iscsi.namePrefix + assetName; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         if (this.options.iscsi.nameSuffix) { |         if (this.options.iscsi.nameSuffix) { | ||||||
|           iscsiName += this.options.iscsi.nameSuffix; |           assetName += this.options.iscsi.nameSuffix; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         iscsiName = iscsiName.toLowerCase(); |         assetName = assetName.toLowerCase(); | ||||||
| 
 | 
 | ||||||
|         let extentDiskName = "zvol/" + datasetName; |         let extentDiskName = "zvol/" + datasetName; | ||||||
| 
 | 
 | ||||||
|  | @ -239,20 +253,20 @@ class ControllerZfsGenericDriver extends ControllerZfsBaseDriver { | ||||||
|                   ` |                   ` | ||||||
| # create target | # create target | ||||||
| cd /iscsi | cd /iscsi | ||||||
| create ${basename}:${iscsiName} | create ${basename}:${assetName} | ||||||
| 
 | 
 | ||||||
| # setup tpg | # setup tpg | ||||||
| cd /iscsi/${basename}:${iscsiName}/tpg1 | cd /iscsi/${basename}:${assetName}/tpg1 | ||||||
| ${setAttributesText} | ${setAttributesText} | ||||||
| ${setAuthText} | ${setAuthText} | ||||||
| 
 | 
 | ||||||
| # create extent | # create extent | ||||||
| cd /backstores/block | cd /backstores/block | ||||||
| create ${iscsiName} /dev/${extentDiskName} | create ${assetName} /dev/${extentDiskName} | ||||||
| 
 | 
 | ||||||
| # add extent to target/tpg | # add extent to target/tpg | ||||||
| cd /iscsi/${basename}:${iscsiName}/tpg1/luns | cd /iscsi/${basename}:${assetName}/tpg1/luns | ||||||
| create /backstores/block/${iscsiName} | create /backstores/block/${assetName} | ||||||
| ` | ` | ||||||
|                 ); |                 ); | ||||||
|               }, |               }, | ||||||
|  | @ -271,12 +285,12 @@ create /backstores/block/${iscsiName} | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // iqn = target
 |         // iqn = target
 | ||||||
|         let iqn = basename + ":" + iscsiName; |         let iqn = basename + ":" + assetName; | ||||||
|         this.ctx.logger.info("iqn: " + iqn); |         this.ctx.logger.info("iqn: " + iqn); | ||||||
| 
 | 
 | ||||||
|         // store this off to make delete process more bullet proof
 |         // store this off to make delete process more bullet proof
 | ||||||
|         await zb.zfs.set(datasetName, { |         await zb.zfs.set(datasetName, { | ||||||
|           [ISCSI_ASSETS_NAME_PROPERTY_NAME]: iscsiName, |           [ISCSI_ASSETS_NAME_PROPERTY_NAME]: assetName, | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         volume_context = { |         volume_context = { | ||||||
|  | @ -290,6 +304,231 @@ create /backstores/block/${iscsiName} | ||||||
|           lun: 0, |           lun: 0, | ||||||
|         }; |         }; | ||||||
|         return volume_context; |         return volume_context; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       case "zfs-generic-nvmeof": { | ||||||
|  |         let basename; | ||||||
|  |         let assetName; | ||||||
|  | 
 | ||||||
|  |         if (this.options.nvmeof.nameTemplate) { | ||||||
|  |           assetName = Handlebars.compile(this.options.nvmeof.nameTemplate)({ | ||||||
|  |             name: call.request.name, | ||||||
|  |             parameters: call.request.parameters, | ||||||
|  |           }); | ||||||
|  |         } else { | ||||||
|  |           assetName = zb.helpers.extractLeafName(datasetName); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (this.options.nvmeof.namePrefix) { | ||||||
|  |           assetName = this.options.nvmeof.namePrefix + assetName; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (this.options.nvmeof.nameSuffix) { | ||||||
|  |           assetName += this.options.nvmeof.nameSuffix; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         assetName = assetName.toLowerCase(); | ||||||
|  | 
 | ||||||
|  |         let extentDiskName = "zvol/" + datasetName; | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * 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) {
 | ||||||
|  |         //  throw new GrpcError(
 | ||||||
|  |         //    grpc.status.FAILED_PRECONDITION,
 | ||||||
|  |         //    `extent disk name cannot exceed 63 characters:  ${extentDiskName}`
 | ||||||
|  |         //  );
 | ||||||
|  |         //}
 | ||||||
|  | 
 | ||||||
|  |         let namespace = 1; | ||||||
|  | 
 | ||||||
|  |         switch (this.options.nvmeof.shareStrategy) { | ||||||
|  |           case "nvmetCli": | ||||||
|  |             { | ||||||
|  |               basename = this.options.nvmeof.shareStrategyNvmetCli.basename; | ||||||
|  |               let savefile = _.get( | ||||||
|  |                 this.options, | ||||||
|  |                 "nvmeof.shareStrategyNvmetCli.configPath", | ||||||
|  |                 "" | ||||||
|  |               ); | ||||||
|  |               if (savefile) { | ||||||
|  |                 savefile = `savefile=${savefile}`; | ||||||
|  |               } | ||||||
|  |               let setSubsystemAttributesText = ""; | ||||||
|  |               if (this.options.nvmeof.shareStrategyNvmetCli.subsystem) { | ||||||
|  |                 if ( | ||||||
|  |                   this.options.nvmeof.shareStrategyNvmetCli.subsystem.attributes | ||||||
|  |                 ) { | ||||||
|  |                   for (const attributeName in this.options.nvmeof | ||||||
|  |                     .shareStrategyNvmetCli.subsystem.attributes) { | ||||||
|  |                     const attributeValue = | ||||||
|  |                       this.options.nvmeof.shareStrategyNvmetCli.subsystem | ||||||
|  |                         .attributes[attributeName]; | ||||||
|  |                     setSubsystemAttributesText += "\n"; | ||||||
|  |                     setSubsystemAttributesText += `set attr ${attributeName}=${attributeValue}`; | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               let portCommands = ""; | ||||||
|  |               this.options.nvmeof.shareStrategyNvmetCli.ports.forEach( | ||||||
|  |                 (port) => { | ||||||
|  |                   portCommands += ` | ||||||
|  | cd /ports/${port}/subsystems | ||||||
|  | create ${basename}:${assetName} | ||||||
|  | `;
 | ||||||
|  |                 } | ||||||
|  |               ); | ||||||
|  | 
 | ||||||
|  |               await GeneralUtils.retry( | ||||||
|  |                 3, | ||||||
|  |                 2000, | ||||||
|  |                 async () => { | ||||||
|  |                   await this.nvmetCliCommand( | ||||||
|  |                     ` | ||||||
|  | # create subsystem | ||||||
|  | cd /subsystems | ||||||
|  | create ${basename}:${assetName} | ||||||
|  | cd ${basename}:${assetName} | ||||||
|  | ${setSubsystemAttributesText} | ||||||
|  | 
 | ||||||
|  | # create subsystem namespace | ||||||
|  | cd namespaces | ||||||
|  | create ${namespace} | ||||||
|  | cd ${namespace} | ||||||
|  | set device path=/dev/${extentDiskName} | ||||||
|  | enable | ||||||
|  | 
 | ||||||
|  | # associate subsystem/target to port(al) | ||||||
|  | ${portCommands} | ||||||
|  | 
 | ||||||
|  | saveconfig ${savefile} | ||||||
|  | ` | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   retryCondition: (err) => { | ||||||
|  |                     if (err.stdout && err.stdout.includes("Ran out of input")) { | ||||||
|  |                       return true; | ||||||
|  |                     } | ||||||
|  |                     return false; | ||||||
|  |                   }, | ||||||
|  |                 } | ||||||
|  |               ); | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  |           case "spdkCli": | ||||||
|  |             { | ||||||
|  |               basename = this.options.nvmeof.shareStrategySpdkCli.basename; | ||||||
|  |               let bdevAttributesText = ""; | ||||||
|  |               if (this.options.nvmeof.shareStrategySpdkCli.bdev) { | ||||||
|  |                 if (this.options.nvmeof.shareStrategySpdkCli.bdev.attributes) { | ||||||
|  |                   for (const attributeName in this.options.nvmeof | ||||||
|  |                     .shareStrategySpdkCli.bdev.attributes) { | ||||||
|  |                     const attributeValue = | ||||||
|  |                       this.options.nvmeof.shareStrategySpdkCli.bdev.attributes[ | ||||||
|  |                         attributeName | ||||||
|  |                       ]; | ||||||
|  |                     bdevAttributesText += `${attributeName}=${attributeValue}`; | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               let subsystemAttributesText = ""; | ||||||
|  |               if (this.options.nvmeof.shareStrategySpdkCli.subsystem) { | ||||||
|  |                 if ( | ||||||
|  |                   this.options.nvmeof.shareStrategySpdkCli.subsystem.attributes | ||||||
|  |                 ) { | ||||||
|  |                   for (const attributeName in this.options.nvmeof | ||||||
|  |                     .shareStrategySpdkCli.subsystem.attributes) { | ||||||
|  |                     const attributeValue = | ||||||
|  |                       this.options.nvmeof.shareStrategySpdkCli.subsystem | ||||||
|  |                         .attributes[attributeName]; | ||||||
|  |                     subsystemAttributesText += `${attributeName}=${attributeValue}`; | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               let listenerCommands = `cd /nvmf/subsystem/${basename}:${assetName}/listen_addresses\n`; | ||||||
|  |               this.options.nvmeof.shareStrategySpdkCli.listeners.forEach( | ||||||
|  |                 (listener) => { | ||||||
|  |                   let listenerAttributesText = ""; | ||||||
|  |                   for (const attributeName in listener) { | ||||||
|  |                     const attributeValue = listener[attributeName]; | ||||||
|  |                     listenerAttributesText += ` ${attributeName}=${attributeValue} `; | ||||||
|  |                   } | ||||||
|  |                   listenerCommands += ` | ||||||
|  | create ${listenerAttributesText} | ||||||
|  | `;
 | ||||||
|  |                 } | ||||||
|  |               ); | ||||||
|  | 
 | ||||||
|  |               await GeneralUtils.retry( | ||||||
|  |                 3, | ||||||
|  |                 2000, | ||||||
|  |                 async () => { | ||||||
|  |                   await this.spdkCliCommand( | ||||||
|  |                     ` | ||||||
|  | # create bdev | ||||||
|  | cd /bdevs/${this.options.nvmeof.shareStrategySpdkCli.bdev.type} | ||||||
|  | create filename=/dev/${extentDiskName} name=${basename}:${assetName} ${bdevAttributesText} | ||||||
|  | 
 | ||||||
|  | # create subsystem | ||||||
|  | cd /nvmf/subsystem | ||||||
|  | create nqn=${basename}:${assetName} ${subsystemAttributesText} | ||||||
|  | cd ${basename}:${assetName} | ||||||
|  | 
 | ||||||
|  | # create namespace | ||||||
|  | cd /nvmf/subsystem/${basename}:${assetName}/namespaces | ||||||
|  | create bdev_name=${basename}:${assetName} nsid=${namespace} | ||||||
|  | 
 | ||||||
|  | # add listener | ||||||
|  | ${listenerCommands} | ||||||
|  | 
 | ||||||
|  | cd / | ||||||
|  | save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} | ||||||
|  | ` | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   retryCondition: (err) => { | ||||||
|  |                     if (err.stdout && err.stdout.includes("Ran out of input")) { | ||||||
|  |                       return true; | ||||||
|  |                     } | ||||||
|  |                     return false; | ||||||
|  |                   }, | ||||||
|  |                 } | ||||||
|  |               ); | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  |           default: | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // iqn = target
 | ||||||
|  |         let nqn = basename + ":" + assetName; | ||||||
|  |         this.ctx.logger.info("nqn: " + nqn); | ||||||
|  | 
 | ||||||
|  |         // store this off to make delete process more bullet proof
 | ||||||
|  |         await zb.zfs.set(datasetName, { | ||||||
|  |           [NVMEOF_ASSETS_NAME_PROPERTY_NAME]: assetName, | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         volume_context = { | ||||||
|  |           node_attach_driver: "nvmeof", | ||||||
|  |           transport: this.options.nvmeof.transport || "", | ||||||
|  |           transports: this.options.nvmeof.transports | ||||||
|  |             ? this.options.nvmeof.transports.join(",") | ||||||
|  |             : "", | ||||||
|  |           nqn, | ||||||
|  |           nsid: namespace, | ||||||
|  |         }; | ||||||
|  |         return volume_context; | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       default: |       default: | ||||||
|         throw new GrpcError( |         throw new GrpcError( | ||||||
|  | @ -367,9 +606,9 @@ create /backstores/block/${iscsiName} | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
| 
 | 
 | ||||||
|       case "zfs-generic-iscsi": |       case "zfs-generic-iscsi": { | ||||||
|         let basename; |         let basename; | ||||||
|         let iscsiName; |         let assetName; | ||||||
| 
 | 
 | ||||||
|         // Delete iscsi assets
 |         // Delete iscsi assets
 | ||||||
|         try { |         try { | ||||||
|  | @ -386,23 +625,23 @@ create /backstores/block/${iscsiName} | ||||||
|         properties = properties[datasetName]; |         properties = properties[datasetName]; | ||||||
|         this.ctx.logger.debug("zfs props data: %j", properties); |         this.ctx.logger.debug("zfs props data: %j", properties); | ||||||
| 
 | 
 | ||||||
|         iscsiName = properties[ISCSI_ASSETS_NAME_PROPERTY_NAME].value; |         assetName = properties[ISCSI_ASSETS_NAME_PROPERTY_NAME].value; | ||||||
| 
 | 
 | ||||||
|         if (zb.helpers.isPropertyValueSet(iscsiName)) { |         if (zb.helpers.isPropertyValueSet(assetName)) { | ||||||
|           //do nothing
 |           //do nothing
 | ||||||
|         } else { |         } else { | ||||||
|           iscsiName = zb.helpers.extractLeafName(datasetName); |           assetName = zb.helpers.extractLeafName(datasetName); | ||||||
| 
 | 
 | ||||||
|           if (this.options.iscsi.namePrefix) { |           if (this.options.iscsi.namePrefix) { | ||||||
|             iscsiName = this.options.iscsi.namePrefix + iscsiName; |             assetName = this.options.iscsi.namePrefix + assetName; | ||||||
|           } |           } | ||||||
| 
 | 
 | ||||||
|           if (this.options.iscsi.nameSuffix) { |           if (this.options.iscsi.nameSuffix) { | ||||||
|             iscsiName += this.options.iscsi.nameSuffix; |             assetName += this.options.iscsi.nameSuffix; | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         iscsiName = iscsiName.toLowerCase(); |         assetName = assetName.toLowerCase(); | ||||||
|         switch (this.options.iscsi.shareStrategy) { |         switch (this.options.iscsi.shareStrategy) { | ||||||
|           case "targetCli": |           case "targetCli": | ||||||
|             basename = this.options.iscsi.shareStrategyTargetCli.basename; |             basename = this.options.iscsi.shareStrategyTargetCli.basename; | ||||||
|  | @ -414,11 +653,11 @@ create /backstores/block/${iscsiName} | ||||||
|                   ` |                   ` | ||||||
| # delete target | # delete target | ||||||
| cd /iscsi | cd /iscsi | ||||||
| delete ${basename}:${iscsiName} | delete ${basename}:${assetName} | ||||||
| 
 | 
 | ||||||
| # delete extent | # delete extent | ||||||
| cd /backstores/block | cd /backstores/block | ||||||
| delete ${iscsiName} | delete ${assetName} | ||||||
| ` | ` | ||||||
|                 ); |                 ); | ||||||
|               }, |               }, | ||||||
|  | @ -437,6 +676,132 @@ delete ${iscsiName} | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|         break; |         break; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       case "zfs-generic-nvmeof": { | ||||||
|  |         let basename; | ||||||
|  |         let assetName; | ||||||
|  | 
 | ||||||
|  |         // Delete nvmeof assets
 | ||||||
|  |         try { | ||||||
|  |           properties = await zb.zfs.get(datasetName, [ | ||||||
|  |             NVMEOF_ASSETS_NAME_PROPERTY_NAME, | ||||||
|  |           ]); | ||||||
|  |         } catch (err) { | ||||||
|  |           if (err.toString().includes("dataset does not exist")) { | ||||||
|  |             return; | ||||||
|  |           } | ||||||
|  |           throw err; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         properties = properties[datasetName]; | ||||||
|  |         this.ctx.logger.debug("zfs props data: %j", properties); | ||||||
|  | 
 | ||||||
|  |         assetName = properties[NVMEOF_ASSETS_NAME_PROPERTY_NAME].value; | ||||||
|  | 
 | ||||||
|  |         if (zb.helpers.isPropertyValueSet(assetName)) { | ||||||
|  |           //do nothing
 | ||||||
|  |         } else { | ||||||
|  |           assetName = zb.helpers.extractLeafName(datasetName); | ||||||
|  | 
 | ||||||
|  |           if (this.options.nvmeof.namePrefix) { | ||||||
|  |             assetName = this.options.nvmeof.namePrefix + assetName; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           if (this.options.nvmeof.nameSuffix) { | ||||||
|  |             assetName += this.options.nvmeof.nameSuffix; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         assetName = assetName.toLowerCase(); | ||||||
|  |         switch (this.options.nvmeof.shareStrategy) { | ||||||
|  |           case "nvmetCli": | ||||||
|  |             { | ||||||
|  |               basename = this.options.nvmeof.shareStrategyNvmetCli.basename; | ||||||
|  |               let savefile = _.get( | ||||||
|  |                 this.options, | ||||||
|  |                 "nvmeof.shareStrategyNvmetCli.configPath", | ||||||
|  |                 "" | ||||||
|  |               ); | ||||||
|  |               if (savefile) { | ||||||
|  |                 savefile = `savefile=${savefile}`; | ||||||
|  |               } | ||||||
|  |               let portCommands = ""; | ||||||
|  |               this.options.nvmeof.shareStrategyNvmetCli.ports.forEach( | ||||||
|  |                 (port) => { | ||||||
|  |                   portCommands += ` | ||||||
|  | cd /ports/${port}/subsystems | ||||||
|  | delete ${basename}:${assetName} | ||||||
|  | `;
 | ||||||
|  |                 } | ||||||
|  |               ); | ||||||
|  |               await GeneralUtils.retry( | ||||||
|  |                 3, | ||||||
|  |                 2000, | ||||||
|  |                 async () => { | ||||||
|  |                   await this.nvmetCliCommand( | ||||||
|  |                     ` | ||||||
|  | # delete subsystem from port | ||||||
|  | ${portCommands} | ||||||
|  | 
 | ||||||
|  | # delete subsystem | ||||||
|  | cd /subsystems | ||||||
|  | delete ${basename}:${assetName} | ||||||
|  | 
 | ||||||
|  | saveconfig ${savefile} | ||||||
|  | ` | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   retryCondition: (err) => { | ||||||
|  |                     if (err.stdout && err.stdout.includes("Ran out of input")) { | ||||||
|  |                       return true; | ||||||
|  |                     } | ||||||
|  |                     return false; | ||||||
|  |                   }, | ||||||
|  |                 } | ||||||
|  |               ); | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  |           case "spdkCli": | ||||||
|  |             { | ||||||
|  |               basename = this.options.nvmeof.shareStrategySpdkCli.basename; | ||||||
|  |               await GeneralUtils.retry( | ||||||
|  |                 3, | ||||||
|  |                 2000, | ||||||
|  |                 async () => { | ||||||
|  |                   await this.spdkCliCommand( | ||||||
|  |                     ` | ||||||
|  | # delete subsystem | ||||||
|  | cd /nvmf/subsystem/ | ||||||
|  | delete subsystem_nqn=${basename}:${assetName} | ||||||
|  | 
 | ||||||
|  | # delete bdev | ||||||
|  | cd /bdevs/${this.options.nvmeof.shareStrategySpdkCli.bdev.type} | ||||||
|  | delete name=${basename}:${assetName} | ||||||
|  | 
 | ||||||
|  | cd / | ||||||
|  | save_config filename=${this.options.nvmeof.shareStrategySpdkCli.configPath} | ||||||
|  | ` | ||||||
|  |                   ); | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   retryCondition: (err) => { | ||||||
|  |                     if (err.stdout && err.stdout.includes("Ran out of input")) { | ||||||
|  |                       return true; | ||||||
|  |                     } | ||||||
|  |                     return false; | ||||||
|  |                   }, | ||||||
|  |                 } | ||||||
|  |               ); | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|  |           default: | ||||||
|  |             break; | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |       } | ||||||
| 
 | 
 | ||||||
|       default: |       default: | ||||||
|         throw new GrpcError( |         throw new GrpcError( | ||||||
|  | @ -477,18 +842,18 @@ delete ${iscsiName} | ||||||
|     let command = "sh"; |     let command = "sh"; | ||||||
|     let args = ["-c"]; |     let args = ["-c"]; | ||||||
| 
 | 
 | ||||||
|     let targetCliArgs = ["targetcli"]; |     let cliArgs = ["targetcli"]; | ||||||
|     if ( |     if ( | ||||||
|       _.get(this.options, "iscsi.shareStrategyTargetCli.sudoEnabled", false) |       _.get(this.options, "iscsi.shareStrategyTargetCli.sudoEnabled", false) | ||||||
|     ) { |     ) { | ||||||
|       targetCliArgs.unshift("sudo"); |       targetCliArgs.unshift("sudo"); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     let targetCliCommand = []; |     let cliCommand = []; | ||||||
|     targetCliCommand.push(`echo "${data}"`.trim()); |     cliCommand.push(`echo "${data}"`.trim()); | ||||||
|     targetCliCommand.push("|"); |     cliCommand.push("|"); | ||||||
|     targetCliCommand.push(targetCliArgs.join(" ")); |     cliCommand.push(cliArgs.join(" ")); | ||||||
|     args.push("'" + targetCliCommand.join(" ") + "'"); |     args.push("'" + cliCommand.join(" ") + "'"); | ||||||
| 
 | 
 | ||||||
|     let logCommandTmp = command + " " + args.join(" "); |     let logCommandTmp = command + " " + args.join(" "); | ||||||
|     let logCommand = ""; |     let logCommand = ""; | ||||||
|  | @ -527,6 +892,130 @@ delete ${iscsiName} | ||||||
|     } |     } | ||||||
|     return response; |     return response; | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   async nvmetCliCommand(data) { | ||||||
|  |     const execClient = this.getExecClient(); | ||||||
|  |     const driver = this; | ||||||
|  | 
 | ||||||
|  |     data = data.trim(); | ||||||
|  | 
 | ||||||
|  |     let command = "sh"; | ||||||
|  |     let args = ["-c"]; | ||||||
|  | 
 | ||||||
|  |     let cliArgs = [ | ||||||
|  |       _.get( | ||||||
|  |         this.options, | ||||||
|  |         "nvmeof.shareStrategyNvmetCli.nvmetcliPath", | ||||||
|  |         "nvmetcli" | ||||||
|  |       ), | ||||||
|  |     ]; | ||||||
|  |     if ( | ||||||
|  |       _.get(this.options, "nvmeof.shareStrategyNvmetCli.sudoEnabled", false) | ||||||
|  |     ) { | ||||||
|  |       cliArgs.unshift("sudo"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let cliCommand = []; | ||||||
|  |     cliCommand.push(`echo "${data}"`.trim()); | ||||||
|  |     cliCommand.push("|"); | ||||||
|  |     cliCommand.push(cliArgs.join(" ")); | ||||||
|  |     args.push("'" + cliCommand.join(" ") + "'"); | ||||||
|  | 
 | ||||||
|  |     let logCommandTmp = command + " " + args.join(" "); | ||||||
|  |     let logCommand = ""; | ||||||
|  | 
 | ||||||
|  |     logCommandTmp.split("\n").forEach((line) => { | ||||||
|  |       if (line.startsWith("set auth password=")) { | ||||||
|  |         logCommand += "set auth password=<redacted>"; | ||||||
|  |       } else if (line.startsWith("set auth mutual_password=")) { | ||||||
|  |         logCommand += "set auth mutual_password=<redacted>"; | ||||||
|  |       } else { | ||||||
|  |         logCommand += line; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       logCommand += "\n"; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     driver.ctx.logger.verbose("nvmetCLI command: " + logCommand); | ||||||
|  |     //process.exit(0);
 | ||||||
|  | 
 | ||||||
|  |     // https://github.com/democratic-csi/democratic-csi/issues/127
 | ||||||
|  |     // https://bugs.launchpad.net/ubuntu/+source/python-configshell-fb/+bug/1776761
 | ||||||
|  |     // can apply the linked patch with some modifications to overcome the
 | ||||||
|  |     // KeyErrors or we can simply start a fake tty which does not seem to have
 | ||||||
|  |     // a detrimental effect, only affects Ubuntu 18.04 and older
 | ||||||
|  |     let options = { | ||||||
|  |       pty: true, | ||||||
|  |     }; | ||||||
|  |     let response = await execClient.exec( | ||||||
|  |       execClient.buildCommand(command, args), | ||||||
|  |       options | ||||||
|  |     ); | ||||||
|  |     driver.ctx.logger.verbose("nvmetCLI response: " + JSON.stringify(response)); | ||||||
|  |     if (response.code != 0) { | ||||||
|  |       throw response; | ||||||
|  |     } | ||||||
|  |     return response; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async spdkCliCommand(data) { | ||||||
|  |     const execClient = this.getExecClient(); | ||||||
|  |     const driver = this; | ||||||
|  | 
 | ||||||
|  |     data = data.trim(); | ||||||
|  | 
 | ||||||
|  |     let command = "sh"; | ||||||
|  |     let args = ["-c"]; | ||||||
|  | 
 | ||||||
|  |     let cliArgs = [ | ||||||
|  |       _.get(this.options, "nvmeof.shareStrategySpdkCli.spdkcliPath", "spdkcli"), | ||||||
|  |     ]; | ||||||
|  |     if (_.get(this.options, "nvmeof.shareStrategySpdkCli.sudoEnabled", false)) { | ||||||
|  |       cliArgs.unshift("sudo"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let cliCommand = []; | ||||||
|  |     cliCommand.push(`echo "${data}"`.trim()); | ||||||
|  |     cliCommand.push("|"); | ||||||
|  |     cliCommand.push(cliArgs.join(" ")); | ||||||
|  |     args.push("'" + cliCommand.join(" ") + "'"); | ||||||
|  | 
 | ||||||
|  |     let logCommandTmp = command + " " + args.join(" "); | ||||||
|  |     let logCommand = ""; | ||||||
|  | 
 | ||||||
|  |     logCommandTmp.split("\n").forEach((line) => { | ||||||
|  |       if (line.startsWith("set auth password=")) { | ||||||
|  |         logCommand += "set auth password=<redacted>"; | ||||||
|  |       } else if (line.startsWith("set auth mutual_password=")) { | ||||||
|  |         logCommand += "set auth mutual_password=<redacted>"; | ||||||
|  |       } else { | ||||||
|  |         logCommand += line; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       logCommand += "\n"; | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     driver.ctx.logger.verbose("spdkCLI command: " + logCommand); | ||||||
|  |     //process.exit(0);
 | ||||||
|  | 
 | ||||||
|  |     // https://github.com/democratic-csi/democratic-csi/issues/127
 | ||||||
|  |     // https://bugs.launchpad.net/ubuntu/+source/python-configshell-fb/+bug/1776761
 | ||||||
|  |     // can apply the linked patch with some modifications to overcome the
 | ||||||
|  |     // KeyErrors or we can simply start a fake tty which does not seem to have
 | ||||||
|  |     // a detrimental effect, only affects Ubuntu 18.04 and older
 | ||||||
|  |     let options = { | ||||||
|  |       pty: true, | ||||||
|  |     }; | ||||||
|  |     let response = await execClient.exec( | ||||||
|  |       execClient.buildCommand(command, args), | ||||||
|  |       options | ||||||
|  |     ); | ||||||
|  |     driver.ctx.logger.verbose("spdkCLI response: " + JSON.stringify(response)); | ||||||
|  |     if (response.code != 0) { | ||||||
|  |       throw response; | ||||||
|  |     } | ||||||
|  |     return response; | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| module.exports.ControllerZfsGenericDriver = ControllerZfsGenericDriver; | module.exports.ControllerZfsGenericDriver = ControllerZfsGenericDriver; | ||||||
|  |  | ||||||
|  | @ -2,7 +2,8 @@ const _ = require("lodash"); | ||||||
| const { ControllerZfsBaseDriver } = require("../controller-zfs"); | const { ControllerZfsBaseDriver } = require("../controller-zfs"); | ||||||
| const { GrpcError, grpc } = require("../../utils/grpc"); | const { GrpcError, grpc } = require("../../utils/grpc"); | ||||||
| const GeneralUtils = require("../../utils/general"); | const GeneralUtils = require("../../utils/general"); | ||||||
| const LocalCliExecClient = require("./exec").LocalCliClient; | const LocalCliExecClient = | ||||||
|  |   require("../../utils/zfs_local_exec_client").LocalCliClient; | ||||||
| const registry = require("../../utils/registry"); | const registry = require("../../utils/registry"); | ||||||
| const { Zetabyte } = require("../../utils/zfs"); | const { Zetabyte } = require("../../utils/zfs"); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -35,6 +35,7 @@ function factory(ctx, options) { | ||||||
|     case "zfs-generic-nfs": |     case "zfs-generic-nfs": | ||||||
|     case "zfs-generic-smb": |     case "zfs-generic-smb": | ||||||
|     case "zfs-generic-iscsi": |     case "zfs-generic-iscsi": | ||||||
|  |     case "zfs-generic-nvmeof": | ||||||
|       return new ControllerZfsGenericDriver(ctx, options); |       return new ControllerZfsGenericDriver(ctx, options); | ||||||
|     case "zfs-local-dataset": |     case "zfs-local-dataset": | ||||||
|     case "zfs-local-zvol": |     case "zfs-local-zvol": | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ const _ = require("lodash"); | ||||||
| const { ControllerZfsBaseDriver } = require("../controller-zfs"); | const { ControllerZfsBaseDriver } = require("../controller-zfs"); | ||||||
| const { GrpcError, grpc } = require("../../utils/grpc"); | const { GrpcError, grpc } = require("../../utils/grpc"); | ||||||
| const registry = require("../../utils/registry"); | const registry = require("../../utils/registry"); | ||||||
| const SshClient = require("../../utils/ssh").SshClient; | const SshClient = require("../../utils/zfs_ssh_exec_client").SshClient; | ||||||
| const HttpClient = require("./http").Client; | const HttpClient = require("./http").Client; | ||||||
| const TrueNASApiClient = require("./http/api").Api; | const TrueNASApiClient = require("./http/api").Api; | ||||||
| const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); | const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ const { Mount } = require("../utils/mount"); | ||||||
| const { OneClient } = require("../utils/oneclient"); | const { OneClient } = require("../utils/oneclient"); | ||||||
| const { Filesystem } = require("../utils/filesystem"); | const { Filesystem } = require("../utils/filesystem"); | ||||||
| const { ISCSI } = require("../utils/iscsi"); | const { ISCSI } = require("../utils/iscsi"); | ||||||
|  | const { NVMEoF } = require("../utils/nvmeof"); | ||||||
| const registry = require("../utils/registry"); | const registry = require("../utils/registry"); | ||||||
| const semver = require("semver"); | const semver = require("semver"); | ||||||
| const GeneralUtils = require("../utils/general"); | const GeneralUtils = require("../utils/general"); | ||||||
|  | @ -139,6 +140,17 @@ class CsiBaseDriver { | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Get an instance of the NVMEoF class | ||||||
|  |    * | ||||||
|  |    * @returns NVMEoF | ||||||
|  |    */ | ||||||
|  |   getDefaultNVMEoFInstance() { | ||||||
|  |     return registry.get(`${__REGISTRY_NS__}:default_nvmeof_instance`, () => { | ||||||
|  |       return new NVMEoF(); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   getDefaultZetabyteInstance() { |   getDefaultZetabyteInstance() { | ||||||
|     return registry.get(`${__REGISTRY_NS__}:default_zb_instance`, () => { |     return registry.get(`${__REGISTRY_NS__}:default_zb_instance`, () => { | ||||||
|       return new Zetabyte({ |       return new Zetabyte({ | ||||||
|  | @ -560,6 +572,7 @@ class CsiBaseDriver { | ||||||
|     const mount = driver.getDefaultMountInstance(); |     const mount = driver.getDefaultMountInstance(); | ||||||
|     const filesystem = driver.getDefaultFilesystemInstance(); |     const filesystem = driver.getDefaultFilesystemInstance(); | ||||||
|     const iscsi = driver.getDefaultISCSIInstance(); |     const iscsi = driver.getDefaultISCSIInstance(); | ||||||
|  |     const nvmeof = driver.getDefaultNVMEoFInstance(); | ||||||
|     let result; |     let result; | ||||||
|     let device; |     let device; | ||||||
|     let block_device_info; |     let block_device_info; | ||||||
|  | @ -792,7 +805,11 @@ class CsiBaseDriver { | ||||||
|               await iscsi.iscsiadm.rescanSession(session); |               await iscsi.iscsiadm.rescanSession(session); | ||||||
| 
 | 
 | ||||||
|               // find device name
 |               // find device name
 | ||||||
|               device = iscsi.devicePathByPortalIQNLUN(iscsiConnection.portal, iscsiConnection.iqn, iscsiConnection.lun) |               device = iscsi.devicePathByPortalIQNLUN( | ||||||
|  |                 iscsiConnection.portal, | ||||||
|  |                 iscsiConnection.iqn, | ||||||
|  |                 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
 | ||||||
|  | @ -887,6 +904,233 @@ class CsiBaseDriver { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             break; |             break; | ||||||
|  | 
 | ||||||
|  |           case "nvmeof": | ||||||
|  |             { | ||||||
|  |               let transports = []; | ||||||
|  |               if (volume_context.transport) { | ||||||
|  |                 transports.push(volume_context.transport.trim()); | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               if (volume_context.transports) { | ||||||
|  |                 volume_context.transports.split(",").forEach((transport) => { | ||||||
|  |                   transports.push(transport.trim()); | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               // ensure unique entries only
 | ||||||
|  |               transports = [...new Set(transports)]; | ||||||
|  | 
 | ||||||
|  |               // stores actual device paths after nvmeof login
 | ||||||
|  |               let nvmeofControllerDevices = []; | ||||||
|  |               let nvmeofNamespaceDevices = []; | ||||||
|  | 
 | ||||||
|  |               // stores configuration of targets/iqn/luns to connect to
 | ||||||
|  |               let nvmeofConnections = []; | ||||||
|  |               for (let transport of transports) { | ||||||
|  |                 nvmeofConnections.push({ | ||||||
|  |                   transport, | ||||||
|  |                   nqn: volume_context.nqn, | ||||||
|  |                   nsid: volume_context.nsid, | ||||||
|  |                 }); | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               for (let nvmeofConnection of nvmeofConnections) { | ||||||
|  |                 // connect
 | ||||||
|  |                 try { | ||||||
|  |                   await GeneralUtils.retry(15, 2000, async () => { | ||||||
|  |                     await nvmeof.connectByNQNTransport( | ||||||
|  |                       nvmeofConnection.nqn, | ||||||
|  |                       nvmeofConnection.transport | ||||||
|  |                     ); | ||||||
|  |                   }); | ||||||
|  |                 } catch (err) { | ||||||
|  |                   driver.ctx.logger.warn( | ||||||
|  |                     `error: ${JSON.stringify(err)} connecting to transport: ${ | ||||||
|  |                       nvmeofConnection.transport | ||||||
|  |                     }` | ||||||
|  |                   ); | ||||||
|  |                   continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // find controller device
 | ||||||
|  |                 let controllerDevice; | ||||||
|  |                 try { | ||||||
|  |                   await GeneralUtils.retry(15, 2000, async () => { | ||||||
|  |                     controllerDevice = | ||||||
|  |                       await nvmeof.controllerDevicePathByTransportNQN( | ||||||
|  |                         nvmeofConnection.transport, | ||||||
|  |                         nvmeofConnection.nqn, | ||||||
|  |                         nvmeofConnection.nsid | ||||||
|  |                       ); | ||||||
|  | 
 | ||||||
|  |                     if (!controllerDevice) { | ||||||
|  |                       throw new Error(`failed to find controller device`); | ||||||
|  |                     } | ||||||
|  |                   }); | ||||||
|  |                 } catch (err) { | ||||||
|  |                   driver.ctx.logger.warn( | ||||||
|  |                     `error finding nvme controller device: ${JSON.stringify( | ||||||
|  |                       err | ||||||
|  |                     )}` | ||||||
|  |                   ); | ||||||
|  |                   continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // find namespace device
 | ||||||
|  |                 let namespaceDevice; | ||||||
|  |                 try { | ||||||
|  |                   await GeneralUtils.retry(15, 2000, async () => { | ||||||
|  |                     namespaceDevice = | ||||||
|  |                       await nvmeof.namespaceDevicePathByNQNNamespace( | ||||||
|  |                         nvmeofConnection.nqn, | ||||||
|  |                         nvmeofConnection.nsid | ||||||
|  |                       ); | ||||||
|  |                     if (!controllerDevice) { | ||||||
|  |                       throw new Error(`failed to find namespace device`); | ||||||
|  |                     } | ||||||
|  |                   }); | ||||||
|  |                 } catch (err) { | ||||||
|  |                   driver.ctx.logger.warn( | ||||||
|  |                     `error finding nvme namespace device: ${JSON.stringify( | ||||||
|  |                       err | ||||||
|  |                     )}` | ||||||
|  |                   ); | ||||||
|  |                   continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // sanity check for device files
 | ||||||
|  |                 if (!namespaceDevice) { | ||||||
|  |                   continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // sanity check for device files
 | ||||||
|  |                 if (!controllerDevice) { | ||||||
|  |                   continue; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // rescan in scenarios when login previously occurred but volumes never appeared
 | ||||||
|  |                 // must be the NVMe char device, not the namespace device
 | ||||||
|  |                 await nvmeof.rescanNamespace(controllerDevice); | ||||||
|  | 
 | ||||||
|  |                 // can take some time for device to show up, loop for some period
 | ||||||
|  |                 result = await filesystem.pathExists(namespaceDevice); | ||||||
|  |                 let timer_start = Math.round(new Date().getTime() / 1000); | ||||||
|  |                 let timer_max = 30; | ||||||
|  |                 let deviceCreated = result; | ||||||
|  |                 while (!result) { | ||||||
|  |                   await GeneralUtils.sleep(2000); | ||||||
|  |                   result = await filesystem.pathExists(namespaceDevice); | ||||||
|  | 
 | ||||||
|  |                   if (result) { | ||||||
|  |                     deviceCreated = true; | ||||||
|  |                     break; | ||||||
|  |                   } | ||||||
|  | 
 | ||||||
|  |                   let current_time = Math.round(new Date().getTime() / 1000); | ||||||
|  |                   if (!result && current_time - timer_start > timer_max) { | ||||||
|  |                     driver.ctx.logger.warn( | ||||||
|  |                       `hit timeout waiting for namespace device node to appear: ${namespaceDevice}` | ||||||
|  |                     ); | ||||||
|  |                     break; | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (deviceCreated) { | ||||||
|  |                   device = await filesystem.realpath(namespaceDevice); | ||||||
|  |                   nvmeofControllerDevices.push(controllerDevice); | ||||||
|  |                   nvmeofNamespaceDevices.push(namespaceDevice); | ||||||
|  | 
 | ||||||
|  |                   driver.ctx.logger.info( | ||||||
|  |                     `successfully logged into nvmeof transport ${nvmeofConnection.transport} and created controller device: ${controllerDevice}, namespace device: ${namespaceDevice}` | ||||||
|  |                   ); | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               // let things settle
 | ||||||
|  |               // this will help in dm scenarios
 | ||||||
|  |               await GeneralUtils.sleep(2000); | ||||||
|  | 
 | ||||||
|  |               // filter duplicates
 | ||||||
|  |               nvmeofNamespaceDevices = nvmeofNamespaceDevices.filter( | ||||||
|  |                 (value, index, self) => { | ||||||
|  |                   return self.indexOf(value) === index; | ||||||
|  |                 } | ||||||
|  |               ); | ||||||
|  | 
 | ||||||
|  |               nvmeofControllerDevices = nvmeofControllerDevices.filter( | ||||||
|  |                 (value, index, self) => { | ||||||
|  |                   return self.indexOf(value) === index; | ||||||
|  |                 } | ||||||
|  |               ); | ||||||
|  | 
 | ||||||
|  |               // only throw an error if we were not able to attach to *any* devices
 | ||||||
|  |               if (nvmeofNamespaceDevices.length < 1) { | ||||||
|  |                 throw new GrpcError( | ||||||
|  |                   grpc.status.UNKNOWN, | ||||||
|  |                   `unable to attach any nvme devices` | ||||||
|  |                 ); | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               if (nvmeofControllerDevices.length != nvmeofConnections.length) { | ||||||
|  |                 driver.ctx.logger.warn( | ||||||
|  |                   `failed to attach all nvmeof devices/subsystems/transports` | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 // TODO: allow a parameter to control this behavior in some form
 | ||||||
|  |                 if (false) { | ||||||
|  |                   throw new GrpcError( | ||||||
|  |                     grpc.status.UNKNOWN, | ||||||
|  |                     `unable to attach all iscsi devices` | ||||||
|  |                   ); | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  | 
 | ||||||
|  |               /** | ||||||
|  |                * NVMEoF has native multipath capabilities without using device mapper | ||||||
|  |                * You can disable the built-in using kernel param nvme_core.multipath=N/Y | ||||||
|  |                */ | ||||||
|  |               let useNativeMultipath = await nvmeof.nativeMultipathEnabled(); | ||||||
|  | 
 | ||||||
|  |               if (useNativeMultipath) { | ||||||
|  |                 // only throw an error if we were not able to attach to *any* devices
 | ||||||
|  |                 if (nvmeofNamespaceDevices.length > 1) { | ||||||
|  |                   throw new GrpcError( | ||||||
|  |                     grpc.status.UNKNOWN, | ||||||
|  |                     `too many nvme namespace devices, native multipath enabled therefore should only have 1` | ||||||
|  |                   ); | ||||||
|  |                 } | ||||||
|  |               } else { | ||||||
|  |                 // compare all device-mapper slaves with the newly created devices
 | ||||||
|  |                 // if any of the new devices are device-mapper slaves treat this as a
 | ||||||
|  |                 // multipath scenario
 | ||||||
|  |                 let allDeviceMapperSlaves = | ||||||
|  |                   await filesystem.getAllDeviceMapperSlaveDevices(); | ||||||
|  |                 let commonDevices = allDeviceMapperSlaves.filter((value) => | ||||||
|  |                   nvmeofNamespaceDevices.includes(value) | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 const useDMMultipath = | ||||||
|  |                   nvmeofConnections.length > 1 || commonDevices.length > 0; | ||||||
|  | 
 | ||||||
|  |                 // discover multipath device to use
 | ||||||
|  |                 if (useDMMultipath) { | ||||||
|  |                   device = await filesystem.getDeviceMapperDeviceFromSlaves( | ||||||
|  |                     nvmeofNamespaceDevices, | ||||||
|  |                     false | ||||||
|  |                   ); | ||||||
|  | 
 | ||||||
|  |                   if (!device) { | ||||||
|  |                     throw new GrpcError( | ||||||
|  |                       grpc.status.UNKNOWN, | ||||||
|  |                       `failed to discover multipath device` | ||||||
|  |                     ); | ||||||
|  |                   } | ||||||
|  |                 } | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |             break; | ||||||
|  | 
 | ||||||
|           case "hostpath": |           case "hostpath": | ||||||
|             result = await mount.pathIsMounted(staging_target_path); |             result = await mount.pathIsMounted(staging_target_path); | ||||||
|             // if not mounted, mount
 |             // if not mounted, mount
 | ||||||
|  | @ -989,6 +1233,7 @@ class CsiBaseDriver { | ||||||
|             let is_block = false; |             let is_block = false; | ||||||
|             switch (node_attach_driver) { |             switch (node_attach_driver) { | ||||||
|               case "iscsi": |               case "iscsi": | ||||||
|  |               case "nvmeof": | ||||||
|                 is_block = true; |                 is_block = true; | ||||||
|                 break; |                 break; | ||||||
|               case "zfs-local": |               case "zfs-local": | ||||||
|  | @ -1093,6 +1338,7 @@ class CsiBaseDriver { | ||||||
|                   fs_type = "cifs"; |                   fs_type = "cifs"; | ||||||
|                   break; |                   break; | ||||||
|                 case "iscsi": |                 case "iscsi": | ||||||
|  |                 case "nvmeof": | ||||||
|                   fs_type = "ext4"; |                   fs_type = "ext4"; | ||||||
|                   break; |                   break; | ||||||
|                 default: |                 default: | ||||||
|  | @ -1988,6 +2234,7 @@ class CsiBaseDriver { | ||||||
|     const mount = driver.getDefaultMountInstance(); |     const mount = driver.getDefaultMountInstance(); | ||||||
|     const filesystem = driver.getDefaultFilesystemInstance(); |     const filesystem = driver.getDefaultFilesystemInstance(); | ||||||
|     const iscsi = driver.getDefaultISCSIInstance(); |     const iscsi = driver.getDefaultISCSIInstance(); | ||||||
|  |     const nvmeof = driver.getDefaultNVMEoFInstance(); | ||||||
|     let result; |     let result; | ||||||
|     let is_block = false; |     let is_block = false; | ||||||
|     let is_device_mapper = false; |     let is_device_mapper = false; | ||||||
|  | @ -2211,6 +2458,13 @@ class CsiBaseDriver { | ||||||
|                 } |                 } | ||||||
|               } |               } | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             if (await filesystem.deviceIsNVMEoF(block_device_info_i.path)) { | ||||||
|  |               let nqn = await nvmeof.nqnByNamespaceDeviceName( | ||||||
|  |                 block_device_info_i.name | ||||||
|  |               ); | ||||||
|  |               await nvmeof.disconnectByNQN(nqn); | ||||||
|  |             } | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -129,6 +129,7 @@ class NodeManualDriver extends CsiBaseDriver { | ||||||
|         driverResourceType = "filesystem"; |         driverResourceType = "filesystem"; | ||||||
|         break; |         break; | ||||||
|       case "iscsi": |       case "iscsi": | ||||||
|  |       case "nvmeof": | ||||||
|         driverResourceType = "volume"; |         driverResourceType = "volume"; | ||||||
|         fs_types = ["btrfs", "ext3", "ext4", "ext4dev", "xfs"]; |         fs_types = ["btrfs", "ext3", "ext4", "ext4dev", "xfs"]; | ||||||
|         break; |         break; | ||||||
|  |  | ||||||
|  | @ -4,7 +4,7 @@ const { GrpcError, grpc } = require("../../utils/grpc"); | ||||||
| const { Filesystem } = require("../../utils/filesystem"); | const { Filesystem } = require("../../utils/filesystem"); | ||||||
| const registry = require("../../utils/registry"); | const registry = require("../../utils/registry"); | ||||||
| const semver = require("semver"); | const semver = require("semver"); | ||||||
| const SshClient = require("../../utils/ssh").SshClient; | const SshClient = require("../../utils/zfs_ssh_exec_client").SshClient; | ||||||
| const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); | const { Zetabyte, ZfsSshProcessManager } = require("../../utils/zfs"); | ||||||
| 
 | 
 | ||||||
| // zfs common properties
 | // zfs common properties
 | ||||||
|  |  | ||||||
|  | @ -504,7 +504,8 @@ class Filesystem { | ||||||
|        * lsblk |        * lsblk | ||||||
|        * blkid |        * blkid | ||||||
|        */ |        */ | ||||||
|       const strategy = process.env.FILESYSTEM_TYPE_DETECTION_STRATEGY || "lsblk"; |       const strategy = | ||||||
|  |         process.env.FILESYSTEM_TYPE_DETECTION_STRATEGY || "lsblk"; | ||||||
| 
 | 
 | ||||||
|       switch (strategy) { |       switch (strategy) { | ||||||
|         // requires udev data to be present otherwise fstype property is always null but otherwise succeeds
 |         // requires udev data to be present otherwise fstype property is always null but otherwise succeeds
 | ||||||
|  | @ -547,6 +548,21 @@ class Filesystem { | ||||||
|     return result && result.tran == "iscsi"; |     return result && result.tran == "iscsi"; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   async deviceIsNVMEoF(device) { | ||||||
|  |     const filesystem = this; | ||||||
|  |     let result; | ||||||
|  | 
 | ||||||
|  |     do { | ||||||
|  |       if (result) { | ||||||
|  |         device = `/dev/${result.pkname}`; | ||||||
|  |       } | ||||||
|  |       result = await filesystem.getBlockDevice(device); | ||||||
|  |     } while (result.pkname); | ||||||
|  | 
 | ||||||
|  |     // TODO: add further logic here to ensure the device is not a local pcie/etc device
 | ||||||
|  |     return result && result.tran == "nvme"; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   async getBlockDeviceParent(device) { |   async getBlockDeviceParent(device) { | ||||||
|     const filesystem = this; |     const filesystem = this; | ||||||
|     let result; |     let result; | ||||||
|  |  | ||||||
|  | @ -8,6 +8,17 @@ function sleep(ms) { | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | function trimchar(str, ch) { | ||||||
|  |   var start = 0, | ||||||
|  |     end = str.length; | ||||||
|  | 
 | ||||||
|  |   while (start < end && str[start] === ch) ++start; | ||||||
|  | 
 | ||||||
|  |   while (end > start && str[end - 1] === ch) --end; | ||||||
|  | 
 | ||||||
|  |   return start > 0 || end < str.length ? str.substring(start, end) : str; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| function md5(val) { | function md5(val) { | ||||||
|   return crypto.createHash("md5").update(val).digest("hex"); |   return crypto.createHash("md5").update(val).digest("hex"); | ||||||
| } | } | ||||||
|  | @ -265,3 +276,4 @@ module.exports.default_supported_block_filesystems = | ||||||
| module.exports.default_supported_file_filesystems = | module.exports.default_supported_file_filesystems = | ||||||
|   default_supported_file_filesystems; |   default_supported_file_filesystems; | ||||||
| module.exports.retry = retry; | module.exports.retry = retry; | ||||||
|  | module.exports.trimchar = trimchar; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,321 @@ | ||||||
|  | const cp = require("child_process"); | ||||||
|  | const { trimchar } = require("./general"); | ||||||
|  | const URI = require("uri-js"); | ||||||
|  | const { deleteItems } = require("@kubernetes/client-node"); | ||||||
|  | 
 | ||||||
|  | const DEFAULT_TIMEOUT = process.env.NVMEOF_DEFAULT_TIMEOUT || 30000; | ||||||
|  | 
 | ||||||
|  | class NVMEoF { | ||||||
|  |   constructor(options = {}) { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     nvmeof.options = options; | ||||||
|  | 
 | ||||||
|  |     options.paths = options.paths || {}; | ||||||
|  |     if (!options.paths.nvme) { | ||||||
|  |       options.paths.nvme = "nvme"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!options.paths.sudo) { | ||||||
|  |       options.paths.sudo = "/usr/bin/sudo"; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!options.executor) { | ||||||
|  |       options.executor = { | ||||||
|  |         spawn: cp.spawn, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * List all NVMe devices and namespaces on machine | ||||||
|  |    * | ||||||
|  |    * @param {*} args | ||||||
|  |    */ | ||||||
|  |   async list(args = []) { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     args.unshift("list", "-o", "json"); | ||||||
|  |     let result = await nvmeof.exec(nvmeof.options.paths.nvme, args); | ||||||
|  |     return result.parsed; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * List nvme subsystems | ||||||
|  |    * | ||||||
|  |    * @param {*} args | ||||||
|  |    */ | ||||||
|  |   async listSubsys(args = []) { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     args.unshift("list-subsys", "-o", "json"); | ||||||
|  |     await nvmeof.exec(nvmeof.options.paths.nvme, args); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Connect to NVMeoF subsystem | ||||||
|  |    * | ||||||
|  |    * @param {*} args | ||||||
|  |    */ | ||||||
|  |   async connectByNQNTransport(nqn, transport, args = []) { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     transport = nvmeof.parseTransport(transport); | ||||||
|  | 
 | ||||||
|  |     let transport_args = []; | ||||||
|  |     if (transport.type) { | ||||||
|  |       transport_args.push("--transport", transport.type); | ||||||
|  |     } | ||||||
|  |     if (transport.address) { | ||||||
|  |       transport_args.push("--traddr", transport.address); | ||||||
|  |     } | ||||||
|  |     if (transport.service) { | ||||||
|  |       transport_args.push("--trsvcid", transport.service); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     args.unshift("connect", "-o", "json", "--nqn", nqn, ...transport_args); | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       await nvmeof.exec(nvmeof.options.paths.nvme, args); | ||||||
|  |     } catch (err) { | ||||||
|  |       if (err.stderr && err.stderr.includes("already connnected")) { | ||||||
|  |         // idempotent
 | ||||||
|  |       } else { | ||||||
|  |         throw err; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Disconnect from NVMeoF subsystem | ||||||
|  |    * | ||||||
|  |    * @param {*} args | ||||||
|  |    */ | ||||||
|  |   async disconnectByNQN(nqn, args = []) { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     args.unshift("disconnect", "--nqn", nqn); | ||||||
|  |     await nvmeof.exec(nvmeof.options.paths.nvme, args); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Disconnect from NVMeoF subsystem | ||||||
|  |    * | ||||||
|  |    * @param {*} args | ||||||
|  |    */ | ||||||
|  |   async disconnectByDevice(device, args = []) { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     args.unshift("disconnect", "--device", device); | ||||||
|  |     await nvmeof.exec(nvmeof.options.paths.nvme, args); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Rescans the NVME namespaces | ||||||
|  |    *  | ||||||
|  |    * @param {*} device  | ||||||
|  |    * @param {*} args  | ||||||
|  |    */ | ||||||
|  |   async rescanNamespace(device, args = []) { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     args.unshift("ns-rescan", device); | ||||||
|  |     await nvmeof.exec(nvmeof.options.paths.nvme, args); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   parseTransport(transport) { | ||||||
|  |     if (typeof transport === "object") { | ||||||
|  |       return transport; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     transport = transport.trim(); | ||||||
|  |     const parsed = URI.parse(transport); | ||||||
|  | 
 | ||||||
|  |     let type = parsed.scheme; | ||||||
|  |     let address = parsed.host; | ||||||
|  |     let service; | ||||||
|  |     switch (parsed.scheme) { | ||||||
|  |       case "fc": | ||||||
|  |       case "rdma": | ||||||
|  |       case "tcp": | ||||||
|  |         type = parsed.scheme; | ||||||
|  |         break; | ||||||
|  |       default: | ||||||
|  |         throw new Error(`unknown nvme transport type: ${parsed.scheme}`); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     switch (type) { | ||||||
|  |       case "fc": | ||||||
|  |         address = trimchar(address, "["); | ||||||
|  |         address = trimchar(address, "]"); | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     switch (type) { | ||||||
|  |       case "rdma": | ||||||
|  |       case "tcp": | ||||||
|  |         service = parsed.port; | ||||||
|  | 
 | ||||||
|  |         if (!service) { | ||||||
|  |           service = 4420; | ||||||
|  |         } | ||||||
|  |         break; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       type, | ||||||
|  |       address, | ||||||
|  |       service, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async nativeMultipathEnabled() { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     let result = await nvmeof.exec("cat", [ | ||||||
|  |       "/sys/module/nvme_core/parameters/multipath", | ||||||
|  |     ]); | ||||||
|  |     return result.stdout.trim() == "Y"; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async namespaceDevicePathByNQNNamespace(nqn, namespace) { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     let result = await nvmeof.list(["-v"]); | ||||||
|  |     for (let device of result.Devices) { | ||||||
|  |       for (let subsytem of device.Subsystems) { | ||||||
|  |         if (subsytem.SubsystemNQN != nqn) { | ||||||
|  |           continue; | ||||||
|  |         } else { | ||||||
|  |           for (let i_namespace of subsytem.Namespaces) { | ||||||
|  |             if (i_namespace.NSID != namespace) { | ||||||
|  |               continue; | ||||||
|  |             } else { | ||||||
|  |               return `/dev/${i_namespace.NameSpace}`; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async controllerDevicePathByTransportNQN(transport, nqn) { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     transport = nvmeof.parseTransport(transport); | ||||||
|  |     let result = await nvmeof.list(["-v"]); | ||||||
|  |     for (let device of result.Devices) { | ||||||
|  |       for (let subsytem of device.Subsystems) { | ||||||
|  |         if (subsytem.SubsystemNQN != nqn) { | ||||||
|  |           continue; | ||||||
|  |         } else { | ||||||
|  |           for (let controller of subsytem.Controllers) { | ||||||
|  |             if (controller.Transport != transport.type) { | ||||||
|  |               continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             let controllerAddress = controller.Address; | ||||||
|  |             let parts = controllerAddress.split(","); | ||||||
|  | 
 | ||||||
|  |             let traddr; | ||||||
|  |             let trsvcid; | ||||||
|  |             for (let i_part of parts) { | ||||||
|  |               let i_parts = i_part.split("="); | ||||||
|  |               switch (i_parts[0]) { | ||||||
|  |                 case "traddr": | ||||||
|  |                   traddr = i_parts[1]; | ||||||
|  |                   break; | ||||||
|  |                 case "trsvcid": | ||||||
|  |                   trsvcid = i_parts[1]; | ||||||
|  |                   break; | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (traddr != transport.address) { | ||||||
|  |               continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (transport.service && trsvcid != transport.service) { | ||||||
|  |               continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return `/dev/${controller.Controller}`; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async nqnByNamespaceDeviceName(name) { | ||||||
|  |     const nvmeof = this; | ||||||
|  |     name = name.replace("/dev/", ""); | ||||||
|  |     let result = await nvmeof.list(["-v"]); | ||||||
|  |     for (let device of result.Devices) { | ||||||
|  |       for (let subsytem of device.Subsystems) { | ||||||
|  |         for (let namespace of subsytem.Namespaces) { | ||||||
|  |           if (namespace.NameSpace != name) { | ||||||
|  |             continue; | ||||||
|  |           } else { | ||||||
|  |             return subsytem.SubsystemNQN; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   devicePathByModelNumberSerialNumber(modelNumber, serialNumber) { | ||||||
|  |     modelNumber = modelNumber.replaceAll(" ", "_"); | ||||||
|  |     serialNumber = serialNumber.replaceAll(" ", "_"); | ||||||
|  |     return `/dev/disk/by-id/nvme-${modelNumber}_${serialNumber}`; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   devicePathByPortalIQNLUN(portal, iqn, lun) { | ||||||
|  |     const parsedPortal = this.parsePortal(portal); | ||||||
|  |     const portalHost = parsedPortal.host | ||||||
|  |       .replaceAll("[", "") | ||||||
|  |       .replaceAll("]", ""); | ||||||
|  |     return `/dev/disk/by-path/ip-${portalHost}:${parsedPortal.port}-iscsi-${iqn}-lun-${lun}`; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   exec(command, args, options = {}) { | ||||||
|  |     if (!options.hasOwnProperty("timeout")) { | ||||||
|  |       options.timeout = DEFAULT_TIMEOUT; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const nvmeof = this; | ||||||
|  |     args = args || []; | ||||||
|  | 
 | ||||||
|  |     if (nvmeof.options.sudo) { | ||||||
|  |       args.unshift(command); | ||||||
|  |       command = nvmeof.options.paths.sudo; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     console.log("executing nvmeof command: %s %s", command, args.join(" ")); | ||||||
|  | 
 | ||||||
|  |     return new Promise((resolve, reject) => { | ||||||
|  |       const child = nvmeof.options.executor.spawn(command, args, options); | ||||||
|  | 
 | ||||||
|  |       let stdout = ""; | ||||||
|  |       let stderr = ""; | ||||||
|  | 
 | ||||||
|  |       child.stdout.on("data", function (data) { | ||||||
|  |         stdout = stdout + data; | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       child.stderr.on("data", function (data) { | ||||||
|  |         stderr = stderr + data; | ||||||
|  |       }); | ||||||
|  | 
 | ||||||
|  |       child.on("close", function (code) { | ||||||
|  |         const result = { code, stdout, stderr, timeout: false }; | ||||||
|  |         try { | ||||||
|  |           result.parsed = JSON.parse(result.stdout); | ||||||
|  |         } catch (err) {} | ||||||
|  | 
 | ||||||
|  |         // timeout scenario
 | ||||||
|  |         if (code === null) { | ||||||
|  |           result.timeout = true; | ||||||
|  |           reject(result); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (code) { | ||||||
|  |           reject(result); | ||||||
|  |         } else { | ||||||
|  |           resolve(result); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | module.exports.NVMEoF = NVMEoF; | ||||||
		Loading…
	
		Reference in New Issue