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;
|