feature/node-rewrite
Sven Slootweg 2 years ago
parent f08044baf3
commit a93e3cf8dd

@ -91,6 +91,9 @@ table.drives td.smart.DETERIORATING {
table.drives td.smart.FAILING { table.drives td.smart.FAILING {
background-color: rgb(230, 0, 0); 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) { table.drives .hasPartitions td:not(.smart), table.drives .partition:not(.last) td:not(.smart) {
border-bottom-color: transparent; border-bottom-color: transparent;
} }
@ -119,6 +122,9 @@ table.drives th.atRisk {
table.drives th.failing { table.drives th.failing {
color: rgb(194, 0, 0); color: rgb(194, 0, 0);
} }
table.drives th.unknown {
color: rgb(59, 59, 59);
}
.stacktrace { .stacktrace {
white-space: pre-wrap; white-space: pre-wrap;

@ -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"} {"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"}

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

@ -12,6 +12,7 @@ let dataSourceFactories = {
lvmPhysicalVolumes: require("./data-sources/lvm/physical-volumes"), lvmPhysicalVolumes: require("./data-sources/lvm/physical-volumes"),
lvmVolumeGroups: require("./data-sources/lvm/volume-groups"), lvmVolumeGroups: require("./data-sources/lvm/volume-groups"),
nvmeListNamespaces: require("./data-sources/nvme/list-namespaces"), nvmeListNamespaces: require("./data-sources/nvme/list-namespaces"),
nvmeIdentifyController: require("./data-sources/nvme/identify-controller"),
}; };
module.exports = function createLoaders() { module.exports = function createLoaders() {

@ -4,6 +4,7 @@ const Promise = require("bluebird");
const dlayerSource = require("../../packages/dlayer-source"); const dlayerSource = require("../../packages/dlayer-source");
const treecutter = require("../../packages/treecutter"); const treecutter = require("../../packages/treecutter");
const upperSnakeCase = require("../../packages/upper-snake-case"); const upperSnakeCase = require("../../packages/upper-snake-case");
const { B } = require("../../packages/unit-bytes-iec");
const types = require("./"); const types = require("./");
@ -38,11 +39,22 @@ module.exports = function Drive ({ path }) {
return resultArray.flat(); 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: { $sources: {
lsblk: {
[dlayerSource.ID]: { path },
size: "size"
},
smartctlScan: { smartctlScan: {
[dlayerSource.ID]: path, [dlayerSource.ID]: path,
interface: "interface" interface: "interface"
@ -72,8 +84,10 @@ module.exports = function Drive ({ path }) {
smartFunctioning: (attributes) => { smartFunctioning: (attributes) => {
return (attributes.isOK); return (attributes.isOK);
}, },
smartAttributes: (attributes) => { smartAttributes: (attributesResult) => {
if (attributes.isOK) { if (attributesResult.isOK) {
let attributes = attributesResult.value();
return attributes.map((attribute) => { return attributes.map((attribute) => {
return { return {
... attribute, ... attribute,
@ -85,10 +99,10 @@ module.exports = function Drive ({ path }) {
return []; return [];
} }
}, },
smartHealth: (attributes) => { smartHealth: (attributesResult) => {
if (attributes.isOK) { if (attributesResult.isOK) {
let attributes = attributesResult.value();
// FIXME: This is getting values in an inconsistent format? Different for SATA vs. NVMe // FIXME: This is getting values in an inconsistent format? Different for SATA vs. NVMe
console.log("foo", {attributes});
let failed = attributes.filter((item) => { let failed = attributes.filter((item) => {
return (item.failingNow === true || item.failedBefore === true); return (item.failingNow === true || item.failedBefore === true);
}); });
@ -105,8 +119,8 @@ module.exports = function Drive ({ path }) {
return "HEALTHY"; return "HEALTHY";
} }
} else { } else {
// If we can't get SMART data, the only safe assumption is that it must be failing // We can't get SMART data
return "FAILING"; return "UNKNOWN";
} }
} }
} }

@ -9,6 +9,7 @@ const ID = Symbol("dlayer-source object ID");
const AllowErrors = Symbol("dlayer-source allow-errors marker"); const AllowErrors = Symbol("dlayer-source allow-errors marker");
// TODO: Make more readable // 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 = { module.exports = {
withSources: function withSources(schemaObject) { withSources: function withSources(schemaObject) {
@ -35,24 +36,29 @@ module.exports = {
} }
}).then((result) => { }).then((result) => {
// console.log(`Result [${source}|${util.inspect(properties[ID])}] ${util.inspect(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? // 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) { 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 undefined;
return (typeof selector === "string")
? undefined
: selector(result);
} else { } else {
// This is equivalent to a `throw`, and so we just propagate it // This is equivalent to a `throw`, and so we just propagate it
return result; 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 { } 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());
}
} }
}); });
}; };

@ -119,15 +119,15 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
let { schemaKey, handler, args, isRecursive, allowErrors, isLeaf } = analyzeQueryKey(schemaObject, queryObject, queryKey); let { schemaKey, handler, args, isRecursive, allowErrors, isLeaf } = analyzeQueryKey(schemaObject, queryObject, queryKey);
if (handler != null) { if (handler != null) {
let nextQueryPath = queryPath.concat([ queryKey ]);
let nextSchemaPath = schemaPath.concat([ schemaKey ]);
let promise = Promise.try(() => { let promise = Promise.try(() => {
// This calls the data provider in the schema // This calls the data provider in the schema
return Result.wrapAsync(() => maybeCall(handler, [ args, context ], schemaObject)); return Result.wrapAsync(() => maybeCall(handler, [ args, context ], schemaObject));
}).then((result) => { }).then((result) => {
if (result.isOK) { if (result.isOK) {
let value = result.value(); let value = result.value();
let nextQueryPath = queryPath.concat([ queryKey ]);
let nextSchemaPath = schemaPath.concat([ schemaKey ]);
return Promise.try(() => { return Promise.try(() => {
if (!isLeaf && value != null) { if (!isLeaf && value != null) {
@ -143,7 +143,8 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
return Promise.try(() => { return Promise.try(() => {
return evaluate(item, effectiveSubquery, context, elementQueryPath, elementSchemaPath); return evaluate(item, effectiveSubquery, context, elementQueryPath, elementSchemaPath);
}).tapCatch((error) => { }).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 { } else {
return evaluate(item, effectiveSubquery, context, nextQueryPath, nextSchemaPath); return evaluate(item, effectiveSubquery, context, nextQueryPath, nextSchemaPath);
@ -154,14 +155,12 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
return value; return value;
} }
}).then((evaluated) => { }).then((evaluated) => {
// FIXME: Verify that this is still necessary here
if (allowErrors) { if (allowErrors) {
return Result.ok(evaluated); return Result.ok(evaluated);
} else { } else {
return evaluated; return evaluated;
} }
}).tapCatch((error) => {
// FIXME: Chain properly
assignErrorPath(error, nextQueryPath, nextSchemaPath);
}); });
} else { } else {
let error = result.error(); let error = result.error();
@ -176,6 +175,9 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
throw error; throw error;
} }
} }
}).tapCatch((error) => {
// FIXME: Chain properly
assignErrorPath(error, nextQueryPath, nextSchemaPath);
}); });
return [ queryKey, promise ]; return [ queryKey, promise ];

@ -1,15 +1,12 @@
"use strict"; "use strict";
const Promise = require("bluebird");
const execBinary = require("../exec-binary");
const { B } = require("../unit-bytes-iec"); const { B } = require("../unit-bytes-iec");
const createJSONParser = require("../text-parser-json");
const thirdFourthByteMask = parseInt("11111111111111110000000000000000", 2); const thirdFourthByteMask = parseInt("11111111111111110000000000000000", 2);
const secondByteMask = parseInt("00000000000000001111111100000000", 2); const secondByteMask = parseInt("00000000000000001111111100000000", 2);
const firstByteMask = parseInt("00000000000000000000000011111111", 2); const firstByteMask = parseInt("00000000000000000000000011111111", 2);
const fieldMapping = { module.exports = {
vid: "vendorID", vid: "vendorID",
ssvi: "subsystemVendorID", ssvi: "subsystemVendorID",
sn: { name: "serialNumber", transform: (string) => string.trim() }, sn: { name: "serialNumber", transform: (string) => string.trim() },
@ -83,8 +80,8 @@ const fieldMapping = {
// mtfa, // mtfa,
// hmpre, // hmpre,
// hmmin, // hmmin,
tnvmcap: { name: "totalSpace", transformer: (value) => B(value) }, tnvmcap: { name: "totalSpace", transform: (value) => B(value) },
unvmcap: { name: "freeSpace", transformer: (value) => B(value) }, unvmcap: { name: "freeSpace", transform: (value) => B(value) },
// TOOD: // TOOD:
// rpmbs, // rpmbs,
// edstt, // edstt,
@ -129,36 +126,3 @@ const fieldMapping = {
// ofcs, // ofcs,
// psds // 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 }))

@ -3,6 +3,9 @@
const Promise = require("bluebird"); const Promise = require("bluebird");
const execAll = require("execall"); const execAll = require("execall");
const execBinary = require("../exec-binary"); const execBinary = require("../exec-binary");
const createJSONParser = require("../text-parser-json");
const controllerFieldMapping = require("./controller-field-mapping");
function createNamespaceParser() { function createNamespaceParser() {
return { return {
@ -38,5 +41,33 @@ module.exports = {
}).then((output) => { }).then((output) => {
return output.result.namespaces; 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);
});
} }
}; };

@ -124,6 +124,10 @@ table.drives {
&.FAILING { &.FAILING {
background-color: rgb(230, 0, 0); background-color: rgb(230, 0, 0);
} }
&.UNKNOWN {
background-color: rgb(177, 177, 177);
}
} }
.hasPartitions, .partition:not(.last) { .hasPartitions, .partition:not(.last) {
@ -165,6 +169,10 @@ table.drives {
&.failing { &.failing {
color: rgb(194, 0, 0); color: rgb(194, 0, 0);
} }
&.unknown {
color: rgb(59, 59, 59);
}
} }
} }

@ -51,7 +51,7 @@ function PartitionEntry({partition, isLast}) {
</td> </td>
<td> <td>
<PartitionIndent> <PartitionIndent>
{partition.size.toString()} {partition.size.toDisplay(2).toString()}
</PartitionIndent> </PartitionIndent>
</td> </td>
<td colSpan={5}> <td colSpan={5}>
@ -139,12 +139,13 @@ module.exports = {
} }
}, },
template: function StorageDeviceList({data}) { 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 totalStorage = sumDriveSizes(data.hardware.drives);
let totalHealthyStorage = sumDriveSizes(drivesByStatus.HEALTHY); let totalHealthyStorage = sumDriveSizes(drivesByStatus.HEALTHY);
let totalAtRiskStorage = sumDriveSizes(drivesByStatus.DETERIORATING); let totalAtRiskStorage = sumDriveSizes(drivesByStatus.DETERIORATING);
let totalFailingStorage = sumDriveSizes(drivesByStatus.FAILING); let totalFailingStorage = sumDriveSizes(drivesByStatus.FAILING);
let totalUnknownStorage = sumDriveSizes(drivesByStatus.UNKNOWN);
return ( return (
<Layout title="Storage Devices"> <Layout title="Storage Devices">
@ -172,6 +173,9 @@ module.exports = {
<TallyRow label="Failing" rowClass="smartStatus" labelClass="failing"> <TallyRow label="Failing" rowClass="smartStatus" labelClass="failing">
{totalFailingStorage.toDisplay(2).toString()} {totalFailingStorage.toDisplay(2).toString()}
</TallyRow> </TallyRow>
<TallyRow label="Unknown" rowClass="smartStatus" labelClass="unknown">
{totalUnknownStorage.toDisplay(2).toString()}
</TallyRow>
</table> </table>
</Layout> </Layout>
); );

Loading…
Cancel
Save