Skip to content

Instantly share code, notes, and snippets.

@phaseOne
Last active September 9, 2022 00:51
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save phaseOne/858e3f15763e1adf650ff7dbdb3139ba to your computer and use it in GitHub Desktop.
Save phaseOne/858e3f15763e1adf650ff7dbdb3139ba to your computer and use it in GitHub Desktop.
An interactive bash script that performs a Balena Offline Update on an SD card
#!/bin/bash
# NOTE: This script requires that jq is installed on your system.
set -euo pipefail
# set -x
echo "Specify the path to your SSH public key. It will be added to the device and used to SSH into the device after provisioning to help in debugging if needed."
read -r -p 'Path to your SSH public key: ' ssh_key
if [ -z "$ssh_key" ]; then
echo 'SSH key path cannot be blank.'
exit 1
fi
ssh_key=$(readlink -f "${ssh_key/#~/${HOME}}")
if [ ! -r "$ssh_key" ]; then
echo "SSH key at path ${ssh_key} does not exist or is not readable."
exit 1
fi
if [ ! "$(ssh-keygen -l -f "${ssh_key}")" ]; then
echo "SSH key at path ${ssh_key} is invalid."
exit 1
fi
echo "Specify the machine name of the device you want to update. The list of names for supported device types and their architectures can be found on the [hardware](https://www.balena.io/docs/reference/hardware/devices/) page."
# shellcheck disable=SC2016 # ticks not for expansion
read -r -p 'machine name (`raspberrypi4-64`, `raspberrypi3`, etc.): ' device_type
if [ -z "$device_type" ]; then
echo 'machine name cannot be blank.'
exit 1
fi
prompt_for_cpu_arch () {
echo "Specify the CPU architecture of the device you want to update."
# shellcheck disable=SC2016 # ticks not for expansion
read -r -p 'arch (`aarch64`, `armv7l`, etc.): ' arch
if [ -z "$arch" ]; then
echo 'arch cannot be blank.'
exit 1
fi
}
match_device_with_cpu_arch () {
if ! arch=$(jq --raw-output --exit-status --arg dt "${device_type}" '.[] | select(.slug == $dt) | .arch' <<< "${device_list}"); then
echo "Unable to find the device specified. Falling back to manual architecture selection…"
prompt_for_cpu_arch
fi
echo "arch: $arch"
}
if ! device_list=$(balena devices supported --json); then
echo "Unable to fetch the device list from balena. Falling back to manual architecture selection…"
prompt_for_cpu_arch
fi
match_device_with_cpu_arch
read -r -p 'Enter an existing fleet name, or leave blank to create a new one: ' fleet_name
if [ -z "$fleet_name" ]; then
fleet_name=offline-${arch}
# Creates a new balenaCloud fleet.
balena fleet create "${fleet_name}" --type "${device_type}"
fi
fleet_slug=$(balena fleet "${fleet_name}" | grep SLUG | awk '{print $2}')
register_new_device () {
device_uuid=$(openssl rand -hex 16)
balena device register "${fleet_slug}" --uuid "${device_uuid}"
}
read -r -p 'Enter a UUID of an existing device to update, or leave blank to generate a UUID for a new device: ' device_uuid
if [ -z "$device_uuid" ]; then
register_new_device
fi
ask_for_variables () {
# shellcheck disable=SC2016 # ticks not for expansion
read -r -p 'Would you like to add any more environment or config variables? (`fleet`, `device`, or leave blank to skip): ' variable_type
if [ -z "$variable_type" ]; then
echo "Skipping…"
return 0
fi
read -r -p 'Enter the variable name and value separated by a space: ' variable_name variable_value
read -r -p 'Do you want the variable to only apply to a specified service? (comma separated service names or leave blank to apply to all services): ' variable_service
if [ "$variable_type" = "fleet" ]; then
# shellcheck disable=SC2086 # no double quotes for service variable so that an empty parameter '' is not inserted
if ! balena env add "${variable_name}" "${variable_value}" --fleet "${fleet_slug}" ${variable_service/*/"--service ${variable_service}"}; then
echo "Unable to set variable."
fi
elif [ "$variable_type" = "device" ]; then
# shellcheck disable=SC2086 # no double quotes for service variable so that an empty parameter '' is not inserted
if ! balena env add "${variable_name}" "${variable_value}" --device "${device_uuid}" ${variable_service/*/"--service ${variable_service}"}; then
echo "Unable to set variable."
fi
else
echo "Unregonized variable type."
fi
echo "Variable ${variable_name} set."
ask_for_variables
}
ask_for_variables
download_os_for_device_type () {
read -r -p 'Choose an image type (dev/prod): ' image_type
if [ -z "$image_type" ]; then
echo 'Image type cannot be blank. Enter either dev or prod.'
exit 1
fi
if ! [[ "$image_type" =~ ^dev|prod$ ]]; then
echo 'Invalid image type. Enter either dev or prod.'
exit 1
fi
os_version=$(balena os versions "${device_type}" | grep "${image_type}" | head -n 1 | awk '{print $1}')
tmpimg=$(mktemp).img
balena os download "${device_type}" \
--output "${tmpimg}" \
--version "${os_version}"
}
prompt_for_local_img () {
read -r -p 'Path to balenaOS image file: ' tmpimg
if [ -z "$tmpimg" ]; then
echo 'Path cannot be blank.'
exit 1
fi
if [ ! -r "$tmpimg" ]; then
echo "Image file at path ${tmpimg} does not exist or is not readable."
exit 1
fi
read -r -p 'balenaOS version (v0.0.0): ' os_version
if [ -z "$os_version" ]; then
echo 'version cannot be blank.'
exit 1
fi
}
read -n 1 -r -p "Do you want to download the latest available OS image for this device? If not, you'll be asked to specify a local img file. (y/n) "
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
download_os_for_device_type
else
prompt_for_local_img
fi
tmpconfig=$(mktemp)
config=$(mktemp)
# balena config generate \
# --device "${device_uuid}" \
# --version "${os_version}" \
# --network ethernet \
# --appUpdatePollInterval 10 \
# --output "${tmpconfig}" \
# && jq . "${tmpconfig}"
balena config generate \
--device "${device_uuid}" \
--version "${os_version}" \
--appUpdatePollInterval 10 \
--output "${tmpconfig}" \
&& jq . "${tmpconfig}"
jq --arg keys "$(cat "${ssh_key}")" '. + {os: {sshKeys: [$keys]}}' "${tmpconfig}" > "${config}" \
&& jq . "${config}"
# balena os configure "${tmpimg}" \
# --fleet "${fleet_slug}" \
# --device-type "${device_type}" \
# --version "${os_version}" \
# --config-network ethernet \
# --config "${config}"
balena os configure "${tmpimg}" \
--fleet "${fleet_slug}" \
--device-type "${device_type}" \
--version "${os_version}" \
--config "${config}"
get_latest_release_in_fleet () {
if ! fleet_details=$(balena fleet "${fleet_slug}"); then
echo "Unable to fetch fleet details."
exit 1
fi
if ! commit=$(grep COMMIT <<< "${fleet_details}" | awk '{print $2}'); then
echo "No releases found."
fi
}
preload_with_release () {
get_latest_release_in_fleet
if [ -z "$commit" ]; then
# If the pre-existing fleet doesn't have any releases, then a release needs to be created using
# balena deploy or push. The following command assumes that you are in the directory of your
# source code folder and will deploy as the latest release of your fleet.
# balena deploy "${fleet_slug}" --build --emulated
echo "Pushing a new release from the source in the current directory…"
if ! balena push "${fleet_slug}"; then
echo "Unable to push a new release."
exit 1
fi
get_latest_release_in_fleet
if [ -z "$commit" ]; then
exit 1
fi
fi
balena preload "${tmpimg}" \
--fleet "${fleet_slug}" \
--commit "${commit}" \
--pin-device-to-release
}
# Currently, Docker for Windows and Docker for Mac only ship with the 'overlay2' storage driver.
# This means that any Fleet image that does not use 'overlay2' as the storage driver (including
# those for the balena Fin), can not be preloaded under these host platforms.
if [[ $OSTYPE != 'darwin'* ]]; then
preload_with_release
# TODO: implement optional preload and remove darwin restriction
else
echo "Skipping preload since Docker deprecated AUFS support for the macOS platform"
fi
# list the devices available to flash
balena util available-drives
read -r -p 'Specify the device path for flashing: ' drive_path
echo
read -n 1 -r -p "Please make sure ${drive_path} is really the disk you want to write to. Confirm? (y/n) "
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
echo "Writing to ${drive_path}..."
sudo balena local flash "${tmpimg}" \
--drive "${drive_path}"
else
echo "Aborting..."
exit 1
fi
finish() {
result=$?
rm "${tmpconfig}"
rm "${config}"
rm "${tmpimg}"
exit ${result}
}
trap finish EXIT ERR
@phaseOne
Copy link
Author

phaseOne commented Aug 1, 2022

This code is based on the steps in the Offline Updates section of the Balena Documentation, and it includes a few improvements.

Tested on macOS 12.4 M1 & Intel, and it should work on other Linux platforms. Checked with ShellCheck.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment