From 52595a26b81e6c04dd8b43a5ffd061d3fa2b6007 Mon Sep 17 00:00:00 2001 From: Markus Tacker Date: Wed, 21 Jun 2023 17:26:24 +0200 Subject: [PATCH 1/2] WIP --- cli/cli.ts | 7 +++ cli/commands/import-devices.ts | 112 +++++++++++++++++++++++++++++++++ cli/commands/show-device.ts | 21 ++++++- nrfcloud/apiClient.ts | 4 ++ 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 cli/commands/import-devices.ts diff --git a/cli/cli.ts b/cli/cli.ts index 16d1d3776..dc18b0c98 100644 --- a/cli/cli.ts +++ b/cli/cli.ts @@ -13,6 +13,7 @@ import type { CommandDefinition } from './commands/CommandDefinition' import { configureDeviceCommand } from './commands/configure-device.js' import { configureCommand } from './commands/configure.js' import { createFakeNrfCloudAccountDeviceCredentials } from './commands/createFakeNrfCloudAccountDeviceCredentials.js' +import { importDevicesCommand } from './commands/import-devices.js' import { initializeNRFCloudAccountCommand } from './commands/initialize-nrfcloud-account.js' import { logsCommand } from './commands/logs.js' import { registerDeviceCommand } from './commands/register-device.js' @@ -101,6 +102,12 @@ const CLI = async ({ isCI }: { isCI: boolean }) => { db, devicesTableName: outputs.devicesTableName, }), + importDevicesCommand({ + db, + devicesTableName: outputs.devicesTableName, + ssm, + stackName: STACK_NAME, + }), ) } catch (error) { console.warn(chalk.yellow('⚠️'), chalk.yellow((error as Error).message)) diff --git a/cli/commands/import-devices.ts b/cli/commands/import-devices.ts new file mode 100644 index 000000000..16512d89a --- /dev/null +++ b/cli/commands/import-devices.ts @@ -0,0 +1,112 @@ +import { type DynamoDBClient } from '@aws-sdk/client-dynamodb' +import type { SSMClient } from '@aws-sdk/client-ssm' +import chalk from 'chalk' +import { readFile } from 'node:fs/promises' +import os from 'node:os' +import { table } from 'table' +import { registerDevice } from '../../devices/registerDevice.js' +import { apiClient } from '../../nrfcloud/apiClient.js' +import { getAPISettings } from '../../nrfcloud/settings.js' +import type { CommandDefinition } from './CommandDefinition.js' + +export const importDevicesCommand = ({ + ssm, + db, + devicesTableName, + stackName, +}: { + ssm: SSMClient + db: DynamoDBClient + devicesTableName: string + stackName: string +}): CommandDefinition => ({ + command: 'import-devices ', + action: async (model, provisioningList) => { + const devices: [imei: string, fingerprint: string, publicKey: string][] = ( + await readFile(provisioningList, 'utf-8') + ) + .trim() + .split('\r\n') + .map((s) => + s.split(';').map((s) => s.replace(/^"/, '').replace(/"$/, '')), + ) + .slice(1) + .map( + ([imei, _, fingerprint, publicKey]) => + [imei, fingerprint, publicKey] as [string, string, string], + ) + + console.log( + table([ + ['Fingerprint', 'Device ID'], + ...devices.map(([imei, fingerprint]) => [ + chalk.green(fingerprint), + chalk.blue(imei), + ]), + ]), + ) + + const { apiKey, apiEndpoint } = await getAPISettings({ + ssm, + stackName, + })() + + const client = apiClient({ + endpoint: apiEndpoint, + apiKey, + }) + + const registration = await client.registerDevices( + devices.map(([imei, _, publicKey]) => { + const deviceId = `oob-${imei}` + const certPem = publicKey.replace(/\\n/g, os.EOL) + return { + deviceId, + subType: model.replace(/[^0-9a-z-]/gi, '-'), + tags: [model.replace(/[^0-9a-z-]/gi, ':')], + certPem, + } + }), + ) + + if ('error' in registration) { + console.error(registration.error.message) + process.exit(1) + } + + if ('success' in registration && registration.success === false) { + console.error(chalk.red(`Registration failed`)) + process.exit(1) + } + + console.log(chalk.green(`Registered devices with nRF Cloud`)) + + for (const [imei, fingerprint] of devices) { + //const deviceId = `oob-${imei}` + const deviceId = imei + + const res = await registerDevice({ + db, + devicesTableName, + })({ + id: deviceId, + model, + fingerprint, + }) + if ('error' in res) { + console.error( + chalk.red(`Failed to store ${deviceId} device fingerprint!`), + ) + console.error(res.error.message) + } else { + console.log( + chalk.green( + `Registered device ${deviceId} with fingerprint ${fingerprint}`, + ), + chalk.cyan(fingerprint), + ) + } + } + }, + help: 'Import factory provisioned devices', +}) diff --git a/cli/commands/show-device.ts b/cli/commands/show-device.ts index 475c17aa7..06cc27fba 100644 --- a/cli/commands/show-device.ts +++ b/cli/commands/show-device.ts @@ -48,10 +48,22 @@ export const showDeviceCommand = ({ }) const maybeNrfCloudDevice = await client.getDevice(device.id) + const account = await client.account() + if ('error' in account) { + console.error(chalk.red('⚠️'), '', chalk.red(account.error.message)) + process.exit(1) + } console.log( table([ - ['Fingerprint', 'Device ID', 'Model', 'nRF Cloud', 'Connected'], + [ + 'Fingerprint', + 'Device ID', + 'Model', + 'nRF Cloud', + 'Connected', + 'nRF Cloud Account', + ], [ chalk.green(device.fingerprint), chalk.blue(device.id), @@ -60,8 +72,11 @@ export const showDeviceCommand = ({ 'device' in maybeNrfCloudDevice && maybeNrfCloudDevice.device?.state?.reported?.connection?.status === 'connected' - ? chalk.green('✅') - : chalk.red('⚠️'), + ? chalk.green('Yes') + : chalk.red('No'), + `${chalk.cyanBright(account.account.team.name)} ${chalk.cyan.dim( + account.account.team.tenantId, + )}`, ], ]), ) diff --git a/nrfcloud/apiClient.ts b/nrfcloud/apiClient.ts index 25063846c..69d5e2fc3 100644 --- a/nrfcloud/apiClient.ts +++ b/nrfcloud/apiClient.ts @@ -42,6 +42,10 @@ type Page = { type AccountInfo = { mqttEndpoint: string // e.g. 'mqtt.nrfcloud.com' mqttTopicPrefix: string // e.g. 'prod/a0673464-e4e1-4b87-bffd-6941a012067b/', + team: { + tenantId: string // e.g. 'bbfe6b73-a46a-43ad-94bd-8e4b4a7847ce', + name: string // e.g. 'hello.nrfcloud.com' + } } export const apiClient = ({ endpoint, From d4e88f094db3f6db42ed796f3406bb4318501bdb Mon Sep 17 00:00:00 2001 From: Markus Tacker Date: Wed, 21 Jun 2023 21:10:02 +0200 Subject: [PATCH 2/2] feat(cli): print bulk ops ID --- cli/commands/import-devices.ts | 12 +++++------- cli/commands/register-simulator-device.ts | 9 +++++---- nrfcloud/apiClient.ts | 13 +++++++++---- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/cli/commands/import-devices.ts b/cli/commands/import-devices.ts index 16512d89a..97692322a 100644 --- a/cli/commands/import-devices.ts +++ b/cli/commands/import-devices.ts @@ -74,16 +74,14 @@ export const importDevicesCommand = ({ process.exit(1) } - if ('success' in registration && registration.success === false) { - console.error(chalk.red(`Registration failed`)) - process.exit(1) - } - console.log(chalk.green(`Registered devices with nRF Cloud`)) + console.log( + chalk.yellow.dim(`Bulk ops ID:`), + chalk.yellow(registration.bulkOpsRequestId), + ) for (const [imei, fingerprint] of devices) { - //const deviceId = `oob-${imei}` - const deviceId = imei + const deviceId = `oob-${imei}` const res = await registerDevice({ db, diff --git a/cli/commands/register-simulator-device.ts b/cli/commands/register-simulator-device.ts index efb2f51d7..117ef8a6b 100644 --- a/cli/commands/register-simulator-device.ts +++ b/cli/commands/register-simulator-device.ts @@ -185,10 +185,11 @@ export const registerSimulatorDeviceCommand = ({ process.exit(1) } - if ('success' in registration && registration.success === false) { - console.error(chalk.red(`Registration failed`)) - process.exit(1) - } + console.log(chalk.green(`Registered devices with nRF Cloud`)) + console.log( + chalk.yellow.dim(`Bulk ops ID:`), + chalk.yellow(registration.bulkOpsRequestId), + ) console.log( chalk.green(`Registered device with nRF Cloud`), diff --git a/nrfcloud/apiClient.ts b/nrfcloud/apiClient.ts index 69d5e2fc3..feaaa560b 100644 --- a/nrfcloud/apiClient.ts +++ b/nrfcloud/apiClient.ts @@ -80,7 +80,7 @@ export const apiClient = ({ // A unique ES256 X.509 certificate in PEM format, wrapped in double quotes (to allow for line breaks in CSV) /^-{5}BEGIN CERTIFICATE-{5}(\r\n|\r|\n)([^-]+)(\r\n|\r|\n)-{5}END CERTIFICATE-{5}(\r\n|\r|\n)$/ certPem: string }[], - ) => Promise<{ error: Error } | { success: boolean }> + ) => Promise<{ error: Error } | { bulkOpsRequestId: string }> account: () => Promise< | { error: Error } | { @@ -160,12 +160,17 @@ export const apiClient = ({ const res = await registrationResult.json() - if ('bulkOpsRequestId' in res) return { success: true } + if ('bulkOpsRequestId' in res) + return { bulkOpsRequestId: res.bulkOpsRequestId } if ('code' in res && 'message' in res) - return { error: new Error(`${res.message} (${res.code})`) } + return { + error: new Error( + `${res.message} (${res.code}): ${JSON.stringify(res)}`, + ), + } - return { success: false } + return { error: new Error(`Import failed: ${JSON.stringify(res)}`) } }, account: async () => fetch(`${slashless(endpoint)}/v1/account`, {