#!/bin/bash
#
# Copyright 2005-2007 VMware, Inc. All rights reserved.
#
# This script will export all virtual machines known to a particular
# VC instance into a specified target directory from where they can
# be picked up by backup software.

vcb_libs=/usr/lib/vmware/vcb
hostd_libs=/usr/lib/vmware/hostd
cfg=${VCB_CONFIG_FILE:-"/etc/vmware/backuptools.conf"}

. "$vcb_libs/plugins/lib"
if [ -e "$cfg" ] ; then
    . "$cfg"
fi

export HOST_THUMBPRINT

# Temporary files used by this script. Will be cleaned up on exit
tmp=${TEMPDIR:-"/tmp"}
LOGDIR=/var/log/vmware/vcbSnapAll-$$

# Override application name displayed by vcbMounter.
export VCB_APPNAME=vcbSnapAll

#
# Exit codes used by this script
#
ERR_OK=0
ERR_NOTALLVMSBACKEDUP=1
ERR_GETVMLIST=2
ERR_NOVMSFOUND=3
ERR_DESTDIR=4
ERR_USAGE=5
ERR_EMPTYLIST=6



#
# Global arrays to hold the list of MoRefs and DisplayNames
# for VMs to be backed up. Set by create_vm_list, read by 
# backup_vms.
#
declare -a VMMOREFS
declare -a VMNAMES



#
# Description:
# Sanity check arguments. -- We could leave this to vcbMounter,
# but then we will get error-reporting on a per-VM basis, which
# is more confusing to the end user, so do it here.
#
# Arguments: 
# None. Checks global variables.
# 
# Result:
# Returns ERR_OK on success or ERR_USAGE on failure.
#
function init
{
    local rv=$ERR_OK
    local msg=""

    if [ -z "$VCHOST" ] ; then
	msg="Host name is unset"
    fi
    
    if [ -z "$USERNAME" ] ; then
	msg=${msg}" - VC user name is unset"
    fi
    
    if [ -z "$SEARCHSPEC" ] ; then
	msg=${msg}" - Search specifier is unset"
    fi

    if [ -z "$DESTDIR" ] ; then
	msg=${msg}" - Destination directory is unset"
    fi
 
    if [ -z "$msg" ] ; then 
        if [ -z "${INTERCEPT_p}" ] ; then
            if [ "${INTERCEPT_p+EMPTY}" = "EMPTY" ] ; then
                PASSWORD="${INTERCEPT_p}"
            else
                if [ -z "${PASSWORD}" ] ; then
                    if [ "${PASSWORD+EMPTY}" != "EMPTY" ] ; then
                        read -s -p "Password:" PASSWORD
	                echo
                    fi
                fi
            fi
        else
            PASSWORD="${INTERCEPT_p}"
        fi
        if [ -z "$PASSWORD" ] ; then
            if [ "${PASSWORD+EMPTY}" != "EMPTY" ] ; then
		msg=${msg}" - VC user password is unset"
            fi
        fi
    fi

    if [ -z "$msg" ] ; then 
	rv=$ERR_OK
    else
	vcbMounter
	echo
	echo "Missing arguments: ${msg}."
	rv=$ERR_USAGE
    fi

    return $rv
}



#
# Description:
# Check whether the VM is already in list.
#
# Arguments:
# $1: Virtual machine moref to be checked
#
# Results:
# Returns ERR_OK when VM is already in list, and return 
# ERR_NOVMSFOUND when VM is not found in list.
#
function check_vm_list
{
    local vmmoref="$1"
    local total
    local i
    declare -i i=0

    total=${#VMMOREFS[*]}
    while [ $i -lt $total ]
    do
        if [ "${VMMOREFS[$i]}" = "$vmmoref" ] ; then
            return $ERR_OK
        fi
        i=i+1
    done

    return $ERR_NOVMSFOUND
}



# 
# Description:
# Writes a list of all virtual machines matching a particular
# search criteria to stdout.
#
# Arguments:
# $1: Virtual machine search specifier
# $2: Name of output file
#
# Result:
# Returns ERR_OK on success, ERR_GETVMLIST or ERR_NOVMSFOUND on
# failure. Sets VMMOREFS and VMNAMES arrays on success.
#
function create_vm_list 
{
    local searchspec="$1"
    local listfile="$2"
    local tmpfile="${tmp}"/vcbSnap-$$
    local line
    local rv
    local prefix
    local value
    local VMS
    local count
    local check
    declare -i count=0

    LD_LIBRARY_PATH="$LD_LIBRARY_PATH":"$hostd_libs" \
    VCB_PASSWORD="$PASSWORD" \
	"$vcb_libs/vcbVmName" -h "$VCHOST" -u "$USERNAME" \
            -s "$searchspec" > "$tmpfile"
    rv=$?
    if [ "$rv" != "0" ] ; then
        echo `grep "Error:" "$tmpfile"`| sed "s_vcbVmName_vcbSnapAll_" >&2
        rm -f "$tmpfile"
        return $ERR_GETVMLIST
    fi

    # Fill in the VMMOREFS and VMNAMES array. We know that 
    # subsequent entries refer to each other, because of the
    # way vcbVmName works.
    while read line
    do
        # see if we got a moref or a uuid...
        prefix="${line%%:*}"
        value="${line#*:}"
        if [ "$prefix" = "moref" ] ; then
            # check if VM is already on list
            check_vm_list "$value"
            check=$?
            # read next line for VM name
            read line
            prefix="${line%%:*}"
            if [ $check -eq $ERR_OK ] ; then
                # VM is already on the list
                if [ "$prefix" = "name" ] ; then
                    value="${line#*:}"
                    echo "Skipping VM (${value}) identified by"\
                    "search criteria (${searchspec}) since it is"\
                    "already in the list of VMs to be backed up."
                fi
            else
                # VM is not on the list
                VMMOREFS[${#VMMOREFS[*]}]="$value"
                if [ "$prefix" = "name" ] ; then
                    value="${line#*:}"
                    VMNAMES[${#VMNAMES[*]}]="$value"
                fi
            fi
            # VM matching search spec is found
            count=count+1
        fi
    done <"${tmpfile}"

    rm -f "${tmpfile}"
    if [ $count -eq 0 ] ; then
        return $ERR_NOVMSFOUND
    fi
    return $ERR_OK
}



#
# Description:
# Get a unique directory name for a VM specified by its Display Name.
# Uses LOGDIR (and the log files accumulating in there) to generate
# a unique name. Also, replace "special characters" in the Display Name
# with underscores.
#
# Writes the unique directory name to stdout.
#
# Arguments:
# $1: Display Name of the VM (used as a basis for the new dir. name)
#
# Result:
# None.
#
function get_vmsubdir_name
{
    local name="$1"
    local newdir
    local count
    declare -i count=1

    # Use the virtual machine's display name as a basis.
    # Cut out any characters from the display name that can cause
    # problems with subsequent backup scripts.
    newdir=`echo "$name" | tr [:blank:]\"\'\\/\* _`

    # Now check if the subdir name is uniqe
    # If not, just make it unique by appending a serial number
    if [ -e "${LOGDIR}/${newdir}-ok" -o \
	 -e "${LOGDIR}/${newdir}-failed" ] ; then
	while [ -e "${LOGDIR}/${newdir}-$count-ok" -o \
                -e "${LOGDIR}/${newdir}-$count-failed" ]
	do
	    count=count+1
	done
	newdir="${newdir}-$count"
    fi
    echo $newdir
}



#
# Description:
# Read a list of MoRefs from stdout and back up all the VMs in the list.
#
# Arguments:
# None.
#
# Result:
# Returns ERR_OK or ERR_NOTALLVMSBACKEDUP if backup failed for at least
# one virtual machine.
#
function backup_vms
{
    local total
    local ok
    local failed
    local i
    local starttime=`date`
    local rv=$ERR_OK
    declare -i total=0 ok=0 failed=0 i=0
    
    if [ -n "$DATASTORE" ] ; then
	DS_ARG="-C \"$DATASTORE\""
    else
	DS_ARG=""
    fi

    # count the number of VMs we are dealing with
    total=${#VMMOREFS[*]}
    echo "Backing up ${total} matching VMs."
    echo "Per VM log files will be in ${LOGDIR}."
    rm -f "$LOGDIR"
    mkdir "$LOGDIR"
    while [ $i -lt $total ]
    do
	echo -n `date`": Exporting VM ${VMNAMES[$i]}..."
	VMSUBDIR=`get_vmsubdir_name "${VMNAMES[$i]}"`
	detaillog="$LOGDIR"/"$VMSUBDIR"
	eval VCB_PASSWORD='"$PASSWORD"' \
	     vcbMounter -h '"$VCHOST"' -u '"$USERNAME"' \
		        -a moref:'${VMMOREFS[$i]}' \
			-r '"$DESTDIR"'/'"$VMSUBDIR"' $DS_ARG \
                        "$UNPARSED_ARGUMENTS" > "${detaillog}-running" 2>&1
	rv=$?
	if [ $rv != $ERR_OK ] ; then
	    mv "${detaillog}"-running "${detaillog}"-failed
	    echo FAILED
	    rv=$ERR_NOTALLVMSBACKEDUP
	    failed=failed+1	    
	else
	    echo SUCCEEDED
	    ok=ok+1
	    mv "${detaillog}"-running "${detaillog}"-ok
	fi
	i=i+1
    done
   
    echo ========================================================
    echo "Backup start time: " $starttime
    echo "Backup end time  : " `date`
    echo
    echo "       Total number of VMs processed: " $total
    echo "Number of VMs backed up successfully: " $ok
    echo "                Number of VMS failed: " $failed
    echo
    echo "For per-VM logs, see directory ${LOGDIR}."
    echo ========================================================
    return $rv
}



#
# Description:
# Check for non-zero return status of a command. If a non-zero return
# status is encountered, clean up temp files, print an error message and
# exit with an appropriate error code.
#
# Arguments:
# $1: Return value to evaluate. Should be one of the ERR_ constants defined
#     in this script
# $2: Search spec.
#
# Result:
# None. Will terminate the shell script in case of the specific error.
#
function error_exit #<errval=$1>
{
    local retval="$1"
    local msg
    local search

    if [ "$retval" != "$ERR_OK" ] ; then
        case "$retval" in
        "$ERR_NOTALLVMSBACKEDUP")
            msg="Backup did not succeed for all virtual machines"
            ;;
        "$ERR_GETVMLIST")
            msg="Could not retrieve a list of virtual machines to back up"
            ;;
        "$ERR_NOVMSFOUND")
            search="$2"
            msg="No virtual machines matching the specified search criteria"
            msg="$msg ($search) were found"
            ;;
        "$ERR_USAGE")
            msg="Script was invoked with illegal or missing arguments"
           ;;
        "$ERR_EMPTYLIST")
            msg="No virtual machines were found"
            ;;
        *)
            msg="Unspecified error during backup. Please refer to logs"
           ;;
        esac
        echo "${msg}." >&2
        if [ "$retval" != "$ERR_NOVMSFOUND" ] ; then
            exit $retval
        fi
    fi
}



#
# Description:
# Collects a list of all virtual machines matching any of specified
# search criteria.
#
# Arguments
# $1: Backup tool name, vcbSnapAll.
# $2 -> $n: Command line argument to evaluate
#
# Result:
# None. Add VMs found for each SEARCHSPEC to VMMOREFS and VMNAMES.
#
function find_vms
{
    local arg
    local match
    local rv=$ERR_OK

    OPTIND=1

    # first thing - verify that mount directory exists. Error otherwise.
    # This check is valid only for local DESTDIR. If DESTDIR is remote -
    # means -r scp://, we shoud skip this check
    echo "$DESTDIR"  | grep -q '://'
    rv=$?

    if [ "$rv" != "0" ]; then
	if [ ! -d "$DESTDIR" ]; then
	    echo "Error: destination directory "$DESTDIR" does not exist" >&2
	    return $ERR_GETVMLIST
	fi
    fi

    while getopts :a: arg ; do
        echo "a:" | grep -q "$arg"
        match=$?
        if [ "$match" = "0" ] ; then
            eval SEARCHSPEC=\$OPTARG
            create_vm_list "$SEARCHSPEC"
            rv=$?
            error_exit $rv "$SEARCHSPEC"
        else
            # skip to the next argument
            shift
        fi
    done
   
    if [ ${#VMMOREFS[*]} -eq 0 ] ; then
        return $ERR_EMPTYLIST
    fi

    return $ERR_OK
}


# We intercept some parameters here, even tho we only modify
# the search specifier and the dest. dir. Doing so will allow
# us to print more useful error messages.
intercept_arguments "a:r:h:u:p:" "$@"
if [ -n "${INTERCEPT_h}" ] ; then 
    VCHOST="${INTERCEPT_h}"
fi
if [ -n "${INTERCEPT_u}" ] ; then 
    USERNAME="${INTERCEPT_u}"
fi
DESTDIR="${INTERCEPT_r}"
SEARCHSPEC="${INTERCEPT_a}"
init
rv=$?
error_exit $rv

find_vms "$@"
rv=$?
error_exit $rv

backup_vms
rv=$?
exit $rv
