You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

239 lines
8.5 KiB
JavaScript

"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?