Recently, I've been making and refining installer and rescue images to boot from USB media, in the form of CD-ROM or hard disk images. After a few occasions where I re-ran the command after removing the media (and ending up with a large file in /dev/disk/by-id/
instead of writing to a device), I wrote a small wrapper to check that the target is a block device.
Since then, it's grown features - notably to take input direct from network sources (e.g. a build server) to save making a local copy first. In these cases, I want to be able to sudo write-image
but still make the network connection as non-root user (partly as good practice, but also so that I can use my ~/.ssh/config
and private keys in the normal way).
The usage()
function ought to explain how it's intended to be used; if I need to explain further, then it's a sign that my documentation needs improving.
The reason that this needs Bash rather than POSIX shell is to use command substitution process substitution. The use of a shell function in process substitution seems to be particularly non-portable, but it works for me with Bash 4.4.
#!/bin/bash
set -eu
die() { echo "$@" >&2; exit 1; }
usage() {
cat <<END
usage: $0 [source] <usb-device>
The source can be
* a filename
* a URL
* ssh:[user@]<host>:<file> (must be passwordless)
* '-' or absent (for piping from standard input)
END
}
as_real_user() {
exec ${SUDO_USER+sudo -n -H -u "$SUDO_USER" }"$@"
}
case $# in
1)
case "$1" in
--help|-h)
usage
exit 0
;;
esac
;;
2)
case "$1" in
ssh:*)
file="${1#*:}"
host="${file%%:*}"
file="${file#*:}"
exec < <(as_real_user ssh "$host" cat "$file")
;;
*://*)
exec < <(as_real_user wget -q -O - "$url")
;;
-)
# standard input is already connected
;;
*)
test -e "$1" || die "$1: not found"
test -f "$1" || die "$1: not a plain file"
test -r "$1" || die "$1: not readable"
exec < "$1"
;;
esac
shift
;;
*)
usage
exit 1;
esac
test -b "$1" || die "$1: not a block device"
test -w "$1" || die "$1: not writable"
cut -d' ' -f1 /proc/mounts | grep -Fxq $(realpath "$1") && die "$1: is mounted"
outfile="of=$1"
# Explanation
# * nocreat - just checking, given above tests of destination
# * fsync - don't return until image is fully written
# * dsync,bs,status - show update every 1 MB written
ddflags="conv=nocreat,fsync oflag=dsync bs=1M status=progress"
exec dd $ddflags $outfile