Files
OpenCellular/user_tools/linux/recovery.sh
Bill Richardson 00a849e16e Use tr instead of sed to unDOSify the config file, so it works on Macs.
Change-Id: Ib41cdd22d542004bd776828a43ae687942bc5ccc

BUG=chromium-os:781
TEST=manual, as before.

Review URL: http://codereview.chromium.org/5516010
2010-12-06 13:56:12 -08:00

745 lines
18 KiB
Bash
Executable File

#!/bin/sh
#
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# This attempts to guide linux users through the process of putting a recovery
# image onto a removeable USB drive.
#
# We may not need root privileges if we have the right permissions.
#
set -eu
##############################################################################
# Configuration goes here
# Where should we do our work? Use 'WORKDIR=' to make a temporary directory,
# but using a persistent location may let us resume interrupted downloads or
# run again without needing to download a second time.
WORKDIR=/tmp/tmp.crosrec
# Where do we look for the config file? Note that we can override this by just
# specifying the config file URL on the command line.
CONFIGURL="${1:-http://www.chromium.org/some/random/place.cfg}"
# What version is this script? It must match the 'recovery_tool_version=' value
# in the config file that we'll download.
MYVERSION='1.0'
##############################################################################
# Some temporary filenames
debug='debug.log'
tmpfile='tmp.txt'
config='config.txt'
version='verson.txt'
##############################################################################
# Various warning messages
DEBUG() {
echo "DEBUG: $@" >>"$debug"
}
warn() {
echo "$@" 1>&2
}
quit() {
warn "quitting..."
exit 1
}
fatal() {
warn "ERROR: $@"
exit 1
}
ufatal() {
warn "
ERROR: $@
You may need to run this program as a different user. If that doesn't help, try
using a different computer, or ask a knowledgeable friend for help.
"
exit 1
}
gfatal() {
warn "
ERROR: $@
You may need to run this program as a different user. If that doesn't help, it
may be a networking problem or a problem with the images provided by Google.
You might want to check to see if there is a newer version of this tool
available, or if someone else has already reported a problem.
If all else fails, you could try using a different computer, or ask a
knowledgeable friend for help.
"
exit 1
}
##############################################################################
# Identify the external utilities that we MUST have available.
#
# I'd like to keep the set of external *NIX commands to an absolute minimum,
# but I have to balance that against producing mysterious errors because the
# shell can't always do everything. Let's make sure that these utilities are
# all in our $PATH, or die with an error.
#
# This also sets the following global variables to select alternative utilities
# when there is more than one equivalent tool available:
#
# FETCH = name of utility used to download files from the web
# CHECK = command to invoke to generate checksums on a file
# CHECKTYPE = type of checksum generated
#
require_utils() {
local external
local errors
local tool
local tmp
external='cat cut dd grep ls mkdir mount readlink sed sync tr umount unzip wc'
if [ -z "$WORKDIR" ]; then
external="$external mktemp"
fi
errors=
for tool in $external ; do
if ! type "$tool" >/dev/null 2>&1 ; then
warn "ERROR: need \"$tool\""
errors=yes
fi
done
# We also need to a way to fetch files from teh internets. Note that the args
# are different depending on which utility we find. We'll use two variants,
# one to fetch fresh every time and one to try again from where we left off.
FETCH=
if [ -z "$FETCH" ] && tmp=$(type curl 2>/dev/null) ; then
FETCH=curl
fi
if [ -z "$FETCH" ] && tmp=$(type wget 2>/dev/null) ; then
FETCH=wget
fi
if [ -z "$FETCH" ]; then
warn "ERROR: need \"curl\" or \"wget\""
errors=yes
fi
# Once we've fetched a file we need to compute its checksum. There are
# multiple possiblities here too.
CHECK=
if [ -z "$CHECK" ] && tmp=$(type md5sum 2>/dev/null) ; then
CHECK="md5sum"
CHECKTYPE="md5"
fi
if [ -z "$CHECK" ] && tmp=$(type sha1sum 2>/dev/null) ; then
CHECK="sha1sum"
CHECKTYPE="sha1"
fi
if [ -z "$CHECK" ] && tmp=$(type openssl 2>/dev/null) ; then
CHECK="openssl"
CHECKTYPE="md5"
fi
if [ -z "$CHECK" ]; then
warn "ERROR: need \"md5sum\" or \"sha1sum\" or \"openssl\""
errors=yes
fi
if [ -n "$errors" ]; then
ufatal "Some required linux utilities are missing."
fi
}
# This retrieves a URL and stores it locally. It uses the global variable
# 'FETCH' to determine the utility (and args) to invoke.
# Args: URL FILENAME [RESUME]
fetch_url() {
local url
local filename
local resume
local err
url="$1"
filename="$2"
resume="${3:-}"
DEBUG "FETCH=($FETCH) url=($url) filename=($filename) resume=($resume)"
if [ "$FETCH" = "curl" ]; then
if [ -z "$resume" ]; then
# quietly fetch a new copy each time
rm -f "$filename"
curl -f -s -S -o "$filename" "$url"
else
# continue where we left off, if possible
curl -f -C - -o "$filename" "$url"
# If you give curl the '-C -' option but the file you want is already
# complete and the server doesn't report the total size correctly, it
# will report an error instead of just doing nothing. We'll try to work
# around that.
err=$?
if [ "$err" = "18" ]; then
warn "Ignoring spurious complaint"
true
fi
fi
elif [ "$FETCH" = "wget" ]; then
if [ -z "$resume" ]; then
# quietly fetch a new copy each time
rm -f "$filename"
wget -nv -q -O "$filename" "$url"
else
# continue where we left off, if possible
wget -c -O "$filename" "$url"
fi
fi
}
# This returns a checksum on a file. It uses the global variable 'CHECK' to
# determine the utility (and args) to invoke.
# Args: FILENAME
compute_checksum() {
local filename
filename="$1"
DEBUG "CHECK=($CHECK) CHECKTYPE=($CHECKTYPE)"
if [ "$CHECK" = "openssl" ]; then
openssl md5 < "$filename"
else
$CHECK "$tarball" | cut -d' ' -f1
fi
}
##############################################################################
# Helper functions to handle the config file and image tarball.
# Each paragraph in the config file should describe a new image. Let's make
# sure it follows all the rules. This scans the config file and returns success
# if it looks valid. As a side-effect, it lists the line numbers of the start
# and end of each stanza in the global variables 'start_lines' and 'end_lines'
# and saves the total number of images in the global variable 'num_images'.
good_config() {
local line
local key
local val
local display_name
local file
local size
local url
local md5
local sha1
local skipping
local errors
local count
local line_num
display_name=
file=
size=
url=
md5=
sha1=
skipping=yes
errors=
count=0
line_num=0
# global
start_lines=
end_lines=
while read line; do
line_num=$(( line_num + 1 ))
# We might have some empty lines before the first stanza. Skip them.
if [ -n "$skipping" ] && [ -z "$line" ]; then
continue
fi
# Got something...
if [ -n "$line" ]; then
key=${line%=*}
val=${line#*=}
if [ -z "$key" ] || [ -z "$val" ] || [ "$key=$val" != "$line" ]; then
DEBUG "ignoring $line"
continue
fi
# right, looks good
if [ -n "$skipping" ]; then
skipping=
start_lines="$start_lines $line_num"
fi
case $key in
display_name)
if [ -n "$display_name" ]; then
DEBUG "duplicate $key"
errors=yes
fi
display_name="$val"
;;
file)
if [ -n "$file" ]; then
DEBUG "duplicate $key"
errors=yes
fi
file="$val"
;;
size)
if [ -n "$size" ]; then
DEBUG "duplicate $key"
errors=yes
fi
size="$val"
;;
url)
url="$val"
;;
md5)
md5="$val"
;;
sha1)
sha1="$val"
;;
esac
else
# Between paragraphs. Time to check what we've found so far.
end_lines="$end_lines $line_num"
count=$(( count + 1))
if [ -z "$display_name" ]; then
DEBUG "image $count is missing display_name"
errors=yes
fi
if [ -z "$file" ]; then
DEBUG "image $count is missing file"
errors=yes
fi
if [ -z "$size" ]; then
DEBUG "image $count is missing size"
errors=yes
fi
if [ -z "$url" ]; then
DEBUG "image $count is missing url"
errors=yes
fi
if [ "$CHECKTYPE" = "md5" ] && [ -z "$md5" ]; then
DEBUG "image $count is missing required md5"
errors=yes
fi
if [ "$CHECKTYPE" = "sha1" ] && [ -z "$sha1" ]; then
DEBUG "image $count is missing required sha1"
errors=yes
fi
# Prepare for next stanza
display_name=
file=
size=
url=
md5=
sha1=
skipping=yes
fi
done < "$config"
DEBUG "$count images found"
num_images="$count"
DEBUG "start_lines=($start_lines)"
DEBUG "end_lines=($end_lines)"
# return error status
[ "$count" != "0" ] && [ -z "$errors" ]
}
# Make the user pick an image to download. On success, it sets the global
# variable 'user_choice' to the selected image number.
choose_image() {
local show
local count
local line
local num
show=yes
while true; do
if [ -n "$show" ]; then
echo
echo "There are $num_images recovery images to choose from:"
echo
count=0
echo "0 - <quit>"
grep '^display_name=' "$config" | while read line; do
count=$(( count + 1 ))
echo "$line" | sed "s/display_name=/$count - /"
done
echo
show=
fi
echo -n "Please select a recovery image to download: "
read num
if [ -z "$num" ] || [ "$num" = "?" ]; then
show=yes
elif echo "$num" | grep -q '[^0-9]'; then
echo "Sorry, I didn't understand that."
else
if [ "$num" -lt "0" ] || [ "$num" -gt "$num_images" ]; then
echo "That's not one of the choices."
elif [ "$num" -eq 0 ]; then
quit
else
break;
fi
fi
done
echo
# global
user_choice="$num"
}
# Fetch and verify the user's chosen image. On success, it sets the global
# variable 'image_file' to indicate the local name of the unpacked binary that
# should be written to the USB drive.
fetch_image() {
local start
local end
local line
local key
local val
local file
local size
local url
local md5
local sha1
local line_num
local tarball
local err
local sum
file=
size=
url=
md5=
sha1=
line_num="0"
# Convert image number to line numbers within config file.
start=$(echo $start_lines | cut -d' ' -f$1)
end=$(echo $end_lines | cut -d' ' -f$1)
while read line; do
# Skip to the start of the desired stanza
line_num=$(( line_num + 1 ))
if [ "$line_num" -lt "$start" ] || [ "$line_num" -ge "$end" ]; then
continue;
fi
# Process the stanza.
if [ -n "$line" ]; then
key=${line%=*}
val=${line#*=}
if [ -z "$key" ] || [ -z "$val" ] || [ "$key=$val" != "$line" ]; then
DEBUG "ignoring $line"
continue
fi
case $key in
# The descriptive stuff we'll just save for later.
file)
file="$val"
;;
size)
size="$val"
;;
md5)
md5="$val"
;;
sha1)
sha1="$val"
;;
url)
# Try to download each url until one works.
if [ -n "$url" ]; then
# We've already got one (it's very nice).
continue;
fi
warn "Downloading image tarball from $val"
warn
tarball=${val##*/}
if fetch_url "$val" "$tarball" "resumeok"; then
# Got it.
url="$val"
fi
;;
esac
fi
done < "$config"
if [ -z "$url" ]; then
DEBUG "couldn't fetch tarball"
return 1
fi
# Verify the tarball
if ! ls -l "$tarball" | grep -q "$size"; then
DEBUG "size is wrong"
return 1
fi
sum=$(compute_checksum "$tarball")
DEBUG "checksum is $sum"
if [ "$CHECKTYPE" = "md5" ] && [ "$sum" != "$md5" ]; then
DEBUG "wrong $CHECK"
return 1
elif [ "$CHECKTYPE" = "sha1" ] && [ "$sum" != "$sha1" ]; then
DEBUG "wrong $CHECK"
return 1
fi
# Unpack the file
warn "Unpacking the tarball"
rm -f "$file"
if ! unzip "$tarball" "$file"; then
DEBUG "Can't unpack the tarball"
return 1
fi
# global
image_file="$file"
}
##############################################################################
# Helper functions to manage USB drives.
# Return a list of base device names ("sda sdb ...") for all USB drives
get_devlist() {
local dev
local t
local r
for dev in $(cat /proc/partitions); do
[ -r "/sys/block/$dev/device/type" ] &&
t=$(cat "/sys/block/$dev/device/type") &&
[ "$t" = "0" ] &&
r=$(cat "/sys/block/$dev/removable") &&
[ "$r" = "1" ] &&
readlink -f "/sys/block/$dev" | grep -q -i usb &&
echo "$dev" || true
done
}
# Return descriptions for each provided base device name ("sda sdb ...")
get_devinfo() {
local dev
local v
local m
local s
local ss
for dev in $1; do
v=$(cat "/sys/block/$dev/device/vendor") &&
m=$(cat "/sys/block/$dev/device/model") &&
s=$(cat "/sys/block/$dev/size") && ss=$(( $s * 512 / 1000000 )) &&
echo "/dev/$dev ${ss}MB $v $m"
done
}
# Enumerate and descript the specified base device names ("sda sdb ...")
get_choices() {
local dev
local desc
local count
count=1
echo "0 - <quit>"
for dev in $1; do
desc=$(get_devinfo "$dev")
echo "$count - Use $desc"
count=$(( count + 1 ))
done
}
# Make the user pick a USB drive to write to. On success, it sets the global
# variable 'user_choice' to the selected device name ("sda", "sdb", etc.)
choose_drive() {
local show
local devlist
local choices
local num_drives
local msg
local num
show=yes
while true; do
if [ -n "$show" ]; then
devlist=$(get_devlist)
choices=$(get_choices "$devlist")
if [ -z "$devlist" ]; then
num_drives="0"
msg="I can't seem to find a valid USB drive."
else
num_drives=$(echo "$devlist" | wc -l)
if [ "$num_drives" != "1" ]; then
msg="I found $num_drives USB drives"
else
msg="I found $num_drives USB drive"
fi
fi
echo -n "
$msg
$choices
"
show=
fi
echo -n "Tell me what to do (or just press Enter to scan again): "
read num
if [ -z "$num" ] || [ "$num" = "?" ]; then
show=yes
elif echo "$num" | grep -q '[^0-9]'; then
echo "Sorry, I didn't understand that."
else
if [ "$num" -lt "0" ] || [ "$num" -gt "$num_drives" ]; then
echo "That's not one of the choices."
elif [ "$num" -eq 0 ]; then
quit
else
break;
fi
fi
done
# global
user_choice=$(echo $devlist | cut -d' ' -f$num)
}
##############################################################################
# Okay, do something...
# Make sure we have the tools we need
require_utils
# Need a place to work. We prefer a fixed location so we can try to resume any
# interrupted downloads.
if [ -n "$WORKDIR" ]; then
if [ ! -d "$WORKDIR" ] && ! mkdir "$WORKDIR" ; then
warn "Using temporary directory"
WORKDIR=
fi
fi
if [ -z "$WORKDIR" ]; then
WORKDIR=$(mktemp -d)
# Clean up temporary directory afterwards
trap "cd; rm -rf ${WORKDIR}" EXIT
fi
cd "$WORKDIR"
warn "Working in $WORKDIR/"
rm -f "$debug"
# Download the config file to see what choices we have.
warn "Downloading config file from $CONFIGURL"
fetch_url "$CONFIGURL" "$tmpfile" || \
gfatal "Unable to download the config file"
# Un-DOS-ify the config file and separate the version info from the images
tr -d '\015' < "$tmpfile" | grep '^recovery_tool' > "$version"
tr -d '\015' < "$tmpfile" | grep -v '^#' | grep -v '^recovery_tool' > "$config"
# Add one empty line to the config file to terminate the last stanza
echo >> "$config"
# Make sure that the config file version matches this script version
tmp=$(grep '^recovery_tool_version=' "$version") || \
gfatal "The config file doesn't contain a version string."
filevers=${tmp#*=}
if [ "$filevers" != "$MYVERSION" ]; then
tmp=$(grep '^recovery_tool_update=' "$version");
msg=${tmp#*=}
warn "This tool is version $MYVERSION." \
"The config file is for version $filevers."
fatal ${msg:-Please download a matching version of the tool and try again.}
fi
# Check the config file to be sure it's valid. As a side-effect, this sets the
# global variable 'num_images' with the number of image stanzas read, but
# that's independent of whether the config is valid.
good_config || gfatal "The config file isn't valid."
# Make the user pick an image to download, or exit.
choose_image
# Download the user's choice
fetch_image "$user_choice" || \
gfatal "Unable to download a valid recovery image."
# Make the user pick a USB drive, or exit.
choose_drive
# Be sure
dev_desc=$(get_devinfo "$user_choice")
echo "
Is this the device you want to put the recovery image on?
$dev_desc
"
echo -n "You must enter 'YES' to continue: "
read tmp
if [ "$tmp" != "YES" ]; then
quit
fi
# Be very sure
echo "
I'm really going to erase this device. This will permanently ERASE
whatever you may have on that drive. You won't be able to undo it.
$dev_desc
"
echo -n "If you're sure that's the device to use, enter 'DoIt' now: "
read tmp
if [ "$tmp" != "DoIt" ]; then
quit
fi
echo "
Installing the recovery image
"
# Unmount anything on that device.
echo "unmounting..."
for tmp in $(mount | grep ^"/dev/${user_choice}" | cut -d' ' -f1); do
umount $tmp || ufatal "Unable to unmount $tmp."
done
# Write it.
echo "copying... (this may take several minutes)"
dd of=/dev/${user_choice} if="$image_file" ||
ufatal "Unable to write the image."
sync
echo "
Done. Remove the USB drive and insert it in your Chrome OS netbook.
"
exit 0