- Start fresh – Often I make temporary changes on the Pi while developing and debugging and want to start with a clean state to make sure I’m not relying on some manual change I’m likely to forget about later.
- Multiple Raspberry Pi devices – While I’m not building a Raspberry Pi Kubernetes Cluster (yet), I’ve certainly gone through more than one Pi device. They’re cheap and there are lots of project ideas and new models coming out.
- SD Card upgrade – Sometimes I might start with a small SD card and later realize I need more storage.
- SD Card failure – SD cards have a limited number of I/O operations and will eventually go bad, especially when opting for a cheap one.
sd-card-write.sh
to automate this on my Mac. Primary functions include:- Gathering external disk info
- Downloading Raspbian Lite zip and extracting the image
- Formatting the SD card
- Copying the OS image to the SD card
- Dealing with disk mounting, unmounting, and ejection
- Configuring SSH and Wi-Fi
- Copying helper scripts that setup the Pi and pull application images
Parameters
Currently the script just has 1 parameter for the final host name which is used to remove any prior ssh keys for the host and to set the host name in a setup script copied to the SD card.
#!/bin/sh # ./sd-card-write.sh --host catsirenpi # Final host name (not initial login) host_name="" while [[ $# -ge 1 ]]; do i="$1" case $i in -h|--host) host_name=$2 shift ;; *) echo "Unrecognized option $1" exit 1 ;; esac shift done if [ -z "$host_name" ]; then echo "Final Pi host name is required (-h | --host)" >&2 exit 1 fi
Gathering Disk Info
First the script gathers some information on external disks in order to confirm the disk to format without relying on the user to supply the disk name. It stops if it doesn’t find 1 external disk, which suited my needs.
disk_name=$(diskutil list external | grep -o '^/dev\S*') if [ -z "$disk_name" ]; then echo "Didn't find an external disk" ; exit -1 fi matches=$(echo -n "$disk_name" | grep -c '^') if [ $matches -ne 1 ]; then echo "Found ${matches} external disk(s); expected 1" ; exit -1 fi disk_free=$(df -l -h | grep "$disk_name" | egrep -oi '(\s+/Volumes/.*)' | egrep -o '(/.*)') if [ -z "$disk_free" ]; then echo "Disk ${disk_name} doesn't appear mounted. Try reinserting SD card" ; exit -1 fi volume=$(echo "$disk_free" | sed -e 's/\/.*\///g') # Spit out disk info for user confirmation diskutil list external echo $disk_free echo read -p "Format ${disk_name} (${volume}) (y/n)?" CONT if [ "$CONT" = "n" ]; then exit -1 fiThe above gets the script execution to this point:
It goes without saying but be extra sure that the right disk is being formatted!
Download and Extraction
The next step is downloading the Raspbian Lite zip file and extracting the image from it.
image_path=./downloads image_zip="$image_path/image.zip" image_iso="$image_path/image.img" # Consider checking latest ver/sha online, download only if newer # https://downloads.raspberrypi.org/raspbian_lite/images/?C=M;O=D # For now just delete any prior download zip to force downloading latest version if [ ! -f $image_zip ]; then mkdir -p ./downloads echo "Downloading latest Raspbian lite image" # curl often gave "error 18 - transfer closed with outstanding read data remaining" wget -O $image_zip "https://downloads.raspberrypi.org/raspbian_lite_latest" if [ $? -ne 0 ]; then echo "Download failed" ; exit -1; fi fi echo "Extracting ${image_zip} ISO" unzip -p $image_zip > $image_iso if [ $? -ne 0 ]; then echo "Unzipping image ${image_zip} failed" ; exit -1; fi
Flashing the Disk
The disk is then formatted and unmounted so the image can be copied to it.
echo "Formatting ${disk_name} as FAT32" sudo diskutil eraseDisk FAT32 PI MBRFormat "$disk_name" if [ $? -ne 0 ]; then echo "Formatting disk ${disk_name} failed" ; exit -1; fi echo "Unmounting ${disk_name} before writing image" diskutil unmountdisk "$disk_name" if [ $? -ne 0 ]; then echo "Unmounting disk ${disk_name} failed" ; exit -1; fi
Data duplicator (dd) is used to copy the image to the SD card. This takes a while but pressing ctrl+t
can be done at any time for progress. Depending upon the machine, the bs
argument (I/O block size) might need adjusting from the 1 MB value used here.
echo "Copying ${image_iso} to ${disk_name}. ctrl+t as desired for status" sudo dd bs=1m if="$image_iso" of="$disk_name" conv=sync if [ $? -ne 0 ]; then echo "Copying ${image_iso} to ${disk_name} failed" ; exit -1 fi
Configuring SSH and Wi-Fi
After the image is written the script remounts the drive to make some modifications – namely writing files to enable SSH and Wi-Fi and to copy over some helper scripts. It also does some cleanup by deleting the image file; currently it leaves the zip to avoid downloading again later – it’s easy enough to just delete the zip to force a new download if a later version is published.
The loop and sleep commands are a failsafe as sometimes there are timing issues from the copy and the disk is not yet ready for mounting again.
# Remount for further SD card mods. Drive may not be quite ready. attempt=0 until [ $attempt -ge 3 ] do sleep 2s echo "Remounting ${disk_name}" diskutil mountDisk "$disk_name" && break attempt=$[$attempt+1] done echo "Removing ${image_iso}. Re-extract later if needed from ${image_zip}" rm $image_iso
Enabling SSH is as simple as writing a ssh
file to the SD card root.
volume="/Volumes/boot" echo "Enabling ssh" touch "$volume"/ssh if [ $? -ne 0 ]; then echo "Configuring ssh failed" ; exit -1 fi
I didn’t want to use a wired connection or plug in a monitor so the script determines the current host WiFi network name (SSID) and then prompts for the password. Afterwards it writes the Wi-Fi credentials to wpa_supplicant.conf on the SD card root.
echo "Configuring Wi-Fi" wifi_ssid=$(/System/Library/PrivateFrameworks/Apple80211.framework/Resources/airport -I | awk -F: '/ SSID/{print $2}') wifi_ssid=`echo $wifi_ssid | sed 's/^ *//g'` # trim echo "Wi-Fi password for ${wifi_ssid}:" read -s wifi_pwd cat >"$volume"/wpa_supplicant.conf <<EOL ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev update_config=1 country=US network={ ssid="${wifi_ssid}" psk="${wifi_pwd}" } EOL if [ $? -ne 0 ]; then echo "Configuring wifi failed" ; exit -1 fi
Copying Scripts and Disk Ejection
After the configuration, the script copies previously written script files that help further setup the Pi by installing apps and updates, setting configuration, and pulling application Docker images (more on these scripts in upcoming posts). After the copy operations the disk is ejected.
echo "Copying setup script. After Pi boot, run: sudo /boot/setup.sh" cp setup.sh "$volume" echo "Modifying setup script" # Replace "${host}" placeholder in the setup script on SD card with final host name passed to script sed -i -e "s/\${host}/${host_name}/" "$volume/setup.sh" echo "Copying docker pull script for app updates" cp pull.sh "$volume" echo "Image burned. Remove SD card, insert in PI and power on" sudo diskutil eject "$disk_name"
SSH Prep
Finally the script uses ssh-keygen to remove any prior keys to the target Pi host. Otherwise there’d be issues connecting to the same device that’s been re-flashed.
echo "Removing any prior PI SSH known hosts entry" ssh-keygen -R raspberrypi.local # initial ssh-keygen -R "$host_name.local" echo "Power up the PI and give it a minute then" echo " ssh pi@raspberrypi.local" echo " yes, raspberry" echo " sudo /boot/setup.sh"
The initial ssh might generate Connection refused
until the Pi is fully ready, which might be up to a minute or so after powering on. A monitor has the advantage of seeing what’s happening here but that hookup isn’t worth the hassle for me. If the ssh hangs, the Wi-Fi configuration details may be incorrect.
Source
The full script can be found here: sd-card-write.sh.
Other Automation Options
Another option of interest is using pi-gen to create pre-configured Pi images. It has a number of configuration options like setting the default username and password, locale info, Wi-Fi and SSH details, host name and more. I ended up doing that later on here. See Using Pi-Gen to Build a Custom Raspbian Lite Image for more on this approach.
Up Next
Automating Raspberry Pi Setup – The next post in this series covers automating Raspberry Pi system configuration and installing applications.