580 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			580 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| const cp = require("child_process");
 | |
| const { hostname_lookup, trimchar } = require("./general");
 | |
| const URI = require("uri-js");
 | |
| const querystring = require("querystring");
 | |
| 
 | |
| 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,
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     if (nvmeof.options.logger) {
 | |
|       nvmeof.logger = nvmeof.options.logger;
 | |
|     } else {
 | |
|       nvmeof.logger = console;
 | |
|       console.verbose = function() {
 | |
|         console.log(...arguments);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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");
 | |
|     let result = await nvmeof.exec(nvmeof.options.paths.nvme, args);
 | |
|     return result.parsed;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Discover NVMeoF subsystems
 | |
|    *
 | |
|    * @param {*} transport
 | |
|    * @param {*} args
 | |
|    * @returns
 | |
|    */
 | |
|   async discover(transport, args = []) {
 | |
|     const nvmeof = this;
 | |
|     transport = await 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("discover", "-o", "json", ...transport_args);
 | |
|     let result = await nvmeof.exec(nvmeof.options.paths.nvme, args);
 | |
|     return result.parsed;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Connect to NVMeoF subsystem
 | |
|    *
 | |
|    * @param {*} args
 | |
|    */
 | |
|   async connectByNQNTransport(nqn, transport, args = []) {
 | |
|     const nvmeof = this;
 | |
|     transport = await 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);
 | |
|     }
 | |
| 
 | |
|     if (transport.args) {
 | |
|       for (let arg in transport.args) {
 | |
|         let value = transport.args[arg];
 | |
|         if (!arg.startsWith("-")) {
 | |
|           arg = `--${arg}`;
 | |
|         }
 | |
|   
 | |
|         transport_args.push(arg, value);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     args.unshift("connect", "--nqn", nqn, ...transport_args);
 | |
| 
 | |
|     try {
 | |
|       await nvmeof.exec(nvmeof.options.paths.nvme, args);
 | |
|     } catch (err) {
 | |
|       if (
 | |
|         err.stderr &&
 | |
|         (err.stderr.includes("already connected") ||
 | |
|           err.stderr.includes("Operation already in progress"))
 | |
|       ) {
 | |
|         // 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);
 | |
|   }
 | |
| 
 | |
|   async deviceIsNamespaceDevice(device) {
 | |
|     const nvmeof = this;
 | |
|     device = device.replace("/dev/", "");
 | |
|     const subsystems = await nvmeof.getSubsystems();
 | |
|     for (let subsystem of subsystems) {
 | |
|       // check subsystem namespaces
 | |
|       if (subsystem.Namespaces) {
 | |
|         for (let namespace of subsystem.Namespaces) {
 | |
|           if (namespace.NameSpace == device) {
 | |
|             return true;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       // check controller namespaces
 | |
|       if (subsystem.Controllers) {
 | |
|         for (let controller of subsystem.Controllers) {
 | |
|           if (controller.Namespaces) {
 | |
|             for (let namespace of controller.Namespaces) {
 | |
|               if (namespace.NameSpace == device) {
 | |
|                 return true;
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   async deviceIsControllerDevice(device) {
 | |
|     const nvmeof = this;
 | |
|     device = device.replace("/dev/", "");
 | |
|     const subsystems = await nvmeof.getSubsystems();
 | |
|     for (let subsystem of subsystems) {
 | |
|       if (subsystem.Controllers) {
 | |
|         for (let controller of subsystem.Controllers) {
 | |
|           if (controller.Controller == device) {
 | |
|             return true;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   async parseTransport(transport) {
 | |
|     if (typeof transport === "object") {
 | |
|       return transport;
 | |
|     }
 | |
| 
 | |
|     transport = transport.trim();
 | |
|     const parsed = URI.parse(transport);
 | |
|     let args = querystring.parse(parsed.query);
 | |
| 
 | |
|     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;
 | |
|       case "tcp":
 | |
|         /**
 | |
|          * kernel stores value as ip, so if address passed as hostname then
 | |
|          * translate to ip address
 | |
|          *
 | |
|          * TODO: this could be brittle
 | |
|          */
 | |
|         let lookup = await hostname_lookup(address);
 | |
|         if (lookup) {
 | |
|           address = lookup;
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
| 
 | |
|     switch (type) {
 | |
|       case "rdma":
 | |
|       case "tcp":
 | |
|         service = parsed.port;
 | |
| 
 | |
|         if (!service) {
 | |
|           service = 4420;
 | |
|         }
 | |
|         break;
 | |
|     }
 | |
| 
 | |
|     return {
 | |
|       type,
 | |
|       address,
 | |
|       service,
 | |
|       args,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   async pathExists(path) {
 | |
|     const nvmeof = this;
 | |
|     try {
 | |
|       await nvmeof.exec("stat", [
 | |
|         path,
 | |
|       ]);
 | |
|       return true;
 | |
|     } catch (err) {
 | |
|       return false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async nativeMultipathEnabled() {
 | |
|     const nvmeof = this;
 | |
|     let result;
 | |
| 
 | |
|     try {
 | |
|       result = await nvmeof.exec("cat", [
 | |
|         "/sys/module/nvme_core/parameters/multipath",
 | |
|       ]);
 | |
|     } catch (err) {
 | |
|       if (err.code == 1 && err.stderr.includes("No such file or directory")) {
 | |
|         return false;
 | |
|       }
 | |
|       throw err;
 | |
|     }
 | |
|     
 | |
|     return result.stdout.trim() == "Y";
 | |
|   }
 | |
| 
 | |
|   async namespaceDevicePathByTransportNQNNamespace(transport, nqn, namespace) {
 | |
|     const nvmeof = this;
 | |
|     transport = await nvmeof.parseTransport(transport);
 | |
|     let nativeMultipathEnabled = await nvmeof.nativeMultipathEnabled();
 | |
| 
 | |
|     if (nativeMultipathEnabled) {
 | |
|       let subsystem = await nvmeof.getSubsystemByNQN(nqn);
 | |
|       if (subsystem) {
 | |
|         for (let i_namespace of subsystem.Namespaces) {
 | |
|           if (i_namespace.NSID != namespace) {
 | |
|             continue;
 | |
|           } else {
 | |
|             return `/dev/${i_namespace.NameSpace}`;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     } else {
 | |
|       let controller = await nvmeof.getControllerByTransportNQN(transport, nqn);
 | |
|       if (controller) {
 | |
|         for (let i_namespace of controller.Namespaces) {
 | |
|           if (i_namespace.NSID != namespace) {
 | |
|             continue;
 | |
|           } else {
 | |
|             return `/dev/${i_namespace.NameSpace}`;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async controllerDevicePathByTransportNQN(transport, nqn) {
 | |
|     const nvmeof = this;
 | |
|     transport = await nvmeof.parseTransport(transport);
 | |
|     let controller = await nvmeof.getControllerByTransportNQN(transport, nqn);
 | |
|     if (controller) {
 | |
|       return `/dev/${controller.Controller}`;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   async getSubsystems() {
 | |
|     const nvmeof = this;
 | |
|     let result = await nvmeof.list(["-v"]);
 | |
| 
 | |
|     return nvmeof.getNormalizedSubsystems(result);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * used to normalize subsystem list/response across different versions of nvme-cli
 | |
|    *
 | |
|    * @param {*} result
 | |
|    * @returns
 | |
|    */
 | |
|   async getNormalizedSubsystems(result) {
 | |
|     let subsystems = [];
 | |
| 
 | |
|     for (let device of result.Devices) {
 | |
|       if (Array.isArray(device.Subsystems)) {
 | |
|         subsystems = subsystems.concat(device.Subsystems);
 | |
|       } else if (device.Subsystem) {
 | |
|         // nvme-cli 1.x support
 | |
|         subsystems.push(device);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return subsystems;
 | |
|   }
 | |
| 
 | |
|   async getSubsystemByNQN(nqn) {
 | |
|     const nvmeof = this;
 | |
|     const subsystems = await nvmeof.getSubsystems();
 | |
|     for (let subsystem of subsystems) {
 | |
|       if (subsystem.SubsystemNQN == nqn) {
 | |
|         return subsystem;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     nvmeof.logger.warn(`failed to find subsystem for nqn: ${nqn}`);
 | |
|   }
 | |
| 
 | |
|   async getControllersByNamespaceDeviceName(name) {
 | |
|     const nvmeof = this;
 | |
|     name = name.replace("/dev/", "");
 | |
|     let nativeMultipathEnabled = await nvmeof.nativeMultipathEnabled();
 | |
|     const subsystems = await nvmeof.getSubsystems();
 | |
| 
 | |
|     if (nativeMultipathEnabled) {
 | |
|       // using per-subsystem namespace
 | |
|       for (let subsystem of subsystems) {
 | |
|         if (subsystem.Namespaces) {
 | |
|           for (let namespace of subsystem.Namespaces) {
 | |
|             if (namespace.NameSpace == name) {
 | |
|               return subsystem.Controllers;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     } else {
 | |
|       // using per-controller namespace
 | |
|       for (let subsystem of subsystems) {
 | |
|         if (subsystem.Controllers) {
 | |
|           for (let controller of subsystem.Controllers) {
 | |
|             if (controller.Namespaces) {
 | |
|               for (let namespace of controller.Namespaces) {
 | |
|                 if (namespace.NameSpace == name) {
 | |
|                   return subsystem.Controllers;
 | |
|                 }
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     nvmeof.logger.warn(`failed to find controllers for device: ${name}`);
 | |
|     return [];
 | |
|   }
 | |
| 
 | |
|   async getControllerByTransportNQN(transport, nqn) {
 | |
|     const nvmeof = this;
 | |
|     transport = await nvmeof.parseTransport(transport);
 | |
|     let subsystem = await nvmeof.getSubsystemByNQN(nqn);
 | |
|     if (subsystem) {
 | |
|       for (let controller of subsystem.Controllers) {
 | |
|         if (controller.Transport != transport.type) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         let controllerAddress = controller.Address;
 | |
|         /**
 | |
|          * For backwards compatibility with older nvme-cli versions (at least < 2.2.1)
 | |
|          * old: "Address":"traddr=127.0.0.1 trsvcid=4420"
 | |
|          * new: "Address":"traddr=127.0.0.1,trsvcid=4420"
 | |
|          */
 | |
|         controllerAddress = controllerAddress.replace(
 | |
|           new RegExp(/ ([a-z_]*=)/, "g"),
 | |
|           ",$1"
 | |
|         );
 | |
|         let parts = controllerAddress.split(",");
 | |
| 
 | |
|         let traddr;
 | |
|         let trsvcid;
 | |
|         for (let i_part of parts) {
 | |
|           let i_parts = i_part.split("=");
 | |
|           switch (i_parts[0].trim()) {
 | |
|             case "traddr":
 | |
|               traddr = i_parts[1].trim();
 | |
|               break;
 | |
|             case "trsvcid":
 | |
|               trsvcid = i_parts[1].trim();
 | |
|               break;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if (traddr != transport.address) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         if (transport.service && trsvcid != transport.service) {
 | |
|           continue;
 | |
|         }
 | |
| 
 | |
|         return controller;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     nvmeof.logger.warn(
 | |
|       `failed to find controller for transport: ${JSON.stringify(
 | |
|         transport
 | |
|       )}, nqn: ${nqn}`
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   async nqnByNamespaceDeviceName(name) {
 | |
|     const nvmeof = this;
 | |
|     name = name.replace("/dev/", "");
 | |
|     let nativeMultipathEnabled = await nvmeof.nativeMultipathEnabled();
 | |
|     const subsystems = await nvmeof.getSubsystems();
 | |
| 
 | |
|     if (nativeMultipathEnabled) {
 | |
|       // using per-subsystem namespace
 | |
|       for (let subsystem of subsystems) {
 | |
|         if (subsystem.Namespaces) {
 | |
|           for (let namespace of subsystem.Namespaces) {
 | |
|             if (namespace.NameSpace == name) {
 | |
|               return subsystem.SubsystemNQN;
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     } else {
 | |
|       // using per-controller namespace
 | |
|       for (let subsystem of subsystems) {
 | |
|         if (subsystem.Controllers) {
 | |
|           for (let controller of subsystem.Controllers) {
 | |
|             if (controller.Namespaces) {
 | |
|               for (let namespace of controller.Namespaces) {
 | |
|                 if (namespace.NameSpace == name) {
 | |
|                   return subsystem.SubsystemNQN;
 | |
|                 }
 | |
|               }
 | |
|             }
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     nvmeof.logger.warn(`failed to find nqn for device: ${name}`);
 | |
|   }
 | |
| 
 | |
|   devicePathByModelNumberSerialNumber(modelNumber, serialNumber) {
 | |
|     modelNumber = modelNumber.replaceAll(" ", "_");
 | |
|     serialNumber = serialNumber.replaceAll(" ", "_");
 | |
|     return `/dev/disk/by-id/nvme-${modelNumber}_${serialNumber}`;
 | |
|   }
 | |
| 
 | |
|   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;
 | |
|     }
 | |
| 
 | |
|     nvmeof.logger.verbose(
 | |
|       "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;
 |