blob: 904c31654a9c55091933969c24efbfe9239afcfe [file] [log] [blame]
# 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.
#
# Common vm functions for use in crosutils.
DEFAULT_PRIVATE_KEY="${GCLIENT_ROOT}/src/scripts/mod_for_test_scripts/\
ssh_keys/testing_rsa"
DEFINE_string kvm_pid "" \
"Use this pid file. If it exists and is set, use the vm specified by pid."
DEFINE_boolean copy ${FLAGS_FALSE} "Copy the image file before starting the VM."
DEFINE_string mem_path "" "VM memory image to save or restore."
DEFINE_boolean no_graphics ${FLAGS_FALSE} "Runs the KVM instance silently."
DEFINE_boolean persist "${FLAGS_FALSE}" "Persist vm."
DEFINE_boolean scsi ${FLAGS_FALSE} "Loads disk as a virtio-scsi-disk. "\
"This option is used for testing Google Compute Engine-compatible images."
DEFINE_boolean snapshot ${FLAGS_FALSE} "Don't commit changes to image."
DEFINE_integer ssh_port 9222 "Port to tunnel ssh traffic over."
DEFINE_string ssh_private_key "${DEFAULT_PRIVATE_KEY}" \
"Path to the private key to use to ssh into test image as the root user."
DEFINE_string vnc "" "VNC Server to display to instead of SDL "\
"(e.g. pass ':1' to listen on 0.0.0.0:5901)."
DEFINE_string usb_devices "" \
"Usb devices for passthrough. Specified in a comma-separated list
where each item is of the form <vendor_id>:<product_id>
(eg. --usb_devices=1050:0211,0409:005a)"
DEFINE_boolean moblab ${FLAGS_FALSE} "Setup environment for moblab"
DEFINE_string qemu_binary \
"${DEFAULT_CHROOT_DIR}"/usr/bin/qemu-system-x86_64 \
"The qemu binary to be used. Defaults to qemu shipped with the SDK."
# TODO(pwang): replace SandyBridge with Haswell-noTSX once all builders
# running VMTest are switched to GCE.
DEFINE_string kvm_cpu "SandyBridge,-invpcid,-tsc-deadline" \
"Configures guest CPU model and features. The default should be the
least restrictive option supported by all of Chrome OS infrastructure.
Other locally valid choices are for instance 'qemu64' (least features)
or 'host' (maximum features)."
DEFINE_string kvm_m 8G "Configure guest RAM"
DEFINE_integer kvm_smp 0 \
"Configure guest CPU counts (0 means auto-detect cpu count)"
DEFINE_string serial "" \
"Configure qemu serial output. By default, serial is output to a file"
KVM_PID_FILE=/tmp/kvm.$$.pid
LIVE_VM_IMAGE=
# Pick which qemu/kvm binary will be used. Must be called before any function
# that needs ${KVM_BINARY}, and *after* the command line has been parsed by the
# calling script. Otherwise the default value can not be overriden by the user.
set_kvm() {
# The value of the flag is only valid after the command line has been parsed.
KVM_BINARY="${FLAGS_qemu_binary}"
if [[ ! -x "${KVM_BINARY}" ]]; then
if ! KVM_BINARY=$(which qemu-system-x86_64 2> /dev/null); then
die "no QEMU binary found"
fi
fi
info "QEMU binary: ${KVM_BINARY}"
# Make sure it's a recent enough version.
# The version string typically looks like this:
#"QEMU emulator version 2.5.0, Copyright (c) 2003-2008 Fabrice Bellard"
# but in Debian/ubuntu distributions, some QEMU binaries have a
# space and a package version rather the comma just after the version number:
#"QEMU emulator version 2.0.0 (Debian 2.0.0+dfsg-2ubuntu1.22), Copyright[...]"
local ver version
version=$("${KVM_BINARY}" --version)
ver="${version#QEMU emulator version }"
info "QEMU version: ${version}"
case ${ver} in
[23].[0-9]*) ;;
*) die "Old/unknown/unsupported version of QEMU: ${ver}" ;;
esac
}
get_pid() {
sudo cat "${KVM_PID_FILE}"
}
get_host_files_prefix() {
echo "${KVM_PID_FILE%.pid}"
}
# Configure paths to KVM pipes. Must not be called until after KVM_PID_FILE
# has been updated. (See, e.g., start_kvm.)
set_kvm_pipes() {
local base="$(get_host_files_prefix)"
KVM_PIPE_PREFIX="${base}.monitor"
KVM_PIPE_IN="${KVM_PIPE_PREFIX}.in" # to KVM
KVM_PIPE_OUT="${KVM_PIPE_PREFIX}.out" # from KVM
KVM_SERIAL_FILE="${base}.serial"
}
# General purpose blocking kill on a pid.
# This function sends a specified kill signal [0-9] to a pid and waits for it
# die up to a given timeout. It exponentially backs off it's timeout starting
# at 1 second.
# $1 the process id.
# $2 signal to send (-#).
# $3 max timeout in seconds.
# Returns 0 on success.
blocking_kill() {
local timeout=1
sudo kill -$2 $1
while ps -p $1 > /dev/null && [ ${timeout} -le $3 ]; do
sleep ${timeout}
timeout=$((timeout*2))
done
! ps -p ${1} > /dev/null
}
# Send a command to the KVM monitor. The caller is responsible for
# escaping the command, so that it survives sudo sh -c "$arg".
# Additionally, |set_kvm_pipes| must have been called before this
# function.
send_monitor_command() {
local command="${1}"
sudo sh -c "echo ${1} > ${KVM_PIPE_IN}"
}
# Send a command to the KVM monitor, and wait for KVM to issue another
# prompt. The caller is responsible for escaping the command, so that
# it survives sudo sh -c "$arg". Additionally, |set_kvm_pipes| must
# have been called before this function.
send_monitor_command_and_wait() {
local command="${1}"
sudo sh -c "echo ${1} > ${KVM_PIPE_IN}"
# Wait for the command prompt. Note that we send an empty command
# before waiting, because the monitor's command prompt doesn't
# include a newline. (And grep waits for a newline.)
sudo sh -c "echo > ${KVM_PIPE_IN}"
sudo grep -F -q "(qemu)" "${KVM_PIPE_OUT}"
}
# Return a command which will read stdin, and write a (compressed)
# bytestream to stdout, for the compression format implied by
# |filename|.
get_compressor() {
local filename="${1}"
local extra_flag="${2:-}"
case "${filename}" in
*.gz)
compressor="pigz -c ${extra_flag}"
;;
*.bz2)
compressor="pbzip2 -c ${extra_flag}"
;;
*)
compressor="cat"
;;
esac
echo "${compressor}"
}
# Return a command which will read stdin, and write a (decompressed)
# bytestream to stdout, for the compression format implied by
# |filename|.
get_decompressor() {
get_compressor "${1}" "-d"
}
# Disable ksmd which causes soft kernel lockups on kernel 3.13.
# For details b/71727384.
disable_ksmd() {
local run_path="/sys/kernel/mm/ksm/run"
local ksm=$(<"${run_path}")
if [[ "${ksm}" == "1" ]]; then
info "Disabling currently enabled ksmd to work around kernel lockups."
# This is for interactive use. If a calling script hangs here please set
# this environment in advance when you are able to sudo.
echo 0 | sudo tee "${run_path}" > /dev/null
fi
}
# $1: Path to the virtual image to start.
# $2: Name of the board to virtualize.
start_kvm() {
local vm_image="$1"
local board="$2"
local extra_args=( "${@:3}" )
disable_ksmd
set_kvm
# Append 'check' option to warn if requested CPU is not fully supported.
local kvm_cpu="${FLAGS_kvm_cpu},check"
# Override default pid file.
local start_vm=0
[ -n "${FLAGS_kvm_pid}" ] && KVM_PID_FILE=${FLAGS_kvm_pid}
if [ -f "${KVM_PID_FILE}" ]; then
local pid=$(get_pid)
# Check if the process exists.
if ps -p ${pid} > /dev/null ; then
echo "Using a pre-created KVM instance specified by ${FLAGS_kvm_pid}." >&2
start_vm=1
else
# Let's be safe in case they specified a file that isn't a pid file.
echo "File ${KVM_PID_FILE} exists but specified pid doesn't." >&2
fi
fi
# No kvm specified by pid file found, start a new one.
if [ ${start_vm} -eq 0 ]; then
echo "Starting a KVM instance" >&2
local kvm_flag=""
local nographics=""
local usesnapshot=""
if [ ${FLAGS_no_graphics} -eq ${FLAGS_TRUE} ]; then
nographics="-display none"
fi
if [ -n "${FLAGS_vnc}" ]; then
nographics="-vnc ${FLAGS_vnc}"
fi
if [ ${FLAGS_snapshot} -eq ${FLAGS_TRUE} ]; then
snapshot="-snapshot"
fi
# When using the regular qemu system binary, force KVM.
case "${KVM_BINARY}" in
*/qemu-system-x86_64)
kvm_flag="-enable-kvm"
;;
esac
if [ ${FLAGS_copy} -eq ${FLAGS_TRUE} ]; then
local our_copy=$(mktemp "${vm_image}.copy.XXXXXXXXXX")
if cp "${vm_image}" "${our_copy}"; then
info "Copied ${vm_image} to ${our_copy}."
vm_image="${our_copy}"
else
die "Copy failed. Aborting."
fi
fi
local kvm_smp=${FLAGS_kvm_smp}
if [[ ${kvm_smp} -eq 0 ]]; then
local kvm_smp_default=8
info "Set kvm_smp to min(${kvm_smp_default}, ${NUM_JOBS})."
kvm_smp=$((kvm_smp_default > NUM_JOBS ? NUM_JOBS : kvm_smp_default))
fi
local net_option="-device virtio-net,netdev=eth0"
local net_user="-netdev user,id=eth0,net=10.0.2.0/27"
net_user+=",hostfwd=tcp:127.0.0.1:${FLAGS_ssh_port}-:22"
local incoming=""
local incoming_option=""
if [ -n "${FLAGS_mem_path}" ]; then
local decompressor=$(get_decompressor "${FLAGS_mem_path}")
incoming="-incoming"
incoming_option="exec: ${decompressor} ${FLAGS_mem_path}"
fi
local usb_passthrough=""
if [ -n "${FLAGS_usb_devices}" ]; then
local bus_id
local usb_devices=(${FLAGS_usb_devices//,/ })
for bus_id in "${usb_devices[@]}"; do
local device=(${bus_id//:/ })
if [ ${#device[@]} -ne 2 ]; then
continue
fi
usb_passthrough+=" -device usb-host,vendorid=$((0x${device[0]}))"
usb_passthrough+=",productid=$((0x${device[1]}))"
done
if [ -n "${usb_passthrough}" ]; then
usb_passthrough="-usb ${usb_passthrough}"
fi
fi
local base="$(get_host_files_prefix)"
local moblab_env=""
if [ ${FLAGS_moblab} -eq ${FLAGS_TRUE} ]; then
# Increase moblab memory size.
moblab_env="-m 4G"
# Add hostforwarding for important moblab pages to the SLIRP connection.
MOB_MONITOR_PORT=$(( FLAGS_ssh_port + 1 ))
AFE_PORT=$(( FLAGS_ssh_port + 2 ))
DEVSERVER_PORT=$(( FLAGS_ssh_port + 3 ))
net_user+=",hostfwd=tcp:127.0.0.1:${MOB_MONITOR_PORT}-:9991"
net_user+=",hostfwd=tcp:127.0.0.1:${AFE_PORT}-:80"
net_user+=",hostfwd=tcp:127.0.0.1:${DEVSERVER_PORT}-:8080"
info "Mob* Monitor: 127.0.0.1:${MOB_MONITOR_PORT}"
info "Autotest: 127.0.0.1:${AFE_PORT}"
info "Devserver: 127.0.0.1:${DEVSERVER_PORT}"
fi
set_kvm_pipes
for pipe in "${KVM_PIPE_IN}" "${KVM_PIPE_OUT}"; do
sudo rm -f "${pipe}" # assumed safe because, the PID is not running
sudo mknod "${pipe}" p
sudo chmod 600 "${pipe}"
done
local serial=""
if [[ -n "${FLAGS_serial}" ]]; then
serial="${FLAGS_serial}"
else
sudo touch "${KVM_SERIAL_FILE}"
sudo chmod a+r "${KVM_SERIAL_FILE}"
serial="file:${KVM_SERIAL_FILE}"
fi
local drive
drive="-drive file=${vm_image},index=0,media=disk,cache=unsafe"
if [ ${FLAGS_scsi} -eq ${FLAGS_TRUE} ]; then
drive=$(echo "-drive if=none,id=hd,file=${vm_image},cache=unsafe"\
"-device virtio-scsi-pci,id=scsi "\
"-device scsi-hd,drive=hd")
fi
# Note: the goofiness around the expansion of |incoming_option| is
# to ensure that it is quoted if set, but _not_ quoted if
# unset. (QEMU chokes on empty arguments).
local cmd=(
"${KVM_BINARY}" ${kvm_flag}
-m ${FLAGS_kvm_m}
-smp ${kvm_smp}
-cpu ${kvm_cpu}
-vga virtio
-pidfile "${KVM_PID_FILE}"
-chardev pipe,id=control_pipe,path="${KVM_PIPE_PREFIX}"
-serial "${serial}"
-mon chardev=control_pipe
-daemonize
${net_option}
${nographics}
${snapshot}
${net_user}
${incoming} ${incoming_option:+"$incoming_option"}
${usb_passthrough}
${moblab_env}
${drive}
"${extra_args[@]}"
)
info "Launching: ${cmd[*]}"
sudo "${cmd[@]}"
info "KVM started with pid stored in ${KVM_PID_FILE}"
if [[ -z "${FLAGS_serial}" ]]; then
info "Serial output, if available, can be found in ${KVM_SERIAL_FILE}"
fi
LIVE_VM_IMAGE="${vm_image}"
fi
}
# Checks to see if we can access the target virtual machine with ssh.
ssh_ping() {
# TODO(sosa): Remove outside chroot use once all callers work inside chroot.
local cmd
if [ $INSIDE_CHROOT -ne 1 ]; then
cmd="${GCLIENT_ROOT}/src/scripts/ssh_test.sh"
else
cmd=/usr/lib/crosutils/ssh_test.sh
fi
"${cmd}" \
--ssh_port=${FLAGS_ssh_port} \
--private_key=${FLAGS_ssh_private_key} \
--remote=127.0.0.1 >&2
}
# Tries to ssh into live image $1 times. After first failure, a try involves
# shutting down and restarting kvm.
retry_until_ssh() {
local can_ssh_into=1
local max_retries=3
local retries=0
ssh_ping && can_ssh_into=0
while [ ${can_ssh_into} -eq 1 ] && [ ${retries} -lt ${max_retries} ]; do
echo "Failed to connect to virtual machine, retrying ... " >&2
stop_kvm || echo "Could not stop kvm. Retrying anyway." >&2
start_kvm "${LIVE_VM_IMAGE}"
ssh_ping && can_ssh_into=0
retries=$((retries + 1))
done
return ${can_ssh_into}
}
stop_kvm() {
set_kvm
if [ "${FLAGS_persist}" -eq "${FLAGS_TRUE}" ]; then
echo "Persist requested. Use --ssh_port ${FLAGS_ssh_port} " \
"--ssh_private_key ${FLAGS_ssh_private_key} " \
"--kvm_pid ${KVM_PID_FILE} to re-connect to it." >&2
else
echo "Stopping the KVM instance" >&2
set_kvm_pipes
local pid=$(get_pid)
if [ -n "${pid}" ]; then
if ! [ -d "/proc/${pid}" ]; then
echo "KVM pid ${pid} no longer running." >&2
return 1
fi
if [ -n "${FLAGS_mem_path}" ]; then
local mem_path="${FLAGS_mem_path}"
local compressor=$(get_compressor "${mem_path}")
echo "Saving memory snapshot to ${mem_path}..."
echo " freezing VM..."
send_monitor_command_and_wait "stop"
echo " saving memory, piping through ${compressor}..."
# Create file as current user, so that it will be readable by
# the current user. (Otherwise, it would be owned by root.)
touch "${mem_path}"
send_monitor_command_and_wait \
"migrate \\\"exec:${compressor} \> ${mem_path}\\\""
# Flush any disk I/O that is buffered in KVM.
echo " flushing disk buffers..."
send_monitor_command_and_wait "commit all"
# Quit KVM now, so that we don't modify the filesystem which
# this memory image depends on.
echo " asking KVM to quit..."
send_monitor_command "quit"
echo " done."
else
# Initiate the power-off sequence inside the guest. Note that
# this monitor command does not wait for the guest to power
# off the system.
send_monitor_command "system_powerdown"
fi
blocking_kill ${pid} 0 16 || blocking_kill ${pid} 9 3
sudo rm -f "${KVM_PID_FILE}" "${KVM_PIPE_IN}" "${KVM_PIPE_OUT}" \
"${KVM_SERIAL_FILE}"
else
echo "No kvm pid found to stop." >&2
return 1
fi
fi
}