Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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))
Expand Down
110 changes: 110 additions & 0 deletions cli/commands/import-devices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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 <model> <provisioningList>',
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)
}

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 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',
})
9 changes: 5 additions & 4 deletions cli/commands/register-simulator-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`),
Expand Down
21 changes: 18 additions & 3 deletions cli/commands/show-device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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,
)}`,
],
]),
)
Expand Down
17 changes: 13 additions & 4 deletions nrfcloud/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ type Page<Item> = {
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,
Expand Down Expand Up @@ -76,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 }
| {
Expand Down Expand Up @@ -156,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`, {
Expand Down