diff --git a/package.json b/package.json index ac5e953..9d70e50 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@validatem/required": "^0.1.1", "@validatem/when": "^0.1.0", "JSONStream": "^1.1.4", + "ansi-html-community": "^0.0.8", "ansi-to-html": "^0.7.2", "argon2": "^0.27.0", "array.prototype.flat": "^1.2.1", @@ -70,6 +71,7 @@ "function-rate-limit": "^1.1.0", "generate-lookup-table": "^1.0.0", "graphql": "^14.2.1", + "htmlentities": "^1.0.0", "is-iterable": "^1.1.1", "is-plain-obj": "^3.0.0", "knex": "^0.21.18", diff --git a/public/css/style.css b/public/css/style.css index e5462f5..ad6b59b 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -126,15 +126,16 @@ table.drives th.unknown { color: rgb(59, 59, 59); } -.stacktrace { +.stacktrace, .debugPrint { white-space: pre-wrap; font-family: monospace; - background-color: rgb(12, 12, 12); + background-color: rgb(20, 20, 20); border: 1px solid black; padding: 0.8em; max-width: 1200px; + color: white; } -.stacktrace .irrelevant { +.stacktrace .irrelevant, .debugPrint .irrelevant { color: gray; } diff --git a/public/css/style.css.map b/public/css/style.css.map index e1afbf4..9f30158 100644 --- a/public/css/style.css.map +++ b/public/css/style.css.map @@ -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;;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"} \ No newline at end of file +{"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;EACA;;AAEA;EACC","file":"style.css"} \ No newline at end of file diff --git a/src/app.js b/src/app.js index 1f9de6f..0e4bd18 100644 --- a/src/app.js +++ b/src/app.js @@ -74,6 +74,10 @@ module.exports = function () { extended: true })); + app.get("/debug-query", (req, res) => { + res.render("debug-query"); + }); + app.use(require("./routes/index")); app.use("/disk-images", require("./routes/disk-images")(state)); app.use("/instances", require("./routes/instances")(state)); diff --git a/src/graphql-test.js b/src/graphql-test.js deleted file mode 100644 index 6dd6529..0000000 --- a/src/graphql-test.js +++ /dev/null @@ -1,197 +0,0 @@ -"use strict"; - -const Promise = require("bluebird"); -const graphql = require("graphql"); -const util = require("util"); -const chalk = require("chalk"); - -const gql = require("./graphql/tag"); -const api = require("./api/index"); - -function debugDisplay(results) { - if (results.errors != null && results.errors.length > 0) { - results.errors.forEach((graphqlError) => { - let errorHeader; - - if (graphqlError.path != null) { - errorHeader = `Error occurred for path: ${graphqlError.path.join(" -> ")}`; - } else if (graphqlError.locations != null && graphqlError.locations.length > 0) { - errorHeader = `Error occurred at line ${graphqlError.locations[0].line}, column ${graphqlError.locations[0].column}`; - } else { - errorHeader = "Error occurred in GraphQL"; - } - - console.log(chalk.bgBlue.white(errorHeader)); - - let error = graphqlError.originalError; - - if (error != null) { - if (error.showChain != null) { - console.log(error.showChain()); - } else { - console.log(error.stack); - } - } else { - console.log(graphqlError.stack); - } - - console.log("-----------------------------"); - }); - } - - console.log(util.inspect(results.data, {colors: true, depth: null})); -} - -// ############################################### - -let makeQuery = api(); - -// FIXME: If we intend to target macOS, a lot of whitespace-based output splitting won't work: https://www.mail-archive.com/austin-group-l@opengroup.org/msg01678.html - -// findmnt --json -o +SIZE,AVAIL -// -> map back to mountPoint stuff? -// blkid -// to discover the filesystem that a given path exists on: stat -c %m -// partx -// (rest of util-linux) -// memory usage: /proc/meminfo - -return Promise.try(() => { - let query = gql` - query { - hardware { - drives { - smartHealth - size - rpm - serialNumber - model - modelFamily - firmwareVersion - - blockDevice { - name - } - - partitions: allBlockDevices(type: PARTITION) { - name - size - - mounts { - mountpoint - } - } - } - } - } - `; - - // let query = gql` - // # query SomeDrives($drivePaths: [String]) { - // query SomeDrives { - // hardware { - // drives { - // path - // interface - - // model - // modelFamily - - // blockDevice { - // submounts: mounts(type: SUBMOUNT) { - // mountpoint - // filesystem - // } - // } - // # smartAvailable - // # smartEnabled - // # serialNumber - // # wwn - // # firmwareVersion - // # size - // # rpm - // # logicalSectorSize - // # physicalSectorSize - // # formFactor - // # ataVersion - // # sataVersion - - // # smartHealth - // # smartAttributes { - // # name - // # type - // # value - // # failingNow - - // # flags { - // # affectsPerformance - // # indicatesFailure - // # } - // # } - - // # blockDevice { - // # removable - - // # children { - // # name - // # mountpoint - // # size - // # } - // # } - // } - // } - - // # resources { - // # blockDevices { - // # name - // # mountpoint - // # size - // # deviceNumber - // # removable - // # readOnly - // # parent { name } - - // # children { - // # name - // # mountpoint - // # size - // # deviceNumber - // # removable - // # readOnly - // # parent { name } - // # } - // # } - - // # lvm { - // # physicalVolumes { - // # path - - // # blockDevice { - // # name - // # deviceNumber - // # } - - // # volumeGroup { - // # name - // # } - - // # format - // # size - // # freeSpace - // # duplicate - // # allocatable - // # used - // # exported - // # missing - // # } - // # } - // # } - // } - // `; - - return makeQuery(query, { - // drivePaths: ["/dev/sda", "/dev/sdb"] - }); -}).then((results) => { - debugDisplay(results); -}); diff --git a/src/packages/exec-lsblk/index.js b/src/packages/exec-lsblk/index.js index 872f712..e5ac6ab 100644 --- a/src/packages/exec-lsblk/index.js +++ b/src/packages/exec-lsblk/index.js @@ -50,6 +50,7 @@ function mapDeviceList(devices) { deviceNumber: device["maj:min"], removable: parseBoolean(device.rm), readOnly: parseBoolean(device.ro), + rotational: parseBoolean(device.rota), size: B(device.size), children: (device.children != null) ? mapDeviceList(device.children) : [] }; diff --git a/src/packages/exec-lvm/commands/version.pegjs b/src/packages/exec-lvm/commands/version.pegjs index 04d4ee0..ce9d6b7 100644 --- a/src/packages/exec-lvm/commands/version.pegjs +++ b/src/packages/exec-lvm/commands/version.pegjs @@ -3,22 +3,20 @@ import { RestOfLine } from "../../peg-rest-of-line" { const syncpipe = require("syncpipe"); - const mapVersionTitle = require("../map-version-title"); + const fromNamedEntries = require("../../from-named-entries"); + + const mapVersionLabel = require("../map-version-label"); } Output = entries:VersionLine+ { - // FIXME/MARKER: Build a generic abstraction for fromNamedEntries or so - return syncpipe(entries, [ - (_) => _.map(({ key, value }) => [ key, value ]), - (_) => Object.fromEntries(_) - ]); + return fromNamedEntries(entries); } VersionLine = _ label:$[A-Za-z ]+ ":" _ version:RestOfLine { return { - key: mapVersionTitle(label), // FIXME/MARKER: Rename to mapVersionLabel + key: mapVersionLabel(label), value: version.trim() }; } diff --git a/src/packages/exec-lvm/map-version-title.js b/src/packages/exec-lvm/map-version-label.js similarity index 100% rename from src/packages/exec-lvm/map-version-title.js rename to src/packages/exec-lvm/map-version-label.js diff --git a/src/packages/from-named-entries/index.js b/src/packages/from-named-entries/index.js new file mode 100644 index 0000000..d730d88 --- /dev/null +++ b/src/packages/from-named-entries/index.js @@ -0,0 +1,10 @@ +"use strict"; + +// Like Object.fromEntries, but for {key,value} instead of [key,value] + +module.exports = function fromNamedEntries(namedEntries) { + return Object.fromEntries(namedEntries.map((entry) => { + let { key, value } = entry; + return [ key, value ]; + })); +}; diff --git a/src/packages/result/index.js b/src/packages/result/index.js index 788e01f..7d1b30b 100644 --- a/src/packages/result/index.js +++ b/src/packages/result/index.js @@ -16,7 +16,6 @@ function createResultObject(isSuccessful, containedValue) { } }, value: function () { - // MARKER; either return value or throw the error it contains, to emulate standard throw behaviour if (isSuccessful) { return containedValue; } else { diff --git a/src/schemas/main.gql b/src/schemas/main.gql deleted file mode 100644 index 1f231ca..0000000 --- a/src/schemas/main.gql +++ /dev/null @@ -1,428 +0,0 @@ -scalar ByteSize -scalar TimeSize - -type AccessPermissions { - read: Boolean - write: Boolean - execute: Boolean -} - -type Permissions { - owner: AccessPermissions - group: AccessPermissions - everybody: AccessPermissions - setUID: Boolean - setGID: Boolean - sticky: Boolean -} - -type User { - name: String - id: Int - # FIXME -} - -type Group { - name: String - id: Int - # FIXME -} - -type RawMountValueOption { - key: String! - value: String -} - -type RawMountFlagOption { - key: String! -} - -union RawMountOption = RawMountFlagOption | RawMountValueOption - -enum MountErrorHandlingMode { - PANIC - CONTINUE - REMOUNT_READ_ONLY -} - -enum ExtAllocator { - OLD - ORLOV -} - -enum ExtJournalingMode { - JOURNAL - ORDERED - WRITEBACK -} - -enum ExtBufferErrorHandlingMode { - IGNORE_ERROR - ABORT_JOURNAL -} - -enum ExtQuotaSystem { - OLD - V0 - V1 -} - -enum FatTimestampsAllowedFrom { - EVERYBODY - GROUP - OWNER -} - -enum FatNameStrictness { - RELAXED - NORMAL - STRICT -} - -enum FatShortNameMode { - LOWER - WINDOWS_95 - WINDOWS_NT - MIXED -} - -enum FatLineEndingConversionPolicy { - BINARY - TEXT - AUTO -} - -type MountOptions { - writable: Boolean - userMountable: Boolean - freelyMountable: Boolean - - asynchronous: Boolean - asynchronousDirectoryUpdates: Boolean - - allowDeviceNodes: Boolean - allowExecution: Boolean - allowMandatoryLocks: Boolean - allowSetUIDBits: Boolean - - requiresNetworkAccess: Boolean - allowOfflineChecks: Boolean - - updateAccessTime: Boolean - updateDirectoryAccessTime: Boolean - updateAccessTimeRelatively: Boolean - updateAccessTimeStrictly: Boolean - - automaticallyMountable: Boolean - incrementIVersion: Boolean - printDebugInformation: Boolean - reportMountingErrors: Boolean - onError: MountErrorHandlingMode - - owner: User - group: Group - - filesystemOwner: User - filesystemGroup: Group - filesystemPermissions: Permissions - - # devpts - newPTYOwner: User - newPTYGroup: Group - newPTYPermissions: Permissions - - isolatePTYs: Boolean - ptmxPermissions: Permissions - - # tmpfs, hugetlbfs - rootOwner: User - rootGroup: Group - rootPermissions: Permissions - maximumInodes: Int - size: ByteSize - - # hugetlbfs - pageSize: ByteSize - sizeAsPoolPercentage: Float - minimumSize: ByteSize - minimumSizeAsPoolPercentage: Float - - # tmpfs - blockCount: Int - numaPolicy: String - - # ext2, ext3, ext4 - allow32bitIdentifiers: Boolean - allow64bitInodeVersions: Boolean - allowExtendedAttributes: Boolean - - supportACL: Boolean - showOnlyUsableSpace: Boolean - useGroupIDFromDirectory: Boolean - - allowAutomaticFilesystemResizing: Boolean - enableMetadataBlockTracking: Boolean - simulateAbort: Boolean - attachBufferHeads: Boolean # obsolete - checkFilesystemOnMount: Boolean - enableWriteBarriers: Boolean - enableHardwareDeleteCalls: Boolean - - allocator: ExtAllocator - reservedSpaceForGroup: Group - reservedSpaceForUser: User - superblockIndex: Int - inodeBlockReadAheadLimit: Int - stripeBlocks: Int - directorySizeLimit: ByteSize - - updateJournalFormat: Boolean - enableJournalChecksumming: Boolean - asynchronousJournalCommits: Boolean - processJournal: Boolean - journalInode: Int - journalDevice: String # FIXME: Translate this into a BlockDevice somehow? Expected 'number' format here is unclear. - journalPath: String - journalingMode: ExtJournalingMode - journalIOPriority: Int - onBufferError: ExtBufferErrorHandlingMode - commitInterval: TimeSize - - allowDeferredAllocations: Boolean - batchingTimeLimit: TimeSize - batchingTimeMinimum: TimeSize - enableMetadataBlockCache: Boolean - enableDirectIOReadLocking: Boolean - enableInodeTableBlockInitialization: Boolean - inodeTableBlockInitializationDelay: Int - automaticallySynchronizeBeforeRename: Boolean - - enableGroupQuota: Boolean - enableUserQuota: Boolean - enableProjectQuota: Boolean - quotaSystem: ExtQuotaSystem - userQuotaFile: String - groupQuotaFile: String - - # fat, vfat - blockSize: ByteSize - allocationTableBitness: Int - compressedVolumeFileModule: String - compressedVolumeFileOption: String - - defaultFileMode: Permissions - defaultFolderMode: Permissions - nameStrictness: FatNameStrictness - timestampChangesAllowedFrom: FatTimestampsAllowedFrom - restrictExecutableModeToWindowsBinaries: Boolean - treatAttrSysFlagAsImmutable: Boolean - - allowDifferentlyCasedNameConflicts: Boolean - shortNameMode: FatShortNameMode - preferShortNamesWithoutSequenceNumber: Boolean - enableUnicodeCharacterEscaping: Boolean - supportUtf8: Boolean - - enableReadOnlyFlagSupport: Boolean - enableDOS1xFallbackConfiguration: Boolean - enableQuietModeFailures: Boolean - enableNFSInodeCache: Boolean - enableEagerFlushing: Boolean - enableFreeClusterCache: Boolean - enableTimezoneConversion: Boolean - - timestampOffset: TimeSize - lineEndingConversionPolicy: FatLineEndingConversionPolicy - conversionCharacterSet: String - codepage: Int -} - -enum MountType { - ROOT_MOUNT - SUBMOUNT -} - -type Mount { - mountpoint: String! - type: MountType! - id: Int! - taskID: Int! - sourceDevice: BlockDevice! - rootPath: String - options: MountOptions! - filesystem: String! - label: String - uuid: String - partitionLabel: String - partitionUUID: String - deviceNumber: String - totalSpace: ByteSize - freeSpace: ByteSize - usedSpace: ByteSize - optionalFields: String - propagationFlags: String - children: [Mount!]! - # FIXME - # rawOptions: [RawMountOption] -} - -type SmartAttributeFlags { - autoKeep: Boolean! - eventCount: Boolean! - errorRate: Boolean! - affectsPerformance: Boolean! - updatedOnline: Boolean! - indicatesFailure: Boolean! -} - -enum SmartAttributeType { - PRE_FAIL - OLD_AGE -} - -enum SmartAttributeUpdateType { - ALWAYS - OFFLINE -} - -enum SmartHealth { - HEALTHY - DETERIORATING - FAILING -} - -type SmartAttribute { - id: Int! - name: String! - flags: SmartAttributeFlags! - value: Int! - rawValue: String! - worstValueSeen: Int! - failureThreshold: Int! - type: SmartAttributeType! - failingNow: Boolean! - failedBefore: Boolean! - updatedWhen: SmartAttributeUpdateType! -} - -enum BlockDeviceType { - DISK - PARTITION - LOOP_DEVICE -} - -type BlockDevice { - name: String! - type: BlockDeviceType! - path: String! - mounts(type: MountType): [Mount!]! - # mountpoint: String - deviceNumber: String! - removable: Boolean! - readOnly: Boolean! - size: ByteSize! - children: [BlockDevice!]! - # For tree linearization - _treecutterDepth: Int - _treecutterSequenceNumber: Int -} - -type PhysicalDrive { - path: String! - interface: String! - blockDevice: BlockDevice - allBlockDevices(type: BlockDeviceType): [BlockDevice!]! - smartAvailable: Boolean! - smartEnabled: Boolean - smartHealth: SmartHealth - smartAttributes: [SmartAttribute!]! - model: String - modelFamily: String - serialNumber: String - wwn: String, - firmwareVersion: String - size: ByteSize - rpm: Int - logicalSectorSize: ByteSize - physicalSectorSize: ByteSize - formFactor: String - ataVersion: String - sataVersion: String -} - -type LVMPhysicalVolume { - path: String! - blockDevice: BlockDevice! - volumeGroup: LVMVolumeGroup! - format: String! - size: ByteSize! - freeSpace: ByteSize! - duplicate: Boolean! - allocatable: Boolean! - used: Boolean! - exported: Boolean! - missing: Boolean! -} - -type LVMVolumeGroup { - name: String! -} - -interface Image { - id: String! - name: String! - description: String - thumbnail: String - originalSource: String! - # The below are only available after the image has been downloaded - filesize: ByteSize - storagePath: String -} - -type InstallationMedium implements Image { - id: String! - name: String! - description: String - thumbnail: String - originalSource: String! - # The below are only available after the image has been downloaded - filesize: ByteSize - storagePath: String -} - -type VMImage implements Image { - id: String! - name: String! - description: String - thumbnail: String - originalSource: String! - # The below are only available after the image has been downloaded - filesize: ByteSize - storagePath: String -} - -type HardwareQuery { - drives(paths: [String]): [PhysicalDrive!]! -} - -type LVMQuery { - physicalVolumes: [LVMPhysicalVolume!]! - volumeGroups: [LVMVolumeGroup!]! -} - -type ImagesQuery { - installationMedia: [InstallationMedium!]! - vmImages: [VMImage!]! -} - -type ResourcesQuery { - blockDevices: [BlockDevice!]! - lvm: LVMQuery - # TODO: RAID - images: ImagesQuery -} - -type Query { - hardware: HardwareQuery! - resources: ResourcesQuery! -} diff --git a/src/scss/style.scss b/src/scss/style.scss index 9aa306f..e7ba345 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -176,13 +176,14 @@ table.drives { } } -.stacktrace { +.stacktrace, .debugPrint { white-space: pre-wrap; font-family: monospace; - background-color: rgb(12, 12, 12); + background-color: rgb(20, 20, 20); border: 1px solid black; padding: .8em; max-width: 1200px; + color: white; // Default text color only .irrelevant { color: gray; diff --git a/src/views/debug-query.jsx b/src/views/debug-query.jsx new file mode 100644 index 0000000..eb0f574 --- /dev/null +++ b/src/views/debug-query.jsx @@ -0,0 +1,47 @@ +"use strict"; + +const React = require("react"); +const ansiHTML = require("ansi-html-community"); +const htmlentities = require("htmlentities"); +const util = require("util"); + +const Layout = require("./layout.jsx"); + +module.exports = { + query: { + hardware: { + drives: { + path: true, + partitions: { + $key: "allBlockDevices", + name: true, + size: true, + + mounts: { + mountpoint: true + }, + + children: { + $recurse: true, + $recurseLimit: Infinity, // 3 by default + } + } + } + } + }, + template: function StorageDeviceList({data}) { + if (process.env.NODE_ENV === "development") { + return ( + +
+				
+			);
+		} else {
+			// FIXME: Proper error, or just prevent the route from being set outside of development mode at all
+			return "This view is only accessible in development mode.";
+		}
+	}
+};
diff --git a/yarn.lock b/yarn.lock
index fb49ced..ba36d1c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1343,6 +1343,11 @@ ansi-escapes@^4.2.1:
   dependencies:
     type-fest "^0.11.0"
 
+ansi-html-community@^0.0.8:
+  version "0.0.8"
+  resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41"
+  integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==
+
 ansi-regex@^0.2.0, ansi-regex@^0.2.1:
   version "0.2.1"
   resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-0.2.1.tgz#0d8e946967a3d8143f93e24e298525fc1b2235f9"
@@ -4146,6 +4151,11 @@ hosted-git-info@^2.1.4:
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
   integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
 
+htmlentities@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/htmlentities/-/htmlentities-1.0.0.tgz#093a8c1fb09dfe5e169fc33d09574825d61e40a4"
+  integrity sha512-FVEsyLbpWqXh/qIaRrOuZo3sKN4bihKYSUKvI0byJhPV9rO6DokXDWew+uFnA41dGmTQqmyF7hSFfV5DDVcL/A==
+
 htmlescape@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"