diff --git a/src/api/data-sources/index.js b/src/api/data-sources/index.js deleted file mode 100644 index 17bd93b..0000000 --- a/src/api/data-sources/index.js +++ /dev/null @@ -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) - ]; - }); -}; diff --git a/src/api/index.js b/src/api/index.js index a18eed9..fce9f72 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -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: [], diff --git a/src/api/test.js b/src/api/test.js deleted file mode 100644 index a67f0a9..0000000 --- a/src/api/test.js +++ /dev/null @@ -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 }); -}); diff --git a/src/api/types/drive.js b/src/api/types/drive.js deleted file mode 100644 index 1bff992..0000000 --- a/src/api/types/drive.js +++ /dev/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"; - } - } - } - } - }); -}; diff --git a/src/api/types/index.js b/src/api/types/index.js deleted file mode 100644 index 57831e9..0000000 --- a/src/api/types/index.js +++ /dev/null @@ -1,5 +0,0 @@ -"use strict"; - -Object.assign(module.exports, { - // Drive: require("./drive"), -}); diff --git a/src/packages/evaluate-single/index.js b/src/packages/evaluate-single/index.js new file mode 100644 index 0000000..b244aa3 --- /dev/null +++ b/src/packages/evaluate-single/index.js @@ -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; + } + }); + }; +}; diff --git a/src/packages/map-flat/index.js b/src/packages/map-flat/index.js new file mode 100644 index 0000000..2c85bbc --- /dev/null +++ b/src/packages/map-flat/index.js @@ -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; + } +}; diff --git a/src/packages/sysquery-drives/index.js b/src/packages/sysquery-drives/index.js new file mode 100644 index 0000000..42470f3 --- /dev/null +++ b/src/packages/sysquery-drives/index.js @@ -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 }); + }); + } + } + } +}; diff --git a/try/api.js b/try/api.js index 29685b2..26a9954 100644 --- a/try/api.js +++ b/try/api.js @@ -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();