diff --git a/src/api/index.js b/src/api/index.js index fb0764a..a18eed9 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -1,28 +1,7 @@ "use strict"; -const Promise = require("bluebird"); const dlayer = require("dlayer"); -const All = require("../packages/graphql-interface/symbols/all"); const loaders = require("./data-sources"); -const types = require("./types"); -const execLVM = require("../packages/exec-lvm"); -const { validateValue } = require("@validatem/core"); -const isString = require("@validatem/is-string"); -const isBoolean = require("@validatem/is-boolean"); -const required = require("@validatem/required"); -const defaultTo = require("@validatem/default-to"); - -function typeFromSource(source, ids, factoryFunction) { - return Promise.try(() => { - if (ids != null) { - return source.loadMany(ids); - } else { - return source.load(All); // FIXME: Symbol - } - }).then((items) => { - return items.map((item) => factoryFunction(item)); - }); -} module.exports = function () { return dlayer({ @@ -32,8 +11,10 @@ module.exports = function () { }; }, modules: [ - require("../packages/sysquery-lvm"), require("../packages/sysquery-core"), + require("../packages/sysquery-block-devices"), + require("../packages/sysquery-mounts"), + require("../packages/sysquery-lvm"), ], schema: { hardware: { diff --git a/src/packages/sysquery-block-devices/index.js b/src/packages/sysquery-block-devices/index.js new file mode 100644 index 0000000..036b9a9 --- /dev/null +++ b/src/packages/sysquery-block-devices/index.js @@ -0,0 +1,117 @@ +"use strict"; + +const DataLoader = require("dataloader"); +const fs = require("fs").promises; +const matchValue = require("match-value"); +const memoizee = require("memoizee"); +const unreachable = require("@joepie91/unreachable")("@sysquery/block-devices"); + +// TODO: Refactor dlayerSource to be object-mergeable instead of all-encompassing +const dlayerSource = require("../dlayer-source"); +const All = require("../graphql-interface/symbols/all"); +const lsblk = require("../exec-lsblk"); +const mapFromSource = require("../map-from-source"); +const treeMapAsync = require("../tree-map-async"); +const treeFind = require("../tree-find"); + +function makePredicate({ path, name }) { + if (path != null) { + return (device) => device.path === path; + } else if (name != null) { + return (device) => device.name === name; + } else { + unreachable("No selector specified for lsblk"); + } +} + +module.exports = { + name: "sysquery.blockDevice", + makeContext: function () { + let lsblkOnce = memoizee(async () => { + return treeMapAsync(await lsblk(), async (device) => { + return { + ... device, + path: await fs.realpath(device.path) + }; + }); + }); + + return { + lsblk: new DataLoader(async function (selectors) { + let blockDeviceTree = await lsblkOnce(); + + return selectors.map((selector) => { + if (selector === All) { + return blockDeviceTree; + } else { + return treeFind(blockDeviceTree, makePredicate(selector)); + } + }); + }) + }; + }, + types: { + "sysquery.blockDevices.BlockDevice": function ({ name, path }) { + return dlayerSource.withSources({ + $sources: { + lsblk: { + [dlayerSource.ID]: { name, path }, + name: "name", + path: (device) => fs.realpath(device.path), + type: (device) => matchValue(device.type, { + partition: "PARTITION", + disk: "DISK", + loopDevice: "LOOP_DEVICE" + }), + size: "size", + mountpoint: "mountpoint", // FIXME: Isn't this obsoleted by `mounts`? + deviceNumber: "deviceNumber", + removable: "removable", + readOnly: "readOnly", + children: (device, { $make }) => device.children.map((child) => { + return $make("sysquery.blockDevices.BlockDevice", { name: child.name }); + }) + } + } + }); + } + }, + extensions: { + "sysquery.mounts.Mount": { + sourceDevice: async function (_, { lsblk, $make, $getProperty }) { + let sourceDevicePath = await $getProperty(this, "sourceDevicePath"); + + if (sourceDevicePath == null) { + // This occurs when the mount is not backed by a device, eg. an sshfs FUSE mount + return null; + } else { + let realSourcePath = await fs.realpath(sourceDevicePath); + + if (await lsblk.load({ path: realSourcePath }) != null) { + return $make("sysquery.blockDevices.BlockDevice", { path: realSourcePath }); + } else { + // This occurs when the `sourceDevice` is a valid device, but it is not a *block* device, eg. `/dev/fuse` + return null; + } + } + } + } + }, + root: { + resources: { + blockDevices: ({ names, paths }, { lsblk, $make }) => { + // TODO: Design better abstraction for this sort of case + let selectors = (names == null && paths == null) + ? null + : [ + ... (names ?? []).map((name) => ({ name: name })), + ... (paths ?? []).map((path) => ({ path: path })) + ]; + + return mapFromSource(lsblk, selectors, (device) => { + return $make("sysquery.blockDevices.BlockDevice", { name: device.name }); + }); + } + } + } +}; diff --git a/src/packages/sysquery-core/findmnt.js b/src/packages/sysquery-core/findmnt.js deleted file mode 100644 index f336819..0000000 --- a/src/packages/sysquery-core/findmnt.js +++ /dev/null @@ -1,37 +0,0 @@ -"use strict"; - -const memoizee = require("memoizee"); -const fs = require("fs").promises; -const findmnt = require("../exec-findmnt"); -const All = require("../graphql-interface/symbols/all"); -const treeMapAsync = require("../tree-map-async"); -const treeFind = require("../tree-find"); - -module.exports = function () { - let findmntOnce = memoizee(async () => { - return treeMapAsync(await findmnt(), async (mount) => { - return { - ... mount, - sourceDevice: (mount.sourceDevice?.startsWith("/")) - ? await fs.realpath(mount.sourceDevice) - : mount.sourceDevice - }; - - }); - }); - - return async function (mountpoints) { - let mounts = await findmntOnce(); - - // TODO: It's kind of strange that it sometimes returns a tree and sometimes a list, this can probably be improved? - let matches = mountpoints.map((mountpoint) => { - if (mountpoint === All) { - return mounts; - } else { - return treeFind(mounts, (mount) => mount.mountpoint === mountpoint); - } - }); - - return matches; - }; -}; diff --git a/src/packages/sysquery-core/index.js b/src/packages/sysquery-core/index.js index 59379b5..fb1f621 100644 --- a/src/packages/sysquery-core/index.js +++ b/src/packages/sysquery-core/index.js @@ -1,152 +1,17 @@ "use strict"; -const fs = require("fs").promises; -const matchValue = require("match-value"); -const DataLoader = require("dataloader"); - -const mapFromSource = require("../map-from-source"); -const dlayerSource = require("../dlayer-source"); -const All = require("../graphql-interface/symbols/all"); -const treeFilterFlatAsync = require("../tree-filter-flat-async"); -const assert = require("assert"); - -// FIXME: Simplify findmnt/lsblk source definitions, and probably separate out mounts and block devices into their own separate modules - module.exports = { name: "sysquery.core", makeContext: function () { - // MARKER: Complete sources migration, test, move smartctl - return { - lsblk: new DataLoader(require("./lsblk")()), - findmnt: new DataLoader(require("./findmnt")()) - }; + return {}; }, types: { - "sysquery.core.Mount": function ({ mountpoint }) { - return dlayerSource.withSources({ - mountpoint: mountpoint, - sourceDevice: async (_, { lsblk, findmnt, $make }) => { - let mount = await findmnt.load(mountpoint); - - if (mount == null) { - // TODO: Can this ever happen for any legitimate reason? - throw new Error(`Mountpoint '${mountpoint}' not found in findmnt output`); - } else if (mount.sourceDevice == null) { - // This occurs when the mount is not backed by a device, eg. an sshfs FUSE mount - return null; - } else { - let sourcePath = await fs.realpath(mount.sourceDevice); - - if (await lsblk.load({ path: sourcePath }) != null) { - return $make("sysquery.core.BlockDevice", { path: sourcePath }); - } else { - // This occurs when the `sourceDevice` is a valid device, but it is not a *block* device, eg. `/dev/fuse` - return null; - } - } - }, - $sources: { - findmnt: { - [dlayerSource.ID]: mountpoint, - id: "id", - // FIXME: Aren't we inferring the below somewhere else in the code, using the square brackets? - type: (mount) => (mount.rootPath === "/") - ? "ROOT_MOUNT" - : "SUBMOUNT", - filesystem: "filesystem", - options: "options", - label: "label", - uuid: "uuid", - partitionLabel: "partitionLabel", - partitionUUID: "partitionUUID", - deviceNumber: "deviceNumber", - totalSpace: "totalSpace", - freeSpace: "freeSpace", - usedSpace: "usedSpace", - rootPath: "rootPath", - taskID: "taskID", - optionalFields: "optionalFields", - propagationFlags: "propagationFlags", - children: (mount, { $make }) => mount.children.map((child) => { - return $make("sysquery.core.Mount", { mountpoint: child.mountpoint }); - }) - } - } - }); - }, - "sysquery.core.BlockDevice": function ({ name, path }) { - return dlayerSource.withSources({ - // TODO: Eventually make this produce a (filtered) tree instead? - mounts: async function ({ type }, { findmnt, $make, $getProperty, $getPropertyPath }) { - let mountTree = await findmnt.load(All); - - let relevantMounts = await treeFilterFlatAsync(mountTree, async (mount) => { - let mountObject = $make("sysquery.core.Mount", { mountpoint: mount.mountpoint }); - // console.log({ sourceDevice: await $getProperty(mountObject, "sourceDevice") }); - let sourcePath = await $getPropertyPath(mountObject, "sourceDevice.path"); - let sourceName = await $getPropertyPath(mountObject, "sourceDevice.name"); - - // TODO: This logic looks strange. Is it actually correct, even when only one of name/path is specified upon BlockDevice construction? - let matchesDevice = ( - (sourcePath != null && sourcePath === path) - || (sourceName != null && sourceName === name) - ); - - let matchesType = ( - type == null - || await $getProperty(mountObject, "type" === type) - ); - - return matchesDevice && matchesType; - }, { recurseFilteredSubtrees: true }); - - // This is a bit hacky; this approach should probably be replaced by a map-filter instead. But for now, this will do - as this all happens in the same request context, there's no real penalty to re-creating the mount objects a second time. - return relevantMounts.map((mount) => { - return $make("sysquery.core.Mount", { mountpoint: mount.mountpoint }); - }); - }, - $sources: { - lsblk: { - [dlayerSource.ID]: { name, path }, - name: "name", - path: (device) => fs.realpath(device.path), - type: (device) => matchValue(device.type, { - partition: "PARTITION", - disk: "DISK", - loopDevice: "LOOP_DEVICE" - }), - size: "size", - mountpoint: "mountpoint", // FIXME: Isn't this obsoleted by `mounts`? - deviceNumber: "deviceNumber", - removable: "removable", - readOnly: "readOnly", - children: (device, { $make }) => device.children.map((child) => { - return $make("sysquery.core.BlockDevice", { name: child.name }); - }) - } - } - }); - } + // None }, extensions: { // None }, root: { - resources: { - blockDevices: ({ names, paths }, { lsblk, $make }) => { - // TODO: Design better abstraction for this sort of case - let selectors = (names == null && paths == null) - ? null - : [ - ... (names ?? []).map((name) => ({ name: name })), - ... (paths ?? []).map((path) => ({ path: path })) - ]; - - return mapFromSource(lsblk, selectors, (device) => { - return $make("sysquery.core.BlockDevice", { name: device.name }); - }); - } - // FIXME: Add mounts - } + // None } }; diff --git a/src/packages/sysquery-core/lsblk.js b/src/packages/sysquery-core/lsblk.js deleted file mode 100644 index 340c356..0000000 --- a/src/packages/sysquery-core/lsblk.js +++ /dev/null @@ -1,42 +0,0 @@ -"use strict"; - -const memoizee = require("memoizee"); -const fs = require("fs").promises; -const unreachable = require("@joepie91/unreachable")("cvm"); -const lsblk = require("../exec-lsblk"); -const All = require("../graphql-interface/symbols/all"); -const treeFind = require("../tree-find"); -const treeMapAsync = require("../tree-map-async"); - -function makePredicate({ path, name }) { - if (path != null) { - return (device) => device.path === path; - } else if (name != null) { - return (device) => device.name === name; - } else { - unreachable("No selector specified for lsblk"); - } -} - -module.exports = function () { - let lsblkOnce = memoizee(async () => { - return treeMapAsync(await lsblk(), async (device) => { - return { - ... device, - path: await fs.realpath(device.path) - }; - }); - }); - - return async function (selectors) { - let blockDeviceTree = await lsblkOnce(); - - return selectors.map((selector) => { - if (selector === All) { - return blockDeviceTree; - } else { - return treeFind(blockDeviceTree, makePredicate(selector)); - } - }); - }; -}; diff --git a/src/packages/sysquery-mounts/index.js b/src/packages/sysquery-mounts/index.js new file mode 100644 index 0000000..c1ce882 --- /dev/null +++ b/src/packages/sysquery-mounts/index.js @@ -0,0 +1,115 @@ +"use strict"; + +const fs = require("fs").promises; +const memoizee = require("memoizee"); +const DataLoader = require("dataloader"); + +const findmnt = require("../exec-findmnt"); +const dlayerSource = require("../dlayer-source"); +const treeFilterFlatAsync = require("../tree-filter-flat-async"); +const treeMapAsync = require("../tree-map-async"); +const treeFind = require("../tree-find"); +const All = require("../graphql-interface/symbols/all"); + +module.exports = { + name: "sysquery.mounts", + makeContext: function () { + let findmntOnce = memoizee(async () => { + return treeMapAsync(await findmnt(), async (mount) => { + return { + ... mount, + sourceDevice: (mount.sourceDevice?.startsWith("/")) + ? await fs.realpath(mount.sourceDevice) + : mount.sourceDevice + }; + + }); + }); + + return { + findmnt: new DataLoader(async function (mountpoints) { + let mounts = await findmntOnce(); + + // TODO: It's kind of strange that it sometimes returns a tree and sometimes a list, this can probably be improved? + let matches = mountpoints.map((mountpoint) => { + if (mountpoint === All) { + return mounts; + } else { + return treeFind(mounts, (mount) => mount.mountpoint === mountpoint); + } + }); + + return matches; + }) + }; + }, + types: { + "sysquery.mounts.Mount": function ({ mountpoint }) { + return dlayerSource.withSources({ + mountpoint: mountpoint, + $sources: { + findmnt: { + [dlayerSource.ID]: mountpoint, + id: "id", + // FIXME: Aren't we inferring the below somewhere else in the code, using the square brackets? + type: (mount) => (mount.rootPath === "/") + ? "ROOT_MOUNT" + : "SUBMOUNT", + filesystem: "filesystem", + options: "options", + label: "label", + uuid: "uuid", + partitionLabel: "partitionLabel", + partitionUUID: "partitionUUID", + deviceNumber: "deviceNumber", + sourceDevicePath: "sourceDevice", + totalSpace: "totalSpace", + freeSpace: "freeSpace", + usedSpace: "usedSpace", + rootPath: "rootPath", + taskID: "taskID", + optionalFields: "optionalFields", + propagationFlags: "propagationFlags", + children: (mount, { $make }) => mount.children.map((child) => { + return $make("sysquery.mounts.Mount", { mountpoint: child.mountpoint }); + }) + } + } + }); + }, + }, + extensions: { + "sysquery.blockDevices.BlockDevice": { + // TODO: Eventually make this produce a (filtered) tree instead? + mounts: async function ({ type }, { findmnt, $make, $getProperty, $getPropertyPath }) { + let mountTree = await findmnt.load(All); + + let relevantMounts = await treeFilterFlatAsync(mountTree, async (mount) => { + let mountObject = $make("sysquery.mounts.Mount", { mountpoint: mount.mountpoint }); + // console.log({ sourceDevice: await $getProperty(mountObject, "sourceDevice") }); + let sourcePath = await $getPropertyPath(mountObject, "sourceDevice.path"); + let sourceName = await $getPropertyPath(mountObject, "sourceDevice.name"); + + // TODO: This logic looks strange. Is it actually correct, even when only one of name/path is specified upon BlockDevice construction? + let matchesDevice = ( + (sourcePath != null && sourcePath === await $getProperty(this, "path")) + || (sourceName != null && sourceName === await $getProperty(this, "name")) + ); + + let matchesType = ( + type == null + || await $getProperty(mountObject, "type" === type) + ); + + return matchesDevice && matchesType; + }, { recurseFilteredSubtrees: true }); + + // This is a bit hacky; this approach should probably be replaced by a map-filter instead. But for now, this will do - as this all happens in the same request context, there's no real penalty to re-creating the mount objects a second time. + return relevantMounts.map((mount) => { + return $make("sysquery.mounts.Mount", { mountpoint: mount.mountpoint }); + }); + } + } + }, + root: {} // FIXME: Expose root mounts endpoint +};