democratic-csi/src/utils/filesystem.js

637 lines
14 KiB
JavaScript

const cp = require("child_process");
const fs = require("fs");
/**
* https://github.com/kubernetes/kubernetes/tree/master/pkg/util/mount
* https://github.com/kubernetes/kubernetes/blob/master/pkg/util/mount/mount_linux.go
*/
class Filesystem {
constructor(options = {}) {
const filesystem = this;
filesystem.options = options;
options.paths = options.paths || {};
if (!options.paths.sudo) {
options.paths.sudo = "/usr/bin/sudo";
}
if (!options.timeout) {
options.timeout = 10 * 60 * 1000;
}
if (!options.executor) {
options.executor = {
spawn: cp.spawn,
};
}
}
/**
* Attempt to discover if device is a block device
*
* @param {*} device
*/
async isBlockDevice(device) {
const filesystem = this;
// nfs paths
if (!device.startsWith("/")) {
return false;
}
// smb paths
if (device.startsWith("//")) {
return false;
}
const device_path = await filesystem.realpath(device);
const blockdevices = await filesystem.getAllBlockDevices();
return blockdevices.some(async (i) => {
if ((await filesystem.realpath(i.path)) == device_path) {
return true;
}
return false;
});
}
/**
* Attempt to discover if the device is a device-mapper device
*
* @param {*} device
*/
async isDeviceMapperDevice(device) {
const filesystem = this;
const isBlock = await filesystem.isBlockDevice(device);
if (!isBlock) {
return false;
}
device = await filesystem.realpath(device);
return device.includes("dm-");
}
async isDeviceMapperSlaveDevice(device) {
const filesystem = this;
device = await filesystem.realpath(device);
}
/**
* Get all device-mapper devices (ie: dm-0, dm-1, dm-N...)
*/
async getAllDeviceMapperDevices() {
const filesystem = this;
let result;
let devices = [];
let args = [
"-c",
'for file in $(ls -la /dev/mapper/* | grep "\\->" | grep -oP "\\-> .+" | grep -oP " .+"); do echo $(F=$(echo $file | grep -oP "[a-z0-9-]+");echo $F":"$(ls "/sys/block/${F}/slaves/");); done;',
];
try {
result = await filesystem.exec("sh", args);
for (const dm of result.stdout.trim().split("\n")) {
if (dm.length < 1) {
continue;
}
devices.push("/dev/" + dm.split(":")[0].trim());
}
return devices;
} catch (err) {
throw err;
}
}
async getAllDeviceMapperSlaveDevices() {
const filesystem = this;
let result;
let args = [
"-c",
'for file in $(ls -la /dev/mapper/* | grep "\\->" | grep -oP "\\-> .+" | grep -oP " .+"); do echo $(F=$(echo $file | grep -oP "[a-z0-9-]+");echo $F":"$(ls "/sys/block/${F}/slaves/");); done;',
];
let slaves = [];
try {
result = await filesystem.exec("sh", args);
for (const dm of result.stdout.trim().split("\n")) {
if (dm.length < 1) {
continue;
}
const realDevices = dm
.split(":")[1]
.split(" ")
.map((value) => {
return "/dev/" + value.trim();
});
slaves.push(...realDevices);
}
return slaves;
} catch (err) {
throw err;
}
}
/**
* Get all slave devices connected to a device-mapper device
*
* @param {*} device
*/
async getDeviceMapperDeviceSlaves(device) {
const filesystem = this;
device = await filesystem.realpath(device);
let device_info = await filesystem.getBlockDevice(device);
const slaves = [];
let result;
let args = [`/sys/block/${device_info.kname}/slaves/`];
try {
result = await filesystem.exec("ls", args);
for (const entry of result.stdout.split("\n")) {
if (entry.trim().length < 1) {
continue;
}
slaves.push("/dev/" + entry.trim());
}
return slaves;
} catch (err) {
throw err;
}
}
async getDeviceMapperDeviceFromSlaves(slaves, matchAll = true) {
const filesystem = this;
let result;
// get mapping of dm devices to real devices
let args = [
"-c",
'for file in $(ls -la /dev/mapper/* | grep "\\->" | grep -oP "\\-> .+" | grep -oP " .+"); do echo $(F=$(echo $file | grep -oP "[a-z0-9-]+");echo $F":"$(ls "/sys/block/${F}/slaves/");); done;',
];
result = await filesystem.exec("sh", args);
for (const dm of result.stdout.trim().split("\n")) {
if (dm.length < 1) {
continue;
}
const dmDevice = "/dev/" + dm.split(":")[0].trim();
const realDevices = dm
.split(":")[1]
.split(" ")
.map((value) => {
return "/dev/" + value.trim();
});
const intersectDevices = slaves.filter((value) =>
realDevices.includes(value)
);
if (matchAll === false && intersectDevices.length > 0) {
return dmDevice;
}
// if all 3 have the same elements we have a winner
if (
intersectDevices.length == realDevices.length &&
realDevices.length == slaves.length
) {
return dmDevice;
}
}
}
/**
* create symlink
*
* @param {*} device
*/
async symlink(target, link, options = []) {
const filesystem = this;
let args = ["-s"];
args = args.concat(options);
args = args.concat([target, link]);
try {
await filesystem.exec("ln", args);
} catch (err) {
throw err;
}
}
/**
* create symlink
*
* @param {*} device
*/
async rm(options = []) {
const filesystem = this;
let args = [];
args = args.concat(options);
try {
await filesystem.exec("rm", args);
} catch (err) {
throw err;
}
}
/**
* touch a path
* @param {*} path
*/
async touch(path, options = []) {
const filesystem = this;
let args = [];
args = args.concat(options);
args.push(path);
try {
await filesystem.exec("touch", args);
} catch (err) {
throw err;
}
}
/**
* touch a path
* @param {*} path
*/
async dirname(path) {
const filesystem = this;
let args = [];
args.push(path);
let result;
try {
result = await filesystem.exec("dirname", args);
return result.stdout.trim();
} catch (err) {
throw err;
}
}
/**
* lsblk -a -b -l -J -O
*/
async getAllBlockDevices() {
const filesystem = this;
let args = ["-a", "-b", "-l", "-J", "-O"];
let result;
try {
result = await filesystem.exec("lsblk", args);
const parsed = JSON.parse(result.stdout);
return parsed.blockdevices;
} catch (err) {
throw err;
}
}
/**
* lsblk -a -b -l -J -O
*/
async getBlockDevice(device) {
const filesystem = this;
device = await filesystem.realpath(device);
let args = ["-a", "-b", "-l", "-J", "-O"];
args.push(device);
let result;
try {
result = await filesystem.exec("lsblk", args);
const parsed = JSON.parse(result.stdout);
return parsed.blockdevices[0];
} catch (err) {
throw err;
}
}
/**
* blkid -p -o export <device>
*
* @param {*} device
*/
async deviceIsFormatted(device) {
const filesystem = this;
let args = ["-p", "-o", "export", device];
let result;
try {
result = await filesystem.exec("blkid", args);
} catch (err) {
if (err.code == 2) {
return false;
}
throw err;
}
return true;
}
/**
* blkid -p -o export <device>
*
* @param {*} device
*/
async getDeviceFilesystemInfo(device) {
const filesystem = this;
let args = ["-p", "-o", "export", device];
let result;
try {
result = await filesystem.exec("blkid", args);
const entries = result.stdout.trim().split("\n");
const properties = {};
let fields, key, value;
entries.forEach((entry) => {
fields = entry.split("=");
key = fields[0].toLowerCase();
value = fields[1];
properties[key] = value;
});
return properties;
} catch (err) {
throw err;
}
}
/**
* mkfs.<fstype> [<options>] device
*
* @param {*} device
* @param {*} fstype
* @param {*} options
*/
async formatDevice(device, fstype, options = []) {
const filesystem = this;
let args = [];
args = args.concat(options);
switch (fstype) {
case "vfat":
args = args.concat(["-I"]);
break;
}
args.push(device);
let result;
try {
result = await filesystem.exec("mkfs." + fstype, args);
return result;
} catch (err) {
throw err;
}
}
async realpath(path) {
const filesystem = this;
let args = [path];
let result;
try {
result = await filesystem.exec("realpath", args);
return result.stdout.trim();
} catch (err) {
throw err;
}
}
async rescanDevice(device) {
const filesystem = this;
let result;
let device_name;
result = await filesystem.isBlockDevice(device);
if (!result) {
throw new Error(
`cannot rescan device ${device} because it is not a block device`
);
}
let is_device_mapper_device = await filesystem.isDeviceMapperDevice(device);
result = await filesystem.realpath(device);
if (is_device_mapper_device) {
// multipath -r /dev/dm-0
result = await filesystem.exec("multipath", ["-r", device]);
} else {
device_name = result.split("/").pop();
// echo 1 > /sys/block/sdb/device/rescan
const sys_file = `/sys/block/${device_name}/device/rescan`;
fs.writeFileSync(sys_file, "1");
}
}
/**
* expand a give filesystem
*
* @param {*} device
* @param {*} fstype
* @param {*} options
*/
async expandFilesystem(device, fstype, options = []) {
const filesystem = this;
let command;
let args = [];
let result;
switch (fstype.toLowerCase()) {
case "ext4":
case "ext3":
case "ext4dev":
command = "resize2fs";
args = args.concat(options);
args.push(device);
break;
case "xfs":
command = "xfs_growfs";
args = args.concat(options);
args.push(device); // in this case should be a mounted path
break;
case "vfat":
// must be unmounted
command = "fatresize";
args = args.concat(options);
args = args.concat(["-s", "max"]);
args.push(device);
break;
}
try {
result = await filesystem.exec(command, args);
return result;
} catch (err) {
throw err;
}
}
/**
* expand a give filesystem
*
* fsck [options] -- [fs-options] [<filesystem> ...]
*
* @param {*} device
* @param {*} fstype
* @param {*} options
* @param {*} fsoptions
*/
async checkFilesystem(device, fstype, options = [], fsoptions = []) {
const filesystem = this;
let command;
let args = [];
let result;
switch (fstype.toLowerCase()) {
case "ext4":
case "ext3":
case "ext4dev":
command = "fsck";
args = args.concat(options);
args.push(device);
args.push("--");
args = args.concat(fsoptions);
args.push("-f");
args.push("-p");
break;
case "xfs":
command = "xfs_repair";
args = args.concat(["-o", "force_geometry"]);
args = args.concat(options);
args.push(device);
break;
default:
command = "fsck";
args = args.concat(options);
args.push(device);
args.push("--");
args = args.concat(fsoptions);
break;
}
try {
result = await filesystem.exec(command, args);
return result;
} catch (err) {
throw err;
}
}
/**
* mkdir [<options>] <path>
*
* @param {*} path
* @param {*} options
*/
async mkdir(path, options = []) {
const filesystem = this;
let args = [];
args = args.concat(options);
args.push(path);
try {
await filesystem.exec("mkdir", args);
} catch (err) {
throw err;
}
return true;
}
/**
* rmdir [<options>] <path>
*
* @param {*} path
* @param {*} options
*/
async rmdir(path, options = []) {
const filesystem = this;
let args = [];
args = args.concat(options);
args.push(path);
try {
await filesystem.exec("rmdir", args);
} catch (err) {
throw err;
}
return true;
}
/**
*
* @param {*} path
*/
async pathExists(path) {
const filesystem = this;
let args = [];
args.push(path);
try {
await filesystem.exec("stat", args);
} catch (err) {
return false;
}
return true;
}
exec(command, args, options) {
const filesystem = this;
args = args || [];
let timeout;
let stdout = "";
let stderr = "";
if (filesystem.options.sudo) {
args.unshift(command);
command = filesystem.options.paths.sudo;
}
console.log("executing fileystem command: %s %s", command, args.join(" "));
const child = filesystem.options.executor.spawn(command, args, options);
let didTimeout = false;
if (options && options.timeout) {
timeout = setTimeout(() => {
didTimeout = true;
child.kill(options.killSignal || "SIGTERM");
}, options.timeout);
}
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 };
if (timeout) {
clearTimeout(timeout);
}
if (code) {
console.log(
"failed to execute filesystem command: %s, response: %j",
[command].concat(args).join(" "),
result
);
reject(result);
} else {
resolve(result);
}
});
});
}
}
module.exports.Filesystem = Filesystem;