Compare commits

...

6 Commits

@ -1,66 +0,0 @@
"use strict";
const Promise = require("bluebird");
const DataLoader = require("dataloader");
const mapObj = require("map-obj");
const lvm = require("../../packages/exec-lvm");
const All = require("../../packages/graphql-interface/symbols/all");
const nvmeCLI = require("../../packages/exec-nvme-cli");
const smartctl = require("../../packages/exec-smartctl");
const dlayerWrap = require("../../packages/dlayer-wrap");
const evaluateAndPick = require("../../packages/evaluate-and-pick");
function makeSingleCommand({ command, selectResult }) {
return function (ids) {
return Promise.map(ids, (id) => {
if (id === All) {
// FIXME: Have some sort of mechanism for making this possible?
throw new Error(`This data source does not support fetching all entries`);
} else {
return command(id);
}
}).map((result) => {
if (selectResult != null) {
return selectResult(result);
} else {
return result;
}
});
};
}
module.exports = function createSources() {
let sources = {
nvmeIdentifyController: makeSingleCommand({
command: (path) => nvmeCLI.identifyController({ devicePath: path })
}),
nvmeListNamespaces: makeSingleCommand({
command: (path) => nvmeCLI.listNamespaces({ devicePath: path })
}),
smartctlScan: evaluateAndPick({
command: smartctl.scan,
selectID: (device) => device.path
}),
smartctlInfo: makeSingleCommand({
command: (path) => dlayerWrap(() => smartctl.info({ devicePath: path }), {
allowedErrors: [ smartctl.InfoError ]
})
}),
smartctlAttributes: makeSingleCommand({
command: (path) => dlayerWrap(() => smartctl.attributes({ devicePath: path }), {
allowedErrors: [ smartctl.AttributesError ]
})
}),
};
// TODO: Consider moving these to be inline as well, somehow
let factoryModules = {};
return mapObj({ ... factoryModules, ... sources }, (name, factory) => {
return [
name,
new DataLoader(factory)
];
});
};

@ -1,27 +1,20 @@
"use strict";
const dlayer = require("dlayer");
const loaders = require("./data-sources");
module.exports = function () {
return dlayer({
makeContext: function () {
return {
sources: loaders()
};
return {};
},
modules: [
require("../packages/sysquery-core"),
require("../packages/sysquery-block-devices"),
require("../packages/sysquery-mounts"),
require("../packages/sysquery-drives"),
require("../packages/sysquery-lvm"),
],
schema: {
hardware: {
// drives: ({ paths }, { sources }) => {
// return typeFromSource(sources.smartctlScan, paths, (device) => types.Drive({ path: device.path }));
// }
},
resources: {
images: {
installationMedia: [],

@ -1,38 +0,0 @@
"use strict";
const Promise = require("bluebird");
const api = require("./");
const loaders = require("./data-sources");
return Promise.try(() => {
return api.query({
hardware: {
drives: {
model: true,
size: true,
interface: true,
smartHealth: true,
blockDevice: {
name: true,
path: true,
type: true,
children: {
name: true,
path: true,
type: true,
mounts: {
mountpoint: true,
filesystem: true,
totalSpace: true
}
}
}
// allBlockDevices
}
}
});
}).then((result) => {
console.dir(result, { depth: null });
}).catch((error) => {
console.dir(error, { depth: null });
});

@ -1,129 +0,0 @@
"use strict";
const Promise = require("bluebird");
const dlayerSource = require("../../packages/dlayer-source");
const treecutter = require("../../packages/treecutter");
const upperSnakeCase = require("../../packages/upper-snake-case");
const { B } = require("../../packages/unit-bytes-iec");
const types = require(".");
module.exports = function Drive ({ path }) {
return dlayerSource.withSources({
path: path,
blockDevice: async function(_, { $getProperty }) {
if (await $getProperty(this, "interface") === "nvme") {
return null;
} else {
return types.BlockDevice({ path: path });
}
},
allBlockDevices: async function(_, { $getProperty, sources }) {
return Promise.try(async () => {
if (await $getProperty(this, "interface") === "nvme") {
return Promise.try(() => {
return sources.nvmeListNamespaces.load(path);
}).map((namespaceID) => {
return `${path}n${namespaceID}`;
});
} else {
return [ path ];
}
}).then((rootPaths) => {
let queries = rootPaths.map((path) => ({ path: path }));
return sources.lsblk.loadMany(queries);
}).map((blockDeviceTree) => {
return treecutter.map(blockDeviceTree, (device) => types.BlockDevice(device));
}).then((resultArray) => {
// Treecutter always returns an array, regardless of whether the input was an array or not, so we need to flatten it since we will only ever have a single root entry per rootPath query here
return resultArray.flat();
});
},
size: async function (_, { $getProperty, sources }) {
if (await $getProperty(this, "interface") === "nvme") {
return Promise.try(() => {
return sources.nvmeIdentifyController.load(path);
}).then((result) => {
return result.totalSpace;
});
} else {
return Promise.try(() => {
return sources.lsblk.load({ path: path });
}).then((result) => {
return result.size;
});
}
},
$sources: {
smartctlScan: {
[dlayerSource.ID]: path,
interface: "interface"
},
smartctlInfo: {
[dlayerSource.ID]: path,
// NOTE: We allow allowable errors here because the SMART subsystem failing doesn't affect any other aspect of the drive's information, so the Drive object as a whole should not yield an error
[dlayerSource.AllowErrors]: true,
model: "model",
modelFamily: "modelFamily",
smartAvailable: "smartAvailable",
smartEnabled: "smartEnabled",
serialNumber: "serialNumber",
wwn: "wwn",
firmwareVersion: "firmwareVersion",
// size: "size",
rpm: "rpm",
logicalSectorSize: (device) => device.sectorSizes.logical,
physicalSectorSize: (device) => device.sectorSizes.physical,
formFactor: "formFactor",
ataVersion: "ataVersion",
sataVersion: "sataVersion"
},
smartctlAttributes: {
[dlayerSource.ID]: path,
[dlayerSource.AllowErrors]: true,
smartFunctioning: (attributes) => {
return (attributes.isOK);
},
smartAttributes: (attributesResult) => {
if (attributesResult.isOK) {
let attributes = attributesResult.value();
return attributes.map((attribute) => {
return {
... attribute,
type: upperSnakeCase(attribute.type),
updatedWhen: upperSnakeCase(attribute.updatedWhen)
};
});
} else {
return [];
}
},
smartHealth: (attributesResult) => {
if (attributesResult.isOK) {
let attributes = attributesResult.value();
// FIXME: This is getting values in an inconsistent format? Different for SATA vs. NVMe
let failed = attributes.filter((item) => {
return (item.failingNow === true || item.failedBefore === true);
});
let deteriorating = attributes.filter((item) => {
return (item.type === "preFail" && item.worstValueSeen < 100);
});
if (failed.length > 0) {
return "FAILING";
} else if (deteriorating.length > 0) {
return "DETERIORATING";
} else {
return "HEALTHY";
}
} else {
// We can't get SMART data
return "UNKNOWN";
}
}
}
}
});
};

@ -1,5 +0,0 @@
"use strict";
Object.assign(module.exports, {
// Drive: require("./drive"),
});

@ -0,0 +1,20 @@
"use strict";
const mapAsync = require("../map-async");
const All = require("../graphql-interface/symbols/all");
module.exports = function evaluateSingle({ command, selectResult }) {
return function (ids) {
return mapAsync(ids, async (id) => {
if (id === All) {
throw new Error(`This data source does not support fetching all entries`);
} else {
let result = await command(id);
return (selectResult != null)
? selectResult(result)
: result;
}
});
};
};

@ -0,0 +1,23 @@
"use strict";
module.exports = function mapFlat(iterable, mapper) {
if (Array.isArray(iterable)) {
// This may have an optimized implementation
return iterable.flatMap(mapper);
} else {
let result = [];
let i = 0;
for (let item of iterable) {
let resultItems = mapper(item, i, iterable);
for (let resultItem of resultItems) {
result.push(resultItem);
}
i++;
}
return result;
}
};

@ -4,6 +4,7 @@ const DataLoader = require("dataloader");
const fs = require("fs").promises;
const matchValue = require("match-value");
const memoizee = require("memoizee");
const { InvalidObject } = require("dlayer");
const unreachable = require("@joepie91/unreachable")("@sysquery/block-devices");
const dlayerSource = require("../dlayer-source");
@ -13,6 +14,7 @@ const mapFromSource = require("../map-from-source");
const treeMapAsync = require("../tree-map-async");
const treeFind = require("../tree-find");
function makePredicate({ path, name }) {
if (path != null) {
return (device) => device.path === path;
@ -36,6 +38,7 @@ module.exports = {
});
return {
// FIXME: This is probably broken; {path,name} selectors are always unique keys!
lsblk: new DataLoader(async function (selectors) {
let blockDeviceTree = await lsblkOnce();
@ -50,46 +53,47 @@ module.exports = {
};
},
types: {
"sysquery.blockDevices.BlockDevice": function ({ name, path }) {
return {
... dlayerSource("lsblk", {
[dlayerSource.ID]: { name, path },
name: "name",
path: (device) => fs.realpath(device.path),
type: (device) => matchValue(device.type, {
partition: "PARTITION",
disk: "DISK",
loopDevice: "LOOP_DEVICE"
}),
size: "size",
mountpoint: "mountpoint", // FIXME: Isn't this obsoleted by `mounts`?
deviceNumber: "deviceNumber",
removable: "removable",
readOnly: "readOnly",
children: (device, { $make }) => device.children.map((child) => {
return $make("sysquery.blockDevices.BlockDevice", { name: child.name });
"sysquery.blockDevices.BlockDevice": async function ({ name, path }, { lsblk }) {
let entry = await lsblk.load({ name, path });
if (entry != null) {
return {
... dlayerSource("lsblk", {
[dlayerSource.ID]: { name, path },
name: "name",
path: (device) => fs.realpath(device.path),
type: (device) => matchValue(device.type, {
partition: "PARTITION",
disk: "DISK",
loopDevice: "LOOP_DEVICE"
}),
size: "size",
mountpoint: "mountpoint", // FIXME: Isn't this obsoleted by `mounts`?
deviceNumber: "deviceNumber",
removable: "removable",
readOnly: "readOnly",
children: (device, { $make }) => device.children.map((child) => {
return $make("sysquery.blockDevices.BlockDevice", { name: child.name });
})
})
})
};
};
} else {
return InvalidObject;
}
}
},
extensions: {
"sysquery.mounts.Mount": {
sourceDevice: async function (_, { lsblk, $make, $getProperty }) {
sourceDevice: async function (_, { $make, $getProperty }) {
let sourceDevicePath = await $getProperty(this, "sourceDevicePath");
if (sourceDevicePath == null) {
// This occurs when the mount is not backed by a device, eg. an sshfs FUSE mount
return null;
} else {
let realSourcePath = await fs.realpath(sourceDevicePath);
if (await lsblk.load({ path: realSourcePath }) != null) {
return $make("sysquery.blockDevices.BlockDevice", { path: realSourcePath });
} else {
// This occurs when the `sourceDevice` is a valid device, but it is not a *block* device, eg. `/dev/fuse`
return null;
}
return $make("sysquery.blockDevices.BlockDevice", {
path: await fs.realpath(sourceDevicePath)
});
}
}
}

@ -0,0 +1,162 @@
"use strict";
const DataLoader = require("dataloader");
const dlayerSource = require("../dlayer-source");
const dlayerWrap = require("../dlayer-wrap");
const nvmeCLI = require("../exec-nvme-cli");
const smartctl = require("../exec-smartctl");
const evaluateSingle = require("../evaluate-single");
const evaluateAndPick = require("../evaluate-and-pick");
const caseSnakeUpper = require("../case-snake-upper");
const map = require("../map");
const mapFlat = require("../map-flat");
const mapFromSource = require("../map-from-source");
function generateNamespacePaths(basePath, namespaceIDs) {
return map(namespaceIDs, (namespaceID) => {
return `${basePath}n${namespaceID}`;
});
}
module.exports = {
name: "sysquery.drives",
makeContext: function () {
return {
nvmeIdentifyController: new DataLoader(evaluateSingle({
command: (path) => nvmeCLI.identifyController({ devicePath: path })
})),
nvmeListNamespaces: new DataLoader(evaluateSingle({
command: (path) => nvmeCLI.listNamespaces({ devicePath: path })
})),
smartctlScan: new DataLoader(evaluateAndPick({
command: smartctl.scan,
selectID: (device) => device.path
})),
smartctlInfo: new DataLoader(evaluateSingle({
command: (path) => dlayerWrap(() => smartctl.info({ devicePath: path }), {
allowedErrors: [ smartctl.InfoError ]
})
})),
smartctlAttributes: new DataLoader(evaluateSingle({
command: (path) => dlayerWrap(() => smartctl.attributes({ devicePath: path }), {
allowedErrors: [ smartctl.AttributesError ]
})
})),
};
},
types: {
"sysquery.drives.Drive": function ({ path }) {
return {
path: path,
blockDevice: async function(_, { $getProperty, $make }) {
// NVMe controllers do not have a single block device at the root; but rather one or more 'namespaces', which may each be block devices
if (await $getProperty(this, "interface") === "nvme") {
return null;
} else {
return $make("sysquery.blockDevices.BlockDevice", { path: path });
}
},
allBlockDevices: async function(_, { nvmeListNamespaces, $getProperty, $make }) {
let rootPaths = (await $getProperty(this, "interface") === "nvme")
? generateNamespacePaths(path, await nvmeListNamespaces.load(path))
: [ path ];
return mapFlat(rootPaths, (path) => {
return $make("sysquery.blockDevices.BlockDevice", { path: path });
});
},
size: async function (_, { nvmeIdentifyController, $getProperty, $make }) {
if (await $getProperty(this, "interface") === "nvme") {
let controllerData = await nvmeIdentifyController.load(path);
return controllerData.totalSpace;
} else {
// NOTE: We're using the BlockDevice module for this because SMART info is not (reliably) available on all controllers
// TODO: Find a better way to obtain this number, that doesn't require the blockDevices module
let blockDevice = await $make("sysquery.blockDevices.BlockDevice", { path: path });
return $getProperty(blockDevice, "size");
}
},
... dlayerSource("smartctlScan", {
[dlayerSource.ID]: path,
interface: "interface"
}),
... dlayerSource("smartctlInfo", {
[dlayerSource.ID]: path,
// NOTE: We allow allowable errors here because the SMART subsystem failing doesn't affect any other aspect of the drive's information, so the Drive object as a whole should not yield an error
[dlayerSource.AllowErrors]: true,
model: "model",
modelFamily: "modelFamily",
smartAvailable: "smartAvailable",
smartEnabled: "smartEnabled",
serialNumber: "serialNumber",
wwn: "wwn",
firmwareVersion: "firmwareVersion",
// size: "size",
rpm: "rpm",
logicalSectorSize: (device) => device.sectorSizes.logical,
physicalSectorSize: (device) => device.sectorSizes.physical,
formFactor: "formFactor",
ataVersion: "ataVersion",
sataVersion: "sataVersion"
}),
... dlayerSource("smartctlAttributes", {
[dlayerSource.ID]: path,
[dlayerSource.AllowErrors]: true,
smartFunctioning: (attributes) => {
return (attributes.isOK);
},
smartAttributes: (attributesResult) => {
if (attributesResult.isOK) {
let attributes = attributesResult.value();
return attributes.map((attribute) => {
return {
... attribute,
type: caseSnakeUpper(attribute.type),
updatedWhen: caseSnakeUpper(attribute.updatedWhen)
};
});
} else {
return [];
}
},
smartHealth: (attributesResult) => {
if (attributesResult.isOK) {
let attributes = attributesResult.value();
// FIXME: This is getting values in an inconsistent format? Different for SATA vs. NVMe
let failed = attributes.filter((item) => {
return (item.failingNow === true || item.failedBefore === true);
});
let deteriorating = attributes.filter((item) => {
return (item.type === "preFail" && item.worstValueSeen < 100);
});
if (failed.length > 0) {
return "FAILING";
} else if (deteriorating.length > 0) {
return "DETERIORATING";
} else {
return "HEALTHY";
}
} else {
// We can't get SMART data
return "UNKNOWN";
}
}
})
};
}
},
extensions: {},
root: {
hardware: {
drives: function ({ paths }, { smartctlScan, $make }) {
return mapFromSource(smartctlScan, paths, (device) => {
return $make("sysquery.drives.Drive", { path: device.path });
});
}
}
}
};

@ -10,6 +10,7 @@ const treeFilterFlatAsync = require("../tree-filter-flat-async");
const treeMapAsync = require("../tree-map-async");
const treeFind = require("../tree-find");
const All = require("../graphql-interface/symbols/all");
const mapAsync = require("../map-async");
module.exports = {
name: "sysquery.mounts",
@ -83,7 +84,8 @@ module.exports = {
let mountTree = await findmnt.load(All);
let relevantMounts = await treeFilterFlatAsync(mountTree, async (mount) => {
let mountObject = $make("sysquery.mounts.Mount", { mountpoint: mount.mountpoint });
let mountObject = await $make("sysquery.mounts.Mount", { mountpoint: mount.mountpoint });
// FIXME: Validate that this isn't an InvalidObject
// console.log({ sourceDevice: await $getProperty(mountObject, "sourceDevice") });
let sourcePath = await $getPropertyPath(mountObject, "sourceDevice.path");
let sourceName = await $getPropertyPath(mountObject, "sourceDevice.name");
@ -103,7 +105,7 @@ module.exports = {
}, { recurseFilteredSubtrees: true });
// This is a bit hacky; this approach should probably be replaced by a map-filter instead. But for now, this will do - as this all happens in the same request context, there's no real penalty to re-creating the mount objects a second time.
return relevantMounts.map((mount) => {
return mapAsync(relevantMounts, (mount) => {
return $make("sysquery.mounts.Mount", { mountpoint: mount.mountpoint });
});
}

@ -0,0 +1,35 @@
"use strict";
const isIterable = require("is-iterable");
const forEach = require("../for-each");
module.exports = function treeMap(tree, mapper, options = {}) {
let key = options.key ?? "children";
let recurseAfterMap = options.recurseAfterMap ?? false;
let results = [];
function step(subtree) {
let mapped = mapper(subtree);
results.push(mapped);
let nextChildren = (recurseAfterMap)
? mapped[key]
: subtree[key];
if (isIterable(nextChildren)) {
forEach(nextChildren, step);
} else if (nextChildren != null && typeof nextChildren === "object") {
step(nextChildren);
}
}
if (isIterable(tree)) {
forEach(tree, step);
} else {
step(tree);
}
return results;
};

@ -7,25 +7,32 @@ const map = require("../map");
module.exports = function treeMap(tree, mapper, options = {}) {
let key = options.key ?? "children";
let recurseAfterMap = options.recurseAfterMap ?? false;
function next(value) {
if (isIterable(value)) {
return map(value, (item) => step(item));
} else if (value != null && typeof value === "object") {
return step(value);
} else {
return value;
}
}
function step(subtree) {
let mapped = mapper(subtree);
let modifiedProperties = {};
if (recurseAfterMap) {
let mapped = mapper(subtree);
mapped[key] = next(mapped[key]);
return mapped;
} else {
let input = {
... subtree,
[key]: next(subtree)
};
if (isIterable(mapped[key])) {
modifiedProperties[key] = mapped[key].map((item) => step(item));
} else if (mapped[key] != null && typeof mapped[key] === "object") {
modifiedProperties[key] = step(mapped[key]);
return mapper(input);
}
// We track modified properties separately and (immutably) merge them at the end, because it's the fastest way to ensure that we don't mutate the input object under any circumstances (even after we add multi-key support)
return {
... mapped,
... modifiedProperties
};
}
return isIterable(tree)
? map(tree, step)
: step(tree);
return next(tree);
};

@ -5,39 +5,39 @@ const Promise = require("bluebird");
const errorChain = require("error-chain");
const createAPI = require("../src/api");
// const query = {
// hardware: {
// drives: {
// path: true,
// smartHealth: true,
// size: true,
// rpm: true,
// serialNumber: true,
// model: true,
// modelFamily: true,
// firmwareVersion: true,
const query = {
hardware: {
drives: {
path: true,
smartHealth: true,
size: true,
rpm: true,
serialNumber: true,
model: true,
modelFamily: true,
firmwareVersion: true,
// blockDevice: {
// name: true
// },
blockDevice: {
name: true
},
// partitions: {
// $key: "allBlockDevices",
// name: true,
// size: true,
partitions: {
$key: "allBlockDevices",
name: true,
size: true,
// mounts: {
// mountpoint: true
// },
mounts: {
mountpoint: true
},
// children: {
// $recurse: true,
// $recurseLimit: Infinity, // 3 by default
// }
// }
// }
// }
// };
children: {
$recurse: true,
$recurseLimit: Infinity, // 3 by default
}
}
}
}
};
// const query = {
// resources: {
@ -54,26 +54,26 @@ const createAPI = require("../src/api");
// }
// };
const query = {
resources: {
blockDevices: {
// $arguments: { names: ["sdb"] },
name: true,
path: true,
mounts: {
mountpoint: true,
filesystem: true,
totalSpace: true,
// children: {
// $recurse: true
// }
},
children: {
$recurse: true
}
}
}
};
// const query = {
// resources: {
// blockDevices: {
// // $arguments: { names: ["sdb"] },
// name: true,
// path: true,
// mounts: {
// mountpoint: true,
// filesystem: true,
// totalSpace: true,
// // children: {
// // $recurse: true
// // }
// },
// children: {
// $recurse: true
// }
// }
// }
// };
const api = createAPI();

Loading…
Cancel
Save