const events = require("events"); const cp = require("child_process"); class Zetabyte { constructor(options = {}) { const zb = this; zb.options = options; options.paths = options.paths || {}; if (!options.paths.zpool) { options.paths.zpool = "/sbin/zpool"; } if (!options.paths.zfs) { options.paths.zfs = "/sbin/zfs"; } if (!options.paths.sudo) { options.paths.sudo = "/usr/bin/sudo"; } if (!options.paths.chroot) { options.paths.chroot = "/usr/sbin/chroot"; } if (!options.timeout) { options.timeout = 10 * 60 * 1000; } if (!options.executor) { options.executor = { spawn: cp.spawn, }; } zb.DEFAULT_ZPOOL_LIST_PROPERTIES = [ "name", "size", "allocated", "free", "cap", "health", "altroot", ]; zb.DEFAULT_ZFS_LIST_PROPERTIES = [ "name", "used", "avail", "refer", "type", "mountpoint", ]; zb.helpers = { zfsErrorStr: function (error, stderr) { if (!error) return null; if (error.killed) return "Process killed due to timeout."; return error.message || (stderr ? stderr.toString() : ""); }, zfsError: function (error, stderr) { return new Error(zb.helpers.zfsErrorStr(error, stderr)); }, parseTabSeperatedTable: function (data) { if (!data) { return []; } const lines = data.trim().split("\n"); const rows = []; for (let i = 0, numLines = lines.length; i < numLines; i++) { if (lines[i]) { rows.push(lines[i].split("\t")); } } return rows; }, /* * Parse the output of `zfs get ...`, invoked by zfs.get below. The output has * the form: * * * * and those fields are tab-separated. */ parsePropertyList: function (data) { if (!data) { return {}; } const lines = data.trim().split("\n"); const properties = {}; lines.forEach(function (line) { const fields = line.split("\t"); if (!properties[fields[0]]) properties[fields[0]] = {}; properties[fields[0]][fields[1]] = { value: fields[2], received: fields[3], source: fields[4], }; }); return properties; }, listTableToPropertyList: function (properties, data) { const entries = []; data.forEach((row) => { let entry = {}; properties.forEach((value, index) => { entry[value] = row[index]; }); entries.push(entry); }); return entries; }, extractSnapshotName: function (datasetName) { return datasetName.substring(datasetName.indexOf("@") + 1); }, extractDatasetName: function (datasetName) { if (datasetName.includes("@")) { return datasetName.substring(0, datasetName.indexOf("@")); } return datasetName; }, isZfsSnapshot: function (snapshotName) { return snapshotName.includes("@"); }, extractPool: function (datasetName) { const parts = datasetName.split("/"); return parts[0]; }, extractParentDatasetName: function (datasetName) { const parts = datasetName.split("/"); parts.pop(); return parts.join("/"); }, extractLeafName: function (datasetName) { return datasetName.split("/").pop(); }, isPropertyValueSet: function (value) { if ( value === undefined || value === null || value == "" || value == "-" ) { return false; } return true; }, generateZvolSize: function (capacity_bytes, block_size) { block_size = "" + block_size; block_size = block_size.toLowerCase(); switch (block_size) { case "512": block_size = 512; break; case "1024": case "1k": block_size = 1024; break; case "2048": case "2k": block_size = 2048; break; case "4096": case "4k": block_size = 4096; break; case "8192": case "8k": block_size = 8192; break; case "16384": case "16k": block_size = 16384; break; case "32768": case "32k": block_size = 32768; break; case "65536": case "64k": block_size = 65536; break; case "131072": case "128k": block_size = 131072; break; } capacity_bytes = Number(capacity_bytes); let result = block_size * Math.round(capacity_bytes / block_size); if (result < capacity_bytes) result = Number(result) + Number(block_size); return result; }, }; zb.zpool = { /** * zpool add [-fn] pool vdev ... * * @param {*} pool * @param {*} vdevs */ add: function (pool, vdevs) { // -f force // -n noop }, /** * zpool attach [-f] pool device new_device * * @param {*} pool * @param {*} device * @param {*} new_device */ attach: function (pool, device, new_device) { // -f Forces use of new_device, even if its appears to be in use. }, /** * zpool checkpoint [-d, --discard] pool * * @param {*} pool */ checkpoint: function (pool) {}, /** * zpool clear [-F [-n]] pool [device] * * @param {*} pool * @param {*} device */ clear: function (pool, device) {}, /** * zpool create [-fnd] [-o property=value] ... [-O * file-system-property=value] ... [-m mountpoint] [-R root] [-t * tempname] pool vdev ... * * This allows fine-grained control and exposes all features of the * zpool create command, including log devices, cache devices, and hot spares. * The input is an object of the form produced by the disklayout library. */ create: function (pool, options) { if (arguments.length != 2) throw Error("Invalid arguments, 2 arguments required"); return new Promise((resolve, reject) => { let args = []; args.push("create"); if (options.force) args.push("-f"); if (options.noop) args.push("-n"); if (options.disableFeatures) args.push("-d"); if (options.properties) { for (const [key, value] of Object.entries(options.properties)) { args.push("-o"); args.push(`${key}=${value}`); } } if (options.fsProperties) { for (const [key, value] of Object.entries(options.fsProperties)) { args.push("-O"); args.push(`${key}=${value}`); } } if (options.mountpoint) args = args.concat(["-m", options.mountpoint]); if (options.root) args = args.concat(["-R", options.root]); if (options.tempname) args = args.concat(["-t", options.tempname]); args.push(pool); options.vdevs.forEach(function (vdev) { if (vdev.type) args.push(vdev.type); if (vdev.devices) { vdev.devices.forEach(function (dev) { args.push(dev.name); }); } else { args.push(vdev.name); } }); if (options.spares) { args.push("spare"); options.spares.forEach(function (dev) { args.push(dev.name); }); } if (options.logs) { args.push("log"); options.logs.forEach(function (dev) { args.push(dev.name); }); } if (options.cache) { args.push("cache"); options.cache.forEach(function (dev) { args.push(dev.name); }); } zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool destroy [-f] pool * * @param {*} pool */ destroy: function (pool) { if (arguments.length != 1) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { let args = []; args.push("destroy"); if (options.force) args.push("-f"); args.push(pool); zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool detach pool device * * @param {*} pool * @param {*} device */ detach: function (pool, device) { if (arguments.length != 2) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { let args = []; args.push("detach"); args.push(pool); args.push(device); zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool export [-f] pool ... * * @param {*} pool */ export: function (pool) { if (arguments.length != 2) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { let args = []; args.push("export"); if (options.force) args.push("-f"); if (Array.isArray(pool)) { pool.forEach((item) => { args.push(item); }); } else { args.push(pool); } zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool get [-Hp] [-o field[,...]] all | property[,...] pool ... */ get: function () {}, /** * zpool history [-il] [pool] ... * * @param {*} pool */ history: function (pool) { return new Promise((resolve, reject) => { let args = []; args.push("history"); if (options.internal) args.push("-i"); if (options.longFormat) args.push("-l"); if (Array.isArray(pool)) { pool.forEach((item) => { args.push(item); }); } else { args.push(pool); } zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool import [-d dir | -c cachefile] [-D] * * zpool import [-o mntopts] [-o property=value] ... [-d dir | -c cachefile] * [-D] [-f] [-m] [-N] [-R root] [-F [-n]] -a * * zpool import [-o mntopts] [-o property=value] ... [-d dir | -c cachefile] * [-D] [-f] [-m] [-N] [-R root] [-t] [-F [-n]] pool | id [newpool] * * * * @param {*} options */ import: function (options = {}) { return new Promise((resolve, reject) => { let args = []; args.push("import"); if (options.dir) args = args.concat(["-d", options.dir]); if (options.cachefile) args = args.concat(["-c", options.cachefile]); if (options.destroyed) args.push("-D"); zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool iostat [-T d|u] [-v] [pool] ... [interval [count]] * * @param {*} options */ iostat: function (options = {}) {}, /** * zpool labelclear [-f] device * * @param {*} device */ labelclear: function (device) {}, /** * zpool list [-Hpv] [-o property[,...]] [-T d|u] [pool] ... [inverval * [count]] * * @param {*} pool * @param {*} options */ list: function (pool, properties, options = {}) { if (!(arguments.length >= 1)) throw Error("Invalid arguments"); if (!properties) properties = zb.DEFAULT_ZPOOL_LIST_PROPERTIES; return new Promise((resolve, reject) => { let args = []; args.push("list"); if (!("parse" in options)) options.parse = true; if (!("parseable" in options)) options.parsable = true; if (options.parseable || options.parse) args.push("-Hp"); if (options.verbose) args.push("-v"); if (properties) { if (Array.isArray(properties)) { if (properties.length == 0) { properties = zb.DEFAULT_ZPOOL_LIST_PROPERTIES; } args.push("-o"); args.push(properties.join(",")); } else { args.push("-o"); args.push(properties); } } if (options.timestamp) args = args.concat(["-T", options.timestamp]); if (pool) { if (Array.isArray(pool)) { pool.forEach((item) => { args.push(item); }); } else { args.push(pool); } } if (options.interval) args.push(options.interval); if (options.count) args.push(options.count); zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); if (options.parse) { let data = zb.helpers.parseTabSeperatedTable(stdout); let indexed = zb.helpers.listTableToPropertyList( properties, data ); return resolve({ properties, data, indexed, }); } return resolve({ properties, data: stdout }); } ); }); }, /** * zpool offline [-t] pool device ... * * @param {*} pool * @param {*} device * @param {*} options */ offline: function (pool, device, options = {}) { return new Promise((resolve, reject) => { let args = []; args.push("offline"); if (options.temporary) args.push("-t"); args.push(pool); args.push(device); zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool online [-e] pool device ... * * @param {*} pool * @param {*} device * @param {*} options */ online: function (pool, device, options = {}) { return new Promise((resolve, reject) => { let args = []; args.push("online"); if (options.expand) args.push("-e"); args.push(pool); args.push(device); zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool reguid pool * * @param {*} pool */ reguid: function (pool) { return new Promise((resolve, reject) => { let args = []; args.push("reguid"); args.push(pool); zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool remove [-np] pool device ... * * zpool remove -s pool * * @param {*} pool * @param {*} device */ remove: function (pool, device, options = {}) { return new Promise((resolve, reject) => { let args = []; args.push("remove"); if (options.noop) args.push("-n"); if (options.parsable) args.push("-p"); if (options.stop) args.push("-s"); args.push(pool); if (device) { args.push(device); } zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool reopen pool * * @param {*} pool */ reopen: function (pool) { return new Promise((resolve, reject) => { let args = []; args.push("reopen"); args.push(pool); zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool replace [-f] pool device [new_device] * * @param {*} pool * @param {*} device * @param {*} new_device */ replace: function (pool, device, new_device) { return new Promise((resolve, reject) => { let args = []; args.push("replace"); if (options.force) args.push("-f"); args.push(pool); args.push(device); if (new_device) { args.push(new_device); } zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool scrub [-s | -p] pool ... * * @param {*} pool */ scrub: function (pool) { return new Promise((resolve, reject) => { let args = []; args.push("scrub"); if (options.stop) args.push("-s"); if (options.pause) args.push("-p"); if (Array.isArray(pool)) { pool.forEach((item) => { args.push(item); }); } else { args.push(pool); } zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool set property=value pool * * @param {*} pool * @param {*} property * @param {*} value */ set: function (pool, property, value) { return new Promise((resolve, reject) => { let args = []; args.push("set"); args.push(`${property}=${value}`); args.push(pool); zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, /** * zpool split [-n] [-R altroot] [-o mntopts] [-o property=value] pool * newpool [device ...] * * @param {*} pool * @param {*} newpool * @param {*} device */ split: function (pool, newpool, device) {}, /** * zpool status [-vx] [-T d|u] [pool] ... [interval [count]] */ status: function (pool, options = {}) { return new Promise((resolve, reject) => { let args = []; if (!("parse" in options)) options.parse = true; args.push("status"); if (options.verbose) args.push("-v"); if (options.exhibiting) args.push("-x"); if (options.timestamp) args = args.concat(["-T", options.timestamp]); if (pool) { if (Array.isArray(pool)) { pool.forEach((item) => { args.push(item); }); } else { args.push(pool); } } if (options.interval) args.push(options.interval); if (options.count) args.push(options.count); zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (options.parse) { stdout = stdout.trim(); if (error || stdout == "no pools available\n") { return resolve("UNKNOWN"); } const lines = stdout.split("\n"); for (var i = 0; i < lines.length; i++) { if (lines[i].trim().substr(0, 5) === "state") { return resolve(lines[i].trim().substr(7)); } } return resolve("UNKNOWN"); } else { if (error) return reject(stderr); return resolve(stdout); } } ); }); }, /** * zpool upgrade [-v] * * zpool upgrade [-V version] -a | pool ... * * @param {*} pool */ upgrade: function (pool) { return new Promise((resolve, reject) => { let args = []; args.push("upgrade"); if (options.version) args = args.concat(["-V", options.version]); if (options.all) args.push("-a"); if (pool) { if (Array.isArray(pool)) { pool.forEach((item) => { args.push(item); }); } else { args.push(pool); } } zb.exec( zb.options.paths.zpool, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(stderr); return resolve(stdout); } ); }); }, }; zb.zfs = { /** * zfs create [-pu] [-o property=value]... filesystem * zfs create [-ps] [-b blocksize] [-o property=value]... -V size volume * * @param {*} dataset * @param {*} options */ create: function (dataset, options = {}) { if (!(arguments.length >= 1)) throw new (Error("Invalid arguments"))(); return new Promise((resolve, reject) => { const idempotent = "idempotent" in options ? options.idempotent : "idempotent" in zb.options ? zb.options.idempotent : false; let args = []; args.push("create"); if (options.parents) args.push("-p"); if (options.unmounted) args.push("-u"); if (options.blocksize) args = args.concat(["-b", options.blocksize]); if (options.properties) { for (const [key, value] of Object.entries(options.properties)) { args.push("-o"); args.push(`${key}=${value}`); } } if (options.size) args = args.concat(["-V", options.size]); args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if ( error && !(idempotent && stderr.includes("dataset already exists")) ) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, /** * zfs destroy [-fnpRrv] filesystem|volume * zfs destroy [-dnpRrv] snapshot[%snapname][,...] * zfs destroy filesystem|volume#bookmark * * * @param {*} dataset * @param {*} options */ destroy: function (dataset, options = {}) { if (!(arguments.length >= 1)) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { const idempotent = "idempotent" in options ? options.idempotent : "idempotent" in zb.options ? zb.options.idempotent : false; let args = []; args.push("destroy"); if (!("parseable" in options)) options.parseable = true; if (options.recurse) args.push("-r"); if (options.dependents) args.push("-R"); if (options.force) args.push("-f"); if (options.noop) args.push("-n"); if (options.parseable) args.push("-p"); if (options.verbose) args.push("-v"); if (options.defer) args.push("-d"); args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if ( error && !( idempotent && (stderr.includes("dataset does not exist") || stderr.includes("could not find any snapshots to destroy")) ) ) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, /** * zfs snapshot|snap [-r] [-o property=value]... * filesystem@snapname|volume@snapname * filesystem@snapname|volume@snapname... * * @param {*} dataset * @param {*} options */ snapshot: function (dataset, options = {}) { if (!(arguments.length >= 1)) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { const idempotent = "idempotent" in options ? options.idempotent : "idempotent" in zb.options ? zb.options.idempotent : false; let args = []; args.push("snapshot"); if (options.recurse) args.push("-r"); if (options.properties) { for (const [key, value] of Object.entries(options.properties)) { args.push("-o"); args.push(`${key}=${value}`); } } if (Array.isArray(dataset)) { dataset = dataset.join(" "); } args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if ( error && !(idempotent && stderr.includes("dataset already exists")) ) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, /** * zfs rollback [-rRf] snapshot * * @param {*} dataset * @param {*} options */ rollback: function (dataset, options = {}) { if (!(arguments.length >= 1)) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { let args = []; args.push("rollback"); if (options.recent) args.push("-r"); if (options.dependents) args.push("-R"); if (options.force) args.push("-f"); args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { /** * cannot rollback to 'foo/bar/baz@foobar': more recent snapshots or bookmarks exist * use '-r' to force deletion of the following snapshots and bookmarks: */ if (error) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, /** * zfs clone [-p] [-o property=value]... snapshot filesystem|volume * * @param {*} snapshot * @param {*} dataset * @param {*} options */ clone: function (snapshot, dataset, options = {}) { if (!(arguments.length >= 2)) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { const idempotent = "idempotent" in options ? options.idempotent : "idempotent" in zb.options ? zb.options.idempotent : false; let args = []; args.push("clone"); if (options.parents) args.push("-p"); if (options.properties) { for (const [key, value] of Object.entries(options.properties)) { args.push("-o"); args.push(`${key}=${value}`); } } args.push(snapshot); args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if ( error && !(idempotent && stderr.includes("dataset already exists")) ) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, /** * /bin/sh -c "zfs send [] | zfs receive [] * * @param {*} source * @param {*} send_options * @param {*} target * @param {*} receive_options */ send_receive(source, send_options = [], target, receive_options = []) { if (arguments.length < 4) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { let args = ["-c"]; let command = []; command = command.concat(["zfs", "send"]); command = command.concat(send_options); command.push(source); command.push("|"); command = command.concat(["zfs", "receive"]); command = command.concat(receive_options); command.push(target); args.push("'" + command.join(" ") + "'"); zb.exec("/bin/sh", args, { timeout: zb.options.timeout }, function ( error, stdout, stderr ) { if (error) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); }); }); }, /** * zfs promote clone-filesystem * * @param {*} dataset */ promote: function (dataset) { if (arguments.length != 1) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { let args = []; args.push("promote"); args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, /** * zfs rename [-f] filesystem|volume|snapshot filesystem|volume|snapshot * zfs rename [-f] -p filesystem|volume filesystem|volume * zfs rename -u [-p] filesystem filesystem * zfs rename -r snapshot snapshot * * @param {*} source * @param {*} target * @param {*} options */ rename: function (source, target, options = {}) { if (!(arguments.length >= 2)) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { let args = []; args.push("rename"); if (options.parents) args.push("-p"); if (options.unmounted) args.push("-u"); if (options.force) args.push("-f"); if (options.recurse) args.push("-r"); args.push(source); args.push(target); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, /** * zfs list [-r|-d depth] [-Hp] [-o property[,property]...] [-t * type[,type]...] [-s property]... [-S property]... * filesystem|volume|snapshot... * * @param {*} dataset * @param {*} options */ list: function (dataset, properties, options = {}) { if (!(arguments.length >= 1)) throw Error("Invalid arguments"); if (!properties) properties = zb.DEFAULT_ZFS_LIST_PROPERTIES; return new Promise((resolve, reject) => { let args = []; args.push("list"); if (!("parse" in options)) options.parse = true; if (!("parseable" in options)) options.parsable = true; if (options.recurse) args.push("-r"); if (options.depth) args = args.concat(["-d", options.depth]); if (options.parseable || options.parse) args.push("-Hp"); if (options.types) { let types; if (Array.isArray(options.types)) { types = options.types.join(","); } else { types = options.types; } args = args.concat(["-t", types]); } if (properties) { if (Array.isArray(properties)) { if (properties.length == 0) { properties = zb.DEFAULT_ZFS_LIST_PROPERTIES; } args.push("-o"); args.push(properties.join(",")); } else { args.push("-o"); args.push(properties); } } args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(zb.helpers.zfsError(error, stderr)); if (options.parse) { let data = zb.helpers.parseTabSeperatedTable(stdout); let indexed = zb.helpers.listTableToPropertyList( properties, data ); return resolve({ properties, data, indexed, }); } return resolve({ properties, data: stdout }); } ); }); }, /** * zfs set property=value [property=value]... filesystem|volume|snapshot * * @param {*} dataset * @param {*} properties */ set: function (dataset, properties) { if (arguments.length != 2) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { if (!Object.keys(properties).length) { resolve(); return; } let args = []; args.push("set"); if (properties) { for (const [key, value] of Object.entries(properties)) { args.push(`${key}=${value}`); } } args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, /** * zfs get [-r|-d depth] [-Hp] [-o all | field[,field]...] [-t * type[,type]...] [-s source[,source]...] all | property[,property]... * filesystem|volume|snapshot|bookmark... * * -o options: name,property,value,received,source - default name,property,value,source * -t options: filesystem, snapshot, volume - default all * -s options: local,default,inherited,temporary,received,none - default all * * @param {*} dataset * @param {*} properties */ get: function (dataset, properties = "all", options = {}) { if (!(arguments.length >= 2)) throw Error("Invalid arguments"); if (!properties) properties = "all"; if (Array.isArray(properties) && !properties.length > 0) properties = "all"; return new Promise((resolve, reject) => { let args = []; args.push("get"); if (!("parse" in options)) options.parse = true; if (!("parseable" in options)) options.parsable = true; if (options.recurse) args.push("-r"); if (options.depth) args.concat(["-d", options.depth]); if (options.parseable || options.parse) args.push("-Hp"); if (options.parse) args = args.concat([ "-o", ["name", "property", "value", "received", "source"], ]); if (options.fields && !options.parse) { let fields; if (Array.isArray(options.fields)) { fields = options.fields.join(","); } else { fields = options.fields; } args = args.concat(["-o", fields]); } if (options.types) { let types; if (Array.isArray(options.types)) { types = options.types.join(","); } else { types = options.types; } args = args.concat(["-t", types]); } if (options.sources) { let sources; if (Array.isArray(options.sources)) { sources = options.sources.join(","); } else { sources = options.sources; } args = args.concat(["-s", sources]); } if (properties) { if (Array.isArray(properties)) { if (properties.length > 0) { args.push(properties.join(",")); } else { args.push("all"); } } else { args.push(properties); } } else { args.push("all"); } args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(zb.helpers.zfsError(error, stderr)); if (options.parse) { return resolve(zb.helpers.parsePropertyList(stdout)); } return resolve(stdout); } ); }); }, /** * zfs inherit [-rS] property filesystem|volume|snapshot... * * @param {*} dataset * @param {*} property */ inherit: function (dataset, property) { if (arguments.length != 2) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { let args = []; args.push("inherit"); if (options.recurse) args.push("-r"); if (options.received) args.push("-S"); args.push(property); args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, /** * zfs remap filesystem|volume * * @param {*} dataset */ remap: function (dataset) { if (arguments.length != 1) throw Error("Invalid arguments"); return new Promise((resolve, reject) => { let args = []; args.push("remap"); args.push(dataset); zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, /** * zfs upgrade [-v] * zfs upgrade [-r] [-V version] -a | filesystem * * @param {*} dataset */ upgrade: function (options = {}, dataset) { return new Promise((resolve, reject) => { let args = []; args.push("upgrade"); if (options.versions) args.push("-v"); if (options.recurse) args.push("-r"); if (options.version) args = args.concat(["-V", options.version]); if (options.all) args = args.push("-a"); if (dataset) { args.push(dataset); } zb.exec( zb.options.paths.zfs, args, { timeout: zb.options.timeout }, function (error, stdout, stderr) { if (error) return reject(zb.helpers.zfsError(error, stderr)); return resolve(stdout); } ); }); }, }; } /** * Should be a matching interface for spawn roughly * */ exec() { const zb = this; let command = arguments[0]; let args, options, callback, timeout; let stdout = ""; let stderr = ""; switch (arguments.length) { case 1: break; case 2: callback = arguments[arguments.length - 1]; break; case 3: callback = arguments[arguments.length - 1]; args = arguments[arguments.length - 2]; break; case 4: callback = arguments[arguments.length - 1]; options = arguments[arguments.length - 2]; args = arguments[arguments.length - 3]; break; } if (zb.options.chroot) { args = args || []; args.unshift(command); args.unshift(zb.options.chroot); command = zb.options.paths.chroot; } if (zb.options.sudo) { args = args || []; args.unshift(command); command = zb.options.paths.sudo; } const child = zb.options.executor.spawn(command, args, options); let didTimeout = false; if (options && options.timeout) { timeout = setTimeout(() => { didTimeout = true; child.kill(options.killSignal || "SIGTERM"); }, options.timeout); } if (callback) { child.stdout.on("data", function (data) { stdout = stdout + data; }); child.stderr.on("data", function (data) { stderr = stderr + data; }); child.on("close", function (error) { if (timeout) { clearTimeout(timeout); } if (error) { if (didTimeout) { error.killed = true; } callback(zb.helpers.zfsError(error, stderr), stdout, stderr); } callback(null, stdout, stderr); }); } return child; } } exports.Zetabyte = Zetabyte; class ZfsSshProcessManager { constructor(client) { this.client = client; } /** * Build a command line from the name and given args * TODO: escape the arguments * * @param {*} name * @param {*} args */ buildCommand(name, args = []) { args.unshift(name); return args.join(" "); } /** * https://nodejs.org/api/child_process.html#child_process_child_process_spawn_command_args_options * * should return something similar to a child_process that handles the following: * - child.stdout.on('data') * - child.stderr.on('data') * - child.on('close') * - child.kill() */ spawn() { const client = this.client; //client.debug("ZfsProcessManager spawn", this); // Create an eventEmitter object var stdout = new events.EventEmitter(); var stderr = new events.EventEmitter(); var proxy = new events.EventEmitter(); proxy.stdout = stdout; proxy.stderr = stderr; proxy.kill = function (signal = "SIGTERM") { proxy.emit("kill", signal); }; const command = this.buildCommand(arguments[0], arguments[1]); client.debug("ZfsProcessManager arguments: " + JSON.stringify(arguments)); client.logger.verbose("ZfsProcessManager command: " + command); client.exec(command, {}, proxy).catch((err) => { proxy.stderr.emit("data", err.message); proxy.emit("close", 1, "SIGQUIT"); }); return proxy; } } exports.ZfsSshProcessManager = ZfsSshProcessManager;