#!/bin/bash
#
#  vboxtool: Utility to retrieve status and control VirtualBox sessions
#
#  Usage: Type 'vboxtool help' for more information
#
#  Copyright (C) 2012 Mark Baaijens <mark.baaijens@gmail.com>
#
#  This file is part of VBoxTool.
#
#  VBoxTool is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  VBoxTool is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

version()
{
  echo "VBoxTool version $version"
  echo "Copyright 2012 Mark Baaijens"
  echo "License GNU GPL version 3 or later"
}

usage()
{
  echo "Usage: vboxtool show|showrun|showconfig|start|autostart|save|stop|backup|version|help [session]"
  echo "Show info about VirtualBox sessions or control those sessions."
  echo "Type 'vboxtool help' for more information."    
}

help()
{
  echo "Usage: vboxtool OPTION [session]"
  echo "Show info about VirtualBox sessions or control those sessions."
  echo ""
  echo "Options:"
  echo "  show              Show status of all sessions."
  echo "  showrun           Only show status of running sessions."
  echo "  showconfig        Show configuration."
  echo "  start [session]   Start all saved sessions or only the given session."
  echo "                    When no session name is given, all saved sessions will be"
  echo "                    started; powered off and aborted sessions are left alone."  
  echo "  autostart         Starts all sessions in a predefined configuration file."
  echo "  save [session]    Save all running sessions or only the given session."
  echo "  stop [session]    Stop all running sessions or only the given session."
  echo "  backup [session]  Backup all running sessions or only the given session."
  echo "  --version|version Version info."
  echo "  --help|help       This help."
  echo ""
  echo "*Configuration. vboxtool depends on two config files, located in /etc/vboxtool."
  echo ""
  echo "Configuration file $machines_conf:"
  echo "- each line in this file is a separate machine" 
  echo "- structure of each line: <session name>,<vrde-port>,<host port>-<guest port>|..."
  echo "- the delimiter ',' between name and VRDE-port only required when configuring"
  echo "  portforwarding"
  echo "- do not use spaces before and after the first ',' delimiter"  
  echo "- lines can be commented out by '#'"
  echo ""  
  echo "Example for $machines_conf:"
  echo "Ubuntu Desktop #1"
  echo "Ubuntu Desktop #2,3391"
  echo "Ubuntu JeOS #1,3392,2022-22|80-80"
  echo "Ubuntu JeOS #2,,2022-22|80-80"
  echo ""
  echo "Example for $vboxtool_conf"
  echo "vbox_user='user'"
  echo "backup_folder=/home/user/vboxbackup"
  echo ""
  echo "*Autostart. Sessions can be started in a controlled way from the command line,"
  echo "only the echo sessions in $machines_conf will be started. As a bonus,"
  echo "the VRDE port and port forwarding can be set at startup time. These"
  echo "options are controlled by $machines_conf. The given ports"
  echo "are set statically to the session, prior to starting. When VRDE port has to be "
  echo "changed, state is discarded when session is in savestate."
  echo ""  
  echo "*Start at boot, save on halt. VBoxTool is capable for autostart sessions at"
  echo "boot time and autosave sessions when host is stopped. This depends on "
  echo "/etc/vboxtool/vboxtool.conf. In here, the variable vbox_user must be filled:"
  echo "vbox_user='<user name>'" 
  echo "Note the quotes. Fill for <user name> the name of the user under which"
  echo "sessions are installed/running."  
  echo ""  
  echo "When vboxtool.conf is not present, no session will start at boot, nor will"
  echo "auto save on host down take place. When vboxtool.conf is present, all sessions"
  echo "in machines.conf will be started because actually, a 'vboxtool autostart'"
  echo "command is issued. Saving sessions when host goes down does not depend on"
  echo "machines.conf: all running sessions will be saved by a 'vboxtool save' command."
  echo ""
  echo "*Stopping sessions. Saving sessions is preferred above stopping: this"
  echo "is faster when restoring and safer because session can appear to be cold booted."
  echo ""
  echo "*The backup command copies all session files to a safe location. This includes"
  echo "the configuration file(s), main VDI file and all snapshots. Running sessions"
  echo "are saved and started after backup has completed. The default backup folder is"
  echo "relative to the vbox folder: <vbox_folder>/.backup. Underneith, subfolders VDI and "
  echo "Machines are created."
  echo "Backups can be automated by putting something like in /etc/crontab:"
  echo "05 2    * * *   <user name>    vboxtool backup &"
  echo "A different backup folder can be used, by defining this in $vboxtool_conf:"
  echo "backup_folder=/home/user/vboxbackup"
  echo ""
  echo "*Logging. All commands will be logged to $log_file"
  echo ""    
  echo "See http://vboxtool.sourceforge.net for more details."  
}

log () { 
  # Log to console and a predefined log file.
  echo $1
  log2file "$1"
}

log2file () { 
  # Log to a predefined log file.
  echo "$(date +%Y-%m-%d) $(date +%H:%M:%S) $1" 1>> "$log_file"
}

showconfig()
{
  echo $vboxtool_conf
  cat $vboxtool_conf | while read conf_line
  do
    echo ' ' $conf_line
  done

  echo $machines_conf
  cat $machines_conf | while read conf_line
  do
    echo ' ' $conf_line
  done
}

start_vbox_session()
{
  log "Starting \"$name\" (vm_starttype=$vm_starttype vrde_port=$vrde_port)"
  $vbox_command startvm $uuid --type $vm_starttype
  log2file "Session \"$name\" started"
}

loop()
{
  # Read commandline parameter(s)
  option=$1
  option_session_name=$2

  # Several state constants
  state_running='running'
  state_saved='saved'
  state_powered_off='powered-off'
  state_aborted='aborted'
  state_paused='paused'
  state_unknown='unknown'
      
  #
  # Iterate over all registered vm's
  #

  # Up to version 2.2.0, output of 'VBoxManage list vms | grep UUID:' looks like this:  
  # "UUID:<12 spaces><uuid>"
  # It is very much verbose, so lines have to be grepped.
  # For iterating over UUID's, this is the initial command:
  # VBoxManage list vms | grep UUID: | awk 'BEGIN{FS="UUID:            "}{print $2}'
  #
  # From version 2.2.0 and higher, output of 'VBoxManage list vms' looks like this:
  # "<session name>" {<uuid>} 
  # It's very compact, but nonetheless different from former versions, thus, not interchangeble.
  #
  # Fortunately, post 2.2.0 VBoxManage has an extra command option, --long. When applying that option, 
  # output is the same as pre 2.2.0 version of VBoxManage. However, simply applying that option on 
  # a pre-2.2.0 version, results in 'invalid command'. So first we have to distinguish if the --long 
  # option is needed. This can be done by retrieving version info of VBoxManage, but as this is error
  # prone (same math calc has to be done), I decided to evaluate the output of VBoxManage.

  # Detect a VBoxManage version post 2.2.0
  force_long_format=""
  long_format_test=$($vbox_command list vms | grep "UUID:")
  if ([ ! -n "$long_format_test" ]) 
  then
    force_long_format="--long"
  fi

  for uuid in $($vbox_command list vms $force_long_format | grep "UUID:            " | awk 'BEGIN{FS="UUID:            "}{print $2}')
  do
    #
    # Extract info from specific vm-session
    #

    # Beware: output from VBoxManage should be something like this
    # "Name:<12 spaces><uuid>"
    # "State:<11 spaces><uuid>"
    name=$($vbox_command showvminfo $uuid      | grep "Name:"  | awk 'BEGIN{FS="Name:            "}{print $2}')
    state_raw=$($vbox_command showvminfo $uuid | grep "State:" | awk 'BEGIN{FS="State:           "}{print $2}')
    vrde_port=`$vbox_command showvminfo $uuid | grep -i $vrde_syntax: | awk '{ print $6}'`
    vrde_port=${vrde_port/,/}  # Remove trailing comma
    
    # Find out if RDP-server is enabled
    if [ "$($vbox_command showvminfo $uuid | grep -i $vrde_syntax: | awk '{ print $2}')" == "enabled" ]
    then
      vrde_enabled=1
    else
      vrde_enabled=0    
    fi  

    # Extract exact state from string state_raw
    # Beware: output from VBoxManage should be exactly as the given strings, i.e. 'running', 'saved', etc.
    echo "$state_raw" | grep -q "running"
    if [ $? -eq 0 ]
    then
      state=$state_running
    else
      echo "$state_raw" | grep -q "saved"
      if [ $? -eq 0 ]
      then
        state=$state_saved
      else
        echo "$state_raw" | grep -q "powered off"
        if [ $? -eq 0 ]
        then
          state=$state_powered_off
        else
          echo "$state_raw" | grep -q "aborted"
          if [ $? -eq 0 ]
          then
            state=$state_aborted
          else
            echo "$state_raw" | grep -q "paused"
            if [ $? -eq 0 ]
            then
              state=$state_paused
            else
              state=$state_unknown
            fi
          fi
        fi
      fi
    fi

    # Check for option-parameter
    case "$option" in
    save) # Save running sessions
      # Go on if there's a specific session name given OR if no session name is given
      if [ "$name" == "$option_session_name" ] || [ ! -n "$option_session_name" ]
      then
        if [ "$state" == "$state_running" ]
        then
          log "Saving \"$name\""
          $vbox_command controlvm $uuid savestate
          log2file "Session \"$name\" saved"
        fi
      fi
      ;;
    backup) # Backup sessions
      # Go on if there's a specific session name given OR if no session name is given
      if [ "$name" == "$option_session_name" ] || [ ! -n "$option_session_name" ]
      then

        # Save the session to provide a stabile snapshot
        if [ "$state" == "$state_running" ]
        then
          log "Pauzing \"$name\""
          $vbox_command controlvm $uuid pause
          log "Session \"$name\" paused"

          # Apparantly, saving a session is asynchronous, i.e. the session is not (entirely) 
          # saved even if the command line has returned. Starting the same session immediately
          # results in an error, stating the session is already running.
          sleep 1
        fi   

        # Files are copied on a individual basis to backup only those files which are essential and
        # to provide a clean backup. Log files under $machine_folder/$name/Logs are left out.
        mkdir -p "$backup_folder/Machines/$name/Snaphots"

        # Copy the session config file
        log "Copy $machine_folder/$name/$name.xml to "$backup_folder/Machines/$name""
        rsync -va "$machine_folder/$name/$name.xml" "$backup_folder/Machines/$name" --quiet

        # Copy any snapshots, i.e. delta's (save state = .vdi) + snapshots (= .sav)
        if [ -d "$machine_folder/$name/Snapshots/" ]; then
          log "Copy $machine_folder/$name/Snapshots/ to "$backup_folder/Machines/$name/Snaphots""
          rsync -va --progress --delete "$machine_folder/$name/Snapshots/" "$backup_folder/Machines/$name/Snaphots"  --quiet
        else
          log "No snapshot(s) in $machine_folder/$name/Snapshots/ to copy"
        fi        

        # Extract the VDI file name (main session file) of the specified session and copy it
        vdifile=$($vbox_command showvminfo "$name" | grep "(UUID:" | cut -d":" -f2 | cut -d "(" -f1 | sed -e 's/^[ \t]*//' | sed 's/[ \t]*$//')
        log "Copy $vdifile to "$backup_folder/VDI""
        rsync -va --progress "$vdifile" "$backup_folder/VDI" --quiet

        # Restart session, only if is it was running before backing up
        if [ "$state" == "$state_running" ]
        then
          log "Resuming \"$name\""
          $vbox_command controlvm $uuid resume
          log "Session \"$name\" resumed"
        fi
      fi
      ;;
    stop) # Stop running sessions
      # Go on if there's a specific session name given OR if no session name is given
      if [ "$name" == "$option_session_name" ] || [ ! -n "$option_session_name" ]
      then
        if [ "$state" == "$state_running" ]
        then
          log "Stopping \"$name\""
          # No reset, stopping is done by the operationg system within the session
          $vbox_command controlvm $uuid poweroff
          log2file "Session \"$name\" stopped"
        fi
      fi
      ;;
    start) # Start saved sessions
      # Sessions are started under the following conditions:
      # - when no session name is given, all saved sessions will be started
      # - (or) when a session name is given, only that specific session will be started
      start_session=0
      if ([ ! -n "$option_session_name" ] && [ "$state" == "$state_saved" ]) 
      then
        start_session=1
      else
        if ([ -n "$option_session_name" ] && [ "$name" == "$option_session_name" ])      
        then
          start_session=1        
        fi
      fi
      
      if [ "$start_session" == "1" ]
      then
        # In any case, the session to start must not be running already
        if [ "$state" != "$state_running" ]
        then   
          start_vbox_session
        fi
      fi
      ;;
    autostart) # Start sessions named in config file
      # Check existence of config file
      if [ -e "$machines_conf" ]
      then
        # Check if session is named in machines.conf. Watch the extra comma after name; 
        # this is to ensure the whole name is searched and found and not a substring.
        # This also requires the config file to be formatted like this: 
        # <session name>,<vrde-port>
        conf_line=`cat $machines_conf | grep "$name,"`
        
        # But we also like to NOT have a trailing ','. So if there's nothing found,
        # we search again, without the comma. But now, the WHOLE line must be equal to 
        # the search string to avoid unwanted substring mismatches.
        if [ -z "$conf_line" ]
        then
          conf_line=`cat $machines_conf | grep "$name"`          
          if [ "$conf_line" != "$name" ]
          then
            conf_line=""
          fi 
        fi
        
        # Only start session when it is found, and not commented out by '#'                 
        if [ -n "$conf_line" ] && [ "${conf_line:0:1}" != "#" ]     
        then       
          # The session to start must not be running already
          if [ "$state" != "$state_running" ]
          then          
            # Extract VRDE port from machines.conf
            vrde_port_config=`echo $conf_line | awk 'BEGIN{FS=","}{print $2}'`

            # Check if configured port equals actual port
            if [ "$vrde_port_config" != "" ]   
            then 
              if [ "$vrde_port_config" != "$vrde_port" ] 
              then 
              
                # Check if RDP-server is enabled; otherwise setting op vrde-port is useless
                if [ "$vrde_enabled" == "1" ]
                then
              
                  # Changing of the VRDE port can only take place on a powered-off session
                  if [ "$state" == "$state_saved" ] 
                  then
                    log "Discarding state of \"$name\""
                    $vbox_command discardstate $uuid
                  fi

                  log "Applying VRDE port $vrde_port_config to \"$name\""
                  $vbox_command modifyvm $uuid --${vrde_syntax}port $vrde_port_config

                  # Update 'real' vrde-port for later purposes
                  vrde_port="$vrde_port_config"
                else
                  log "RDP-server is disabled, cannot apply VRDE port $vrde_port_config to \"$name\""                  
                fi
              fi
            fi

            #
            # Port forwarding
            #
            
            # Remove all port forwarding pairs containing 'vboxtool'; these are considered
            # 'property' of VBoxTool. Hence, they may be deleted at will (by VBoxTool).
            # By using such a strategy, we do not have to check if and how a particular 
            # port pair is defined; it's a kind of 'brute force' but it's very simple and
            # bullet proof to implement (KISS principle). This strategy also ensures that 
            # settngs are always removed, so that so setting becomes orphaned.
            for data_key in $($vbox_command getextradata $uuid enumerate | grep "VBoxInternal/Devices" | grep vboxtool | awk 'BEGIN{FS=","}{print $1}' | awk 'BEGIN{FS=": "}{print $2}')
            do
              # Variable data_key consist op the whole specifier, so inclusive trailing 'Protocol',
              # 'HostPort' or 'GuestPort'.
              $vbox_command setextradata $uuid $data_key
            done

            # Extract portforwarding definition from machines.conf
            # This string has the following syntax: <host port>-<guest port>|... 
            # For example: 2022-22|80-80
            port_forward_config=`echo $conf_line | awk 'BEGIN{FS=","}{print $3}'`

            # Is port forwarding defined?
            if [ -n "$port_forward_config" ] 
            then  
              # Iterate over all port-pairs defined in port_forward_config, separated by '|'
              port_forward_list=(`echo $port_forward_config | tr '|' ' '`)
              
              for port_pair in ${port_forward_list[@]}
              do 
                # Because port forwarding configuration can be made to the session, 
                # even when it is running (!) or when it is in save-state, there's no need 
                # to check if the session is in save-state (unlike configuring the VRDE port).
                         
                # Apply port forwarding settings
                log "Apply port forwarding $port_pair to \"$name\""
                
                # Variable data_id is only a party specifier, so without trailing 'Protocol',
                # 'HostPort' or 'GuestPort'.              
                data_id="VBoxInternal/Devices/pcnet/0/LUN#0/Config/vboxtool-tcp-$port_pair"
                $vbox_command setextradata $uuid $data_id/Protocol TCP
                $vbox_command setextradata $uuid $data_id/HostPort `echo $port_pair | awk 'BEGIN{FS="-"}{print $1}'`
                $vbox_command setextradata $uuid $data_id/GuestPort `echo $port_pair | awk 'BEGIN{FS="-"}{print $2}'`
              done
            fi
            
            # And finally, start the session
            start_vbox_session
          fi 
        else 
          log "No (valid) entry for \"$name\" found in $machines_conf"                 
        fi
      fi
      ;;      
    *) # Remaining parameters
      if [ "$state" == "$state_running" ]
      then
        #
        # Retrieve some runtime info for a running session
        #

        # Retrieve the pid of the vbox-session throuh 'ps'; note that only pid is extracted, not 
        # cpu or other info. These are drawn from the 'top' command because especially cpu from 'ps'
        # is not what is expected: it's an average cpu-load since the process started and not 
        # the actual cpu-load. 
        pid=$(ps -ef | grep "$uuid" | grep "[v]irtualbox" | grep -v grep | awk '{print $2 }')

        # The 'top' command delivers the actual cpu-load and memory consumed
        top=$(top -b -n1 -p $pid | grep $pid)
        cpu=`echo $top | awk '{ print $9}'`
        mem=`echo $top | awk '{ print $5}'`

        # Show some output
        echo "$name: state=$state vrde_enabled=$vrde_enabled vrde_port=$vrde_port cpu=$cpu% mem=$mem"
      else # Session is not running

        # Only show info when no option is given or the option is 'showrun'
        if [ -z "$option" ] || [ $option != "showrun" ] 
        then
          # Show some output
          echo "$name: state=$state vrde_enabled=$vrde_enabled vrde_port=$vrde_port"
        fi
      fi
      ;;
    esac
  done
}


# Retrieve vbox executable name
#
# The OSE-version uses a all lower case name, i.e. 'vboxmanage' so we
# have to find out which executable is available.
if [ -n $(whereis VBoxManage | awk 'BEGIN{FS=" "}{print $2}') ]
then
  vbox_command='VBoxManage --nologo'
else
  if [ -n $(whereis vboxmanage | awk 'BEGIN{FS=" "}{print $2}') ]
  then
    vbox_command='vboxmanage --nologo'
  else
    log "Either 'VBoxManage' or 'vboxmanage' is not available, exiting."
    exit 1
  fi
fi

# Find the correct start-type (vrdp/headless)
#
# Differrent (kind of) versions use different kind of type(s) for starting the vm:
# - 3.x/ose.....: gui|sdl|headless  
# - 3.x/non-ose.: gui|sdl|vrdp|headless (vrdp is called crde in 4.x)
# - 4.x/ose.....: gui|sdl|headless
# - 4.x/non-ose.: gui|sdl|headless
#
# Note that on 3.x/non-ose using the headless option, vrdp is disabled (seems logical), 
# so we cannot safely choose headless as the only one option. 
#
# The strategy will be like this: 
# 1. extract the options, and if there is a vrdp-option available, take that
# 2. if there is no vrdp-option, use 'headless'
# 
vm_starttype=$( $vbox_command help | grep "gui|sdl|vrdp|headless")
if ([ -n "$vm_starttype" ]) 
then
  vm_starttype="vrdp"
else
  vm_starttype="headless"
fi

# Find out the syntax used for vrdp or vrde: from 4.x on, vrde instead of vrdp
# is used; the use of vrdp is deprecated in 4.x but still works, for now.
# To provide backwards compatability with 3.x, find out the preferred syntax 
# by polling help text.
vrde_syntax=$( $vbox_command help | grep "vrdeport")
if ([ ! -n "$vrde_syntax" ]) 
then
  vrde_syntax="vrdp"
else
  vrde_syntax="vrde"
fi

# Some constants
version='0.5'
machines_conf='/etc/vboxtool/machines.conf'
vboxtool_conf='/etc/vboxtool/vboxtool.conf'
vbox_folder="$HOME/.VirtualBox"   
log_file="$vbox_folder/vboxtool.log"

# Retrieve settings from config file, just by executing the config file.
# Config file $config_file should look like this:
# backup_folder="$vbox_folder/.backup"
if [ -f $vboxtool_conf ]
then
  . $vboxtool_conf
fi

# If no backup folder defined, use default
if [ ! -n "$backup_folder" ]
then
  backup_folder="$vbox_folder/.backup"
fi

#
# Check for a commandline option
#
case "$1" in
start)
  log2file "Started command: $1 $2"
  loop start "$2"
  log2file "Finished command: $1 $2"
  ;;
save)
  log2file "Started command: $1 $2"
  loop save "$2"
  log2file "Finished command: $1 $2"
  ;;
autostart)
  log2file "Started command: $1"

  # Check if config file exists
  if [ ! -e "$machines_conf" ]
  then
    log "Configuration file $machines_conf not found"
  fi
  loop autostart
  log2file "Finished command: $1"
  ;;  
stop)
  log2file "Started command: $1 $2"
  loop stop "$2"
  log2file "Finished command: $1 $2"
  ;;
backup)
  log2file "Started command: $1 $2"

  # Dynamic folders
  vdi_folder=$($vbox_command list systemproperties | grep "Default VDI" | cut -d":" -f2 | sed -e 's/[[:space:]]//g')

  # Output stating VDI location is changed (somewhere in 2.x?) to "Default hard disk folder"; to provide 
  # backwards compatability, we should check that also
  if [ ! -n "$vdi_folder" ]
  then
    vdi_folder=$($vbox_command list systemproperties | grep "Default hard disk" | cut -d":" -f2 | sed -e 's/[[:space:]]//g')
  fi

  machine_folder=$($vbox_command list systemproperties | grep "Default machine" | cut -d":" -f2 | sed -e 's/[[:space:]]//g')

  log "Default hard disk folder: $vdi_folder"
  log "Default machine folder: $machine_folder"

  # Backup folder is fixed and relative to the vbox folder
  log "Backup folder: $backup_folder"

  # Create a backup folder and subfolders
  mkdir -p $backup_folder
  mkdir -p $backup_folder/Machines
  mkdir -p $backup_folder/VDI

  # Copy the vbox config file
  log "Copy $vbox_folder/VirtualBox.xml to $backup_folder" 
  rsync -va "$vbox_folder/VirtualBox.xml" "$backup_folder" --quiet

  loop backup "$2"
  log2file "Finished command: $1 $2"
  ;;
show)
  loop show
  ;;
showrun)
  loop showrun
  ;;
showconfig)
  showconfig
  ;;
help)
  help
  ;;
--help)
  help  
  ;;
version)
  version
  ;;
--version)
  version
  ;;
*)
  usage
esac

exit 0

