Skip to content

Instantly share code, notes, and snippets.

@fhunleth
Created September 20, 2020 12:48
Show Gist options
  • Save fhunleth/fd4dd332227c6cebc3820ea493b579da to your computer and use it in GitHub Desktop.
Save fhunleth/fd4dd332227c6cebc3820ea493b579da to your computer and use it in GitHub Desktop.
nerves_firmware_ssh upload script
#!/bin/sh
#
# Upload new firmware to a target running nerves_firmware_ssh
#
# Usage:
# upload.sh [destination IP] [Path to .fw file]
#
# If unspecifed, the destination is nerves.local and the .fw file is naively
# guessed
#
# You may want to add the following to your `~/.ssh/config` to avoid recording
# the IP addresses of the target:
#
# Host nerves.local
# UserKnownHostsFile /dev/null
# StrictHostKeyChecking no
#
# The firmware update protocol is:
#
# 1. Connect to the nerves_firmware_ssh service running on port 8989
# 2. Send "fwup:$FILESIZE,reboot\n" where `$FILESIZE` is the size of the file
# being uploaded
# 3. Send the firmware file
# 4. The response from the device is a progress bar from fwup that can either
# be ignored or shown to the user.
# 5. The ssh connection is closed with an exit code to indicate success or
# failure
#
# Feel free to copy this script whereever is convenient. The template is at
# https://github.com/nerves-project/nerves_firmware_ssh/blob/master/priv/templates/script.upload.eex
#
set -e
DESTINATION=$1
FILENAME="$2"
help() {
echo
echo "upload.sh [destination IP] [Path to .fw file]"
echo
echo "Default destination IP is 'nerves.local'"
echo "Default firmware bundle is the first .fw file in '_build/\${MIX_TARGET}_\${MIX_ENV}/nerves/images'"
echo
echo "MIX_TARGET=$MIX_TARGET"
echo "MIX_ENV=$MIX_ENV"
exit 1
}
[ -n "$DESTINATION" ] || DESTINATION=nerves.local
[ -n "$MIX_TARGET" ] || MIX_TARGET=rpi0
[ -n "$MIX_ENV" ] || MIX_ENV=dev
if [ -z "$FILENAME" ]; then
FIRMWARE_PATH="./_build/${MIX_TARGET}_${MIX_ENV}/nerves/images"
if [ ! -d "$FIRMWARE_PATH" ]; then
# Try the Nerves 1.4 path if the user hasn't upgraded their mix.exs
FIRMWARE_PATH="./_build/${MIX_TARGET}/${MIX_TARGET}_${MIX_ENV}/nerves/images"
if [ ! -d "$FIRMWARE_PATH" ]; then
# Try the pre-Nerves 1.4 path
FIRMWARE_PATH="./_build/${MIX_TARGET}/${MIX_ENV}/nerves/images"
if [ ! -d "$FIRMWARE_PATH" ]; then
echo "Can't find the build products. Specify path to .fw file or try running 'mix firmware'"
exit 1
fi
fi
fi
FILENAME=$(ls "$FIRMWARE_PATH/"*.fw 2> /dev/null | head -n 1)
fi
[ -n "$FILENAME" ] || (echo "Error: error determining firmware bundle."; help)
[ -f "$FILENAME" ] || (echo "Error: can't find '$FILENAME'"; help)
# Check the flavor of stat for sending the filesize
if stat --version 2>/dev/null | grep GNU >/dev/null; then
# The QNU way
FILESIZE=$(stat -c%s "$FILENAME")
else
# Else default to the BSD way
FILESIZE=$(stat -f %z "$FILENAME")
fi
FIRMWARE_METADATA=$(fwup -m -i "$FILENAME" || echo "meta-product=Error reading metadata!")
FIRMWARE_PRODUCT=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-product=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"')
FIRMWARE_VERSION=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-version=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"')
FIRMWARE_PLATFORM=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-platform=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"')
FIRMWARE_UUID=$(echo "$FIRMWARE_METADATA" | grep -E "^meta-uuid=" -m 1 2>/dev/null | cut -d '=' -f 2- | tr -d '"')
echo "Path: $FILENAME"
echo "Product: $FIRMWARE_PRODUCT $FIRMWARE_VERSION"
echo "UUID: $FIRMWARE_UUID"
echo "Platform: $FIRMWARE_PLATFORM"
echo
echo "Uploading to $DESTINATION..."
# Don't fall back to asking for passwords, since that won't work
# and it's easy to misread the message thinking that it's asking
# for the private key password
SSH_OPTIONS="-o PreferredAuthentications=publickey"
if [ "$(uname -s)" = "Darwin" ]; then
DESTINATION_IP=$(arp -n $DESTINATION | sed 's/.* (\([0-9.]*\).*/\1/' || exit 0)
if [ -z "$DESTINATION_IP" ]; then
echo "Can't resolve $DESTINATION"
exit 1
fi
TEST_DESTINATION_IP=$(printf "$DESTINATION_IP" | head -n 1)
if [ "$DESTINATION_IP" != "$TEST_DESTINATION_IP" ]; then
echo "Multiple destination IP addresses for $DESTINATION found:"
echo "$DESTINATION_IP"
echo "Guessing the first one..."
DESTINATION_IP=$TEST_DESTINATION_IP
fi
IS_DEST_LL=$(echo $DESTINATION_IP | grep '^169\.254\.' || exit 0)
if [ -n "$IS_DEST_LL" ]; then
LINK_LOCAL_IP=$(ifconfig | grep 169.254 | sed 's/.*inet \([0-9.]*\) .*/\1/')
if [ -z "$LINK_LOCAL_IP" ]; then
echo "Can't find an interface with a link local address?"
exit 1
fi
TEST_LINK_LOCAL_IP=$(printf "$LINK_LOCAL_IP" | tail -n 1)
if [ "$LINK_LOCAL_IP" != "$TEST_LINK_LOCAL_IP" ]; then
echo "Multiple interfaces with link local addresses:"
echo "$LINK_LOCAL_IP"
echo "Guessing the last one, but YMMV..."
LINK_LOCAL_IP=$TEST_LINK_LOCAL_IP
fi
# If a link local address, then force ssh to bind to the link local IP
# when connecting. This fixes an issue where the ssh connection is bound
# to another Ethernet interface. The TCP SYN packet that goes out has no
# chance of working when this happens.
SSH_OPTIONS="$SSH_OPTIONS -b $LINK_LOCAL_IP"
fi
fi
printf "fwup:$FILESIZE,reboot\n" | cat - $FILENAME | ssh -s -p 8989 $SSH_OPTIONS $DESTINATION nerves_firmware_ssh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment