Bosch IoT Rollouts

Create SWUpdate image (*.swu) from recipe

Disclaimer

This script is for demonstration purposes only.

Table of contents:

Introduction

SWUpdate is a popular in-field software update solution for embedded Linux systems. For SWUpdate clients, the update is provided as an image or container (*.swu), which is a CPIO archive that follows a defined internal structure (cf. SWUpdate: Single image delivery):

Position

SWUpdate

System Software Update

1

CPIO header


2

sw-description

Default recipe file is a sw-description compatible JSON.

3

sw-description.sig

Signature of the recipe file.

4 - n

script

Scripts contained in System distribution sets with a distribution set of type scripts

n - m

artifacts

Artifacts contained in Module update definitions with a distribution set, that contains a software module of type firmware

The provided script uses the given recipe Id and access token to download the required artifacts from the backend. Afterwards, it creates the image.

Usage

To use the script follow the following steps:

  1. Download the script and store as swu-creator.sh

  2. Obtain recipe Id and access token from the UI.

  3. Run the script

The CPIO executable on MacOs (bsdcpio) does not contain the required format option --format crc It is only available on Linux with gnucpio.

Example

$ ./swu-creator.sh -a "$ACCESS_TOKEN" -r "9yNAUeKHtGGZ"
 
+===========================================================================+
| _______ ___ _ _____ _ |
| / ____\ \ / / | | | / ____| | | |
|| (___ \ \ /\ / /| | | | ______ | | _ __ ___ __ _| |_ ___ _ __ |
| \___ \ \ \/ \/ / | | | | |______| | | | '__/ _ \/ _' | __/ _ \| '__||
| ____) | \ /\ / | |__| | | |____| | | __/ (_| | || (_) | | |
||_____/ \/ \/ \____/ \_____|_| \___|\__,_|\__\___/|_| |
| by Bosch Digital|
+===========================================================================+
 
Version: 1.2.0
Endpoint: https://system.eu1.bosch-iot-rollouts.com
 
--- Start downloading required artifacts
+ Download recipe file for recipe id: 9yNAUeKHtGGZ
+ Download signature
+ Download script: install.lua (28376 bytes)
+ Download artifact: artifact1.bin (1704474 bytes)
+ Download artifact: artifact2.bin (1704436 bytes)
--- Finished downloading required artifacts
 
--- Create CPIO archive
sw-description
sw-description.sig
install.lua
artifact1.bin
artifact2.bin
6734 blocks
--- Done
 
SWU image: 9yNAUeKHtGGZ.swu

Script

#!/usr/bin/env bash
#
# Copyright (c) 2024. Robert Bosch GmbH
#
# DISCLAIMER: This script is for demonstration purposes only.
#
set -e # fail fast
 
VERSION="1.2.1"
SOUP_ENDPOINT="https://system.eu1.bosch-iot-rollouts.com"
INSTALL_API="${SOUP_ENDPOINT}/api/install/v1"
FILES=""
FILE_SEPERATOR="%"
 
USAGE="""
Version: ${VERSION}
Usage: ./$(basename "$0") [-arhv]
 
Script to download recipe, signature, and all artifacts and package them
into a *.swu (CPIO) archive.
 
Parameter:
-a, --access-token Access token with scope test-installer
-r, --recipe-id Recipe ID of a recipe in state > RELEASE_CANDIATE
-h, --help Get usage information
-v, --version Get version information
 
Example
./$(basename "$0") -a \$"ACCESS_TOKEN" -r "l71jmPfjDl2r"
 
"""
 
if ! command -v jq 2>&1 >/dev/null; then
echo "jq could not be found"
exit 1
fi
 
POSITIONAL=()
while [[ $# -gt 0 ]]; do
key="$1"
 
case $key in
-h | --help)
echo "${USAGE}"
exit
;;
-v | --version)
echo "Version: ${VERSION}"
exit
;;
-a | --access-token)
ACCESS_TOKEN="$2"
shift # past argument
shift # past value
;;
-r | --recipe)
RECIPE="$2"
shift # past argument
shift # past value
;;
esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters
 
if [ -z "${ACCESS_TOKEN}" ]; then
echo "Parameter --access-token (-a) is mandatory! For more information, use --help."
exit 1
fi
 
if [ -z "${RECIPE}" ]; then
echo "Parameter --recipe-id (-r) is mandatory! For more information, use --help."
exit 1
fi
 
download() {
local url=${1}
local filename=${2}
 
status_code=$(curl --write-out %{http_code} --silent --location \
-X 'GET' \
"${url}" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H 'accept: application/octet-stream' \
-o "${filename}")
 
if [[ "$status_code" -eq 401 ]]; then
echo "[ERROR] HTTP 401 - Unauthorized: Please provide a valid access token."
exit 1
fi
if [[ "$status_code" -eq 403 ]]; then
echo "[ERROR] HTTP 403 - Forbidden: Please verify that your user has the required roles or scopes."
exit 1
fi
if [[ "$status_code" -eq 404 ]]; then
message=$(curl -X 'GET' "${url}" --location -H "Authorization: Bearer ${ACCESS_TOKEN}" --silent -H 'accept: application/octet-stream')
printf "[ERROR] HTTP 404 - Not found: ${message}"
printf "\n For missing recipes, please verify that the recipe is in one of the following states: RELEASE_CANDIDATE, RELEASED, REVOKED, INACTIVE."
printf "\n For missing artifacts, check if the distribtion set and its software modules are still present via the UI: https://console.eu1.bosch-iot-rollouts.com/distribution-sets \n"
exit 1
fi
}
 
echo ""
echo " +===========================================================================+ "
echo " | _______ ___ _ _____ _ | "
echo " | / ____\ \ / / | | | / ____| | | | "
echo " || (___ \ \ /\ / /| | | | ______ | | _ __ ___ __ _| |_ ___ _ __ | "
echo " | \___ \ \ \/ \/ / | | | | |______| | | | '__/ _ \/ _' | __/ _ \| '__|| "
echo " | ____) | \ /\ / | |__| | | |____| | | __/ (_| | || (_) | | | "
echo " ||_____/ \/ \/ \____/ \_____|_| \___|\__,_|\__\___/|_| | "
echo " | by Bosch Digital| "
echo " +===========================================================================+ "
echo ""
echo " Version: ${VERSION}"
echo " Endpoint: ${SOUP_ENDPOINT}"
echo ""
echo "--- Start downloading required artifacts"
 
# Download recipe + signature
echo "+ Download recipe file for recipe id: ${RECIPE}"
download "${INSTALL_API}/recipes/${RECIPE}/file" "sw-description"
FILES+="sw-description"
 
echo "+ Download signature"
download "${INSTALL_API}/recipes/${RECIPE}/signature" "sw-description.sig"
FILES+="${FILE_SEPERATOR}sw-description.sig"
 
# iterate over recipe to download all scripts to artifacts folder
while read script; do
FILENAME=$(echo "$script" | jq -r .filename)
ID=$(echo "$script" | jq -r .id)
SIZE=$(echo "$script" | jq -r .byteSize)
 
# handle case, if no script exists
if [ -z "${ID}" ]; then break; fi
 
echo "+ Download script: ${FILENAME} (${SIZE} bytes)"
download "${INSTALL_API}/software-artifacts/${ID}/file" "${FILENAME}"
FILES+="${FILE_SEPERATOR}${FILENAME}"
 
done <<<"$(jq -c '.software.scripts[]' sw-description)"
 
# iterate over recipe to download all firmware to artifacts folder
while read image; do
 
FILENAME=$(echo $image | jq -r '.filename')
ARTIFACT=$(echo $image | jq --arg filename "${FILENAME}" '.artifacts[] | select(.filename == $filename)')
 
ID=$(echo "$ARTIFACT" | jq -r .id)
SIZE=$(echo "$ARTIFACT" | jq -r .byteSize)
 
echo "+ Download artifact: ${FILENAME} (${SIZE} bytes)"
download "${INSTALL_API}/software-artifacts/${ID}/file" "${FILENAME}"
FILES+="${FILE_SEPERATOR}${FILENAME}"
 
done <<<"$(jq -c '.software.images[] | { "filename": .filename, "artifacts": ._software.artifacts }' sw-description)"
 
echo "--- Finished downloading required artifacts"
echo ""
 
echo "--- Create CPIO archive"
LIST_OF_FILES=$(echo "$FILES" | tr "${FILE_SEPERATOR}" '\n')
while read file; do
echo $file
done <<<"$(echo "$LIST_OF_FILES")" | cpio --create --format crc --verbose >${RECIPE}.swu
# '--format crc' only works with gnucpio (i.e. not on MacOs)
 
echo "--- Done"
echo ""
echo "SWU image: ${RECIPE}.swu"
echo "+===========================================================================+ "