Last active
September 9, 2022 00:51
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.