blob: a4a00b4fd63c6eee2699738528633f6383fc4f0d [file] [log] [blame]
# Copyright 2017 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.
. /usr/share/misc/ || exit 1
. /usr/share/misc/shflags || exit 1
. /usr/sbin/ || exit 1
# Helpful constants.
readonly LOGFILE_PATH="/var/log/quick-provision.log"
readonly KERN_IMAGE="full_dev_part_KERN.bin.gz"
readonly ROOT_IMAGE="full_dev_part_ROOT.bin.gz"
readonly STATEFUL_TGZ="stateful.tgz"
readonly STATEFUL_DIR="/mnt/stateful_partition"
readonly UPDATE_STATE_FILE=".update_available"
# Persistent file to log the hashes of contents of files downloaded from
# the devserver to help debug
readonly HASHES_LOG="/tmp/hashes"
# Lock file to ensure quick provisioning script isn't run concurrently
# by SSH retries.
readonly LOCKFILE="/var/lock/quick-provision.lock"
# File to indicate that quick provision has already successfully completed.
readonly COMPLETED_FILE="/tmp/quick-provision-complete"
readonly PROGRAM="$(basename $0)"
readonly FLAGS_HELP="Usage:
${PROGRAM} [flags] <build> <url>
DEFINE_string logfile "${LOGFILE_PATH}" "Path to record logs to."
DEFINE_string status_url "" "URL of devserver to post status to."
DEFINE_boolean reboot "${FLAGS_TRUE}" "Whether to reboot at completion"
# Parse command line.
FLAGS "$@" || exit 1
eval set -- "${FLAGS_ARGV}"
usage() {
echo "$@"
exit 1
# Log an info line.
info() {
local line="$*"
echo "$(date --rfc-3339=seconds) INFO: ${line}"
# Log an error line.
error() {
local line="$*"
echo "$(date --rfc-3339=seconds) ERROR: ${line}" >&2
# Log a keyval.
keyval() {
local line="$*"
echo "KEYVAL: ${line}"
# Fatal with a message, updating status prior to exiting.
die() {
local line="$*"
error "FATAL: ${line}"
post_status "FATAL: ${line}"
exit 1
# Get the current time as a timestamp.
get_timestamp() {
date +%s
# Generate keyvals based on the timing of an event.
end_timing() {
local start_time="$1"
local event_name="$2"
local end_time="$(get_timestamp)"
local elapsed_time="$((end_time - start_time))"
keyval "${event_name}_start=${start_time}"
keyval "${event_name}_end=${end_time}"
keyval "${event_name}_elapsed=${elapsed_time}"
time_cmd() {
local event_name="$1"
local start_time="$(get_timestamp)"
end_timing "${start_time}" "${event_name}"
# Attempt to post a status update for the provision process.
# Requires status_url FLAGS to be set.
post_status() {
local status="$*"
logger -t quick-provision "Updated status: ${status}"
info "Updated status: ${status}"
if [[ -n "${FLAGS_status_url}" ]]; then
# Send status in the background -- don't let an RPC call blockage hold up
# provisioning. Ignore output unless there is an error.
curl -sS -o /dev/null -F "status=<-" "${FLAGS_status_url}" <<< "${status}" &
# Retrieves a URL to stdout.
get_url_to_stdout() {
local url="$1"
# TODO(davidriley): Switch to curl once curl has better retry/resume
# semantics. See for details.
# TODO(davidriley): Configure timeouts and retries.
# TODO(davidriley): curl options: --speed-time, --speed-limit,
# --connect-timeout, --max-time, --retry, --retry-delay, --retry-max-time
if type wget >/dev/null 2>&1; then
wget --progress=dot:giga -S --tries=1 -O - "${url}" | \
tee >(md5sum >>"${HASHES_LOG}")
return "${PIPESTATUS[0]}"
curl "${url}"
# Writes to a partition from stdin.
write_partition() {
local part="$1"
# TODO(davidriley): Use tool that only verifies zero blocks before writing.
# conv=sparse assumes that zero blocks were previous zero so is not safe
# to use.
dd of="${part}" obs=2M
# Updates a partition on disk with a given gzip compressed partition URL.
# Function will exit script on failure.
update_partition() {
local url="$1"
local part="$2"
# TODO(davidriley): Enable blkdiscard when moving to verifying zero blocks
# before writing them.
# info blkdiscard "${part}"
# blkdiscard "${part}"
info Updating "${part}" with "${url}"
get_url_to_stdout "${url}" | gzip -d | write_partition "${part}"
local pipestatus=("${PIPESTATUS[@]}")
if [[ "${pipestatus[0]}" -ne "0" ]]; then
die "Retrieving ${url} failed. (statuses ${pipestatus[*]})"
elif [[ "${pipestatus[1]}" -ne "0" ]]; then
die "Decompressing ${url} failed. (statuses ${pipestatus[*]})"
elif [[ "${pipestatus[2]}" -ne "0" ]]; then
die "Writing to ${part} failed. (statuses ${pipestatus[*]})"
# Performs a stateful update using a specified stateful.tgz URL.
# Function will exit script on failure.
stateful_update() {
local url="$1"
# Stateful reset.
info "Stateful reset"
post_status "DUT: Stateful reset"
"${STATEFUL_DIR}/var_new" \
"${STATEFUL_DIR}/dev_image_new" || die "Unable to reset stateful."
# Stateful update.
info "Stateful update"
post_status "DUT: Stateful update"
get_url_to_stdout "${url}" |
tar --ignore-command-error --overwrite --directory="${STATEFUL_DIR}" -xzf -
local pipestatus=("${PIPESTATUS[@]}")
if [[ "${pipestatus[0]}" -ne "0" ]]; then
die "Retrieving ${url} failed. (statuses ${pipestatus[*]})"
elif [[ "${pipestatus[1]}" -ne "0" ]]; then
die "Untarring to ${STATEFUL_DIR} failed. (statuses ${pipestatus[*]})"
# Stateful clean.
info "Stateful clean"
post_status "DUT: Stateful clean"
printf "clobber" > "${STATEFUL_DIR}/${UPDATE_STATE_FILE}" || \
die "Unable to clean stateful."
# Performs postinst and sets the next kernel.
# Function will exit script on failure.
set_next_kernel() {
local part="$1"
# TODO(davidriley): Fix postinst to avoid unnecessary operations like
# rewriting hashes and unnecessary delays.
info "Update next kernel to try (via postinst)"
local tmpmnt="$(mktemp -d)"
mount -o ro "${NEXT_ROOT}" "${tmpmnt}" || die "Unable to mount ${NEXT_ROOT}."
"${tmpmnt}/postinst" "${NEXT_ROOT}"
local retval="$?"
umount "${tmpmnt}"
rmdir "${tmpmnt}"
if [[ "${retval}" -ne "0" ]]; then
echo "Downloaded hashes prior to postinst failure:"
cat "${HASHES_LOG}"
die "postinst failed."
provision_device() {
local build="$1"
local static_url="$2"
local script_start_time="$3"
if [[ -f "${COMPLETED_FILE}" ]]; then
if cmp -s <(echo "${build}") "${COMPLETED_FILE}"; then
info "Quick provision already complete to desired version: ${build}"
exit 0
local other_version="$(<"${COMPLETED_FILE}")"
die "Previous quick provision attempt to unexpected version has" \
"completed and waiting for reboot: ${other_version}"
# Example 1: "/dev/nvme0n1p3"
# Example 2: "/dev/sda3"
local current_root="$(rootdev -s)"
local root_disk="$(rootdev -s -d)"
info "Current root ${current_root}, disk ${root_disk}"
keyval "CURRENT_ROOT=${current_root}"
# Handle /dev/mmcblk0pX, /dev/sdaX, etc style partitions.
# Example 1: "3"
# Example 2: "3"
local root_part_num="$(get_partition_number ${current_root})"
# Example 1: "p3"
# Example 2: "3"
local root_part_num_with_delim="${current_root#${root_disk}}"
# Example 1: "p"
# Example 2: ""
local root_part_delim="${root_part_num_with_delim%${root_part_num}}"
if [[ "${root_part_num}" == "${PARTITION_NUM_ROOT_A}" ]]; then
elif [[ "${root_part_num}" == "${PARTITION_NUM_ROOT_B}" ]]; then
die "Unexpected root partition ${current_root}"
info "Will update kern ${NEXT_KERN}, root ${NEXT_ROOT}"
# Shutdown ui, update-engine
info "Shutting down ui, update-engine"
stop ui
stop update-engine
# Kernel.
info "Update kernel ${NEXT_KERN}"
post_status "DUT: Updating kernel ${NEXT_KERN}"
time_cmd UPDATE_KERNEL \
update_partition "${static_url}/${build}/${KERN_IMAGE}" ${NEXT_KERN}
# Rootfs.
info "Update rootfs ${NEXT_ROOT}"
post_status "DUT: Updating rootfs ${NEXT_ROOT}"
time_cmd UPDATE_ROOTFS \
update_partition "${static_url}/${build}/${ROOT_IMAGE}" ${NEXT_ROOT}
# Stateful.
stateful_update "${static_url}/${build}/${STATEFUL_TGZ}"
# Boot the next kernel.
time_cmd SET_NEXT_KERNEL \
set_next_kernel "${NEXT_KERN_PART}"
# Record that quick provision is complete to avoid another attempt.
echo "${build}" >"${COMPLETED_FILE}"
keyval "COMPLETED=${build}"
if [[ ${FLAGS_reboot} -eq ${FLAGS_TRUE} ]]; then
# Reboot in the background, giving time for the ssh invocation to
# cleanly terminate.
info "Reboot (into ${build})"
post_status "DUT: Reboot"
(sleep 2; reboot) &
end_timing "${script_start_time}" QUICK_PROVISION
main() {
if [[ "$#" -ne 2 ]]; then
usage "ERROR: Incorrect number of arguments."
local build="$1"
local static_url="$2"
local script_start_time="$(get_timestamp)"
info "Provisioning ${build} from ${static_url}"
keyval "BOOT_ID=$(</proc/sys/kernel/random/boot_id)"
keyval "$(grep CHROMEOS_RELEASE_BUILDER_PATH /etc/lsb-release | \
# Ensure no concurrent quick provision attempts.
time_cmd LOCK_LOCKFILE flock 9
provision_device "${build}" "${static_url}" "${script_start_time}"
) 9>"${LOCKFILE}"
main "$@" |& tee -a "${FLAGS_logfile}"
# Return the exit status of the main function.
exit "${PIPESTATUS[0]}"