Move drives API to sysquery module
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,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 });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue