democratic-csi/src/utils/nvmeof.js

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;