From 4b3d6a7ba3075b6d760ef4bac95b44ffbd6e9c39 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Wed, 29 Jan 2025 10:27:23 -0500 Subject: [PATCH] fix: report issues + pm2 issues --- api/package-lock.json | 17 - api/package.json | 1 - api/src/core/utils/pm2/unraid-api-running.ts | 9 +- api/src/unraid-api/cli/report.command.ts | 386 +++---------------- api/src/unraid-api/rest/rest.service.ts | 2 +- 5 files changed, 48 insertions(+), 367 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index e827371d7..62bc1b3af 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -62,7 +62,6 @@ "graphql-ws": "^5.16.0", "ini": "^4.1.2", "ip": "^2.0.1", - "ip-regex": "^5.0.0", "jose": "^5.9.6", "lodash-es": "^4.17.21", "multi-ini": "^2.3.2", @@ -10924,17 +10923,6 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, - "node_modules/ip-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", - "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "license": "MIT", @@ -23875,11 +23863,6 @@ } } }, - "ip-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-5.0.0.tgz", - "integrity": "sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw==" - }, "ipaddr.js": { "version": "1.9.1" }, diff --git a/api/package.json b/api/package.json index 3c6f390a5..6383cb7f6 100644 --- a/api/package.json +++ b/api/package.json @@ -95,7 +95,6 @@ "graphql-ws": "^5.16.0", "ini": "^4.1.2", "ip": "^2.0.1", - "ip-regex": "^5.0.0", "jose": "^5.9.6", "lodash-es": "^4.17.21", "multi-ini": "^2.3.2", diff --git a/api/src/core/utils/pm2/unraid-api-running.ts b/api/src/core/utils/pm2/unraid-api-running.ts index b7fd6b5be..2c89035cc 100644 --- a/api/src/core/utils/pm2/unraid-api-running.ts +++ b/api/src/core/utils/pm2/unraid-api-running.ts @@ -1,14 +1,13 @@ -import pm2 from 'pm2'; - export const isUnraidApiRunning = async (): Promise => { + const { connect, describe, disconnect } = await import('pm2'); return new Promise((resolve, reject) => { - pm2.connect(function (err) { + connect(function (err) { if (err) { console.error(err); reject('Could not connect to pm2'); } - pm2.describe('unraid-api', function (err, processDescription) { + describe('unraid-api', function (err, processDescription) { console.log(err); if (err || processDescription.length === 0) { console.log(false); // Service not found or error occurred @@ -19,7 +18,7 @@ export const isUnraidApiRunning = async (): Promise => { resolve(isOnline); } - pm2.disconnect(); + disconnect(); }); }); }); diff --git a/api/src/unraid-api/cli/report.command.ts b/api/src/unraid-api/cli/report.command.ts index 22e54facb..f993e55fe 100644 --- a/api/src/unraid-api/cli/report.command.ts +++ b/api/src/unraid-api/cli/report.command.ts @@ -1,212 +1,18 @@ -import { ApolloClient, ApolloQueryResult, NormalizedCacheObject } from '@apollo/client/core/index.js'; -import ipRegex from 'ip-regex'; +import { readFile } from 'fs/promises'; + import { Command, CommandRunner, Option } from 'nest-commander'; -import type { getCloudQuery, getServersQuery } from '@app/graphql/generated/api/operations'; -import { isUnraidApiRunning } from '@app/core/utils/pm2/unraid-api-running'; -import { API_VERSION } from '@app/environment'; -import { getApiApolloClient } from '@app/graphql/client/api/get-api-client'; -import { getCloudDocument, getServersDocument } from '@app/graphql/generated/api/operations'; -import { MinigraphStatus } from '@app/graphql/generated/api/types'; -import { getters, store } from '@app/store'; -import { loadConfigFile } from '@app/store/modules/config'; -import { loadStateFiles } from '@app/store/modules/emhttp'; +import type { MyServersConfigMemory } from '@app/types/my-servers-config'; +import { getters } from '@app/store'; +import { MyServersConfigMemorySchema } from '@app/types/my-servers-config'; import { LogService } from '@app/unraid-api/cli/log.service'; -type CloudQueryResult = NonNullable['data']['cloud']>; -type ServersQueryResultServer = NonNullable['data']['servers']>[0]; - -type ServersPayload = { - online: ServersQueryResultServer[]; - offline: ServersQueryResultServer[]; - invalid: ServersQueryResultServer[]; -}; - -type ReportObject = { - os: { - serverName: string; - version: string; - }; - api: { - version: string; - status: 'running' | 'stopped'; - environment: string; - nodeVersion: string; - }; - apiKey: 'valid' | 'invalid' | string; - servers?: ServersPayload | null; - myServers: { - status: 'authenticated' | 'signed out'; - myServersUsername?: string; - }; - minigraph: { - status: MinigraphStatus; - timeout: number | null; - error: string | null; - }; - cloud: { - status: string; - error?: string; - ip?: string; - allowedOrigins?: string[] | null; - }; -}; - -// This should return the status of the apiKey and mothership -export const getCloudData = async ( - client: ApolloClient -): Promise => { - const cloud = await client.query({ query: getCloudDocument }); - return cloud.data.cloud ?? null; -}; - -export const getServersData = async ({ - client, - verbosity, -}: { - client: ApolloClient; - verbosity: number; -}): Promise => { - if (verbosity === 0) { - return null; - } - - try { - const servers = await client.query({ query: getServersDocument }); - const foundServers = servers.data.servers.reduce( - (acc, curr) => { - switch (curr.status) { - case 'online': - acc.online.push(curr); - break; - case 'offline': - acc.offline.push(curr); - break; - default: - acc.invalid.push(curr); - break; - } - - return acc; - }, - { online: [], offline: [], invalid: [] } - ); - return foundServers; - } catch (error: unknown) { - return { - online: [], - offline: [], - invalid: [], - }; - } -}; - -const hashUrlRegex = () => /(.*)([a-z0-9]{40})(.*)/g; - -export const anonymiseOrigins = (origins?: string[]): string[] => { - const originsWithoutSocks = origins?.filter((url) => !url.endsWith('.sock')) ?? []; - return originsWithoutSocks - .map((origin) => - origin - // Replace 40 char hash string with "HASH" - .replace(hashUrlRegex(), '$1HASH$3') - // Replace ipv4 address using . separator with "IPV4ADDRESS" - .replace(ipRegex(), 'IPV4ADDRESS') - // Replace ipv4 address using - separator with "IPV4ADDRESS" - .replace(new RegExp(ipRegex().toString().replace('\\.', '-')), '/IPV4ADDRESS') - // Report WAN port - .replace(`:${getters.config().remote.wanport || 443}`, ':WANPORT') - ) - .filter(Boolean); -}; - -const getAllowedOrigins = (cloud: CloudQueryResult | null, verbosity: number): string[] | null => { - if (verbosity > 1) { - return cloud?.allowedOrigins.filter((url) => !url.endsWith('.sock')) ?? []; - } else if (verbosity === 1) { - return anonymiseOrigins(cloud?.allowedOrigins ?? []); - } - return null; -}; - -const getReadableCloudDetails = (reportObject: ReportObject, verbosity: number): string => { - const error = reportObject.cloud.error ? `\n ERROR [${reportObject.cloud.error}]` : ''; - const status = reportObject.cloud.status ? reportObject.cloud.status : 'disconnected'; - const ip = reportObject.cloud.ip && verbosity !== 0 ? `\n IP: [${reportObject.cloud.ip}]` : ''; - return ` - STATUS: [${status}] ${ip} ${error}`; -}; - -const getReadableMinigraphDetails = (reportObject: ReportObject): string => { - const statusLine = `STATUS: [${reportObject.minigraph.status}]`; - const errorLine = reportObject.minigraph.error ? ` ERROR: [${reportObject.minigraph.error}]` : null; - const timeoutLine = reportObject.minigraph.timeout - ? ` TIMEOUT: [${(reportObject.minigraph.timeout || 1) / 1_000}s]` - : null; // 1 in case of divide by zero - - return ` - ${statusLine}${errorLine ? `\n${errorLine}` : ''}${timeoutLine ? `\n${timeoutLine}` : ''}`; -}; - -// Convert server to string output -const serverToString = (verbosity: number) => (server: ServersQueryResultServer) => - `${server?.name ?? 'No Server Name'}${ - verbosity > 0 - ? `[owner="${server.owner?.username ?? 'No Owner Found'}"${ - verbosity > 1 ? ` guid="${server.guid ?? 'No GUID'}"]` : ']' - }` - : '' - }`; - -const getReadableServerDetails = (reportObject: ReportObject, verbosity: number): string => { - if (!reportObject.servers) { - return ''; - } - - if (reportObject.api.status === 'stopped') { - return '\nSERVERS: API is offline'; - } - - const invalid = - verbosity > 0 && reportObject.servers.invalid.length > 0 - ? ` - INVALID: ${reportObject.servers.invalid.map(serverToString(verbosity)).join(',')}` - : ''; - - return ` -SERVERS: - ONLINE: ${reportObject.servers.online.map(serverToString(verbosity)).join(',')} - OFFLINE: ${reportObject.servers.offline.map(serverToString(verbosity)).join(',')}${invalid}`; -}; - -const getReadableAllowedOrigins = (reportObject: ReportObject): string => { - const { cloud } = reportObject; - if (cloud?.allowedOrigins) { - return ` -ALLOWED_ORIGINS: ${cloud.allowedOrigins.join(', ').trim()}`; - } - - return ''; -}; - -interface ReportOptions { - raw: boolean; - json: boolean; - verbose: number; -} - @Command({ name: 'report' }) export class ReportCommand extends CommandRunner { constructor(private readonly logger: LogService) { super(); } - private defaultOptions: ReportOptions = { - raw: false, - json: false, - verbose: 0, - }; - @Option({ flags: '-r, --raw', description: 'whether to enable raw command output', @@ -225,158 +31,52 @@ export class ReportCommand extends CommandRunner { return true; } - @Option({ - flags: '-v, --verbose', - description: 'Verbosity level (-v -vv -vvv)', - defaultValue: 0, - }) - handleVerbose(value: string | boolean, previous: number = 0): number { - if (typeof value === 'boolean') { - // Single `-v` or `--verbose` flag increments verbosity - return previous + 1; - } else if (value === undefined) { - return previous + 1; // Increment if flag is used without value - } else { - // If `-vvv` is passed as one flag, count the number of `v`s - return previous + value.length; + async getBothMyServersConfigsWithoutError(): Promise { + const ini = await import('ini'); + const diskConfig = await readFile(getters.paths()['myservers-config'], 'utf-8').catch( + (_) => null + ); + const memoryConfig = await readFile(getters.paths()['myservers-config-states'], 'utf-8').catch( + (_) => null + ); + + if (memoryConfig) { + return MyServersConfigMemorySchema.parse(ini.parse(memoryConfig)); + } else if (diskConfig) { + return MyServersConfigMemorySchema.parse(ini.parse(diskConfig)); } + return null; } - async report(options: ReportOptions = this.defaultOptions): Promise { - // Check if we have a tty attached to stdout - // If we don't then this is being piped to a log file, etc. - const hasTty = process.stdout.isTTY; - - // Check if we should show interactive logs - // If this has a tty it's interactive - // AND - // If they don't have --raw - const isInteractive = hasTty && !options.raw; - + async report(): Promise { try { - // Show loading message - if (isInteractive) { - this.logger.info('Generating report please wait…'); - } + const { isUnraidApiRunning } = await import('@app/core/utils/pm2/unraid-api-running'); - const jsonReport = options?.json ?? false; + const apiRunning = await isUnraidApiRunning().catch((err) => { + this.logger.debug('failed to get PM2 state with error: ' + err); + return false; + }); - // Find all processes called "unraid-api" which aren't this process - const unraidApiRunning = await isUnraidApiRunning(); - - // Load my servers config file into store - await store.dispatch(loadConfigFile()); - await store.dispatch(loadStateFiles()); - - const { config, emhttp } = store.getState(); - - const client = getApiApolloClient({ localApiKey: config.remote.localApiKey || '' }); - // Fetch the cloud endpoint - const cloud = await getCloudData(client) - .then((data) => { - this.logger.debug('Cloud Data', data); - return data; - }) - .catch((error) => { - this.logger.debug( - 'Failed fetching cloud from local graphql with "%s"', - error instanceof Error ? error.message : 'Unknown Error' - ); - return null; - }); - - // Query local graphql using upc's API key - // Get the servers array - const servers = await getServersData({ client, verbosity: options.verbose }); - - // Check if the API key is valid - const isApiKeyValid = cloud?.apiKey.valid ?? false; - - const reportObject: ReportObject = { - os: { - serverName: emhttp.var.name, - version: emhttp.var.version, - }, - api: { - version: API_VERSION, - status: unraidApiRunning ? 'running' : 'stopped', - environment: process.env.ENVIRONMENT ?? 'THIS_WILL_BE_REPLACED_WHEN_BUILT', - nodeVersion: process.version, - }, - apiKey: isApiKeyValid ? 'valid' : (cloud?.apiKey.error ?? 'invalid'), - ...(servers ? { servers } : {}), - myServers: { - status: config?.remote?.username ? 'authenticated' : 'signed out', - ...(config?.remote?.username - ? { - myServersUsername: config?.remote?.username?.includes('@') - ? 'REDACTED' - : config?.remote.username, - } - : {}), - }, - minigraph: { - status: cloud?.minigraphql.status ?? MinigraphStatus.PRE_INIT, - timeout: cloud?.minigraphql.timeout ?? null, - error: - (cloud?.minigraphql.error ?? !cloud?.minigraphql.status) - ? 'API Disconnected' - : null, - }, - cloud: { - status: cloud?.cloud.status ?? 'error', - ...(cloud?.cloud.error ? { error: cloud.cloud.error } : {}), - ...(cloud?.cloud.status === 'ok' ? { ip: cloud.cloud.ip ?? 'NO_IP' } : {}), - ...(getAllowedOrigins(cloud, options.verbose) - ? { allowedOrigins: getAllowedOrigins(cloud, options.verbose) } - : {}), - }, - }; - - if (jsonReport) { - this.logger.clear(); - this.logger.info(JSON.stringify(reportObject) + '\n'); - return reportObject; - } else { - // Generate the actual report - const report = ` -<-----UNRAID-API-REPORT-----> -SERVER_NAME: ${reportObject.os.serverName} -ENVIRONMENT: ${reportObject.api.environment} -UNRAID_VERSION: ${reportObject.os.version} -UNRAID_API_VERSION: ${reportObject.api.version} -UNRAID_API_STATUS: ${reportObject.api.status} -API_KEY: ${reportObject.apiKey} -MY_SERVERS: ${reportObject.myServers.status}${ - reportObject.myServers.myServersUsername - ? `\nMY_SERVERS_USERNAME: ${reportObject.myServers.myServersUsername}` - : '' - } -CLOUD: ${getReadableCloudDetails(reportObject, options.verbose)} -MINI-GRAPH: ${getReadableMinigraphDetails(reportObject)}${getReadableServerDetails( - reportObject, - options.verbose - )}${getReadableAllowedOrigins(reportObject)} - -`; - this.logger.clear(); - - this.logger.info(report); - return report; - } - } catch (error: unknown) { - if (error instanceof Error) { - this.logger.debug(error); - this.logger.error(`\nFailed generating report with "${error.message}"\n`); - return; - } else { - this.logger.error('Failed generating report'); - return; - } + const config = + (await this.getBothMyServersConfigsWithoutError()) as MyServersConfigMemory & { + connectionStatus: { running: 'yes' | 'no' }; + }; + config.connectionStatus.running = apiRunning ? 'yes' : 'no'; + this.logger.clear(); + this.logger.info(JSON.stringify(config, null, 2)); + } catch (error) { + this.logger.debug('Error Generating Config: ' + error); + this.logger.warn( + JSON.stringify( + { error: 'Please ensure the API is configured before attempting to run a report' }, + null, + 2 + ) + ); } } - async run(_: string[], options?: ReportOptions): Promise { - await this.report(options); + async run(): Promise { + await this.report(); } } diff --git a/api/src/unraid-api/rest/rest.service.ts b/api/src/unraid-api/rest/rest.service.ts index 73e89c628..4121b183c 100644 --- a/api/src/unraid-api/rest/rest.service.ts +++ b/api/src/unraid-api/rest/rest.service.ts @@ -19,7 +19,7 @@ export class RestService { try { const reportCommand = new ReportCommand(new LogService()); - const apiReport = await reportCommand.report({ json: true, verbose: 2, raw: false }); + const apiReport = await reportCommand.report(); this.logger.debug('Report object %o', apiReport); await writeFile(pathToReport, JSON.stringify(apiReport, null, 2), 'utf-8'); } catch (error) {