minor bug fixes, iscsi multipath/device-mapper support

This commit is contained in:
Travis Glenn Hansen 2020-11-28 22:56:28 -07:00
parent c4a36750cd
commit 5076ca3664
3 changed files with 391 additions and 125 deletions

View File

@ -254,6 +254,7 @@ class CsiBaseDriver {
* @param {*} call * @param {*} call
*/ */
async NodeStageVolume(call) { async NodeStageVolume(call) {
const driver = this;
const mount = new Mount(); const mount = new Mount();
const filesystem = new Filesystem(); const filesystem = new Filesystem();
const iscsi = new ISCSI(); const iscsi = new ISCSI();
@ -310,46 +311,141 @@ class CsiBaseDriver {
device = `//${volume_context.server}/${volume_context.share}`; device = `//${volume_context.server}/${volume_context.share}`;
break; break;
case "iscsi": case "iscsi":
// create DB entry let portals = [];
// https://library.netapp.com/ecmdocs/ECMP1654943/html/GUID-8EC685B4-8CB6-40D8-A8D5-031A3899BCDC.html if (volume_context.portal) {
// put these options in place to force targets managed by csi to be explicitly attached (in the case of unclearn shutdown etc) portals.push(volume_context.portal.trim());
let nodeDB = {
"node.startup": "manual",
};
const nodeDBKeyPrefix = "node-db.";
for (const key in normalizedSecrets) {
if (key.startsWith(nodeDBKeyPrefix)) {
nodeDB[key.substr(nodeDBKeyPrefix.length)] = normalizedSecrets[key];
}
} }
await iscsi.iscsiadm.createNodeDBEntry(
volume_context.iqn,
volume_context.portal,
nodeDB
);
// login
await iscsi.iscsiadm.login(volume_context.iqn, volume_context.portal);
// find device name if (volume_context.portals) {
device = `/dev/disk/by-path/ip-${volume_context.portal}-iscsi-${volume_context.iqn}-lun-${volume_context.lun}`; volume_context.portals.split(",").forEach((portal) => {
portals.push(portal.trim());
});
}
// can take some time for device to show up, loop for some period // ensure full portal value
result = await filesystem.pathExists(device); portals = portals.map((value) => {
let timer_start = Math.round(new Date().getTime() / 1000); if (!value.includes(":")) {
let timer_max = 30; value += ":3260";
while (!result) { }
await sleep(2000);
return value.trim();
});
// ensure unique entries only
portals = [...new Set(portals)];
let iscsiDevices = [];
for (let portal of portals) {
// create DB entry
// https://library.netapp.com/ecmdocs/ECMP1654943/html/GUID-8EC685B4-8CB6-40D8-A8D5-031A3899BCDC.html
// put these options in place to force targets managed by csi to be explicitly attached (in the case of unclearn shutdown etc)
let nodeDB = {
"node.startup": "manual",
};
const nodeDBKeyPrefix = "node-db.";
for (const key in normalizedSecrets) {
if (key.startsWith(nodeDBKeyPrefix)) {
nodeDB[key.substr(nodeDBKeyPrefix.length)] =
normalizedSecrets[key];
}
}
await iscsi.iscsiadm.createNodeDBEntry(
volume_context.iqn,
portal,
nodeDB
);
// login
await iscsi.iscsiadm.login(volume_context.iqn, portal);
// find device name
device = `/dev/disk/by-path/ip-${portal}-iscsi-${volume_context.iqn}-lun-${volume_context.lun}`;
let deviceByPath = device;
// can take some time for device to show up, loop for some period
result = await filesystem.pathExists(device); result = await filesystem.pathExists(device);
let current_time = Math.round(new Date().getTime() / 1000); let timer_start = Math.round(new Date().getTime() / 1000);
if (!result && current_time - timer_start > timer_max) { let timer_max = 30;
throw new GrpcError( let deviceCreated = result;
grpc.status.UNKNOWN, while (!result) {
`hit timeout waiting for device node to appear: ${device}` await sleep(2000);
result = await filesystem.pathExists(device);
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 device node to appear: ${device}`
);
break;
}
}
if (deviceCreated) {
device = await filesystem.realpath(device);
iscsiDevices.push(device);
driver.ctx.logger.info(
`successfully logged into portal ${portal} and created device ${deviceByPath} with realpath ${device}`
); );
} }
} }
device = await filesystem.realpath(device); // filter duplicates
iscsiDevices = iscsiDevices.filter((value, index, self) => {
return self.indexOf(value) === index;
});
// only throw an error if we were not able to attach to *any* devices
if (iscsiDevices.length < 1) {
throw new GrpcError(
grpc.status.UNKNOWN,
`unable to attach any iscsi devices`
);
}
if (iscsiDevices.length != portals.length) {
driver.ctx.logger.warn(
`failed to attach all iscsi devices/targets/portals`
);
// 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`
);
}
}
// 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) =>
iscsiDevices.includes(value)
);
const useMultipath = portals.length > 1 || commonDevices.length > 0;
// discover multipath device to use
if (useMultipath) {
device = await filesystem.getDeviceMapperDeviceFromSlaves(
iscsiDevices,
false
);
if (!device) {
throw new GrpcError(
grpc.status.UNKNOWN,
`failed to discover multipath device`
);
}
}
break; break;
default: default:
throw new GrpcError( throw new GrpcError(
@ -463,6 +559,7 @@ class CsiBaseDriver {
const iscsi = new ISCSI(); const iscsi = new ISCSI();
let result; let result;
let is_block = false; let is_block = false;
let is_device_mapper = false;
let block_device_info; let block_device_info;
let access_type = "mount"; let access_type = "mount";
@ -505,78 +602,100 @@ class CsiBaseDriver {
} }
if (is_block) { if (is_block) {
if (block_device_info.tran == "iscsi") { let realBlockDeviceInfos = [];
// figure out which iscsi session this belongs to and logout // detect if is a multipath device
// scan /dev/disk/by-path/ip-*? is_device_mapper = await filesystem.isDeviceMapperDevice(
// device = `/dev/disk/by-path/ip-${volume_context.portal}-iscsi-${volume_context.iqn}-lun-${volume_context.lun}`; block_device_info.path
// parse output from `iscsiadm -m session -P 3` );
let sessions = await iscsi.iscsiadm.getSessionsDetails();
for (let i = 0; i < sessions.length; i++) {
let session = sessions[i];
let is_attached_to_session = false;
if ( if (is_device_mapper) {
session.attached_scsi_devices && let realBlockDevices = await filesystem.getDeviceMapperDeviceSlaves(
session.attached_scsi_devices.host && block_device_info.path
session.attached_scsi_devices.host.devices );
) { for (const realBlockDevice of realBlockDevices) {
is_attached_to_session = session.attached_scsi_devices.host.devices.some( realBlockDeviceInfos.push(
(device) => { await filesystem.getBlockDevice(realBlockDevice)
if (device.attached_scsi_disk == block_device_info.name) { );
return true; }
} else {
realBlockDeviceInfos = [block_device_info];
}
// TODO: this could be made async to detach all simultaneously
for (const block_device_info_i of realBlockDeviceInfos) {
if (block_device_info_i.tran == "iscsi") {
// figure out which iscsi session this belongs to and logout
// scan /dev/disk/by-path/ip-*?
// device = `/dev/disk/by-path/ip-${volume_context.portal}-iscsi-${volume_context.iqn}-lun-${volume_context.lun}`;
// parse output from `iscsiadm -m session -P 3`
let sessions = await iscsi.iscsiadm.getSessionsDetails();
for (let i = 0; i < sessions.length; i++) {
let session = sessions[i];
let is_attached_to_session = false;
if (
session.attached_scsi_devices &&
session.attached_scsi_devices.host &&
session.attached_scsi_devices.host.devices
) {
is_attached_to_session = session.attached_scsi_devices.host.devices.some(
(device) => {
if (device.attached_scsi_disk == block_device_info_i.name) {
return true;
}
return false;
} }
return false; );
}
);
}
if (is_attached_to_session) {
let timer_start;
let timer_max;
timer_start = Math.round(new Date().getTime() / 1000);
timer_max = 30;
let loggedOut = false;
while (!loggedOut) {
try {
await iscsi.iscsiadm.logout(session.target, [
session.persistent_portal,
]);
loggedOut = true;
} catch (err) {
await sleep(2000);
let current_time = Math.round(new Date().getTime() / 1000);
if (current_time - timer_start > timer_max) {
// not throwing error for now as future invocations would not enter code path anyhow
loggedOut = true;
//throw new GrpcError(
// grpc.status.UNKNOWN,
// `hit timeout trying to logout of iscsi target: ${session.persistent_portal}`
//);
}
}
} }
timer_start = Math.round(new Date().getTime() / 1000); if (is_attached_to_session) {
timer_max = 30; let timer_start;
let deletedEntry = false; let timer_max;
while (!deletedEntry) {
try { timer_start = Math.round(new Date().getTime() / 1000);
await iscsi.iscsiadm.deleteNodeDBEntry( timer_max = 30;
session.target, let loggedOut = false;
session.persistent_portal while (!loggedOut) {
); try {
deletedEntry = true; await iscsi.iscsiadm.logout(session.target, [
} catch (err) { session.persistent_portal,
await sleep(2000); ]);
let current_time = Math.round(new Date().getTime() / 1000); loggedOut = true;
if (current_time - timer_start > timer_max) { } catch (err) {
// not throwing error for now as future invocations would not enter code path anyhow await sleep(2000);
let current_time = Math.round(new Date().getTime() / 1000);
if (current_time - timer_start > timer_max) {
// not throwing error for now as future invocations would not enter code path anyhow
loggedOut = true;
//throw new GrpcError(
// grpc.status.UNKNOWN,
// `hit timeout trying to logout of iscsi target: ${session.persistent_portal}`
//);
}
}
}
timer_start = Math.round(new Date().getTime() / 1000);
timer_max = 30;
let deletedEntry = false;
while (!deletedEntry) {
try {
await iscsi.iscsiadm.deleteNodeDBEntry(
session.target,
session.persistent_portal
);
deletedEntry = true; deletedEntry = true;
//throw new GrpcError( } catch (err) {
// grpc.status.UNKNOWN, await sleep(2000);
// `hit timeout trying to delete iscsi node DB entry: ${session.target}, ${session.persistent_portal}` let current_time = Math.round(new Date().getTime() / 1000);
//); if (current_time - timer_start > timer_max) {
// not throwing error for now as future invocations would not enter code path anyhow
deletedEntry = true;
//throw new GrpcError(
// grpc.status.UNKNOWN,
// `hit timeout trying to delete iscsi node DB entry: ${session.target}, ${session.persistent_portal}`
//);
}
} }
} }
} }

View File

@ -48,14 +48,156 @@ class Filesystem {
const device_path = await filesystem.realpath(device); const device_path = await filesystem.realpath(device);
const blockdevices = await filesystem.getAllBlockDevices(); const blockdevices = await filesystem.getAllBlockDevices();
return blockdevices.some((i) => { return blockdevices.some(async (i) => {
if (i.path == device_path) { if ((await filesystem.realpath(i.path)) == device_path) {
return true; return true;
} }
return false; return false;
}); });
} }
/**
* Attempt to discover if the device is a device-mapper device
*
* @param {*} device
*/
async isDeviceMapperDevice(device) {
const filesystem = this;
const isBlock = await filesystem.isBlockDevice(device);
if (!isBlock) {
return false;
}
device = await filesystem.realpath(device);
return device.includes("dm-");
}
async isDeviceMapperSlaveDevice(device) {
const filesystem = this;
device = await filesystem.realpath(device);
}
/**
* Get all device-mapper devices (ie: dm-0, dm-1, dm-N...)
*/
async getAllDeviceMapperDevices() {
const filesystem = this;
let result;
let devices = [];
let args = [
"-c",
'for file in $(ls -la /dev/mapper/* | grep "\\->" | grep -oP "\\-> .+" | grep -oP " .+"); do echo $(F=$(echo $file | grep -oP "[a-z0-9-]+");echo $F":"$(ls "/sys/block/${F}/slaves/");); done;',
];
try {
result = await filesystem.exec("sh", args);
for (const dm of result.stdout.trim().split("\n")) {
devices.push("/dev/" + dm.split(":")[0].trim());
}
return devices;
} catch (err) {
throw err;
}
}
async getAllDeviceMapperSlaveDevices() {
const filesystem = this;
let result;
let args = [
"-c",
'for file in $(ls -la /dev/mapper/* | grep "\\->" | grep -oP "\\-> .+" | grep -oP " .+"); do echo $(F=$(echo $file | grep -oP "[a-z0-9-]+");echo $F":"$(ls "/sys/block/${F}/slaves/");); done;',
];
let slaves = [];
try {
result = await filesystem.exec("sh", args);
for (const dm of result.stdout.trim().split("\n")) {
const realDevices = dm
.split(":")[1]
.split(" ")
.map((value) => {
return "/dev/" + value.trim();
});
slaves.push(...realDevices);
}
return slaves;
} catch (err) {
throw err;
}
}
/**
* Get all slave devices connected to a device-mapper device
*
* @param {*} device
*/
async getDeviceMapperDeviceSlaves(device) {
const filesystem = this;
device = await filesystem.realpath(device);
let device_info = await filesystem.getBlockDevice(device);
const slaves = [];
let result;
let args = [`/sys/block/${device_info.kname}/slaves/`];
try {
result = await filesystem.exec("ls", args);
for (const entry of result.stdout.split("\n")) {
if (entry.trim().length < 1) {
continue;
}
slaves.push("/dev/" + entry.trim());
}
return slaves;
} catch (err) {
throw err;
}
}
async getDeviceMapperDeviceFromSlaves(slaves, matchAll = true) {
const filesystem = this;
let result;
// get mapping of dm devices to real devices
let args = [
"-c",
'for file in $(ls -la /dev/mapper/* | grep "\\->" | grep -oP "\\-> .+" | grep -oP " .+"); do echo $(F=$(echo $file | grep -oP "[a-z0-9-]+");echo $F":"$(ls "/sys/block/${F}/slaves/");); done;',
];
result = await filesystem.exec("sh", args);
for (const dm of result.stdout.trim().split("\n")) {
const dmDevice = "/dev/" + dm.split(":")[0].trim();
const realDevices = dm
.split(":")[1]
.split(" ")
.map((value) => {
return "/dev/" + value.trim();
});
const intersectDevices = slaves.filter((value) =>
realDevices.includes(value)
);
if (matchAll === false && intersectDevices.length > 0) {
return dmDevice;
}
// if all 3 have the same elements we have a winner
if (
intersectDevices.length == realDevices.length &&
realDevices.length == slaves.length
) {
return dmDevice;
}
}
}
/** /**
* create symlink * create symlink
* *

View File

@ -25,7 +25,7 @@ class ISCSI {
if (!options.executor) { if (!options.executor) {
options.executor = { options.executor = {
spawn: cp.spawn spawn: cp.spawn,
}; };
} }
@ -47,7 +47,7 @@ class ISCSI {
const entries = result.stdout.trim().split("\n"); const entries = result.stdout.trim().split("\n");
const interfaces = []; const interfaces = [];
let fields; let fields;
entries.forEach(entry => { entries.forEach((entry) => {
fields = entry.split(" "); fields = entry.split(" ");
interfaces.push({ interfaces.push({
iface_name: fields[0], iface_name: fields[0],
@ -55,7 +55,7 @@ class ISCSI {
hwaddress: getIscsiValue(fields[1].split(",")[1]), hwaddress: getIscsiValue(fields[1].split(",")[1]),
ipaddress: getIscsiValue(fields[1].split(",")[2]), ipaddress: getIscsiValue(fields[1].split(",")[2]),
net_ifacename: getIscsiValue(fields[1].split(",")[3]), net_ifacename: getIscsiValue(fields[1].split(",")[3]),
initiatorname: getIscsiValue(fields[1].split(",")[4]) initiatorname: getIscsiValue(fields[1].split(",")[4]),
}); });
}); });
@ -75,7 +75,7 @@ class ISCSI {
const entries = result.stdout.trim().split("\n"); const entries = result.stdout.trim().split("\n");
const i = {}; const i = {};
let fields, key, value; let fields, key, value;
entries.forEach(entry => { entries.forEach((entry) => {
if (entry.startsWith("#")) return; if (entry.startsWith("#")) return;
fields = entry.split("="); fields = entry.split("=");
key = fields[0].trim(); key = fields[0].trim();
@ -103,7 +103,7 @@ class ISCSI {
"-p", "-p",
portal, portal,
"-o", "-o",
"new" "new",
]); ]);
await iscsi.exec(options.paths.iscsiadm, args); await iscsi.exec(options.paths.iscsiadm, args);
for (let attribute in attributes) { for (let attribute in attributes) {
@ -120,7 +120,7 @@ class ISCSI {
"--name", "--name",
attribute, attribute,
"--value", "--value",
attributes[attribute] attributes[attribute],
]); ]);
await iscsi.exec(options.paths.iscsiadm, args); await iscsi.exec(options.paths.iscsiadm, args);
} }
@ -142,7 +142,7 @@ class ISCSI {
"-p", "-p",
portal, portal,
"-o", "-o",
"delete" "delete",
]); ]);
await iscsi.exec(options.paths.iscsiadm, args); await iscsi.exec(options.paths.iscsiadm, args);
}, },
@ -174,7 +174,7 @@ class ISCSI {
const entries = result.stdout.trim().split("\n"); const entries = result.stdout.trim().split("\n");
const sessions = []; const sessions = [];
let fields; let fields;
entries.forEach(entry => { entries.forEach((entry) => {
fields = entry.split(" "); fields = entry.split(" ");
sessions.push({ sessions.push({
protocol: entry.split(":")[0], protocol: entry.split(":")[0],
@ -182,7 +182,7 @@ class ISCSI {
portal: fields[2].split(",")[0], portal: fields[2].split(",")[0],
target_portal_group_tag: fields[2].split(",")[1], target_portal_group_tag: fields[2].split(",")[1],
iqn: fields[3].split(":")[0], iqn: fields[3].split(":")[0],
target: fields[3].split(":")[1] target: fields[3].split(":")[1],
}); });
}); });
@ -212,6 +212,7 @@ class ISCSI {
return []; return [];
} }
let currentTarget;
let sessionGroups = []; let sessionGroups = [];
let currentSession = []; let currentSession = [];
@ -221,13 +222,21 @@ class ISCSI {
entries.shift(); entries.shift();
entries.shift(); entries.shift();
// this should break up the lines into groups of lines
// where each group is the full details of a single session
// note that the output of the command bundles/groups all sessions
// by target so extra logic is needed to hanle that
// alternatively we could get all sessions using getSessions()
// and then invoke `iscsiadm -m session -P 3 -r <session id>` in a loop
for (let i = 0; i < entries.length; i++) { for (let i = 0; i < entries.length; i++) {
let entry = entries[i]; let entry = entries[i];
if (entry.startsWith("Target:")) { if (entry.startsWith("Target:")) {
currentTarget = entry;
} else if (entry.trim().startsWith("Current Portal:")) {
if (currentSession.length > 0) { if (currentSession.length > 0) {
sessionGroups.push(currentSession); sessionGroups.push(currentSession);
} }
currentSession = [entry]; currentSession = [currentTarget, entry];
} else { } else {
currentSession.push(entry); currentSession.push(entry);
} }
@ -261,11 +270,7 @@ class ISCSI {
.trim() .trim()
.replace(/ /g, "_") .replace(/ /g, "_")
.replace(/\W/g, ""); .replace(/\W/g, "");
let value = line let value = line.split(":").slice(1).join(":").trim();
.split(":")
.slice(1)
.join(":")
.trim();
if (currentSection) { if (currentSection) {
session[currentSection] = session[currentSection] || {}; session[currentSection] = session[currentSection] || {};
@ -282,7 +287,7 @@ class ISCSI {
.split(":") .split(":")
.slice(1) .slice(1)
.join(":") .join(":")
.trim() .trim(),
}; };
while ( while (
sessionLines[j + 1] && sessionLines[j + 1] &&
@ -308,7 +313,7 @@ class ISCSI {
.split(":") .split(":")
.slice(1) .slice(1)
.join(":") .join(":")
.trim() .trim(),
}); });
} }
@ -322,7 +327,7 @@ class ISCSI {
key = key.charAt(0).toLowerCase() + key.slice(1); key = key.charAt(0).toLowerCase() + key.slice(1);
key = key.replace( key = key.replace(
/[A-Z]/g, /[A-Z]/g,
letter => `_${letter.toLowerCase()}` (letter) => `_${letter.toLowerCase()}`
); );
break; break;
} }
@ -367,12 +372,12 @@ class ISCSI {
const entries = result.stdout.trim().split("\n"); const entries = result.stdout.trim().split("\n");
const targets = []; const targets = [];
entries.forEach(entry => { entries.forEach((entry) => {
targets.push({ targets.push({
portal: entry.split(",")[0], portal: entry.split(",")[0],
target_portal_group_tag: entry.split(" ")[0].split(",")[1], target_portal_group_tag: entry.split(" ")[0].split(",")[1],
iqn: entry.split(" ")[1].split(":")[0], iqn: entry.split(" ")[1].split(":")[0],
target: entry.split(" ")[1].split(":")[1] target: entry.split(" ")[1].split(":")[1],
}); });
}); });
@ -432,7 +437,7 @@ class ISCSI {
} }
return true; return true;
} },
}; };
} }
@ -460,15 +465,15 @@ class ISCSI {
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
child.stdout.on("data", function(data) { child.stdout.on("data", function (data) {
stdout = stdout + data; stdout = stdout + data;
}); });
child.stderr.on("data", function(data) { child.stderr.on("data", function (data) {
stderr = stderr + data; stderr = stderr + data;
}); });
child.on("close", function(code) { child.on("close", function (code) {
const result = { code, stdout, stderr }; const result = { code, stdout, stderr };
if (timeout) { if (timeout) {
clearTimeout(timeout); clearTimeout(timeout);