Compare commits

...

2 Commits

@ -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: {

@ -1,126 +0,0 @@
"use strict";
const Promise = require("bluebird");
const objectFromEntries = require("object.fromentries");
const util = require("util");
function resolveFromDataSource(dataContext, dataSource, id) {
if (dataContext[dataSource] != null) {
return dataContext[dataSource].load(id);
} else {
throw new Error(`Specified data source '${dataSource}' does not exist`);
}
}
function withProperty(dataSource, id, property) {
return withData(dataSource, id, (value) => {
return value[property];
});
}
function withData(dataSource, id, callback) {
return function (args, context) {
let { data } = context;
return Promise.try(() => {
return resolveFromDataSource(data, dataSource, id);
}).then((value) => {
if (value != null) {
// FIXME: Inject 'properties'
return callback(value, args, context);
} else {
// QUESTION: Why do we disallow this again?
throw new Error(`Got a null-ish value from data source '${dataSource}' for ID '${util.inspect(id)}'`);
}
});
};
}
function withDynamicHandler(handler, object) {
return function (args, context) {
let { data } = context;
function resolveProperty(property, fromObject = object) {
if (typeof fromObject[property] !== "function") {
throw new Error(`FIXME: Properties can apparently be non-functions`);
}
return fromObject[property](args, context);
}
let extendedContext = {
... context,
resolveProperty: resolveProperty,
resolveProperties: function (properties, fromObject) {
return Promise.map(properties, (property) => {
return Promise.try(() => {
return resolveProperty(property, fromObject);
}).then((value) => {
return [ property, value ];
});
}).then((entries) => {
return objectFromEntries(entries);
});
},
resolvePropertyPath: function (propertyPath, fromObject) {
let initialObject = fromObject ?? object;
return Promise.reduce(propertyPath, (last, property) => {
if (last != null) {
return resolveProperty(property, last);
}
}, initialObject);
},
resolveDataSource: function (dataSource, id) {
return resolveFromDataSource(data, dataSource, id);
}
};
return handler(args, extendedContext);
};
}
let ID = Symbol("ID");
let LocalProperties = Symbol("LocalProperties");
let Dynamic = Symbol("Dynamic");
module.exports = {
ID: ID,
Dynamic: Dynamic,
LocalProperties: LocalProperties,
createDataObject: function createDataObject(mappings) {
let object = {};
if (mappings[LocalProperties] != null) {
Object.assign(object, mappings[LocalProperties]);
}
if (mappings[Dynamic] != null) {
for (let [property, handler] of Object.entries(mappings[Dynamic])) {
object[property] = withDynamicHandler(handler, object);
}
}
for (let [dataSource, items] of Object.entries(mappings)) {
if (items[ID] != null) {
let id = items[ID];
for (let [property, source] of Object.entries(items)) {
if (object[property] == null) {
if (typeof source === "string") {
object[property] = withProperty(dataSource, id, source);
} else if (typeof source === "function") {
object[property] = withData(dataSource, id, source);
} /* FIXME: else */
} else {
throw new Error(`Handler already defined for property '${property}' - maybe you specified it twice for different data sources?`);
}
}
} else {
throw new Error(`No object ID was provided for the '${dataSource}' data source`);
}
}
return object;
}
};

@ -1,17 +0,0 @@
"use strict";
const graphql = require("graphql");
module.exports = function createGraphQLInterface(schema, options, root) {
return function makeQuery(query, args) {
return graphql.graphql({
schema: schema,
source: query,
rootValue: root,
contextValue: {
data: (options.loaderFactory != null) ? options.loaderFactory() : {}
},
variableValues: args
});
};
};

@ -1,5 +0,0 @@
"use strict";
module.exports = function gql(strings) {
return strings.join("");
};

@ -1,11 +0,0 @@
"use strict";
module.exports = function loadTypes(types) {
let loadedTypes = {};
for (let [name, module_] of Object.entries(types)) {
loadedTypes[name] = module_(loadedTypes);
}
return loadedTypes;
};

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

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

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

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

@ -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
};
Loading…
Cancel
Save