From a17aa7c02a1a435d52a728cb7e322abec8d5ea8f Mon Sep 17 00:00:00 2001 From: Roger Luethi Date: Tue, 17 Jun 2014 08:34:35 +0200 Subject: [PATCH] Add library of VirtualBox functions This changeset adds a library of functions for interacting with VirtualBox via VBoxManage. It contains functions for creating and booting VMs, taking snapshots, configuring disks, networking, and shared folders. In addition, its vm_attach_guestadd-iso can ask VirtualBox for the guest-additions ISO and, if that fails, look for it on the local filesystem or download the correct version from virtualbox.org. On Windows (wbatch), the function relies on VirtualBox providing the guest-additions ISO (which on that platform appears to be a safe bet). Partial-Bug: 1312764 Implements: blueprint openstack-training-labs Change-Id: Id5529432eba01bd1e3e67a1d64b7517efd1966d0 --- labs/lib/osbash/virtualbox.functions | 624 +++++++++++++++++++++++++++ 1 file changed, 624 insertions(+) create mode 100644 labs/lib/osbash/virtualbox.functions diff --git a/labs/lib/osbash/virtualbox.functions b/labs/lib/osbash/virtualbox.functions new file mode 100644 index 00000000..6b9e4512 --- /dev/null +++ b/labs/lib/osbash/virtualbox.functions @@ -0,0 +1,624 @@ +#------------------------------------------------------------------------------- +# VirtualBoxManage +#------------------------------------------------------------------------------- + +VBM=vbm +: ${VBM_LOG:=$LOG_DIR/vbm.log} + +function vbm { + ${WBATCH:-:} wbatch_log_vbm "$@" + + mkdir -p "$(dirname "$VBM_LOG")" + + if [[ -n "${OSBASH:-}" ]]; then + echo "$@" >> "$VBM_LOG" + local rc=0 + "$VBM_EXE" "$@" || rc=$? + if [ $rc -ne 0 ]; then + echo >&2 "FAILURE: VBoxManage: $@" + return 1 + fi + else + echo "(not executed) $@" >> "$VBM_LOG" + fi +} + +function get_vb_version { + local VERSION="" + local RAW=$(WBATCH= $VBM --version) + local re='([0-9]+\.[0-9]+\.[0-9]+).*' + if [[ $RAW =~ $re ]]; then + VERSION=${BASH_REMATCH[1]} + fi + echo "$VERSION" +} + +#------------------------------------------------------------------------------- +# VM status +#------------------------------------------------------------------------------- + +function vm_exists { + local VM_NAME=$1 + return $(WBATCH= $VBM list vms | grep -q "\"$VM_NAME\"") +} + +function vm_is_running { + local VM_NAME=$1 + return $(WBATCH= $VBM showvminfo --machinereadable "$VM_NAME" | \ + grep -q 'VMState="running"') +} + +function vm_wait_for_shutdown { + local VM=$1 + + ${WBATCH:-:} wbatch_wait_poweroff "$VM" + # Return if we are just faking it for wbatch + ${OSBASH:+:} return 0 + + echo >&2 -n "Machine shutting down" + until WBATCH= $VBM showvminfo --machinereadable "$VM" 2>/dev/null | \ + grep -q '="poweroff"'; do + echo -n . + sleep 1 + done + echo >&2 -e "\nMachine powered off." +} + +function vm_power_off { + local VM_NAME=$1 + if vm_is_running "$VM_NAME"; then + echo >&2 "Powering off VM \"$VM_NAME\"" + $VBM controlvm "$VM_NAME" poweroff + fi + # VirtualBox VM needs a break before taking new commands + vbox_sleep 1 +} + +function vm_snapshot { + local VM_NAME=$1 + local SHOT_NAME=$2 + + # Blanks would fail in Windows batch files; space becomes underscore + SHOT_NAME="${SHOT_NAME// /_}" + + $VBM snapshot "$VM_NAME" take "$SHOT_NAME" + # VirtualBox VM needs a break before taking new commands + vbox_sleep 1 +} + +#------------------------------------------------------------------------------- +# Host-only network functions +#------------------------------------------------------------------------------- + +function hostonlyif_in_use { + local NAME=$1 + return $(WBATCH= $VBM list -l runningvms | \ + grep -q "Host-only Interface '$NAME'") +} + +function ip_to_hostonlyif { + local IP=$1 + local prevline="" + WBATCH= $VBM list hostonlyifs | grep -e "^Name:" -e "^IPAddress:" | \ + while read line; do + if [[ "$line" == *$IP* ]]; then + # match longest string that ends with a space + echo ${prevline##Name:* } + break + fi + prevline=$line + done +} + +function create_hostonlyif { + local OUT=$(WBATCH= $VBM hostonlyif create 2> /dev/null | grep "^Interface") + # OUT is something like "Interface 'vboxnet3' was successfully created" + local re="Interface '(.*)' was successfully created" + if [[ $OUT =~ $re ]]; then + echo "${BASH_REMATCH[1]}" + else + echo >&2 "Host-only interface creation failed" + return 1 + fi +} + +function create_network { + local IP=$1 + + # XXX We need host-only interface names as identifiers for wbatch; by + # always executing VBoxManage calls to ip_to_hostonlyif and + # create_hostonlyif we avoid the need to invent fake interface names + + local NAME="$(OSBASH=exec_cmd ip_to_hostonlyif "$IP")" + if [ -n "$NAME" ]; then + if hostonlyif_in_use "$NAME"; then + echo >&2 "Host-only interface $NAME ($IP) is in use. Using it, too." + fi + else + echo >&2 "Creating host-only interface" + NAME=$(OSBASH=exec_cmd create_hostonlyif) + fi + + echo >&2 "Configuring host-only network $IP ($NAME)" + $VBM hostonlyif ipconfig "$NAME" \ + --ip "$IP" \ + --netmask 255.255.255.0 >/dev/null + echo "$NAME" +} + +#------------------------------------------------------------------------------- +# Disk functions +#------------------------------------------------------------------------------- + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Creating, registering and unregistering disk images with VirtualBox +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# DISK can be either a path or a disk UUID +function disk_registered { + local DISK=$1 + return $(WBATCH= $VBM list hdds | grep -q "$DISK") +} + +# DISK can be either a path or a disk UUID +function disk_unregister { + local DISK=$1 + echo >&2 -e "Unregistering disk\n\t$DISK" + $VBM closemedium disk "$DISK" +} + +function create_vdi { + local HDPATH=$1 + local SIZE=$2 + echo >&2 -e "Creating disk:\n\t$HDPATH" + $VBM createhd --format VDI --filename "$HDPATH" --size "$SIZE" +} + +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +# Attaching and detaching disks from VMs +# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +# DISK can be either a path or a disk UUID +function get_next_child_uuid { + local DISK=$1 + local CHILD_UUID="" + if disk_registered "$DISK"; then + local LINE=$(WBATCH= $VBM showhdinfo "$DISK" | grep -e "^Child UUIDs:") + CHILD_UUID=${LINE##Child UUIDs:* } + fi + echo -e "next_child_uuid $DISK:\n\t$LINE\n\t$CHILD_UUID" >> "$VBM_LOG" + echo "$CHILD_UUID" +} + +# DISK can be either a path or a disk UUID +function path_to_disk_uuid { + local DISK=$1 + local UUID="" + local LINE=$(WBATCH= $VBM showhdinfo "$DISK" | grep -e "^UUID:") + local re='UUID:[ ]+([^ ]+)' + if [[ $LINE =~ $re ]]; then + UUID=${BASH_REMATCH[1]} + fi + echo -e "path_to_disk_uuid $DISK:\n\t$LINE\n\t$UUID" >> "$VBM_LOG" + echo "$UUID" +} + +# DISK can be either a path or a disk UUID +function disk_to_path { + local DISK=$1 + local FPATH="" + local LINE=$(WBATCH= $VBM showhdinfo "$DISK" | grep -e "^Location:") + local re='Location:[ ]+([^ ]+)' + if [[ $LINE =~ $re ]]; then + FPATH=${BASH_REMATCH[1]} + fi + echo -e "disk_to_path $DISK:\n\t$LINE\n\t$FPATH" >> "$VBM_LOG" + echo "$FPATH" +} + +# DISK can be either a path or a disk UUID +function disk_to_vm { + local DISK=$1 + local VM_NAME="" + local LINE=$(WBATCH= $VBM showhdinfo "$DISK" | grep -e "^In use by VMs:") + local re='In use by VMs:[ ]+([^ ]+) ' + if [[ $LINE =~ $re ]]; then + VM_NAME=${BASH_REMATCH[1]} + fi + echo -e "disk_to_vm $DISK:\n\t$LINE\n\t$VM_NAME" >> "$VBM_LOG" + echo "$VM_NAME" +} + +function vm_get_disk_path { + local VM_NAME=$1 + local LINE=$(WBATCH= $VBM showvminfo --machinereadable "$VM_NAME" | \ + grep '^"SATA-0-0"=.*vdi"$') + local HDPATH=${LINE##\"SATA-0-0\"=\"} + HDPATH=${HDPATH%\"} + echo "$HDPATH" +} + +function vm_detach_disk { + local VM_NAME=$1 + echo >&2 "Detaching disk from VM \"$VM_NAME\"" + $VBM storageattach "$VM_NAME" \ + --storagectl SATA \ + --port 0 \ + --device 0 \ + --type hdd \ + --medium none + # VirtualBox VM needs a break before taking new commands + vbox_sleep 1 +} + +# DISK can be either a path or a disk UUID +function vm_attach_disk { + local VM_NAME=$1 + local DISK=$2 + echo >&2 -e "Attaching to VM \"$VM_NAME\":\n\t$DISK" + $VBM storageattach "$VM_NAME" \ + --storagectl SATA \ + --port 0 \ + --device 0 \ + --type hdd \ + --medium "$DISK" +} + +# DISK can be either a path or a disk UUID +function vm_attach_disk_multi { + local VM_NAME=$1 + local DISK=$2 + echo >&2 -e "Attaching to VM \"$VM_NAME\":\n\t$DISK" + $VBM storageattach "$VM_NAME" \ + --storagectl SATA \ + --port 0 \ + --device 0 \ + --type hdd \ + --medium "$DISK" \ + --mtype multiattach +} + +#------------------------------------------------------------------------------- +# VM create and configure +#------------------------------------------------------------------------------- + +function vm_mem { + local NAME="$1" + local MEM="$2" + $VBM modifyvm "$NAME" --memory "$MEM" +} + +function vm_port { + local NAME="$1" + local DESC="$2" + local HOSTPORT="$3" + local GUESTPORT="$4" + $VBM modifyvm "$NAME" --natpf1 "$DESC,tcp,,$HOSTPORT,,$GUESTPORT" +} + +function vm_nic_hostonly { + local VM=$1 + # We start counting interfaces at 0, but VirtualBox starts NICs at 1 + local NIC=$(($2 + 1)) + local NETNAME=$3 + $VBM modifyvm "$VM" \ + "--nictype$NIC" "$NICTYPE" \ + "--nic$NIC" hostonly \ + "--hostonlyadapter$NIC" "$NETNAME" \ + "--nicpromisc$NIC" allow-all +} + +function vm_nic_nat { + local VM=$1 + # We start counting interfaces at 0, but VirtualBox starts NICs at 1 + local NIC=$(($2 + 1)) + $VBM modifyvm "$VM" "--nictype$NIC" "$NICTYPE" "--nic$NIC" nat +} + +function vm_create { + # NOTE: We assume that a VM with a matching name is ours. + # Remove and recreate just in case someone messed with it. + local VM_NAME="$1" + + ${WBATCH:-:} wbatch_abort_if_vm_exists "$VM_NAME" + + # Don't write to wbatch scripts, and don't execute when we are faking it + # it for wbatch + WBATCH= ${OSBASH:-:} vm_delete "$VM_NAME" + + # XXX ostype is distro-specific; moving it to modifyvm disables networking + + # Note: The VirtualBox GUI may not notice group changes after VM creation + # until GUI is restarted. Moving a VM with group membership will + # fail in cases (lingering files from old VM) where creating a + # VM in that location succeeds. + # + # XXX temporary hack + # --groups not supported in VirtualBox 4.1 (Mac OS X 10.5) + echo >&2 "Creating VM \"$VM_NAME\"" + local VER=$(get_vb_version) + if [[ $VER = 4.1* ]]; then + $VBM createvm \ + --name "$VM_NAME" \ + --register \ + --ostype Ubuntu_64 >/dev/null + else + $VBM createvm \ + --name "$VM_NAME" \ + --register \ + --ostype Ubuntu_64 \ + --groups "/$VM_GROUP" >/dev/null + fi + + $VBM modifyvm "$VM_NAME" --rtcuseutc on + $VBM modifyvm "$VM_NAME" --biosbootmenu disabled + $VBM modifyvm "$VM_NAME" --largepages on + $VBM modifyvm "$VM_NAME" --boot1 disk + + # XXX temporary hack + # --portcount not supported in VirtualBox 4.1 (Mac OS X 10.5) + if [[ $VER == 4.1* ]]; then + $VBM storagectl "$VM_NAME" --name SATA --add sata + else + $VBM storagectl "$VM_NAME" --name SATA --add sata --portcount 1 + fi + + $VBM storagectl "$VM_NAME" --name IDE --add ide + echo >&2 "Created VM \"$VM_NAME\"" +} + +#------------------------------------------------------------------------------- +# VM unregister, remove, delete +#------------------------------------------------------------------------------- + +function vm_unregister_del { + local VM_NAME=$1 + echo >&2 "Unregistering and deleting VM \"$VM_NAME\"" + $VBM unregistervm "$VM_NAME" --delete +} + +function vm_delete { + local VM_NAME=$1 + echo >&2 -n "Asked to delete VM \"$VM_NAME\" " + if vm_exists "$VM_NAME"; then + echo >&2 "(found)" + vm_power_off "$VM_NAME" + local HDPATH="$(vm_get_disk_path "$VM_NAME")" + if [ -n "$HDPATH" ]; then + echo >&2 -e "Disk attached: $HDPATH" + vm_detach_disk "$VM_NAME" + disk_unregister "$HDPATH" + echo >&2 -e "Deleting: $HDPATH" + rm -f "$HDPATH" + fi + vm_unregister_del "$VM_NAME" + else + echo >&2 "(not found)" + fi +} + +# Remove VMs using disk and its children disks +# DISK can be either a path or a disk UUID +function disk_delete_child_vms { + local DISK=$1 + if ! disk_registered "$DISK"; then + # VirtualBox doesn't know this disk; we are done + echo >&2 -e "Disk not registered with VirtualBox:\n\t$DISK" + return 0 + fi + + # XXX temporary hack + # No Child UUIDs through showhdinfo in VirtualBox 4.1 (Mac OS X 10.5) + local VER=$(get_vb_version) + if [[ $VER == 4.1* ]]; then + local VM="" + for VM in controller network compute base; do + vm_delete "$VM" + done + return 0 + fi + + while [ : ]; do + local CHILD_UUID=$(get_next_child_uuid "$DISK") + if [ -n "$CHILD_UUID" ]; then + local CHILD_DISK="$(disk_to_path "$CHILD_UUID")" + echo >&2 -e "\nChild disk UUID: $CHILD_UUID\n\t$CHILD_DISK" + + local VM="$(disk_to_vm "$CHILD_UUID")" + if [ -n "$VM" ]; then + echo 2>&1 -e "\tstill attached to VM \"$VM\"" + vm_delete "$VM" + else + echo >&2 "Unregistering and deleting: $CHILD_UUID" + disk_unregister "$CHILD_UUID" + echo >&2 -e "\t$CHILD_DISK" + rm -f "$CHILD_DISK" + fi + else + break + fi + done +} + +#------------------------------------------------------------------------------- +# VM shared folders +#------------------------------------------------------------------------------- + +function vm_add_share_automount { + local VM_NAME=$1 + local SHARE_DIR=$2 + local SHARE_NAME=$3 + $VBM sharedfolder add "$VM_NAME" \ + --name "$SHARE_NAME" \ + --hostpath "$SHARE_DIR" \ + --automount +} + +function vm_add_share { + local VM_NAME=$1 + local SHARE_DIR=$2 + local SHARE_NAME=$3 + $VBM sharedfolder add "$VM_NAME" \ + --name "$SHARE_NAME" \ + --hostpath "$SHARE_DIR" +} + +function vm_rm_share { + local VM_NAME=$1 + local SHARE_NAME=$2 + $VBM sharedfolder remove "$VM_NAME" --name "$SHARE_NAME" +} + +#------------------------------------------------------------------------------- +# VirtualBox guest add-ons +#------------------------------------------------------------------------------- + +function _download_guestadd-iso { + # e.g. 4.1.32r92798 4.3.10_RPMFusionr93012 4.3.10_Debianr93012 + local ISO=VBoxGuestAdditions.iso + local VER=$(get_vb_version) + if [[ -n "$VER" ]]; then + local URL="http://download.virtualbox.org/virtualbox/$VER/VBoxGuestAdditions_$VER.iso" + download "$URL" "$ISO_DIR" $ISO + fi + GUESTADD_ISO="$ISO_DIR/$ISO" +} + +function _get_guestadd-iso { + local ISO=VBoxGuestAdditions.iso + + local ADD_ISO="$IMG_DIR/$ISO" + if [ -f "$ADD_ISO" ]; then + echo "$ADD_ISO" + return 0 + fi + + ADD_ISO="/Applications/VirtualBox.app/Contents/MacOS/$ISO" + if [ -f "$ADD_ISO" ]; then + echo "$ADD_ISO" + return 0 + fi + + echo >&2 "Searching filesystem for VBoxGuestAdditions. This may take a while..." + ADD_ISO=$(find / -name "$ISO" 2>/dev/null) || true + if [ -n "$ADD_ISO" ]; then + echo "$ADD_ISO" + return 0 + fi + + echo >&2 "Looking on the Internet" + _download_guestadd-iso + if [ -f "$ADD_ISO" ]; then + echo "$ADD_ISO" + return 0 + fi +} + +function _vm_attach_guestadd-iso { + local VM=$1 + local GUESTADD_ISO=$2 + local rc=0 + $VBM storageattach "$VM" --storagectl IDE --port 1 --device 0 --type dvddrive --medium "$GUESTADD_ISO" 2>/dev/null || rc=$? + return $rc +} + +function vm_attach_guestadd-iso { + local VM=$1 + + OSBASH= ${WBATCH:-:} _vm_attach_guestadd-iso "$VM" emptydrive + OSBASH= ${WBATCH:-:} _vm_attach_guestadd-iso "$VM" additions + # Return if we are just faking it for wbatch + ${OSBASH:+:} return 0 + + if [ -z "${GUESTADD_ISO-}" ]; then + + # No location provided, asking VirtualBox for one + + # An existing drive is needed to make additions shortcut work + # (at least VirtualBox 4.3.12 and below) + WBATCH= _vm_attach_guestadd-iso "$VM" emptydrive + + if WBATCH= _vm_attach_guestadd-iso "$VM" additions; then + echo >&2 "Using VBoxGuestAdditions provided by VirtualBox" + return 0 + fi + # Neither user nor VirtualBox are helping, let's go guessing + GUESTADD_ISO=$(_get_guestadd-iso) + if [ -z "GUESTADD_ISO" ]; then + # No ISO found + return 2 + fi + fi + if WBATCH= _vm_attach_guestadd-iso "$VM" "$GUESTADD_ISO"; then + echo >&2 "Attached $GUESTADD_ISO" + return 0 + else + echo >&2 "Failed to attach $GUESTADD_ISO" + return 3 + fi +} + +#------------------------------------------------------------------------------- +# Sleep +#------------------------------------------------------------------------------- + +function vbox_sleep { + SEC=$1 + + # Don't sleep if we are just faking it for wbatch + ${OSBASH:-:} sleep "$SEC" + ${WBATCH:-:} wbatch_sleep "$SEC" +} + +#------------------------------------------------------------------------------- +# Booting a VM and passing boot parameters +#------------------------------------------------------------------------------- + +source "$OSBASH_LIB_DIR/scanlib" + +function _vbox_push_scancode { + local VM_NAME=$1 + shift + # Split string (e.g. '01 81') into arguments (works also if we + # get each hexbyte as a separate argument) + # Not quoting $@ is intentional -- we want to split on blanks + local SCANCODE=( $@ ) + $VBM controlvm "$VM_NAME" keyboardputscancode "${SCANCODE[@]}" +} + +function vbox_kbd_escape_key { + _vbox_push_scancode "$VM_NAME" "$(esc2scancode)" +} + +function vbox_kbd_enter_key { + _vbox_push_scancode "$VM_NAME" "$(enter2scancode)" +} + +function vbox_kbd_string_input { + local VM_NAME=$1 + local STR=$2 + + # This loop is inefficient enough that we don't overrun the keyboard input + # buffer when pushing scancodes to the VirtualBox. + while IFS= read -r -n1 char; do + if [ -n "$char" ]; then + SC=$(char2scancode "$char") + if [ -n "$SC" ]; then + _vbox_push_scancode "$VM_NAME" "$SC" + else + echo >&2 "not found: $char" + fi + fi + done <<< "$STR" +} + +function vbox_boot { + local VM=$1 + + echo >&2 "Starting VM \"$VM\"" + $VBM startvm "$VM" +} + +#------------------------------------------------------------------------------- + +# vim: set ai ts=4 sw=4 et ft=sh: