"use strict"; const Promise = require("bluebird"); const execBinary = require("../exec-binary"); const errors = require("../errors"); const parseIECBytes = require("../parse/bytes/iec"); function mapVersionTitle(title) { if (title === "LVM version") { return "lvm"; } else if (title === "Library version") { return "library"; } else if (title === "Driver version") { return "driver"; } else if (title === "Configuration") { return "configuration"; } else { throw new Error(`Unrecognized version type for LVM: ${title}`); } } function unattendedFlags(command) { /* This will answer "no" to any safety prompts, cancelling the operation if safety issues arise. */ return command.withFlags({ q: [true, true] }); } function forceFlags(command) { /* This will force-bypass safety checks, for when the administrator has indicated that they want to take the risk. */ return command.withFlags({ force: true }); } function asJson(resultMapper) { return function (command) { return command .expectJsonStdout(resultMapper) .withFlags({ reportformat: "json" }); }; } function hasFlag(flag) { return function (error) { if (error.getAllContext != null) { let context = error.getAllContext(); /* The below counts *any* kind of non-null value as having a flag set, to accommodate matchAll scenarios and scenarios where the flag needs to contain further information. */ return (context.result != null && context.result[flag] != null); } }; } module.exports = { getVersions: function () { return Promise.try(() => { return execBinary("lvm", ["version"]) .asRoot() .singleResult() .expectStdout("versions", /^\s*([^:]+):\s*(.+)$/gm, { required: true, matchAll: true, result: ([title, version]) => { return { key: mapVersionTitle(title), value: version }; } }) .execute(); }).then(({result}) => { return result.reduce((object, entry) => { return Object.assign(object, { [entry.key]: entry.value }); }, {}); }); }, getPhysicalVolumes: function () { return Promise.try(() => { return execBinary("pvs") .asRoot() .singleResult() .withModifier(asJson((result) => { return result.report[0].pv.map((volume) => { return { path: volume.pv_name, volumeGroup: (volume.vg_name === "") ? null : volume.vg_name, format: volume.pv_fmt, totalSpace: parseIECBytes(volume.pv_size), freeSpace: parseIECBytes(volume.pv_free), isDuplicate: volume.pv_attr.includes("d"), isAllocatable: volume.pv_attr.includes("a"), isUsed: volume.pv_attr.includes("u"), isExported: volume.pv_attr.includes("x"), isMissing: volume.pv_attr.includes("m"), }; }); })) .execute(); }).then((output) => { return output.result; }); }, createPhysicalVolume: function ({ devicePath, force }) { return Promise.try(() => { return execBinary("pvcreate", [devicePath]) .asRoot() .withModifier((force === true) ? forceFlags : unattendedFlags) .expectStderr("deviceNotFound", /Device .+ not found\./, { result: () => true }) .expectStderr("partitionTableExists", /WARNING: [a-z]+ signature detected on/, { result: () => true }) .execute(); }).then((_output) => { return true; }).catch(hasFlag("deviceNotFound"), (error) => { throw errors.InvalidPath.chain(error, `Specified device '${devicePath}' does not exist`, { path: devicePath }); }).catch(hasFlag("partitionTableExists"), (error) => { throw errors.PartitionExists.chain(error, `Refused to create a Physical Volume, as a partition or partition table already exists on device '${devicePath}'`, { path: devicePath }); }); }, destroyPhysicalVolume: function ({ devicePath }) { return Promise.try(() => { return execBinary("pvremove", [devicePath]) .asRoot() .atLeastOneResult() .expectStdout("success", /Labels on physical volume "[^"]+" successfully wiped\./) .expectStderr("deviceNotFound", /Device .+ not found\./, { result: () => true }) .expectStderr("notAPhysicalVolume", /No PV label found on .+\./, { result: () => true }) .execute(); }).then((_output) => { return true; }).catch(hasFlag("deviceNotFound"), (error) => { throw errors.InvalidPath.chain(error, `Specified device '${devicePath}' does not exist`, { path: devicePath }); }).catch(hasFlag("notAPhysicalVolume"), (error) => { throw errors.InvalidPath.chain(error, `Specified device '${devicePath}' is not a Physical Volume`, { path: devicePath }); }); }, createVolumeGroup: function ({ name, physicalVolumes }) { return Promise.try(() => { if (/^[a-zA-Z0-9_][a-zA-Z0-9+_.-]*$/.test(name)) { return execBinary("vgcreate", [name, ...physicalVolumes]) .asRoot() .withModifier(unattendedFlags) .expectStderr("volumeGroupExists", /A volume group called ([^"]+) already exists\./, { result: () => true }) .expectStderr("partitionTableExists", /WARNING: [a-z]+ signature detected on (.+) at offset/g, { result: ([device]) => device, matchAll: true }) .expectStderr("deviceNotFound", /Device (.+) not found\./g, { result: ([device]) => device, matchAll: true }) .expectStderr("physicalVolumeInUse", /Physical volume '([^']+)' is already in volume group '([^']+)'/g, { result: ([device, volumeGroup]) => ({device, volumeGroup}), matchAll: true }) .execute(); } else { throw new errors.InvalidName(`The specified Volume Group name '${name}' contains invalid characters`); } }).then((_output) => { return true; }).catch(hasFlag("deviceNotFound"), (error) => { let failedDevices = error.getAllContext().result.deviceNotFound; throw errors.InvalidPath.chain(error, `The following specified devices do not exist: ${failedDevices.join(", ")}`, { paths: failedDevices }); }).catch(hasFlag("partitionTableExists"), (error) => { let failedDevices = error.getAllContext().result.partitionTableExists; throw errors.PartitionExists.chain(error, `Refused to create a Volume Group, as partitions or partition tables already exist on the following devices: ${failedDevices.join(", ")}`, { paths: failedDevices }); }).catch(hasFlag("volumeGroupExists"), (error) => { throw errors.VolumeGroupExists.chain(error, `A volume group with the name '${name}' already exists`, { volumeGroupName: name }); }).catch(hasFlag("physicalVolumeInUse"), (error) => { let failedItems = error.getAllContext().result.physicalVolumeInUse; let failedItemString = failedItems.map(({device, volumeGroup}) => { return `${device} (${volumeGroup})`; }).join(", "); throw errors.PhysicalVolumeInUse.chain(error, `The following specified Physical Volumes are already in use in another Volume Group: ${failedItemString}`, { volumes: failedItems }); }); }, addVolumeToVolumeGroup: function ({ physicalVolume, volumeGroup }) { return Promise.try(() => { return execBinary("vgextend", [volumeGroup, physicalVolume]) .asRoot() .withModifier(unattendedFlags) .expectStderr("deviceNotFound", /Device .+ not found\./, { result: () => true }) .expectStderr("volumeGroupNotFound", /Volume group "[^"]+" not found/, { result: () => true }) .expectStderr("partitionTableExists", /WARNING: [a-z]+ signature detected on/, { result: () => true }) .expectStderr("physicalVolumeInUse", /Physical volume '([^']+)' is already in volume group '([^']+)'/, { result: ([device, volumeGroup]) => ({device, volumeGroup}) }) .execute(); }).then((_output) => { return true; }).catch(hasFlag("deviceNotFound"), (error) => { throw errors.InvalidPath.chain(error, `Specified device '${physicalVolume}' does not exist`, { path: physicalVolume }); }).catch(hasFlag("volumeGroupNotFound"), (error) => { throw errors.InvalidVolumeGroup.chain(error, `Specified Volume Group '${volumeGroup}' does not exist`, { volumeGroupName: volumeGroup }); }).catch(hasFlag("physicalVolumeInUse"), (error) => { let volume = error.getAllContext().result.physicalVolumeInUse; throw errors.PhysicalVolumeInUse.chain(error, `Specified Physical Volume '${physicalVolume}' is already in use in another Volume Group (${volume.volumeGroup})`, { volume: volume }); }).catch(hasFlag("partitionTableExists"), (error) => { throw errors.PartitionExists.chain(error, `Refused to add device to Volume Group, as a partition or partition table already exists on device '${physicalVolume}'`, { path: physicalVolume }); }); } }; // TODO: Need to check if cache service running?