From 300c58533fad54ceb27151d3235244b4bec3a4fa Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Fri, 7 Jul 2023 01:18:31 +0200 Subject: [PATCH] Move LVM schema into dlayer module --- package.json | 2 +- src/api/data-sources/index.js | 60 ++----- src/api/index.js | 34 +--- src/api/types/index.js | 3 - src/packages/dlayer-source/index.js | 8 +- src/packages/evaluate-and-pick/index.js | 36 ++++ src/packages/map-from-source/index.js | 12 ++ src/packages/sysquery-lvm/index.js | 221 ++++++++++++++++++------ yarn.lock | 8 +- 9 files changed, 247 insertions(+), 137 deletions(-) create mode 100644 src/packages/evaluate-and-pick/index.js create mode 100644 src/packages/map-from-source/index.js diff --git a/package.json b/package.json index de5e189..34cd2b6 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "debounce": "^1.0.0", "debug": "^4.1.1", "default-value": "^1.0.0", - "dlayer": "^0.1.0", + "dlayer": "^0.1.2", "dotty": "^0.1.0", "end-of-stream": "^1.1.0", "entities": "^2.0.0", diff --git a/src/api/data-sources/index.js b/src/api/data-sources/index.js index 4c9efa1..649e111 100644 --- a/src/api/data-sources/index.js +++ b/src/api/data-sources/index.js @@ -1,7 +1,6 @@ "use strict"; const Promise = require("bluebird"); -const memoizee = require("memoizee"); const DataLoader = require("dataloader"); const mapObj = require("map-obj"); @@ -10,32 +9,7 @@ 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"); - -// This generates a (memoized) source function for commands that always produce an entire list, that needs to be filtered for the desired item(s) -function makeListCommand({ command, selectResult, selectID }) { - let commandOnce = memoizee(command); - - return function (ids) { - return Promise.try(() => { - return commandOnce(); - }).then((result) => { - if (selectResult != null) { - return selectResult(result); - } else { - return result; - } - }).then((items) => { - return ids.map((id) => { - if (id === All) { - return items; - } else { - // TODO: Can this be more performant? Currently it is a nested loop - return items.find((item) => selectID(item) === id); - } - }); - }); - }; -} +const evaluateAndPick = require("../../packages/evaluate-and-pick"); function makeSingleCommand({ command, selectResult }) { return function (ids) { @@ -58,28 +32,28 @@ function makeSingleCommand({ command, selectResult }) { module.exports = function createSources() { let sources = { - lvmLogicalVolumes: makeListCommand({ - command: lvm.getLogicalVolumes, - selectResult: (result) => result.volumes, - selectID: (volume) => volume.path - }), - lvmPhysicalVolumes: makeListCommand({ - command: lvm.getPhysicalVolumes, - selectResult: (result) => result.volumes, - selectID: (device) => device.path - }), - lvmVolumeGroups: makeListCommand({ - command: lvm.getVolumeGroups, - selectResult: (result) => result.groups, - selectID: (group) => group.name - }), + // lvmLogicalVolumes: evaluateAndPick({ + // command: lvm.getLogicalVolumes, + // selectResult: (result) => result.volumes, + // selectID: (volume) => volume.path + // }), + // lvmPhysicalVolumes: evaluateAndPick({ + // command: lvm.getPhysicalVolumes, + // selectResult: (result) => result.volumes, + // selectID: (device) => device.path + // }), + // lvmVolumeGroups: evaluateAndPick({ + // command: lvm.getVolumeGroups, + // selectResult: (result) => result.groups, + // selectID: (group) => group.name + // }), nvmeIdentifyController: makeSingleCommand({ command: (path) => nvmeCLI.identifyController({ devicePath: path }) }), nvmeListNamespaces: makeSingleCommand({ command: (path) => nvmeCLI.listNamespaces({ devicePath: path }) }), - smartctlScan: makeListCommand({ + smartctlScan: evaluateAndPick({ command: smartctl.scan, selectID: (device) => device.path }), diff --git a/src/api/index.js b/src/api/index.js index a135d6b..4924255 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -31,6 +31,9 @@ module.exports = function () { sources: loaders() }; }, + modules: [ + require("../packages/sysquery-lvm") + ], schema: { hardware: { drives: ({ paths }, { sources }) => { @@ -41,37 +44,6 @@ module.exports = function () { blockDevices: ({ names }, { sources }) => { return typeFromSource(sources.lsblk, names, (device) => types.BlockDevice({ name: device.name })); }, - lvm: { - physicalVolumes: ({ paths }, { sources }) => { - return typeFromSource(sources.lvmPhysicalVolumes, paths, (volume) => types.LVMPhysicalVolume({ path: volume.path })); - }, - volumeGroups: ({ names }, { sources }) => { - return typeFromSource(sources.lvmVolumeGroups, names, (group) => types.LVMVolumeGroup({ name: group.name })); - }, - logicalVolumes: ({ paths }, { sources }) => { - return typeFromSource(sources.lvmLogicalVolumes, paths, (volume) => types.LVMPhysicalVolume({ path: volume.path })); - }, - createPhysicalVolume: (args, { sources }) => { - let { path, force } = validateValue(args, { - path: [ required, isString ], - force: [ defaultTo(false), isBoolean ] - }); - - return Promise.try(() => { - return execLVM.createPhysicalVolume({ devicePath: path, force: force }); - }).then(() => { - return types.LVMPhysicalVolume({ path }); - }); - }, - createVolumeGroup: ({ name, physicalVolumes }) => { - return Promise.try(() => { - return execLVM.createVolumeGroup({ name, physicalVolumes }); - }).then(() => { - return types.LVMVolumeGroup({ name }); - }); - } - // createLogicalVolume: , - }, images: { installationMedia: [], vmImages: [] diff --git a/src/api/types/index.js b/src/api/types/index.js index dc4dfa1..adadb7d 100644 --- a/src/api/types/index.js +++ b/src/api/types/index.js @@ -4,7 +4,4 @@ Object.assign(module.exports, { Drive: require("./drive"), BlockDevice: require("./block-device"), Mount: require("./mount"), - LVMPhysicalVolume: require("./lvm/physical-volume"), - LVMVolumeGroup: require("./lvm/volume-group"), - LVMLogicalVolume: require("./lvm/logical-volume"), }); diff --git a/src/packages/dlayer-source/index.js b/src/packages/dlayer-source/index.js index 5de77ce..10efbd4 100644 --- a/src/packages/dlayer-source/index.js +++ b/src/packages/dlayer-source/index.js @@ -22,13 +22,13 @@ module.exports = { let getter = function (_args, context) { return Promise.try(() => { if (properties[ID] != null) { - let dataSource = context.sources[source]; + let dataSource = context[source]; if (dataSource != null) { // console.log(`Calling source '${source}' with ID ${util.inspect(properties[ID])}`); return Result.wrapAsync(() => dataSource.load(properties[ID])); } else { - throw new Error(`Attempted to read from source '${source}', but no such source is registered`); + throw new Error(`Attempted to read from context property '${source}', but no such property exists`); } } else { // FIXME: Better error message @@ -41,7 +41,7 @@ module.exports = { // TODO: How to deal with null results? Allow them or not? Make it an option? 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!`); + throw new Error(`Null-ish result returned for ID ${util.inspect(properties[ID])} from source at context property '${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); @@ -57,7 +57,7 @@ module.exports = { if (typeof selector === "string") { return result.value()[selector]; } else { - return selector(result.value()); + return selector(result.value(), context); } } }); diff --git a/src/packages/evaluate-and-pick/index.js b/src/packages/evaluate-and-pick/index.js new file mode 100644 index 0000000..3abdfca --- /dev/null +++ b/src/packages/evaluate-and-pick/index.js @@ -0,0 +1,36 @@ +"use strict"; + +const Promise = require("bluebird"); +const memoizee = require("memoizee"); + +// FIXME: Figure out a reasonable way to make this symbol its own (conflict-free) package, given that it'll be used all across both sysquery and CVM +const All = require("../graphql-interface/symbols/all"); + +// This generates a (memoized) source function for commands that always produce an entire list, that needs to be filtered for the desired item(s) +module.exports = function evaluateAndPick({ command, selectResult, selectID, many }) { + let commandOnce = memoizee(command); + + return function (ids) { + return Promise.try(() => { + return commandOnce(); + }).then((result) => { + if (selectResult != null) { + return selectResult(result); + } else { + return result; + } + }).then((items) => { + return ids.map((id) => { + if (id === All) { + return items; + } else if (many === true) { + // NOTE: This produces nested arrays! One array for each input ID. + return items.filter((item) => selectID(item) === id); + } else { + // TODO: Can this be more performant? Currently it is a nested loop + return items.find((item) => selectID(item) === id); + } + }); + }); + }; +}; diff --git a/src/packages/map-from-source/index.js b/src/packages/map-from-source/index.js new file mode 100644 index 0000000..b848e2f --- /dev/null +++ b/src/packages/map-from-source/index.js @@ -0,0 +1,12 @@ +"use strict"; + +const Promise = require("bluebird"); +const All = require("../graphql-interface/symbols/all"); + +module.exports = async function mapFromSource(source, ids, mapper) { + let results = (ids === All || ids == null) + ? await source.load(All) + : await Promise.map(ids, (id) => source.load(id)); + + return results.map(mapper); +}; diff --git a/src/packages/sysquery-lvm/index.js b/src/packages/sysquery-lvm/index.js index cf55538..2526693 100644 --- a/src/packages/sysquery-lvm/index.js +++ b/src/packages/sysquery-lvm/index.js @@ -1,64 +1,183 @@ "use strict"; +const Promise = require("bluebird"); +const DataLoader = require("dataloader"); +const dlayerSource = require("../dlayer-source"); +const evaluateAndPick = require("../evaluate-and-pick"); +const mapFromSource = require("../map-from-source"); +const lvm = require("../exec-lvm"); +const All = require("../graphql-interface/symbols/all"); + module.exports = { name: "LVM", - initialize: function () { - let types = { - "sysquery.lvm.PhysicalVolume": function PhysicalVolume({ path }) { - - }, - "sysquery.lvm.VolumeGroup": function VolumeGroup({ name }) { - - }, - "sysquery.lvm.LogicalVolume": function LogicalVolume({ path }) { - - } + makeContext: function () { + return { + physicalVolumes: new DataLoader(evaluateAndPick({ + command: lvm.getPhysicalVolumes, + selectResult: (result) => result.volumes, + selectID: (device) => device.path + })), + volumeGroups: new DataLoader(evaluateAndPick({ + command: lvm.getVolumeGroups, + selectResult: (result) => result.groups, + selectID: (group) => group.name + })), + logicalVolumes: new DataLoader(evaluateAndPick({ + command: lvm.getLogicalVolumes, + selectResult: (result) => result.volumes, + selectID: (volume) => volume.path + })) }; + }, + root: { + resources: { + lvm: { + physicalVolumes: ({ paths }, { physicalVolumes, $make }) => { + return mapFromSource(physicalVolumes, paths, (volume) => { + return $make("sysquery.lvm.PhysicalVolume", { path: volume.path }); + }); + }, + volumeGroups: ({ names }, { volumeGroups, $make }) => { + return mapFromSource(volumeGroups, names, (group) => { + return $make("sysquery.lvm.VolumeGroup", { name: group.name }); + }); + }, + logicalVolumes: ({ paths }, { logicalVolumes, $make }) => { + // FIXME: Aren't these scoped to a volume group? + return mapFromSource(logicalVolumes, paths, (volume) => { + return $make("sysquery.lvm.LogicalVolume", { path: volume.path }); + }); + } + } + } + }, + extensions: { + "sysquery.core.BlockDevice": { + lvmPhysicalVolume: async function (_, { physicalVolumes, $getProperty, $make }) { + let volume = physicalVolumes.get(await $getProperty(this, "path")); - return { - sources: () => { - return { - physicalVolumes: , - volumeGroups: , - logicalVolumes: - }; - }, - types: types, - extensions: { - "sysquery.core.BlockDevice": { - lvmPhysicalVolume: function (_, { $getProperty, sources }) { - + if (volume != null) { + return $make("sysquery.lvm.PhysicalVolume", { path: volume.path }); + } else { + return null; + } + } + } + }, + types: { + "sysquery.lvm.PhysicalVolume": function PhysicalVolume({ path }) { + return dlayerSource.withSources({ + $sources: { + physicalVolumes: { + [dlayerSource.ID]: path, + path: "path", + format: "format", + totalSpace: "totalSpace", + freeSpace: "freeSpace", + isExported: "isExported", + isMissing: "isMissing", + isAllocatable: "isAllocatable", + isDuplicate: "isDuplicate", + isUsed: "isUsed", + volumeGroup: (volume, { $make }) => { + return $make("sysquery.lvm.VolumeGroup", { name: volume.volumeGroup }); + } + } + } + }); + }, + "sysquery.lvm.VolumeGroup": function VolumeGroup({ name }) { + return dlayerSource.withSources({ + physicalVolumes: function (_args, { physicalVolumes, $make }) { + return Promise.try(() => { + return physicalVolumes.load(All); + }).filter((volume) => { + return (volume.volumeGroup === name); + }).map((volume) => { + return $make("sysquery.lvm.PhysicalVolume", { path: volume.path }); + }); + }, + logicalVolumes: function (_args, { logicalVolumes, $make }) { + return Promise.try(() => { + return logicalVolumes.load(All); + }).filter((volume) => { + return (volume.volumeGroup === name); + }).map((volume) => { + return $make("sysquery.lvm.LogicalVolume", { path: volume.path }); + }); + }, + $sources: { + volumeGroups: { + [dlayerSource.ID]: name, + name: "name", + totalSpace: "totalSpace", + freeSpace: "freeSpace", + physicalVolumeCount: "physicalVolumeCount", + logicalVolumeCount: "logicalVolumeCount", + snapshotCount: "snapshotCount", + isReadOnly: "isReadOnly", + isResizeable: "isResizeable", + isExported: "isExported", + isIncomplete: "isIncomplete", + allocationPolicy: "allocationPolicy", + mode: "mode" } } - }, - root: { - resources: { - lvm: { - physicalVolumes: ({ paths }, { sources }) => { - return makeTypeFromSource({ - source: sources.physicalVolumes, - ids: paths, - make: (result) => types.PhysicalVolume({ path: result.path }), - }); - }, - volumeGroups: ({ names }, { sources }) => { - return makeTypeFromSource({ - source: sources.volumeGroups, - ids: names, - make: (result) => types.VolumeGroup({ name: result.name }), - }); - }, - logicalVolumes: ({ paths }, { sources }) => { - // FIXME: Aren't these scoped to a volume group? - return makeTypeFromSource({ - source: sources.logicalVolumes, - ids: paths, - make: (result) => types.LogicalVolume({ path: result.path }), - }); + }); + }, + "sysquery.lvm.LogicalVolume": function LogicalVolume({ path }) { + return dlayerSource.withSources({ + $sources: { + logicalVolumes: { + [dlayerSource.ID]: path, + path: "path", + name: "name", + fullName: "fullName", + size: "size", + uuid: "uuid", + deviceMapperPath: "deviceMapperPath", + layoutAttributes: "layoutAttributes", + roles: "roles", + tags: "tags", + configurationProfile: "configurationProfile", + creationTime: "creationTime", + creationHost: "creationHost", + neededKernelModules: "neededKernelModules", + dataVolume: "dataVolume", // FIXME: Reference? + metadataVolume: "metadataVolume", // FIXME: Reference? + poolVolume: "poolVolume", // FIXME: Reference? + persistentMajorNumber: "persistentMajorNumber", + persistentMinorNumber: "persistentMinorNumber", + type: "type", + isReadOnly: "isReadOnly", + isCurrentlyReadOnly: "isCurrentlyReadOnly", + isAllocationLocked: "isAllocationLocked", + allocationPolicy: "allocationPolicy", + status: "status", + healthStatus: "healthStatus", + isInitiallySynchronized: "isInitiallySynchronized", + isCurrentlySynchronized: "isCurrentlySynchronized", + isMerging: "isMerging", + isConverting: "isConverting", + isSuspended: "isSuspended", + isActivationSkipped: "isActivationSkipped", + isOpened: "isOpened", + isActiveLocally: "isActiveLocally", + isActiveRemotely: "isActiveRemotely", + isActiveExclusively: "isActiveExclusively", + isMergeFailed: "isMergeFailed", + isSnapshotInvalid: "isSnapshotInvalid", + isLiveTablePresent: "isLiveTablePresent", + isInactiveTablePresent: "isInactiveTablePresent", + isZeroFilled: "isZeroFilled", + hasFixedMinorNumber: "hasFixedMinorNumber", + outOfSpacePolicy: "outOfSpacePolicy", + volumeGroup: (volume, { $make }) => { + return $make("sysquery.lvm.VolumeGroup", { name: volume.volumeGroup }); } } } - } - }; + }); + } } }; diff --git a/yarn.lock b/yarn.lock index f319d84..d1cfa8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2904,10 +2904,10 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" -dlayer@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/dlayer/-/dlayer-0.1.0.tgz#cef49d283e1a9d83a8bd69a4c01d468ddd0b898c" - integrity sha512-UrdQdihmoNbWv49YFD/MBspaS2ILEfrNBoIVvZb1fhwvnPoRQ3ZOAbyiyV1sdfTrCASWVMz4i2NFczOVoVWJxw== +dlayer@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/dlayer/-/dlayer-0.1.2.tgz#3dbc43e55746988480bc8a4926aa523909661284" + integrity sha512-ZB709Ld/2TxUHMAKPfbvW2f0rscQAnu65j4/Nw8YRDpATFZd/NGmIXtOyEmYV7D904aLZLeKCaerfY+4Bu0i7Q== dependencies: "@joepie91/result" "^0.1.0" bluebird "^3.4.6"