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