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.
244 lines
8.6 KiB
JavaScript
244 lines
8.6 KiB
JavaScript
"use strict";
|
|
|
|
const Promise = require("bluebird");
|
|
const { chain } = require("error-chain");
|
|
const execBinary = require("../exec-binary");
|
|
const parseIECBytes = require("../parse-bytes-iec");
|
|
|
|
const errors = require("./errors");
|
|
|
|
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);
|
|
} else {
|
|
return false;
|
|
}
|
|
};
|
|
}
|
|
|
|
// FIXME: Convert to new execBinary API
|
|
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 chain(error, errors.InvalidPath, `Specified device '${devicePath}' does not exist`, {
|
|
path: devicePath
|
|
});
|
|
}).catch(hasFlag("partitionTableExists"), (error) => {
|
|
throw chain(error, errors.PartitionExists, `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 chain(error, errors.InvalidPath, `Specified device '${devicePath}' does not exist`, {
|
|
path: devicePath
|
|
});
|
|
}).catch(hasFlag("notAPhysicalVolume"), (error) => {
|
|
throw chain(error, errors.InvalidPath, `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 chain(error, errors.InvalidPath, `The following specified devices do not exist: ${failedDevices.join(", ")}`, {
|
|
paths: failedDevices
|
|
});
|
|
}).catch(hasFlag("partitionTableExists"), (error) => {
|
|
let failedDevices = error.getAllContext().result.partitionTableExists;
|
|
|
|
throw chain(error, errors.PartitionExists, `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 chain(error, errors.VolumeGroupExists, `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 chain(error, errors.PhysicalVolumeInUse, `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 chain(error, errors.InvalidPath, `Specified device '${physicalVolume}' does not exist`, {
|
|
path: physicalVolume
|
|
});
|
|
}).catch(hasFlag("volumeGroupNotFound"), (error) => {
|
|
throw chain(error, errors.InvalidVolumeGroup, `Specified Volume Group '${volumeGroup}' does not exist`, {
|
|
volumeGroupName: volumeGroup
|
|
});
|
|
}).catch(hasFlag("physicalVolumeInUse"), (error) => {
|
|
let volume = error.getAllContext().result.physicalVolumeInUse;
|
|
|
|
throw chain(error, errors.PhysicalVolumeInUse, `Specified Physical Volume '${physicalVolume}' is already in use in another Volume Group (${volume.volumeGroup})`, {
|
|
volume: volume
|
|
});
|
|
}).catch(hasFlag("partitionTableExists"), (error) => {
|
|
throw chain(error, errors.PartitionExists, `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?
|