From a93e3cf8dda854975923e0bb09a08c38f8e7978e Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Sat, 21 May 2022 17:43:54 +0200 Subject: [PATCH] WIP --- public/css/style.css | 6 +++ public/css/style.css.map | 2 +- .../data-sources/nvme/identify-controller.js | 13 ++++++ src/api/loaders.js | 1 + src/api/types/drive.js | 36 +++++++++++----- src/packages/dlayer-source/index.js | 28 ++++++++----- src/packages/dlayer/index.js | 16 +++---- .../controller-field-mapping.js} | 42 ++----------------- src/packages/exec-nvme-cli/index.js | 31 ++++++++++++++ src/scss/style.scss | 8 ++++ src/views/hardware/storage-devices/list.jsx | 8 +++- 11 files changed, 120 insertions(+), 71 deletions(-) create mode 100644 src/api/data-sources/nvme/identify-controller.js rename src/packages/{exec-nvme/index.js => exec-nvme-cli/controller-field-mapping.js} (67%) diff --git a/public/css/style.css b/public/css/style.css index 7ccf896..e5462f5 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -91,6 +91,9 @@ table.drives td.smart.DETERIORATING { table.drives td.smart.FAILING { background-color: rgb(230, 0, 0); } +table.drives td.smart.UNKNOWN { + background-color: rgb(177, 177, 177); +} table.drives .hasPartitions td:not(.smart), table.drives .partition:not(.last) td:not(.smart) { border-bottom-color: transparent; } @@ -119,6 +122,9 @@ table.drives th.atRisk { table.drives th.failing { color: rgb(194, 0, 0); } +table.drives th.unknown { + color: rgb(59, 59, 59); +} .stacktrace { white-space: pre-wrap; diff --git a/public/css/style.css.map b/public/css/style.css.map index 98a4f43..e1afbf4 100644 --- a/public/css/style.css.map +++ b/public/css/style.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["../../src/scss/style.scss"],"names":[],"mappings":"AAIA;EACC,kBALqB;EAMrB;EACA;;;AAGD;EACC;;;AAOD;EACC;;;AAGD;EACC,kBAtBqB;;AAwBrB;EACC;EACA;;AAGD;EACC;;AAIA;EACC;EACA;EACA;;AAIA;AAEC;EACA,kBA3CqB;EA4CrB;;AAKD;EACC;EACA;;;AAMJ;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;AAEA;EACC;EACA;EACA;EACA;;AAEA;EACC;EACA;;AAKD;EACC,kBArFmB;EAsFnB;EACA;;;AAKH;EACC;;AAEA;EACC;EACA;;AAGD;EACC;;AAGD;EACC;;;AAKD;EACC;;AAIA;EACC;;AAGD;EACC;;AAGD;EACC;;AAKD;EACC;;AAIF;EACC;EACA;;AAEA;EACC;;AAGD;EACC;;AAIF;EACC;;AAEA;EACC;;AAKD;EACC;;AAGD;EACC;;AAGD;EACC;;;AAKH;EACC;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC","file":"style.css"} \ No newline at end of file +{"version":3,"sourceRoot":"","sources":["../../src/scss/style.scss"],"names":[],"mappings":"AAIA;EACC,kBALqB;EAMrB;EACA;;;AAGD;EACC;;;AAOD;EACC;;;AAGD;EACC,kBAtBqB;;AAwBrB;EACC;EACA;;AAGD;EACC;;AAIA;EACC;EACA;EACA;;AAIA;AAEC;EACA,kBA3CqB;EA4CrB;;AAKD;EACC;EACA;;;AAMJ;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;AAEA;EACC;EACA;EACA;EACA;;AAEA;EACC;EACA;;AAKD;EACC,kBArFmB;EAsFnB;EACA;;;AAKH;EACC;;AAEA;EACC;EACA;;AAGD;EACC;;AAGD;EACC;;;AAKD;EACC;;AAIA;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;AAKD;EACC;;AAIF;EACC;EACA;;AAEA;EACC;;AAGD;EACC;;AAIF;EACC;;AAEA;EACC;;AAKD;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;;AAKH;EACC;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC","file":"style.css"} \ No newline at end of file diff --git a/src/api/data-sources/nvme/identify-controller.js b/src/api/data-sources/nvme/identify-controller.js new file mode 100644 index 0000000..ece8eac --- /dev/null +++ b/src/api/data-sources/nvme/identify-controller.js @@ -0,0 +1,13 @@ +"use strict"; + +const Promise = require("bluebird"); + +const nvmeCli = require("../../../packages/exec-nvme-cli"); + +module.exports = function () { + return function (controllerPaths) { + return Promise.map(controllerPaths, (path) => { + return nvmeCli.identifyController({ devicePath: path }); + }); + }; +}; diff --git a/src/api/loaders.js b/src/api/loaders.js index ed1cca4..9002bc8 100644 --- a/src/api/loaders.js +++ b/src/api/loaders.js @@ -12,6 +12,7 @@ let dataSourceFactories = { lvmPhysicalVolumes: require("./data-sources/lvm/physical-volumes"), lvmVolumeGroups: require("./data-sources/lvm/volume-groups"), nvmeListNamespaces: require("./data-sources/nvme/list-namespaces"), + nvmeIdentifyController: require("./data-sources/nvme/identify-controller"), }; module.exports = function createLoaders() { diff --git a/src/api/types/drive.js b/src/api/types/drive.js index 64b2966..54f4d6a 100644 --- a/src/api/types/drive.js +++ b/src/api/types/drive.js @@ -4,6 +4,7 @@ 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("./"); @@ -38,11 +39,22 @@ module.exports = function Drive ({ path }) { 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: { - lsblk: { - [dlayerSource.ID]: { path }, - size: "size" - }, smartctlScan: { [dlayerSource.ID]: path, interface: "interface" @@ -72,8 +84,10 @@ module.exports = function Drive ({ path }) { smartFunctioning: (attributes) => { return (attributes.isOK); }, - smartAttributes: (attributes) => { - if (attributes.isOK) { + smartAttributes: (attributesResult) => { + if (attributesResult.isOK) { + let attributes = attributesResult.value(); + return attributes.map((attribute) => { return { ... attribute, @@ -85,10 +99,10 @@ module.exports = function Drive ({ path }) { return []; } }, - smartHealth: (attributes) => { - if (attributes.isOK) { + smartHealth: (attributesResult) => { + if (attributesResult.isOK) { + let attributes = attributesResult.value(); // FIXME: This is getting values in an inconsistent format? Different for SATA vs. NVMe - console.log("foo", {attributes}); let failed = attributes.filter((item) => { return (item.failingNow === true || item.failedBefore === true); }); @@ -105,8 +119,8 @@ module.exports = function Drive ({ path }) { return "HEALTHY"; } } else { - // If we can't get SMART data, the only safe assumption is that it must be failing - return "FAILING"; + // We can't get SMART data + return "UNKNOWN"; } } } diff --git a/src/packages/dlayer-source/index.js b/src/packages/dlayer-source/index.js index 27bc161..8b8a467 100644 --- a/src/packages/dlayer-source/index.js +++ b/src/packages/dlayer-source/index.js @@ -9,6 +9,7 @@ const ID = Symbol("dlayer-source object ID"); const AllowErrors = Symbol("dlayer-source allow-errors marker"); // TODO: Make more readable +// TODO: Refactor allowErrors logic so that it's actually part of the internal $getProperty implementation in dlayer itself, and this abstraction uses that tool? module.exports = { withSources: function withSources(schemaObject) { @@ -35,24 +36,29 @@ module.exports = { } }).then((result) => { // console.log(`Result [${source}|${util.inspect(properties[ID])}] ${util.inspect(result)}`); + + // The AllowErrors option is set when a source definition has its own way to deal with (allowable) errors. Instead of simply propagating the error for all affected attributes, it calls the attribute handlers with the Result (or returns `undefined` if only a property is specified). // TODO: How to deal with null results? Allow them or not? Make it an option? - if (result.isError) { + if (result.isOK && result.value() == null) { + // TODO: Change implementation to allow `Result.ok(null|undefined)` but not `null|undefined` directly? + throw new Error(`Null-ish result returned for ID ${util.inspect(properties[ID])} from source '${source}'; this is not allowed, and there is probably a bug in your code. Please file a ticket if you have a good usecase for null-ish results!`); + } else if (properties[AllowErrors] === true && typeof selector !== "string") { + // Custom selectors always receive the Result as-is + return selector(result); + } else if (result.isError) { if (properties[AllowErrors] === true) { - // This option is set when a source definition has its own way to deal with (allowable) errors. Instead of simply propagating the error for all affected attributes, it calls the attribute handlers with the Result (or returns `undefined` if only a property is specified). - return (typeof selector === "string") - ? undefined - : selector(result); + return undefined; } else { // This is equivalent to a `throw`, and so we just propagate it return result; } - } else if (result.value() != null) { - // This is to support property name shorthand used in place of a selector function - return (typeof selector === "string") - ? result.value()[selector] - : selector(result.value()); } else { - throw new Error(`Null-ish result returned for ID ${util.inspect(properties[ID])} from source '${source}'; this is not allowed, and there is probably a bug in your code. Please file a ticket if you have a good usecase for null-ish results!`); + // This is to support property name shorthand used in place of a selector function + if (typeof selector === "string") { + return result.value()[selector]; + } else { + return selector(result.value()); + } } }); }; diff --git a/src/packages/dlayer/index.js b/src/packages/dlayer/index.js index 33be9cd..ae703e8 100644 --- a/src/packages/dlayer/index.js +++ b/src/packages/dlayer/index.js @@ -119,15 +119,15 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) { let { schemaKey, handler, args, isRecursive, allowErrors, isLeaf } = analyzeQueryKey(schemaObject, queryObject, queryKey); if (handler != null) { + let nextQueryPath = queryPath.concat([ queryKey ]); + let nextSchemaPath = schemaPath.concat([ schemaKey ]); + let promise = Promise.try(() => { // This calls the data provider in the schema return Result.wrapAsync(() => maybeCall(handler, [ args, context ], schemaObject)); }).then((result) => { if (result.isOK) { let value = result.value(); - - let nextQueryPath = queryPath.concat([ queryKey ]); - let nextSchemaPath = schemaPath.concat([ schemaKey ]); return Promise.try(() => { if (!isLeaf && value != null) { @@ -143,7 +143,8 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) { return Promise.try(() => { return evaluate(item, effectiveSubquery, context, elementQueryPath, elementSchemaPath); }).tapCatch((error) => { - assignErrorPath(error, elementQueryPath, elementSchemaPath); + // FIXME: Verify that this is no longer needed, since moving the path-assigning logic + // assignErrorPath(error, elementQueryPath, elementSchemaPath); }); } else { return evaluate(item, effectiveSubquery, context, nextQueryPath, nextSchemaPath); @@ -154,14 +155,12 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) { return value; } }).then((evaluated) => { + // FIXME: Verify that this is still necessary here if (allowErrors) { return Result.ok(evaluated); } else { return evaluated; } - }).tapCatch((error) => { - // FIXME: Chain properly - assignErrorPath(error, nextQueryPath, nextSchemaPath); }); } else { let error = result.error(); @@ -176,6 +175,9 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) { throw error; } } + }).tapCatch((error) => { + // FIXME: Chain properly + assignErrorPath(error, nextQueryPath, nextSchemaPath); }); return [ queryKey, promise ]; diff --git a/src/packages/exec-nvme/index.js b/src/packages/exec-nvme-cli/controller-field-mapping.js similarity index 67% rename from src/packages/exec-nvme/index.js rename to src/packages/exec-nvme-cli/controller-field-mapping.js index bd4befc..04078e7 100644 --- a/src/packages/exec-nvme/index.js +++ b/src/packages/exec-nvme-cli/controller-field-mapping.js @@ -1,15 +1,12 @@ "use strict"; -const Promise = require("bluebird"); -const execBinary = require("../exec-binary"); const { B } = require("../unit-bytes-iec"); -const createJSONParser = require("../text-parser-json"); const thirdFourthByteMask = parseInt("11111111111111110000000000000000", 2); const secondByteMask = parseInt("00000000000000001111111100000000", 2); const firstByteMask = parseInt("00000000000000000000000011111111", 2); -const fieldMapping = { +module.exports = { vid: "vendorID", ssvi: "subsystemVendorID", sn: { name: "serialNumber", transform: (string) => string.trim() }, @@ -83,8 +80,8 @@ const fieldMapping = { // mtfa, // hmpre, // hmmin, - tnvmcap: { name: "totalSpace", transformer: (value) => B(value) }, - unvmcap: { name: "freeSpace", transformer: (value) => B(value) }, + tnvmcap: { name: "totalSpace", transform: (value) => B(value) }, + unvmcap: { name: "freeSpace", transform: (value) => B(value) }, // TOOD: // rpmbs, // edstt, @@ -129,36 +126,3 @@ const fieldMapping = { // ofcs, // psds }; - -module.exports = { - identifyController: function (path) { - return Promise.try(() => { - return execBinary([ "nvme", "id-ctrl" ], [ path ]) - .asRoot() - .withFlags({ "output-format": "json" }) - .requireOnStderr(createJSONParser()) - .execute(); - }).then((output) => { - let result = {}; - - for (let key of Object.keys(output)) { - let mapping = fieldMapping[key]; - - if (mapping != null) { - let { name, transform } = (typeof mapping === "string") - ? { name: mapping, transform: (value) => value } - : { name: mapping.name, transform: mapping.transform }; - - result[name] = transform(output[key]); - } - } - - // TODO: Warn on unrecognized keys - return result; - }).catch((error) => { - console.dir(error); - }); - } -}; - -// module.exports.identifyController("/dev/nvme0").then((result) => console.dir(result, { depth: null })) diff --git a/src/packages/exec-nvme-cli/index.js b/src/packages/exec-nvme-cli/index.js index 7a3f91c..f57fb63 100644 --- a/src/packages/exec-nvme-cli/index.js +++ b/src/packages/exec-nvme-cli/index.js @@ -3,6 +3,9 @@ const Promise = require("bluebird"); const execAll = require("execall"); const execBinary = require("../exec-binary"); +const createJSONParser = require("../text-parser-json"); + +const controllerFieldMapping = require("./controller-field-mapping"); function createNamespaceParser() { return { @@ -38,5 +41,33 @@ module.exports = { }).then((output) => { return output.result.namespaces; }); + }, + identifyController: function ({ devicePath }) { + return Promise.try(() => { + return execBinary([ "nvme", "id-ctrl" ], [ devicePath ]) + .asRoot() + .withFlags({ "output-format": "json" }) + .requireOnStdout(createJSONParser()) + .execute(); + }).then(({ result }) => { + let transformed = {}; + + for (let key of Object.keys(result)) { + let mapping = controllerFieldMapping[key]; + + if (mapping != null) { + let { name, transform } = (typeof mapping === "string") + ? { name: mapping, transform: (value) => value } + : { name: mapping.name, transform: mapping.transform }; + + transformed[name] = transform(result[key]); + } + } + + // TODO: Warn on unrecognized keys + return transformed; + // }).catch((error) => { + // console.dir(error); + }); } }; diff --git a/src/scss/style.scss b/src/scss/style.scss index ec9cae1..9aa306f 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -124,6 +124,10 @@ table.drives { &.FAILING { background-color: rgb(230, 0, 0); } + + &.UNKNOWN { + background-color: rgb(177, 177, 177); + } } .hasPartitions, .partition:not(.last) { @@ -165,6 +169,10 @@ table.drives { &.failing { color: rgb(194, 0, 0); } + + &.unknown { + color: rgb(59, 59, 59); + } } } diff --git a/src/views/hardware/storage-devices/list.jsx b/src/views/hardware/storage-devices/list.jsx index 0e2c776..5b5c3ee 100644 --- a/src/views/hardware/storage-devices/list.jsx +++ b/src/views/hardware/storage-devices/list.jsx @@ -51,7 +51,7 @@ function PartitionEntry({partition, isLast}) { - {partition.size.toString()} + {partition.size.toDisplay(2).toString()} @@ -139,12 +139,13 @@ module.exports = { } }, template: function StorageDeviceList({data}) { - let drivesByStatus = splitFilterN(data.hardware.drives, [ "HEALTHY", "DETERIORATING", "FAILING" ], (drive) => drive.smartHealth); + let drivesByStatus = splitFilterN(data.hardware.drives, [ "HEALTHY", "DETERIORATING", "FAILING", "UNKNOWN" ], (drive) => drive.smartHealth); let totalStorage = sumDriveSizes(data.hardware.drives); let totalHealthyStorage = sumDriveSizes(drivesByStatus.HEALTHY); let totalAtRiskStorage = sumDriveSizes(drivesByStatus.DETERIORATING); let totalFailingStorage = sumDriveSizes(drivesByStatus.FAILING); + let totalUnknownStorage = sumDriveSizes(drivesByStatus.UNKNOWN); return ( @@ -172,6 +173,9 @@ module.exports = { {totalFailingStorage.toDisplay(2).toString()} + + {totalUnknownStorage.toDisplay(2).toString()} + );