This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Device Provisioning

The device needs to be configured before it can make a connection to the cloud.

The following initial configuration steps are required:

  • Create a device in the cloud backend, such as Azure IoT Hub
  • Configure authentication on device
  • Configure credentials for accessing private container registries

1 - Manual Provisioning

Follow these steps to do a manual device provisioning:

  • Generate the device certificate (eg using openssl) and sign it with your CA.
  • Log in to Azure Portal, Go to Azure Iot Hub and create a new device
  • Select the proper authentication type, e.g. X.509 Self-signed or X.509 CA Signed
  • Copy the device certificate (cert file and key file) to the device to /data/var/certificate
  • Restart cloud connector service or container.

Create a device in Azure IoT Hub

For the device to be connectable, it needs to be known to the cloud service first. In these steps, we will create a new device identity by using Azure IoT Hub.

Pre-Requisites:

  • Virtual device must already be started with runqemu ... or leda

    Note: For Raspberry Pi, please follow the manual steps below and adapt the SSH connection options to the IP of your Raspbery Pi.

  • The virtual device needs to be remotely accessible via ssh port 2222 on the host’s localhost (Qemu port forwarding in userspace) or via ssh port 22 on the IP address 192.168.7.2 (Qemu virtual networking using TAP network interface)

  • The container runtime needs to have started successfully, check with sdv-health

  • A Device has been created in Azure IoT Hub

    Note: Do NOT create an “edge” device.

Configure authentication on device

For the proper device authentication, the device management backend authority needs to issue a device-specific certificate and sign it. This is a complex process and subject to the specific situation.

For the Leda quickstart images, the software configuration is prepared with dummy certificates which need to be replaced.

ATTENTION: The Leda example device certificates are public and insecure, they only serve demonstration purposes. You need to replace the intermediate certificates and device certificates with your own.

  • Generate a device certificate using openssl
  • Sign it with your intermediate CA certificate
  • Put it into /data/var/certificate/
  • Restart the cloud connector service or container: systemctl restart cloud-connector or kanto-cm stop -n cloudconnector --force; kanto-cm start -n cloudconnector

When finished, continue with

Private container registries

Please refer to the Container Registries on how to configure private container registries.

2 - Provisioning with sdv-provision

Meta-leda and the Leda-quickstart image provide a utility sdv-provision that simplifies the device provisioning procedure.

Connecting to the device

Since the procedure includes copying and pasting device IDs, it is recommended to connect to the device over a ssh connection. sdv-motd provides an easy way to find your device’s ip. More information on connecting via ssh can be found here.

Provisioning via a connection string

Device-side

  1. After connecting via ssh run sdv-provision.

    Example output:

    root@qemux86-64:~# sdv-provision
    Checking Eclipse Leda Device Provisioning configuration...
    - Certificates directory exists
    Checking Device ID
    - Based on network device: eth0
    - File does not exist, creating: /etc/deviceid
    - Device ID: XX-XX-XX-XX-XX-XX
    Checking whether either IdScope or ConnectionString is configured
    - Neither Id Scope file nor ConnectionString found, needs manual configuration
    Do you want to use the global Azure IoT Device Provisioning Service (DPS) by using an Id Scope, or do you want to use a direct connection to a specific Azure IoT Hub using a Connection String?
    d) Azure IoT Device Provisioning Service (DPS) with Id Scope
    h) Azure IoT Hub with Connection String
    Choose:
    
  2. Note the generated Device ID (XX-XX-XX-XX-XX-XX).

  3. Type h /Azure IoT Hub with Connection String/ and press Enter.

  4. Paste your Azure IoT Hub Connection String: HostName=<IoT Hub in Azure>.azure-devices.net;DeviceId=<XX-XX-XX-XX-XX-XX> and press enter. Where <IoT Hub in Azure> is your Azure IoT Hub name.

Azure Portal

  1. Go to https://portal.azure.com/ and to your Azure IoT Hub named <IoT Hub in Azure>.
  2. Choose Devices -> Add Device.
  3. Enter the Device ID XX-XX-XX-XX-XX-XX generated on the previous step as Device ID.
  4. Pick X.509 Self-Signed and paste the two thumbprints generated by sdv-provision.
  5. Save.

Device-Side

After all of the above steps have been completed, connect back to your device and restart the cloudconnector container by running:

kanto-cm stop -n cloudconnector --force
kanto-cm start -n cloudconnector

Or alternatively use:

$ kantui

And restart the container from the TUI.

3 - Vehicle Update Manager

The Vehicle Update Manager delegates two different types of updates:

  1. The Desired State on the container layer
  2. The Self Update on operating system layer

Vehicle Update Manager Architecture Overview

Desired State

The Desired State is applied at runtime on the container layer.

This type of update mechanism can update vehicle applications, vehicle services and other containers together with configuration resources or data files at runtime. If the applications support it, the rollout can also use high-availability strategies, such as rolling deployments. You can find out more about more about the Container Update Agent here.

Self Update

The Self Update is applied on reboot of the device only.

This type of update mechanism is used for system-level updates which require the operating system to be rebooted to take effect. You can find out more about the Self Update Agent in the tutorial.

3.1 - Configuration

Config file

The default location for the VUM service on the Leda Distro image can be found at /etc/update-manager/config.json. The location of the config file is specified with the --cfg-file [PATH] flag when starting the binary.

Minimal configuration

A minimal configuration (that is used by the Leda Distro) is the following:

{
    "log": {
      "logFile": "/var/log/update-manager/update-manager.log"
    },
    "domain": "vehicle",
    "agents": {
        "containers": {
            "name": "containers",
            "rebootRequired": false,
            "readTimeout": "30s"
        },
        "self-update": {
            "name": "self-update",
            "rebootRequired": true,
            "readTimeout": "30s"
        }
    },
    "thingsEnabled": false
}

Where:

  • “domain”: specifies the prefix for the MQTT topic, e.g. in this case it’s set to vehicle, so all VUM related topic are prefixed with vehicleupdate/[TOPIC]
  • “agents”: configures the update agents that would be available to VUM. Here the “readTimeout” key specifies how long should VUM wait for a response from the configured agent.
  • “thingsEnabled”: whether VUM should use the Kanto Things API for communication or not.

Full configuration

An example configuration file for VUM with all available options is the following one:

{
  "log": {
    "logFile": "log/update-manager.log",
    "logLevel": "ERROR",
    "logFileSize": 3,
    "logFileCount": 6,
    "logFileMaxAge": 29
  },
  "connection": {
    "broker":"www",
    "keepAlive": 500,
    "disconnectTimeout": 500,
    "username":"username",
    "password":"pass",
    "connectTimeout": 500,
    "acknowledgeTimeout": 500,
    "subscribeTimeout": 500,
    "unsubscribeTimeout": 500
  },
  "domain": "vehicle",
  "thingsEnabled": false,
  "rebootEnabled": true,
  "rebootAfter": "1m",
  "reportFeedbackInterval": "2m",
  "currentStateDelay": "1m",
  "phaseTimeout": "2m",
  "agents": {
    "self-update": {
      "rebootRequired": false,
      "readTimeout": "20s"
    },
    "containers": {
      "rebootRequired": true,
      "readTimeout": "30s"
    }
  }
}

Here keys (other the ones above) are self-explanatory. The “connection” section object specifies configuration options for the MQTT connection. Allowed log leves (in order of increasing verbosity)are: ERROR, WARN, INFO, DEBUG, TRACE.

3.2 - Message Flow

Note: This part of the Leda OSS stack is still in active development and there might be differences between the documented and the current version.

As described in Vehicle Update Manager takes a full update message for all domains, identifies the domains affected, the current component versions, actions to be taken, etc., and delegates those actions to the correct update agent (e.g. self-update/container update).

The update manager (UM) in Leda-distro is configured as specified in UM’s config.json, which on the final image is usually located in /etc/update-manager/config.json. The standard two domains supported are containers and self-update, with the latter requiring a reboot on a successful update.

Note: UM allows a custom prefix for all of its topics to be defined (“domain”) in its config.json On the Leda Distro image the default prefix is “vehicle”. If you decide to change it, replace “vehicle” in all MQTT topics mentioned below with your custom prefix.

A full specification of UM’s API and the relevant MQTT topics can be found in its documentation.

Update Message

The general structure of the update message is as follows:

{
  "activityId": "correlation-activity-uuid",
  "timestamp": 123456789,
  "payload": {
    "domains": [
      {
        "id": "domain",
        "config": [],
        "components": [
            {
                "id": "component1",
                "version": "component1-version",
                "config": [
                    {
                        "key": "component1-config-key-1",
                        "value": "component1-config-value-1"
                    }
                ]
            }
        ]
      }
    ]
  }
}

Where multiple domains and components per domain (each with multiple configuration key-value pairs) are allowed. To trigger an update, publish your full update message on the vehicleupdate/desiredstate MQTT topic. When UM receives your message, it splits it across the required domains and publishes messages to the topics that the domain-specific agents are monitoring.

Similarly, update progress can be monitored on the vehicleupdate/desiredstatefeedback topic.

Self-update

The messages for triggering a self-update (image update) are the same as in The Self Update Tutorial. The only difference here is that when you publish a self-update message on the vehicleupdate/desiredstate topic, UM will automatically take care to forward your message to the self-update agent, including forwarding back the update feedback on the vehicleupdate/-namespaced topics.

Containers update (desired state)

Similarly to the self-update, you should start by understanding the operation of the Container Update Agent. After constructing the desired state message you can publish it on the vehicleupdate/desiredstate and UM will, again, forward it to CUA automatically, based on the domain specified in the update message.

Combined update messages

The real strength of UM is deploying updates accross multiple domains with a single update message. For example, if you’d like to deploy an image update bundle (self-update) and a single hello-world container image with the environment variable FOO=BAR set, you can construct the following message:

{
   "activityId":"random-uuid-as-string",
   "timestamp":123456789,
   "payload":{
      "domains":[
         {
            "id":"self-update",
            "components":[
               {
                  "id":"os-image",
                  "version":"${VERSION_ID}",
                  "config":[
                     {
                        "key":"image",
                        "value":"https://leda-bundle-server/sdv-rauc-bundle-minimal-qemux86-64.raucb"
                     }
                  ]
               }
            ]
         },
         {
            "id":"containers",
            "config":[],
            "components":[
               {
                  "id":"hello-world",
                  "version":"latest",
                  "config":[
                     {
                        "key":"image",
                        "value":"docker.io/library/hello-world:latest"
                     },
                     {
                        "key":"env",
                        "value":"FOO=BAR"
                     }
                  ]
               }
            ]
         }
      ]
   }
}

And publish the message on the MQTT topic vehicleupdate/desiredstate. UM will take actions to identify the affected update domains and publish the correct messages on the respective topics. All feedback from the specific update agents will be forwarded back to the UM and published on the vehicleupdate/desiredstatefeedback topic. All these messages will use the same activityId so they can be correlated with each other.

4 - Self Updates

In general, the self-update mechanism for operating system level updates is done with two separate partitions. While one partition is the actively booted partition and in use, the other partition can be updated by writing a partition image to it, as it is unused or inactive.

Once the download and writing is done, a reboot is triggered and the boot loader will now switch to the newly updated partition.

If the booting of the updated partition fails, the self update mechanism can revert back to the previous partition or boot to a rescue partition.

Leda Self Update

As updating the running operating system cannot be done at runtime, the approach requires additional disk space, a second partition and also requires the device to be rebooted for the updates to take effect.

In a vehicle, the self-updater cannot decide on its own when to do a reboot, as the vehicle must be in a safe condition (eg parked, state of charge etc.). Hence, the trigger for performaing the switch to another slot and a subsequent reboot is handed over to a higher level component, such as the vehicle update manager, which may in turn rely on driver feedback or other conditions.

Implementation with RAUC Update Service

RAUC is a lightweight update client that runs on your embedded device and reliably controls the procedure of updating your device with a new firmware revision.

For general usage of the RAUC tool, please see the RAUC User manual

Reference configuration

The project contains an example reference implementation and configuration using RAUC, which allows the evaluation of the concepts, mechanisms and involved software components in an emulated, virtual environment.

The Leda quickstart image contains the following disk partitions:

  • a small rescue partition
  • a full SDV installation with a container runtime, pre-loaded SDV container images and deployment specifications and additional developer tools such as nerdctl and kantui.
  • a minimal SDV installation with a container runtime, but no additional examples or developer tools. This partition is used to demonstrate the self-update functionality.
  • additional boot and data partitions for keeping system state information

Note: All three rootfs partitions (rootfs) initially contain the same identical copies of the base operating system. Both SDV Root partitions will use the same shared data partition for the container persistent state.

4.1 - Self Update Tutorial

This chapter describes the steps necessary to perform a local (without cloud) self update of the operating system.

Self Update Architecture

Self-Update using RAUC Update Bundles

  • On host: Update bundle sdv-rauc-bundle-qemux86-64.raucb is in current folder

    Note: In the development environment, the update RAUC Update Bundle is located in the BitBake machine-specific output folder Example location is tmp/deploy/images/qemux86-64

  • On host: Start a dummy web server for serving the update file

    python3 -m http.server --bind 192.168.7.1
    
  • On host: open two new terminals - one for monitoring and one for triggering the self-update

    • Terminal 1: To view the progress, watch the MQTT topics selfupdate/desiredstate and selfupdate/desiredstatefeedback:

      mosquitto_sub -h 192.168.7.2 -p 1883 -t "selfupdate/#"
      
    • Terminal 2: Trigger the actual self update process by publishing an MQTT message to selfupdate/desiredstate:

      mosquitto_pub -h 192.168.7.2 -p 1883 -t "selfupdate/desiredstate" -f start-update-example.json
      mosquitto_pub -h 192.168.7.2 -p 1883 -t "selfupdate/desiredstate/command" -f download-command.json
      mosquitto_pub -h 192.168.7.2 -p 1883 -t "selfupdate/desiredstate/command" -f update-command.json
      mosquitto_pub -h 192.168.7.2 -p 1883 -t "selfupdate/desiredstate/command" -f activate-command.json
      mosquitto_pub -h 192.168.7.2 -p 1883 -t "selfupdate/desiredstate/command" -f cleanup-command.json
      

      Files:

      start-update-example.json

      download-command.json

      update-command.json

      activate-command.json

      cleanup-command.json

  • Switch to a terminal in the guest

  • On guest: After the self update process completed, check the status:

    rauc status --detailed
    

Self-Update Trigger Message

start-update-example.json file:

{
    "activityId": "random-uuid-as-string",
    "timestamp": 123456789,
    "payload": {
        "domains": [
            {
                "id": "self-update",
                "components": [
                    {
                        "id": "os-image",
                        "version": "${VERSION_ID}",
                        "config": [
                            {
                                "key": "image",
                                "value": "http://leda-bundle-server/sdv-rauc-bundle-minimal-qemux86-64.raucb"
                            }
                        ]
                    }
                ]
            }
        ]
    }
}

Example Message Flows

Current State

  1. Initial response message on startup from self update agent in topic selfupdate/currentstate, or upon request by sending message to selfupdate/currentstate/get

     {
           "activityId": "1234567890",
           "timestamp": 1687510087
     }
    

    Response by Self Update Agent on topic selfupdate/currentstate

     {
           "activityId": "1234567890",
           "timestamp": 1687787461,
           "payload": {
                 "softwareNodes": [
                 {
                       "id": "self-update-agent",
                       "version": "build-152",
                       "name": "OTA NG Self Update Agent",
                       "type": "APPLICATION"
                 },
                 {
                       "id": "self-update:leda-deviceimage",
                       "version": "v0.0.10-194-g3d48cb8",
                       "name": "Official Leda device image",
                       "type": "IMAGE"
                 }
                 ],
                 "hardwareNodes": [],
                 "associations": [
                 {
                       "sourceId": "self-update-agent",
                       "targetId": "self-update:leda-deviceimage"
                 }
                 ]
           }
     }
    

Desired State

  1. External trigger to update via desired state on topic selfupdate/desiredstate:

     {
           "activityId": "<uuid>",
           "timestamp": 123456789,
           "payload": {
                 "domains": [
                       {
                       "id": "self-update",
                       "components": [
                             {
                                   "id": "os-image",
                                   "version": "${VERSION_ID}",
                                   "config": [
                                   {
                                         "key": "image",
                                         "value": "http://leda-bundle-server/sdv-rauc-bundle-qemux86-64.raucb"
                                   }
                                   ]
                             }
                       ]
                       }
                 ]
           }
     }
    

    Response on topic selfupdate/desiredstatefeedback

     {
           "activityId": "1234567890",
           "timestamp": 1687786390,
           "payload": {
                 "status": "IDENTIFYING",
                 "message": "Self-update agent has received new desired state request and is evaluating it.",
                 "actions": []
           }
     }
    
     {
           "activityId": "1234567890",
           "timestamp": 1687786390,
           "payload": {
                 "status": "IDENTIFIED",
                 "message": "Self-update agent is about to perform an OS image update.",
                 "actions": [
                 {
                       "component": {
                             "id": "self-update:os-image",
                             "version": "v0.0.10-194-g3d48cb8"
                       },
                       "status": "IDENTIFIED",
                       "progress": 0,
                       "message": "Self-update agent is about to perform an OS image update."
                 }
                 ]
           }
     }
    
  2. External trigger to download command on topic selfupdate/desiredstate/command:

     {
           "activityId": "1234567890",
           "timestamp": 1687510087,
           "payload": {
                 "baseline": "BASELINE NAME",
                 "command": "DOWNLOAD"
           }
     }
    

    Response on topic selfupdate/desiredstatefeedback

     {
           "activityId": "1234567890",
           "timestamp": 1687786931,
           "payload": {
                 "status": "DOWNLOADING",
                 "message": "Self-update agent is performing an OS image update.",
                 "actions": [
                 {
                       "component": {
                             "id": "self-update:os-image",
                             "version": "v0.0.10-194-g3d48cb8"
                       },
                       "status": "DOWNLOADING",
                       "progress": 0,
                       "message": "Downloading 0.0 MiB..."
                 }
                 ]
           }
     }     
    
    {
          "activityId": "1234567890",
          "timestamp": 1687786936,
          "payload": {
                "status": "DOWNLOAD_SUCCESS",
                "message": "Self-update agent is performing an OS image update.",
                "actions": [
                {
                      "component": {
                            "id": "self-update:os-image",
                            "version": "v0.0.10-194-g3d48cb8"
                      },
                      "status": "DOWNLOAD_SUCCESS",
                      "progress": 100,
                      "message": "Downloaded 106.3 MiB..."
                }
                ]
          }
    }
    
  3. External trigger to update command on topic selfupdate/desiredstate/command:

     {
           "activityId": "1234567890",
           "timestamp": 1687510087,
           "payload": {
                 "baseline": "BASELINE NAME",
                 "command": "UPDATE"
           }
     }
    

    Response on topic selfupdate/desiredstatefeedback

     {
           "activityId": "1234567890",
           "timestamp": 1687787145,
           "payload": {
                 "status": "UPDATING",
                 "message": "Self-update agent is performing an OS image update.",
                 "actions": [
                 {
                       "component": {
                             "id": "self-update:os-image",
                             "version": "v0.0.10-194-g3d48cb8"
                       },
                       "status": "UPDATING",
                       "progress": 0,
                       "message": "Checking bundle version and version in desired state request."
                 }
                 ]
           }
     }
    
    {
          "activityId": "1234567890",
          "timestamp": 1687787228,
          "payload": {
                "status": "UPDATE_SUCCESS",
                "message": "Self-update completed, reboot required.",
                "actions": [
                {
                      "component": {
                            "id": "self-update:os-image",
                            "version": "v0.0.10-194-g3d48cb8"
                      },
                      "status": "UPDATING",
                      "progress": 100,
                      "message": "Writing partition completed, reboot required."
                }
                ]
          }
    }
    
  4. External trigger to activate command on topic selfupdate/desiredstate/command:

     {
           "activityId": "1234567890",
           "timestamp": 1687510087,
           "payload": {
                 "baseline": "BASELINE NAME",
                 "command": "ACTIVATE"
           }
     }
    

    Response on topic selfupdate/desiredstatefeedback

     {
           "activityId": "1234567890",
           "timestamp": 1687787302,
           "payload": {
                 "status": "ACTIVATING",
                 "message": "Self-update agent is performing an OS image activation.",
                 "actions": [
                 {
                       "component": {
                             "id": "self-update:os-image",
                             "version": "v0.0.10-194-g3d48cb8"
                       },
                       "status": "UPDATING",
                       "progress": 0,
                       "message": "Self-update agent is performing an OS image activation."
                 }
                 ]
           }
     }
    
    {
          "activityId": "1234567890",
          "timestamp": 1687787303,
          "payload": {
                "status": "ACTIVATION_SUCCESS",
                "message": "Self-update agent has activated the new OS image.",
                "actions": [
                {
                      "component": {
                            "id": "self-update:os-image",
                            "version": "v0.0.10-194-g3d48cb8"
                      },
                      "status": "UPDATED",
                      "progress": 0,
                      "message": "Self-update agent has activated the new OS image."
                }
                ]
          }
    }
    
  5. External trigger to cleanup command on topic selfupdate/desiredstate/command:

     {
           "activityId": "1234567890",
           "timestamp": 1687510087,
           "payload": {
                 "baseline": "BASELINE NAME",
                 "command": "CLEANUP"
           }
     }
    

    Response on topic selfupdate/desiredstatefeedback

     {
           "activityId": "1234567890",
           "timestamp": 1687787382,
           "payload": {
                 "status": "CLEANUP_SUCCESS",
                 "message": "Self-update agent has cleaned up after itself.",
                 "actions": [
                 {
                       "component": {
                             "id": "self-update:os-image",
                             "version": "v0.0.10-194-g3d48cb8"
                       },
                       "status": "UPDATE_SUCCESS",
                       "progress": 0,
                       "message": "Self-update agent has activated the new OS image."
                 }
                 ]
           }
     }
    
    {
          "activityId": "1234567890",
          "timestamp": 1687787382,
          "payload": {
                "status": "COMPLETE",
                "message": "Self-update completed.",
                "actions": [
                {
                      "component": {
                            "id": "self-update:os-image",
                            "version": "v0.0.10-194-g3d48cb8"
                      },
                      "status": "UPDATE_SUCCESS",
                      "progress": 0,
                      "message": "Self-update agent has activated the new OS image."
                }
                ]
          }
    }
    

Rollback command

  • External trigger to rollback command on topic selfupdate/desiredstate/command:

     {
           "activityId": "1234567890",
           "timestamp": 1687510087,
           "payload": {
                 "baseline": "BASELINE NAME",
                 "command": "ROLLBACK"
           }
     }
    

    Self Update Agent returns in Idle state

Can be used from

  • Downloading (when it is waiting for DOWNLOAD command)
  • Installing (when it is waiting for UPDATE command)
  • Installed (when it is waiting for ACTIVATE command)
  • Failed (when it is waiting for CLEANUP command)

4.2 - RAUC Integration

Leda integrates RAUC as a reference implementation and example configuration. It allows the evaluation of the concepts, mechanisms and involved software components in an emulated, virtual environment or on physical devices.

Checking the RAUC Status

Get the current RAUC boot status:

rauc status

Example output:

root@qemux86-64:~# rauc status
=== System Info ===
Compatible:  Eclipse Leda qemu86-64
Variant:     
Booted from: rootfs.1 (SDV_B)

=== Bootloader ===
Activated: rootfs.1 (SDV_B)

=== Slot States ===

o [rootfs.1] (/dev/sda5, ext4, inactive)
        bootname: SDV_B
        mounted: /
        boot status: good

x [rootfs.0] (/dev/sda4, ext4, booted)
        bootname: SDV_A
        boot status: good

Forcing to boot the other slot

To manually force the device to boot into another slot, mark the current booted slot as bad, mark the other partitions as active and perform a reboot:

rauc status mark-bad booted
rauc status mark-active other
reboot now

Testing the rescue system

By marking both root slots as bad, the bootloader is supposed to boot the rescue system:

rauc status mark-bad rootfs.0
rauc status mark-bad rootfs.1
reboot now

Example output of rauc:

o [rootfs.1] (/dev/sda5, ext4, inactive)
        bootname: B
        boot status: bad

o [rootfs.0] (/dev/sda4, ext4, booted)
        bootname: A
        mounted: /
        boot status: bad

Customizations

The configurations can be customized by applying or patching the following files:

  • RAUC Configuration file: meta-leda/recipes-bsp/rauc/files/qemux86-64/system.conf
  • Bootloader Configuration file: meta-leda/recipes-bsp/grub/files/grub.cfg
  • The physical disk partition configuration: meta-leda/recipes-sdv/wic/qemux86-grub-efi.wks

RAUC System Configuration

The RAUC System Configuration is the central configuration of the RAUC Update system.

Example:

[system]
compatible=Eclipse Leda qemu86-64
bootloader=grub
grubenv=/grubenv/grubenv
statusfile=/data/rauc.status

[keyring]
path=ca.cert.pem

[slot.efi.0]
device=/dev/sda
type=boot-gpt-switch
region-start=4M
region-size=100M

[slot.rescue.0]
device=/dev/sda3
type=ext4
readonly=true

[slot.rootfs.0]
device=/dev/sda4
type=ext4
bootname=SDV_A

[slot.rootfs.1]
device=/dev/sda5
type=ext4
bootname=SDV_B

GRUB Bootloader Configuration

The GRUB bootloader has a configuration file which describes which partitions are bootable, which partition they are located at and a reference to RAUC’s slot name.

The configuration also contains RAUC specific logic and variables required for a proper integration. Please see the full grub.cfg in the source repository and RAUC Documentation - Integration - GRUB for details.

Excerpt:

...

menuentry "SDV Slot A (OK=$SDV_A_OK TRY=$SDV_A_TRY)" {
    linux (hd0,4)/boot/bzImage root=/dev/vda4 $CMDLINE rauc.slot=SDV_A
}

menuentry "SDV Slot B (OK=$SDV_B_OK TRY=$SDV_B_TRY)" {
    linux (hd0,5)/boot/bzImage root=/dev/vda5 $CMDLINE rauc.slot=SDV_B
}

U-Boot Bootloader Configuration

Similarly to GRUB, integration of RAUC with U-Boot requires custom boot scripting. A highly detailed explaination can, again, be found in the official RAUC Documentation - Integration - U-Boot.

Meta-Leda provides such integration recipes and scripts for all U-boot based targets, for which a Leda Quickstart image is available (qemuarm64, qemuarm and rpi4-64). For example:

Note: A custom U-Boot device defconfig might be required for some devices to be integrated with RAUC. Leda Quickstart images patch the default defconfigs for qemuarm64 and qemuarm to save the U-Boot environment in a VFAT BOOT partition.

Disk Partitioning with OpenEmbedded Image Creator (WIC)

The OpenEmbedded Image Creator is used in BitBake to actually create full disk images with multiple partitions.

These disk images are machine specific and the structure of the partitions are configured in OpenEmbedded Kickstart files (*.wks).

Excerpt qemux86-grub-efi.wks

Note: The excerpt is exemplary, please see the sources for a full representation and documentation.

bootloader --ptable gpt

part --fixed-size 50M --source rawcopy --sourceparams="file=efi-boot.vfat" --fstype=vfat --label boot --active

part --fixed-size 10M --source rawcopy --sourceparams="file=grubenv.vfat" --fstype=vfat --label grubenv

part /rescue --source rootfs --fstype=ext4 --label rescue

part / --source rootfs --fstype=ext4 --label root_a

part / --source rootfs --fstype=ext4 --label root_b

part /data --fixed-size 4G --fstype=ext4 --label data

4.3 - API Reference

The self update agent (SUA) is a component responsible for the OS Update process SUA is communicating on MQTT interface via usage of defined messages. Internally, SUA uses RAUC to perform the update

Following sequence diagram shows the happy path example of communication between components.

Process Overview

sequenceDiagram participant m as MQTT Broker participant s as SUA participant r as RAUC s -->> m: connect loop Wait for OTA trigger Note left of s: Initial start s ->> m: Current state (installed version from booted partition) Note left of s: Trigger for OTA m ->> s: Desired state request (new version and url to bundle) s ->> m: Feedback (update actions are identified) Note left of s: Command for Download m ->> s: Download command s ->> s: Download bundle s ->> m: Feedback (downloading/downloaded/failed) Note left of s: Command for Update m ->> s: Update command s ->> r: Flash image to partition r ->> r: Flashing... s ->> m: Feedback (updating with percentage) r ->> s: Flash completed/failed s ->> m: Feedback (updated/failed) Note left of s: Command for Activate m ->> s: Activate command s ->> r: Switch partitions (booted <-> other) r ->> s: Switch completed/failed s ->> m: Feedback (activated/failed) Note left of s: Command for Cleanup m ->> s: Cleanup command s ->> s: Remove temporary files s ->> m: Cleanup completed + status from previously failed state
(completed/failed) end
stateDiagram Uninitialized --> Connected: Connected Connected --> Identified: Start (OTA trigger) Identified --> Downloading: Command download Identified --> Failed: If OTA trigger is invalid Downloading --> Updating: Command update Downloading --> Failed: If download has failed Updating --> Activate: Command activate Updating --> Failed: If update has failed Activate --> Cleanup: Command cleanup Activate --> Failed: If activate has failed Failed --> Cleanup: Command cleanup Cleanup --> Idle

Important: Uninitialized state is the default entry state or state in case connection is lost. To simplify reading of the diagram arrows from other states to Unitialized have been removed.

MQTT communication is done over 5 MQTT topics:

Trigger OTA

Topic Direction Description
selfupdate/desiredstate IN This message triggers the update process
The payload shall contain all data necessary to obtain the update bundle and to install it

Trigger self-update step/action

Topic Direction Description
selfupdate/desiredstate/command IN This message triggers the single step in update process (download/flash/activate/cleanup)

Report current state

Topic Direction Description
selfupdate/currentstate OUT This message is being sent either once on SUA start or as an answer to response received by selfupdate/currentstate/get
It contains information about the currently installed OS version

Get current state

Topic Direction Description
selfupdate/currentstate/get IN This message can be received at any point of time
Indicates that SUA should send back the version of the installed OS as current state.

Report status of self-update process

Topic Direction Description
selfupdate/desiredstatefeedback OUT This message is being sent by SUA to share the current progress of the triggered update process
This is the OUT counterpart of selfupdate/desiredstate input message

Checkout

SUA links to some 3rd party libraries, which are fetched as submodules, therefore the cloning shall be performed with recursive option:

git clone --recursive https://github.com/eclipse-leda/leda-contrib-self-update-agent.git

or if was cloned non recursively

git submodule init
git submodule update

5 - Container Management

Leda is using Kanto Container Management as the upper-layer container runtime and container orchestration engine.

Besides the command line tool kanto-cm, Kanto also has remote interfaces to manage containers.

Remote Interface

Kanto’s container-management service offers a remote interface via local messaging (MQTT) to interact with a digital twin on the cloud side. This feature can be easily enabled by setting "things": { "enabled": true } in /etc/container-management/config.toml.

When one of the cloud connector components, such as leda-contrib-cloud-connector or Kanto’s azure-connector, is connected to a cloud backend, the container-management will publish its own information using Eclipse Ditto and Eclipse Hono messages. For this, container-management only needs the device Id, gateway Id and tenant Id.

5.1 - Cloud Connectivity

Simulation of cloud connectivity

  1. Container Management starts and subscribes to edge/thing/response
  2. Cloud Connector starts and publishes the following message to edge/thing/response as soon as the connection is online:
{
    "deviceId":"<namespace>:<gatewayId>:<deviceId>",
    "tenantId":"<tenantId>"
}
  • namespace is azure.edge for Kanto’s Azure Cloud Connector
  • gatewayId indicates the hostname of the Azure IoT Hub
  • deviceId is the identifier for the device, this can either be part of the Azure Connection String or part of the device authentication certificate (CN)
  • tenantId is a configuration setting in the cloud connector

Note: You can simulate the cloud connector trigger by issueing the MQTT message manually on command line:

mosquitto_pub -t 'edge/thing/response' -m '{"deviceId":"dummy-namespace:dummy-gateway:dummy-device-id","tenantId":"dummy-tenant-id"}'

5.2 - Container Update Agent

About the Container Update Agent (CUA)

The container update agent is a component of kanto container management, that can be enabled in the Container Management’s config.json. When it receives a desired state message on the containersupdate/desiredstate topic it compares the containers (with their current versions and configuration) that are managed by Kanto Container Management and those described in the desired state messages. When it has identified the differences between the two, it resolves them by deleting, downloading, or re-creating containers.

The desired state message

A basic desired state message has a similar structure to all update messages. To deploy the hello-world docker container (removing all other containers currently deployed on the device), you can publish the following desired state message:

{
   "activityId":"test-correlation-activity-uuid",
   "payload":{
      "domains":[
         {
            "id":"containers",
            "config":[],
            "components":[
               {
                  "id":"hello-world",
                  "version":"latest",
                  "config":[
                     {
                        "key":"image",
                        "value":"docker.io/library/hello-world:latest"
                     }
                  ]
               }
            ]
         }
      ]
   }
}

You can configure port mappings, environment variables, device mapping, logging, and more, by adding additional key-value pairs to the “config” array of the given container. For example, if you want to set the environment variable FOO="BAR" inside the container, you can specify:

{
   "activityId":"test-correlation-activity-uuid",
   "payload":{
      "domains":[
         {
            "id":"containers",
            "config":[],
            "components":[
               {
                  "id":"hello-world",
                  "version":"latest",
                  "config":[
                     {
                        "key":"image",
                        "value":"docker.io/library/hello-world:latest"
                     },
                     {
                        "key": "env",
                        "value": "FOO=BAR"
                     }
                  ]
               }
            ]
         }
      ]
   }
}

A full list of all key-value pairs with their defaults can be found in this doc for CUA.

System containers

If a container is not specified in the desired state message, it would be deleted by CUA when the message is received. This might be undesirable for containers that are supposed to run as system services from the first boot of the device through its operation. Such a container might be the Self Update Agent container on the Leda Distro image, which we do not want to specify in every desired state message and deleting/recreating-it during an update might lead to unexpected behaviour. That’s why in Container Management’s config.json in the CUA section, you can specify in the “system_containers” array the names of those containers that would like to be treated as such.

MQTT Topics

Since the MQTT interface of the update agent is abstract, the following message flows are similar to those for the self-update agent mentioned in its tutorial

Getting the current state

On initialization, CUA will publish the current state of all containers on the containersupdate/currentstate topic, the activityId for such a message would be initial-current-state-<TIMESTAMP>. This message can be understood as the “current container inventory”. An example inventory from a running Leda image is currentstate-leda.json.

You can request an update of the current state at any point by publishing an empty message on the: containersupdate/currentstate/get:

{
   "activityId": "<UUID>",
   "timestamp": 123456789,
   "payload": {}
}

The updated inventory will, again, be published on the containersupdate/currentstate topic with correlation ID of <UUID>.

Deploying a desired state message

A desired state message describing the containers and their environments of a typical Leda Distro deployment is leda-desired-state.json. To trigger a desired state deployment directly from CUA, publish the message above on containersupdate/desiredstate, e.g. by running:

   mosquitto_pub -t containersupdate/desiredstate -f leda-desired-state.json

You can monitor the update feedback from CUA, by listening on the topic containersupdate/desiredstatefeedback.

Note: Since the desired state feedback can be quite verbose and happens in real time during the update it could be hard to understand manually. However it’s structured json, so it could be easily machine-parsed and monitored for specific key-value pairs.

5.3 - Orchestration

Container Management

The container management will respond to the cloud connectivity status message by initially sending a list of containers:

  1. Container Management responds with a list of containers in the topic e/<tenantId>/<gatewayId>:<deviceId>:edge:containers
{
    "topic": "<tenantId>/<gatewayId>:<deviceId>:edge:containers/things/twin/commands/modify",
    "headers": {
        "response-required": false
    },
    "path": "/features/SoftwareUpdatable",
    "value": {
        "definition": [
            "org.eclipse.hawkbit.swupdatable:SoftwareUpdatable:2.0.0"
        ],
        "properties": {
            "status": {
                "softwareModuleType": "oci:container",
                "installedDependencies": {
                    "ghcr.io%2Feclipse%2Fkuksa.val%2Fdatabroker.<uuid>:0.2.5": {
                        "group": "ghcr.io/eclipse/kuksa.val/databroker",
                        "name": "<uuid>",
                        "version": "0.2.5"
                    },
                    "ghcr.io%2Feclipse%2Fkuksa.val.services%2Fseat_service.<uuid>:v0.1.0": {
                        "group": "ghcr.io/eclipse/kuksa.val.services/seat_service",
                        "name": "<uuid>",
                        "version": "v0.1.0"
                    },
                    "ghcr.io%2Feclipse-leda%2Fleda-contrib-cloud-connector%2Fcloudconnector.<uuid>:latest": {
                        "group": "ghcr.io/eclipse-leda/leda-contrib-cloud-connector/cloudconnector",
                        "name": "<uuid>",
                        "version": "latest"
                    },
                    "ghcr.io%2Feclipse-leda%2Fleda-contrib-self-update-agent%2Fself-update-agent.<uuid>:build-20": {
                        "group": "ghcr.io/eclipse-leda/leda-contrib-self-update-agent/self-update-agent",
                        "name": "<uuid>",
                        "version": "build-20"
                    },
                    "ghcr.io%2Feclipse-leda%2Fleda-contrib-vehicle-update-manager%2Fvehicleupdatemanager.<uuid>:latest": {
                        "group": "ghcr.io/eclipse-leda/leda-contrib-vehicle-update-manager/vehicleupdatemanager",
                        "name": "<uuid>",
                        "version": "latest"
                    }
                }
            }
        }
    }
}
  1. Container Management answers with an additional message for each container in the topic e/<tenantId>/<gatewayId>:<deviceId>:edge:containers
{
    "topic": "<tenantId>/<gatewayId>:<deviceId>:edge:containers/things/twin/commands/modify",
    "headers": {
        "response-required": false
    },
    "path": "/features/Container:<uuid>",
    "value": {
        "definition": [
            "com.bosch.iot.suite.edge.containers:Container:1.5.0"
        ],
        "properties": {
            "status": {
                "name": "seatservice-example",
                "imageRef": "ghcr.io/eclipse/kuksa.val.services/seat_service:v0.1.0",
                "config": {
                    "domainName": "seatservice-example-domain",
                    "hostName": "seatservice-example-host",
                    "env": [
                        "VEHICLEDATABROKER_DAPR_APP_ID=databroker",
                        "BROKER_ADDR=databroker-host:30555",
                        "RUST_LOG=info",
                        "vehicle_data_broker=info"
                    ],
                    "restartPolicy": {
                        "type": "UNLESS_STOPPED"
                    },
                    "extraHosts": [
                        "databroker-host:host_ip"
                    ],
                    "portMappings": [
                        {
                            "proto": "tcp",
                            "hostPort": 30051,
                            "hostPortEnd": 30051,
                            "containerPort": 50051,
                            "hostIP": "localhost"
                        }
                    ],
                    "networkMode": "BRIDGE",
                    "log": {
                        "type": "JSON_FILE",
                        "maxFiles": 2,
                        "maxSize": "1M",
                        "mode": "BLOCKING"
                    }
                },
                "createdAt": "2023-02-02T08:46:33.792687313Z",
                "state": {
                    "status": "RUNNING",
                    "pid": 13627,
                    "startedAt": "2023-02-02T09:51:08.049572297Z",
                    "finishedAt": "2023-02-02T09:50:51.752255799Z"
                }
            }
        }
    }
}    

5.4 - Container Metrics

Container Metrics

Container-level metrics, such as CPU utilization or memory usage, can be retrieved on-demand and continuously by enabling the container metrics events. This is done by sending a request to the topic command//<namespaceId>:<gatewayId>:<deviceId>:edge:containers/req/<correlationId>/request:

  1. Enable Container Metrics with a frequency of 5s:
{
    "topic": "dummy-namespace/dummy-gateway:dummy-device-id:edge:containers/things/live/messages/request",
    "headers": {
        "timeout": "10",
        "response-required": true,
        "content-type": "application/json",
        "correlation-id": "3fdc463c-293c-4f39-ab19-24aef7944550"
    },
    "path": "/features/Metrics/inbox/messages/request",
    "value": {
        "frequency": "5s"
    }
}

Example command line:

mosquitto_pub -t command//dummy-namespace:dummy-gateway:dummy-device-id:edge:containers/req/3fdc463c-293c-4f39-ab19-24aef7944550/request -m '{"topic":"dummy-namespace/dummy-gateway:dummy-device-id:edge:containers/things/live/messages/request","headers":{"timeout":"10","response-required":true,"content-type":"application/json","correlation-id":"3fdc463c-293c-4f39-ab19-24aef7944550"},"path":"/features/Metrics/inbox/messages/request","value":{"frequency":"5s"}}'
  1. Container Metrics answers with an additional message for each container in the topic e/<tenantId>/<gatewayId>:<deviceId>:edge:containers
{
    "topic": "<tenantId>/<gatewayId>:<deviceId>:edge:containers/things/twin/commands/modify",
    "headers": {
        "response-required": false
    },
    "path": "/features/Container:<uuid>",
    "value": {
        "definition": [
            "com.bosch.iot.suite.edge.containers:Container:1.5.0"
        ],
        "properties": {
            "status": {
                "name": "seatservice-example",
                "imageRef": "ghcr.io/eclipse/kuksa.val.services/seat_service:v0.1.0",
                "config": {
                    "domainName": "seatservice-example-domain",
                    "hostName": "seatservice-example-host",
                    "env": [
                        "VEHICLEDATABROKER_DAPR_APP_ID=databroker",
                        "BROKER_ADDR=databroker-host:30555",
                        "RUST_LOG=info",
                        "vehicle_data_broker=info"
                    ],
                    "restartPolicy": {
                        "type": "UNLESS_STOPPED"
                    },
                    "extraHosts": [
                        "databroker-host:host_ip"
                    ],
                    "portMappings": [
                        {
                            "proto": "tcp",
                            "hostPort": 30051,
                            "hostPortEnd": 30051,
                            "containerPort": 50051,
                            "hostIP": "localhost"
                        }
                    ],
                    "networkMode": "BRIDGE",
                    "log": {
                        "type": "JSON_FILE",
                        "maxFiles": 2,
                        "maxSize": "1M",
                        "mode": "BLOCKING"
                    }
                },
                "createdAt": "2023-02-02T08:46:33.792687313Z",
                "state": {
                    "status": "RUNNING",
                    "pid": 13627,
                    "startedAt": "2023-02-02T09:51:08.049572297Z",
                    "finishedAt": "2023-02-02T09:50:51.752255799Z"
                }
            }
        }
    }
}    
  1. To disable Container Metrics, send a request with frequency of 0s:

    mosquitto_pub -t command//dummy-namespace:dummy-gateway:dummy-device-id:edge:containers/req/3fdc463c-293c-4f39-ab19-24aef7944550/request -m '{"topic":"dummy-namespace/dummy-gateway:dummy-device-id:edge:containers/things/live/messages/request","headers":{"timeout":"10","response-required":true,"content-type":"application/json","correlation-id":"3fdc463c-293c-4f39-ab19-24aef7944550"},"path":"/features/Metrics/inbox/messages/request","value":{"frequency":"0s"}}'
    

5.5 - Container Registries

When deploying containerized applications, the container runtime will pull container images from a (remote) container registry.

The pulled container images and their layers are then stored in a local storage.

Private Container Registries

To be able to pull container images, the container runtime needs access to the container registry. Some container registries require authentication. The Kanto Container Manager can be configured to use credentials when accessing remote container registries.

In the Leda images, the sdv-kanto-ctl tools allows to easily add authentication to the container manager configuration:

sdv-kanto-ctl add-registry -h <registryhostname> -u <your_username> -p <your_password>

For example, to access container images from GitHub Packages in a private repository, you need a GitHub Personal Access Token (PAT) with the read: packages scope. Then, add the repository as shown below:

sdv-kanto-ctl add-registry -h ghcr.io -u github -p <Your_GitHub_PersonalAccessToken>

sdv-kanto-ctl will make the necessary modifications to /etc/container-management/config.json and restarts the container-management.service systemd unit, so that the changes take effect. You may need to recreate or restart the container if a previous pull failed.

Please see the Eclipse Kanto Container Manager Configuration reference for details.