Move drives API to sysquery module

feature/node-rewrite
Sven Slootweg 10 months ago
parent 28e61a0d83
commit 146f94bf65

@ -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;
}
};

@ -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 });
});
}
}
}
};

@ -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