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: