const cp = require("child_process"); const { sleep } = require("./general"); function getIscsiValue(value) { if (value == "") return null; return value; } const DEFAULT_TIMEOUT = process.env.ISCSI_DEFAULT_TIMEOUT || 30000; class ISCSI { constructor(options = {}) { const iscsi = this; iscsi.options = options; options.paths = options.paths || {}; if (!options.paths.iscsiadm) { options.paths.iscsiadm = "iscsiadm"; } if (!options.paths.sudo) { options.paths.sudo = "/usr/bin/sudo"; } if (!options.executor) { options.executor = { spawn: cp.spawn, }; } iscsi.iscsiadm = { /** * iscsiadm -m iface -o show * iface_name transport_name,hwaddress,ipaddress,net_ifacename,initiatorname */ async listInterfaces() { let args = []; args = args.concat(["-m", "iface", "-o", "show"]); const result = await iscsi.exec(options.paths.iscsiadm, args); // return empty list if no stdout data if (!result.stdout) { return []; } const entries = result.stdout.trim().split("\n"); const interfaces = []; let fields; entries.forEach((entry) => { fields = entry.split(" "); interfaces.push({ iface_name: fields[0], transport_name: fields[1].split(",")[0], hwaddress: getIscsiValue(fields[1].split(",")[1]), ipaddress: getIscsiValue(fields[1].split(",")[2]), net_ifacename: getIscsiValue(fields[1].split(",")[3]), initiatorname: getIscsiValue(fields[1].split(",")[4]), }); }); return interfaces; }, /** * iscsiadm -m iface -o show -I * * @param {*} iface */ async showInterface(iface) { let args = []; args = args.concat(["-m", "iface", "-o", "show", "-I", iface]); let result = await iscsi.exec(options.paths.iscsiadm, args); const entries = result.stdout.trim().split("\n"); const i = {}; let fields, key, value; entries.forEach((entry) => { if (entry.startsWith("#")) return; fields = entry.split("="); key = fields[0].trim(); value = fields[1].trim(); i[key] = getIscsiValue(value); }); return i; }, /** * iscsiadm --mode node -T -p -o new * * @param {*} tgtIQN * @param {*} portal * @param {*} attributes */ async createNodeDBEntry(tgtIQN, portal, attributes = {}) { let args = []; args = args.concat([ "-m", "node", "-T", tgtIQN, "-p", portal, "-o", "new", ]); // create DB entry await iscsi.exec(options.paths.iscsiadm, args); // update attributes 1 by 1 for (let attribute in attributes) { let args = []; args = args.concat([ "-m", "node", "-T", tgtIQN, "-p", portal, "-o", "update", "--name", attribute, "--value", attributes[attribute], ]); // https://bugzilla.redhat.com/show_bug.cgi?id=884427 // Could not execute operation on all records: encountered iSCSI database failure let retries = 0; let maxRetries = 5; let retryWait = 1000; while (retries < maxRetries) { retries++; try { //throw {stderr: "Could not execute operation on all records: encountered iSCSI database failure"}; await iscsi.exec(options.paths.iscsiadm, args); break; } catch (err) { if ( retries < maxRetries && err.stderr.includes( "Could not execute operation on all records: encountered iSCSI database failure" ) ) { await sleep(retryWait); } else { throw err; } } } } }, /** * iscsiadm --mode node -T -p -o delete * * @param {*} tgtIQN * @param {*} portal */ async deleteNodeDBEntry(tgtIQN, portal) { let args = []; args = args.concat([ "-m", "node", "-T", tgtIQN, "-p", portal, "-o", "delete", ]); await iscsi.exec(options.paths.iscsiadm, args); }, /** * get session object by iqn/portal */ async getSession(tgtIQN, portal) { const sessions = await iscsi.iscsiadm.getSessions(); let session = false; sessions.every((i_session) => { if (`${i_session.iqn}` == tgtIQN && portal == i_session.portal) { session = i_session; return false; } return true; }); return session; }, /** * iscsiadm -m session */ async getSessions() { let args = []; args = args.concat(["-m", "session"]); let result; try { result = await iscsi.exec(options.paths.iscsiadm, args); } catch (err) { // no active sessions if (err.code == 21) { result = err; } else { throw err; } } // return empty list if no stdout data if (!result.stdout) { return []; } // protocol: [id] ip:port,target_portal_group_tag targetname const entries = result.stdout.trim().split("\n"); const sessions = []; let fields; entries.forEach((entry) => { fields = entry.split(" "); sessions.push({ protocol: entry.split(":")[0], id: Number(fields[1].replace("[", "").replace("]", "")), portal: fields[2].replace("[", "").replace("]", "").split(",")[0], target_portal_group_tag: fields[2].split(",")[1], iqn: fields[3].trim(), //iqn: fields[3].split(":")[0], //target: fields[3].split(":")[1], }); }); return sessions; }, /** * iscsiadm -m session */ async getSessionsDetails() { let args = []; args = args.concat(["-m", "session", "-P", "3"]); let result; try { result = await iscsi.exec(options.paths.iscsiadm, args); } catch (err) { // no active sessions if (err.code == 21) { result = err; } else { throw err; } } // return empty list if no stdout data if (!result.stdout) { return []; } let currentTarget; let sessionGroups = []; let currentSession = []; // protocol: [id] ip:port,target_portal_group_tag targetname const entries = result.stdout.trim().split("\n"); // remove first 2 lines 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 ` in a loop for (let i = 0; i < entries.length; i++) { let entry = entries[i]; if (entry.startsWith("Target:")) { currentTarget = entry; } else if (entry.trim().startsWith("Current Portal:")) { if (currentSession.length > 0) { sessionGroups.push(currentSession); } currentSession = [currentTarget, entry]; } else { currentSession.push(entry); } if (i + 1 == entries.length) { sessionGroups.push(currentSession); } } const sessions = []; for (let i = 0; i < sessionGroups.length; i++) { let sessionLines = sessionGroups[i]; let session = {}; let currentSection; for (let j = 0; j < sessionLines.length; j++) { let line = sessionLines[j].trim(); let uniqueChars = String.prototype.concat(...new Set(line)); if (uniqueChars == "*") { currentSection = sessionLines[j + 1] .trim() .toLowerCase() .replace(/ /g, "_") .replace(/\W/g, ""); j++; j++; continue; } let key = line .split(":", 1)[0] .trim() .replace(/ /g, "_") .replace(/\W/g, ""); let value = line.split(":").slice(1).join(":").trim(); if (currentSection) { session[currentSection] = session[currentSection] || {}; switch (currentSection) { case "attached_scsi_devices": key = key.toLowerCase(); if (key == "host_number") { session[currentSection]["host"] = { number: value.split("\t")[0], state: value .split("\t") .slice(1) .join("\t") .split(":") .slice(1) .join(":") .trim(), }; while ( sessionLines[j + 1] && sessionLines[j + 1].trim().startsWith("scsi") ) { session[currentSection]["host"]["devices"] = session[currentSection]["host"]["devices"] || []; let line1p = sessionLines[j + 1].split(" "); let line2 = sessionLines[j + 2]; let line2p = ""; if (line2) { line2p = line2.split(" "); session[currentSection]["host"]["devices"].push({ channel: line1p[2], id: line1p[4], lun: line1p[6], attached_scsi_disk: line2p[3].split("\t")[0], state: line2 .trim() .split("\t") .slice(1) .join("\t") .split(":") .slice(1) .join(":") .trim(), }); } j++; j++; } continue; } break; case "negotiated_iscsi_params": key = key.charAt(0).toLowerCase() + key.slice(1); key = key.replace( /[A-Z]/g, (letter) => `_${letter.toLowerCase()}` ); break; } key = key.toLowerCase(); session[currentSection][key] = value; } else { key = key.toLowerCase(); if (key == "target") { value = value.split(" ")[0]; } session[key.trim()] = value.trim(); } } sessions.push(session); } return sessions; }, /** * iscsiadm -m discovery -t st -p * * @param {*} portal */ async discoverTargets(portal) { let args = []; args = args.concat(["-m", "discovery"]); args = args.concat(["-t", "sendtargets"]); args = args.concat(["-p", portal]); let result; try { result = await iscsi.exec(options.paths.iscsiadm, args); } catch (err) { throw err; } // return empty list if no stdout data if (!result.stdout) { return []; } const entries = result.stdout.trim().split("\n"); const targets = []; entries.forEach((entry) => { targets.push({ portal: entry.split(",")[0], target_portal_group_tag: entry.split(" ")[0].split(",")[1], iqn: entry.split(" ")[1].split(":")[0], target: entry.split(" ")[1].split(":")[1], }); }); return targets; }, /** * iscsiadm -m node -T -p -l * * @param {*} tgtIQN * @param {*} portal */ async login(tgtIQN, portal) { let args = []; args = args.concat(["-m", "node", "-T", tgtIQN, "-p", portal, "-l"]); try { await iscsi.exec(options.paths.iscsiadm, args); } catch (err) { // already logged in if (err.code == 15) { return true; } throw err; } return true; }, /** * * * @param {*} tgtIQN * @param {*} portals */ async logout(tgtIQN, portals) { let args = []; args = args.concat(["-m", "node", "-T", tgtIQN]); if (!Array.isArray(portals)) { portals = [portals]; } for (let i = 0; i < portals.length; i++) { let p = portals[i]; try { await iscsi.exec( options.paths.iscsiadm, args.concat(["-p", p, "-u"]) ); } catch (err) { if (err.code == 21) { // no matching sessions } else { throw err; } } } return true; }, /** * iscsiadm -m session -r SID --rescan * * @param {*} session */ async rescanSession(session) { let sid; if (typeof session === "object") { sid = session.id; } else { sid = session; } // make sure session is a valid number if (session !== 0 && session > 0) { throw new Error("cannot scan empty session id"); } let args = []; args = args.concat(["-m", "session", "-r", sid, "--rescan"]); try { await iscsi.exec(options.paths.iscsiadm, args); } catch (err) { throw err; } return true; }, }; } exec(command, args, options = {}) { if (!options.hasOwnProperty("timeout")) { options.timeout = DEFAULT_TIMEOUT; } const iscsi = this; args = args || []; let stdout = ""; let stderr = ""; if (iscsi.options.sudo) { args.unshift(command); command = iscsi.options.paths.sudo; } console.log("executing iscsi command: %s %s", command, args.join(" ")); const child = iscsi.options.executor.spawn(command, args, options); return new Promise((resolve, reject) => { 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 }; // timeout scenario if (code === null) { result.timeout = true; reject(result); } if (code) { reject(result); } else { resolve(result); } }); }); } } module.exports.ISCSI = ISCSI;