Skip to content

Commit

Permalink
feat(fota): abort fota jobs
Browse files Browse the repository at this point in the history
  • Loading branch information
coderbyheart committed Sep 23, 2024
1 parent dc80605 commit 59ce779
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 10 deletions.
5 changes: 5 additions & 0 deletions cdk/BackendStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,11 @@ export class BackendStack extends Stack {
mbff.startMultiBundleFOTAFlow.fn,
)

api.addRoute(
'DELETE /device/{deviceId}/fota/job/{jobId}',
mbff.abortMultiBundleFOTAFlow.fn,
)

const updateDevice = new UpdateDevice(this, {
lambdaSources,
layers: [baseLayerVersion],
Expand Down
10 changes: 10 additions & 0 deletions cdk/packBackendLambdas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export type BackendLambdas = {
createCNAMERecord: PackedLambda
multiBundleFOTAFlow: {
start: PackedLambda
abort: PackedLambda
onFail: PackedLambda
getDeviceFirmwareDetails: PackedLambda
getNextBundle: PackedLambda
createFOTAJob: PackedLambda
Expand Down Expand Up @@ -134,6 +136,14 @@ export const packBackendLambdas = async (): Promise<BackendLambdas> => ({
'multiBundleFOTAFlowStart',
'lambda/fota/multi-bundle-flow/start.ts',
),
abort: await packLambdaFromPath(
'multiBundleFOTAFlowAbort',
'lambda/fota/multi-bundle-flow/abort.ts',
),
onFail: await packLambdaFromPath(
'multiBundleFOTAFlowOnFail',
'lambda/fota/multi-bundle-flow/onFail.ts',
),
getDeviceFirmwareDetails: await packLambdaFromPath(
'multiBundleFOTAFlowGetDeviceFirmareDetails',
'lambda/fota/multi-bundle-flow/getDeviceFirmwareDetails.ts',
Expand Down
69 changes: 62 additions & 7 deletions cdk/resources/FOTA/MultiBundleFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { definitions, LwM2MObjectID } from '@hello.nrfcloud.com/proto-map/lwm2m'
import { FOTAJobStatus } from '@hello.nrfcloud.com/proto/hello'
import {
Duration,
aws_events as Events,
aws_lambda_event_sources as EventSources,
aws_events_targets as EventsTargets,
aws_iam as IAM,
aws_iot as IoT,
aws_lambda as Lambda,
Stack,
aws_stepfunctions_tasks as StepFunctionsTasks,
type aws_logs as Logs,
} from 'aws-cdk-lib'
Expand All @@ -27,7 +30,6 @@ import {
StateMachineType,
Succeed,
TaskInput,
type IStateMachine,
} from 'aws-cdk-lib/aws-stepfunctions'
import {
DynamoAttributeValue,
Expand All @@ -44,7 +46,7 @@ import type { DeviceStorage } from '../DeviceStorage.js'
* save the amount of data that needs to be transferred.
*/
export class MultiBundleFOTAFlow extends Construct {
public readonly stateMachine: IStateMachine
public readonly stateMachine: StateMachine
public readonly GetDeviceFirmwareDetails: PackedLambdaFn
public readonly GetNextBundle: PackedLambdaFn
public readonly CreateFOTAJob: PackedLambdaFn
Expand All @@ -53,6 +55,7 @@ export class MultiBundleFOTAFlow extends Construct {
public readonly WaitForUpdateAppliedCallback: PackedLambdaFn
public readonly WaitForUpdateApplied: PackedLambdaFn
public readonly startMultiBundleFOTAFlow: PackedLambdaFn
public readonly abortMultiBundleFOTAFlow: PackedLambdaFn

public constructor(
parent: Construct,
Expand Down Expand Up @@ -437,7 +440,7 @@ export class MultiBundleFOTAFlow extends Construct {
})
deviceFOTA.nrfCloudJobStatusTable.grantReadWriteData(this.stateMachine)

const startMultiBundleFOTAFlow = new PackedLambdaFn(
this.startMultiBundleFOTAFlow = new PackedLambdaFn(
this,
'startMultiBundleFOTAFlow',
lambdas.start,
Expand All @@ -459,10 +462,62 @@ export class MultiBundleFOTAFlow extends Construct {
logGroup: deviceFOTA.logGroup,
},
)
this.startMultiBundleFOTAFlow = startMultiBundleFOTAFlow
this.stateMachine.grantStartExecution(startMultiBundleFOTAFlow.fn)
deviceStorage.devicesTable.grantReadData(startMultiBundleFOTAFlow.fn)
deviceFOTA.jobTable.grantWriteData(startMultiBundleFOTAFlow.fn)
this.stateMachine.grantStartExecution(this.startMultiBundleFOTAFlow.fn)
deviceStorage.devicesTable.grantReadData(this.startMultiBundleFOTAFlow.fn)
deviceFOTA.jobTable.grantWriteData(this.startMultiBundleFOTAFlow.fn)

this.abortMultiBundleFOTAFlow = new PackedLambdaFn(
this,
'abortMultiBundleFOTAFlow',
lambdas.abort,
{
description: 'REST entry point for aborting running FOTA flows',
environment: {
DEVICES_TABLE_NAME: deviceStorage.devicesTable.tableName,
STATE_MACHINE_ARN: this.stateMachine.stateMachineArn,
},
layers,
logGroup: deviceFOTA.logGroup,
initialPolicy: [
new IAM.PolicyStatement({
actions: ['states:DescribeExecution', 'states:StopExecution'],
resources: [
`arn:aws:states:${Stack.of(this).region}:${Stack.of(this).account}:execution:${this.stateMachine.stateMachineName}:*`,
],
}),
],
},
)
deviceStorage.devicesTable.grantReadData(this.abortMultiBundleFOTAFlow.fn)

const onStepFunctionFail = new PackedLambdaFn(
this,
'onFail',
lambdas.onFail,
{
description: 'Handles failed or cancelled step function executions',
environment: {
STATE_MACHINE_ARN: this.stateMachine.stateMachineArn,
JOB_TABLE_NAME: deviceFOTA.jobTable.tableName,
},
layers,
logGroup: deviceFOTA.logGroup,
},
)
deviceFOTA.jobTable.grantWriteData(onStepFunctionFail.fn)
// FIXME: connect to state machine
const eventBus = new Events.EventBus(this, 'eventBus', {})
new Events.Rule(this, 'onStepFunctionFailRule', {
eventPattern: {
source: ['aws.states'],
detail: {
status: ['FAILED', 'TIMED_OUT', 'ABORTED'],
stateMachineArn: [this.stateMachine.stateMachineArn],
},
},
targets: [new EventsTargets.LambdaFunction(onStepFunctionFail.fn)],
eventBus,
})

this.WaitForFOTAJobCompletion = new PackedLambdaFn(
this,
Expand Down
213 changes: 213 additions & 0 deletions features/FOTA-abort.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
---
exampleContext:
fingerprint: 92b.y7i24q
fingerprint_deviceId: oob-352656108602296
APIURL: https://api.hello.nordicsemi.cloud
tsJob1CreatedISO: 2023-09-12T00:01:00.000Z
tsJob1CancelledISO: 2023-09-12T00:03:00.000Z
nrfCloudJobId: bc631093-7f7c-4c1b-aa63-a68c759bcd5c
jobId: 01J861VKYH5QVD6QQ5YXXF20EF
needs:
- Device FOTA
run: only
---

# Abort Device FOTA jobs

> A user abort a running firmware update job for a device.
## Background

Given I have the fingerprint for a `PCA20065` device in `fingerprint`

And I have a random UUIDv4 in `nrfCloudJobId`

And I store `$fromMillis($millis())` into `tsJob1CreatedISO`

And I store `$fromMillis($millis() + 60 * 1000)` into `tsJob1CancelledISO`

## The device reports that it is eligible for FOTA

<!-- Devices have to report that they support FOTA. -->

Given there is this device shadow data for `${fingerprint_deviceId}` in nRF
Cloud

```json
{
"items": [
{
"id": "${fingerprint_deviceId}",
"$meta": {
"createdAt": "${$fromMillis($millis())}",
"updatedAt": "${$fromMillis($millis())}"
},
"state": {
"reported": {
"device": {
"deviceInfo": {
"appVersion": "2.0.0",
"modemFirmware": "mfw_nrf91x1_2.0.1",
"imei": "355025930003908",
"board": "thingy91x",
"hwVer": "nRF9151 LACA ADA"
},
"serviceInfo": {
"fota_v2": ["BOOT", "MODEM", "APP"]
}
}
},
"metadata": {
"reported": {
"device": {
"deviceInfo": {
"appVersion": { "timestamp": 1716801888 },
"modemFirmware": { "timestamp": 1716801888 },
"imei": { "timestamp": 1716801888 },
"board": { "timestamp": 1716801888 },
"hwVer": { "timestamp": 1716801888 }
},
"serviceInfo": {
"fota_v2": [
{
"timestamp": 1717409966
},
{
"timestamp": 1717409966
},
{
"timestamp": 1717409966
}
]
}
}
}
},
"version": 8835
}
}
],
"total": 1
}
```

And I connect to the websocket using fingerprint `${fingerprint}`

Soon I should receive a message on the websocket that matches after 20 retries

```json
{
"@context": "https://github.com/hello-nrfcloud/proto/shadow",
"reported": [
{
"ObjectID": 14401,
"Resources": {
"0": ["BOOT", "MODEM", "APP"],
"99": 1717409966
}
}
]
}
```

## Schedule the FOTA job

Given this nRF Cloud API request is queued for a `POST /v1/fota-jobs` request

```
HTTP/1.1 200 OK
Content-Type: application/json
{"jobId": "${nrfCloudJobId}"}
```

And this nRF Cloud API request is queued for a
`GET /v1/fota-jobs/${nrfCloudJobId}` request

```
HTTP/1.1 200 OK
Content-Type: application/json
{
"createdAt": "${tsJob1CreatedISO}",
"firmware": {
"bundleId": "APP*1e29dfa3*v2.0.1",
"fileSize": 425860,
"firmwareType": "APP",
"host": "firmware.nrfcloud.com",
"uris": [
"bbfe6b73-a46a-43ad-94bd-8e4b4a7847ce/APP*1e29dfa3*v2.0.1/hello-nrfcloud-thingy91x-v2.0.1-fwupd.bin"
],
"version": "v2.0.1"
},
"jobId": "${nrfCloudJobId}",
"lastUpdatedAt": "${tsJob1CreatedISO}",
"name": "${nrfCloudJobId}",
"status": "IN_PROGRESS",
"statusDetail": "Job auto applied",
"target": {
"deviceIds": [
"${fingerprint_deviceId}"
],
"tags": []
}
}
```

When I `POST`
`${APIURL}/device/${fingerprint_deviceId}/fota/app?fingerprint=${fingerprint}`
with

```json
{
"upgradePath": {
">=0.0.0": "APP*1e29dfa3*v2.0.1"
}
}
```

Then the status code of the last response should be `201`

And I should receive a `https://github.com/hello-nrfcloud/proto/fota/job`
response

And I store `id` of the last response into `jobId`

## Cancel the job

When I `DELETE`
`${APIURL}/device/${fingerprint_deviceId}/fota/job/${jobId}?fingerprint=${fingerprint}`

Then the status code of the last response should be `202`

## Job is cancelled

When I `GET`
`${APIURL}/device/${fingerprint_deviceId}/fota/jobs?fingerprint=${fingerprint}`
retrying 10 times

Soon I should receive a `https://github.com/hello-nrfcloud/proto/fota/jobs`
response

And `$.jobs[0]` of the last response should match

```json
{
"deviceId": "${fingerprint_deviceId}",
"status": "FAILED",
"statusDetail": "The job was cancelled."
}
```

## Receive a notification

Soon I should receive a message on the websocket that matches

```json
{
"@context": "https://github.com/hello-nrfcloud/proto/fota/job",
"deviceId": "${fingerprint_deviceId}",
"status": "FAILED",
"statusDetail": "The job was cancelled."
}
```
3 changes: 0 additions & 3 deletions features/FOTA.feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,6 @@ exampleContext:
>
> An update routine consists of one or more update jobs to execute to upgrade
> the device from one (modem) firmware version to another.
>
> TODO: multi-path FOTA needs to wait for the device to report the updated
> version before progressing to the next update
## Background

Expand Down
Loading

0 comments on commit 59ce779

Please sign in to comment.