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