#!/bin/bash
# This script manages xenguest
#
set -u
this="$0"

XENGUEST_CONF_BASE="/etc/xenguest"
LOGFILE="/var/log/xenguest"

# Valid values for log level
LOG_LEVEL_VALID="FATAL ERROR INFO VERBOSE"

# Log levels being written to logfile
LOG_LEVEL_LIST="ERROR INFO VERBOSE"
# Affected by -v(v) param and conf file

# Log levels being written to terminal
VERBOSE_LOG_LEVEL="ERROR"
# Constant

# Highest Log Level: Default is ERROR only
LOG_LEVEL="ERROR"
# Used to update LOG_LEVEL_LIST


# This should only be called from either log() or log_command.
# It expectd $loglevel and $text to already be in scope
function log_to_file ()
{
    if [[ ${LOG_LEVEL_LIST} = *${loglevel}* ]]; then
        tstamp="$(date +"%d-%m-%Y %T")"
        tag="[${loglevel}]"

        printf "%s %-9s %s\n" "$tstamp" "$tag" "$text" >> ${LOGFILE}
    fi
}

# Write a log to the logfile, and to the console
# Messages are written to the log with the date and a timestamp
function log ()
{
    # Inputs:
    # $1 - optional level to log at, one of ${LOG_LEVEL_VALID}
    #      Default: INFO
    # $@ - log message body

    # get loglevel from parameter and capitalise
    loglevel=${1^^}

    # If no loglevel is passed use INFO
    if [[ ${LOG_LEVEL_VALID} = *${loglevel:-INVALID}* ]]; then
        shift
    else
        loglevel="INFO"
    fi

    # Kill script immediately after a fatal log
    killscript=0
    if [ "FATAL" = ${loglevel} ]; then
        killscript=1
        # Log at error level for the user
        loglevel="ERROR"
    fi

    text="$*"
    log_to_file

    # Write to terminal if level is high enough
    if [[ ${VERBOSE_LOG_LEVEL} = *${loglevel}* ]]; then
        echo "${loglevel}: ${text}"
    fi

    # if Log was fatal, kill the script
    if [[ ${killscript} = 1 ]]; then
        exit 1
    fi
}

# Write a shell command to the log and execute it
# The stdout and stderr output of the command is captured in a variable,
# and written to the logfile in two cases:
#  1. The script is in verbose mode
#  2. The command returns a non-zero status AND
#     The loglevel parameter (default: ERROR) is in $LOG_LEVEL_LIST
#
# This means by default a non-zero status results in a log tagged [ERROR],
# but if a command is expected to fail, the tag can be reduced for visual
# clarity
log_command ()
{
    # Inputs:
    # $1 - optional level to write errors at, one of ${LOG_LEVEL_VALID}
    #      Default: ERROR
    # $@ - command to execute

    # get loglevel from parameter and capitalise
    loglevel=${1^^}

    # If no level passed, log output on failure at ERROR
    if [[ ${LOG_LEVEL_VALID} = *"${loglevel:-INVALID}"* ]]; then
        shift
    else
        loglevel="ERROR"
    fi

    # Commands cannot be logged at FATAL.
    if [ "FATAL" = ${loglevel} ]; then
        loglevel="ERROR"
    fi
    local command="$*"
    local output=""
    local status=0

    # Capture stdout and sterr to write to logfile
    output=$(eval "${command} 2>&1")
    status=$?
    # If command failed, or verbose mode, write log
    if [[ ${status} -ne 0 ]] || [[ ${LOG_LEVEL_LIST} = *VERBOSE* ]]; then

        # if command didn't fail write it at verbose level
        if [[ ${status} -eq 0 ]]; then
            loglevel="VERBOSE"
        fi
        # otherwise write it at ${loglevel} from arguments

        local append_to="/dev/null"
        # If we are writing ${loglevel} logs to file, use file as append_to
        if [[ ${LOG_LEVEL_LIST} = *${loglevel}* ]]; then
            append_to=${LOGFILE}
        fi

        # Log that command was called
        text="> ${command}"
        log_to_file

        # Write command output to logfile or /dev/null, indent to match rest of logs
        if [[ -n ${output} ]]; then
            echo "${output}" | sed 's/^/                              /' >> ${append_to}
        fi
        # Log exit status
        text="< Exited with status ${status}"
        log_to_file
    fi
    # Ensure return status is captured
    return $status
}

# Sources a shell script and logs it
log_source ()
{
    local script=${1}
    log verbose "> source ${script}"

    ( . ${script} )

    status=$?
    log verbose "< Exited with status ${status}"

    return $status
}

if [ ! -f ${XENGUEST_CONF_BASE}/xenguest-manager.conf ]; then
    log fatal "Cannot find xenguest manager configuration"
fi

# Following variables must be set in configuration:
# XENGUEST_VOLUME_DEVICE: device to use for lvm
# XENGUEST_VOLUME_NAME: lvm volume name to create on device
# Optionally set:
# XENGUEST_LOG_LEVEL: the loglevel for terminal and logfile
source ${XENGUEST_CONF_BASE}/xenguest-manager.conf

# Check that VERBOSE level from config file is valid
if [[ ${LOG_LEVEL_LIST} = *${XENGUEST_LOG_LEVEL}* ]]; then
    LOG_LEVEL=${XENGUEST_LOG_LEVEL}
else
    log error "Invalid log level '${XENGUEST_LOG_LEVEL}' found in xenguest-manager.conf"
fi

function usage() {
    cat <<EOF
Usage $this [-v(v)] ACTION [OPTIONS]

with ACTION being one of:
 help
   Display this help

 create GUESTFILE [GUESTNAME]
   Create a guest using xenguest image GUESTFILE and name it GUESTNAME.
   This will extract and configure the guest and will also create the guest
   disk if guest has one configured.
   GUESTNAME is set to the basename of GUESTFILE if unspecified.
   GUESTNAME guest must not exist

 remove GUESTNAME
   Remove GUESTNAME and destroy its disk (if it has one)

 start GUESTNAME
   Start guest GUESTNAME

 stop|shutdown GUESTNAME
   Stop guest GUESTNAME (send stop signal and let it shutdown normally)
   Pass 'stop|shutdown GUESTNAME --nowait' to return immediately, rather
   than waiting for success or failure to return.
   Pass 'stop|shutdown GUESTNAME --kill' to force kill the guest if
   signalling the graceful shutdown fails for any reason

   These two parameters are incompatible, so only one should be passed

 kill|destroy GUESTNAME
   Kill guest GUESTNAME (stop directly the guest without signaling it)

 list
   List configured guests

 status
   List guests and their current status (running or stopped)

Passing -v will enable INFO logs, and -vv will enable VERBOSE and INFO logs.
Both increase what is written to ${LOGFILE},
rather than the terminal.
EOF
}

# Ensure init scripts in subshells do not call private functions
function check_private()
{

    # Return:
    # 0 - success
    # 1 - failure

    if [ $BASH_SUBSHELL -ne 0 ]; then
        log fatal "Attempted to execute private function '${FUNCNAME[1]}()' in a subshell!"
    fi
}

# Public
is_integer() {

    if ! [[ "${1}" =~ ^[0-9]+$ ]]; then
        log fatal "invalid number '${1}'"
    fi
}

# Public
# check size and convert it to MB, e.g '1[G]' => '1000M'
check_size() {
    local disksize="${1}"

    [ -n "${disksize}" ] || disksize="invalid"

    # disksize may have appended M or G suffix, let's extract it
    # ${var:offset:length}, where #var is var length
    local lastchar="${disksize:${#disksize}-1}"
    case "${lastchar}" in
        [0-9])
            # backwards compatibility
            is_integer "${disksize}"
            echo -e "$((${disksize} * 1000))M"
            return
            ;;
        G|M)
            if [ "${#disksize}" -gt "1" ]; then
                local size="${disksize::${#disksize}-1}"
                is_integer "${size}"
                # convert GB to MB
                [ "${lastchar}" = "M" ] || size=$((${size} * 1000))
                echo -e "${size}M"
                return
            fi
            ;;
        *)
            ;;
    esac

    log fatal "Invalid size format '${1}'. Supported size format is e.g 1000M or 2[G]"
}

# Private
function xenguest_volume_init()
{
    # Inputs:
    # $1 - diskdevice
    # $2 - volumename
    #
    # Outputs:
    # 0 - success
    # 1 - failure

    local diskdevice
    local volumename

    diskdevice="${1}"
    volumename="${2}"

    log info "Attempting to initialise xenguest volume '${volumename}'"

    check_private

    if [ -z "${diskdevice}" -o ! -b "${diskdevice}" ]; then
        log error "Invalid volume device in configuration: '${diskdevice}'"
        return 1
    fi

    if [ -z "${volumename}" ]; then
        log error "Invalid volume name in configuration: '${volumename}'"
        return 1
    fi

    log_command verbose "pvs ${diskdevice}"
    if [ $? -ne 0 ]; then
        # Check if there is no filesystem in the block device
        log verbose "Checking for existing filesystem"
        filesystem=$(lsblk -n -o FSTYPE ${diskdevice})
        if [[ $? -eq 0 && -z "$filesystem" ]]; then
            log verbose "No filesystem found"
            log info "Initializing lvm on ${diskdevice}"
            log_command "pvcreate -f ${diskdevice}"
            if [ $? -ne 0 ]; then
                log error "Initialing lvm on ${diskdevice} failed."
                return 1
            fi
        else
            [ -z "$filesystem" ] || \
                log error "${diskdevice} is already formatted as $filesystem."
            return 1
        fi
    fi

    log_command verbose "vgs ${volumename}"
    if [ $? -ne 0 ]; then
        log info "Creating ${volumename} volume"
        log_command "vgcreate ${volumename} ${diskdevice}"
        if [ $? -ne 0 ]; then
            log error "Creating ${volumename} volume failed."
            return 1
        fi
    fi

    log info "xenguest volume '${volumename}' initialised successfully"

    return 0
}

# Private
# Detach a disk we attached to xen
function xenguest_detach_disk()
{
    log verbose "Attempting to detach partition '${part}'"

    check_private

    log_command "xl block-detach 0 \$(xl block-list 0 | grep \"domain/0\" | awk '{print \$1}')"
    if [ $? -ne 0 ]; then
        log error "Detaching partition '${part}' failed."
        return 1
    fi

    log verbose "Partition '${part}' detached successfully"
}

#Private
function xenguest_volume_remove()
{
    # Inputs:
    # $1 - volumename
    # $2 - guestname

    local volumename
    local guestname

    volumename="${1}"
    guestname="${2}"

    devname="/dev/${volumename}/${guestname}"

    # Remove volume if it exists
    log verbose "Checking for volume ${devname}"
    log_command verbose "lvs ${volumename}/${guestname}"
    if [ $? -eq 0 ]; then
        log info "Removing volume ${devname}"
        log_command "lvremove -y ${devname}"
        if [ $? -ne 0 ]; then
            log error "Removing volume ${devname} failed."
            return 1
        else
            log verbose "Volume ${devname} removed successfully"
            return 0
        fi
    fi

    log verbose "Volume ${devname} not found"
}

# Private
function xenguest_disk_init()
{
    # Inputs:
    # $1 - guestname
    # $2 - guestfile
    #
    # Outputs:
    # 0 - success
    # 1 - failed at guest disk preparation
    # 2 - failed at guest disk creation

    guestname="$1"
    guestfile="$2"

    log info "Attempting to initialise disk for guest '${guestname}'"

    check_private

    source ${XENGUEST_CONF_BASE}/guests/${guestname}/disk.cfg
    if [ -z "${DISK_DEVICE}" ]; then
        log info "Using disk device and volume name from xenguest-manager.conf"
        # By default guest is using disk defined inside xenguest-manager.conf
        diskdevice="${XENGUEST_VOLUME_DEVICE}"
        volumename="${XENGUEST_VOLUME_NAME}"
    else
        log info "Using disk device set in disk.cfg"
        # If guest configuration contains custom disk setting,
        # overwrite default one
        diskdevice="${DISK_DEVICE}"
        volumename="vg-xen-$(basename ${diskdevice})"
    fi

    log verbose "Disk Device = ${diskdevice}"
    log verbose "Volume Name = ${volumename}"

    devname="/dev/${volumename}/${guestname}"

    DISK_SIZE=$(check_size "${DISK_SIZE}")
    if [ -z "${DISK_SIZE}" ] || [ "${DISK_SIZE}" = "0M" ]; then
        log info "No disk for ${guestname}"
        return
    fi

    # Init our volume
    xenguest_volume_init "${diskdevice}" "${volumename}"
    if [ $? -ne 0 ]; then
        return 1
    fi

    log info "Creating hard drive for guest '${guestname}'. This might take a while..."

    # Remove volume if it already exist
    xenguest_volume_remove ${volumename} ${guestname}
    if [ $? -ne 0 ]; then
        return 1
    fi

    # Create volume
    log info "Creating volume '${volumename}/${guestname}'"
    log_command "lvcreate -y -L ${DISK_SIZE} -n ${guestname} ${volumename}"
    if [ $? -ne 0 ]; then
        log error "Creating volume '${volumename}/${guestname}' failed."
        return 1
    fi

    # Add partition table
    log verbose "Creating partition table on ${devname}"
    log_command "parted -s \"${devname}\" mklabel msdos"
    if [ $? -ne 0 ]; then
        log error "Creating partition table on ${devname} failed."
        return 1
    fi

    # Setup disk name in xen configuration
    log verbose "Setting disk name in xen configuration"
    log_command "xenguest-mkimage update \"${XENGUEST_CONF_BASE}/guests/${guestname}\" --xen-disk=\"${devname}\""
    if [ $? -ne 0 ]; then
        log error "Setting disk name in xen configuration failed."
        return 1
    fi

    # Create partitions
    partstart="0"

    # For each partition X the disk.cfg file should set a variable DISK_PARTX
    # with a : separated list defining the partition:
    # DISK_PART3="4:ext4:disk.tgz" means that partition 3 should be 4G formated
    # with ext4 and initialized with the content of disk.tgz
    # Keep user defined partition order,
    # even if previous partitions are not defined.
    # Create 2MB partitions in this case
    lastpart="0"
    for partidx in $(seq 1 4); do
        local _part="DISK_PART${partidx}"
        if [ -n "${!_part:=}" ]; then
            lastpart="${partidx}"
        fi
    done

    if [ "${lastpart}" -eq "0" ]; then
        # Nothing to be added here
        # No partition definition found
        return
    fi

    for part in $(seq 1 "${lastpart}"); do
        eval partdesc="\${DISK_PART${part}:=}"
        size=$(echo ${partdesc} | sed -e "s/\(.*\):.*:.*/\1/")
        fstype=$(echo ${partdesc} | sed -e "s/.*:\(.*\):.*/\1/")
        content=$(echo ${partdesc} | sed -e "s/.*:.*:\(.*\)/\1/")

        local _part="DISK_PART${part}"
        [ -n "${!_part:=}" ] || size="2M"

        size=$(check_size "${size}")
        if [ -n "${size}" ] && [ "${size}" != "0M" ]; then
            # size has appended M or G suffix, let's extract just the value
            # ${var:offset:length}, where #var is var length
            size="${size::${#size}-1}"
            partend=$(expr ${partstart} + ${size})

            # Let first MB of disk free for partition table
            if [ ${partstart} -eq 0 ]; then
                partstart="1"
            fi

            # Create partition
            log verbose "Adding partition ${part}"
            log_command "parted -s \"${devname}\" unit MB mkpart primary \"${partstart}\" \"${partend}\""
            if [ $? -ne 0 ]; then
                log error "Adding partition ${part} failed."
                return 1
            fi

            # Set next partition start to current partition end
            partstart="${partend}"

            # Sync to see the created partition
            log verbose "Sync created partition"
            log_command "sync"

            # Prepare format command
            if [ -n "${fstype}" ]; then
                case ${fstype} in
                    vfat|ext2|ext3|ext4)
                        formatcmd="mkfs.${fstype} -F"
                        ;;
                    swap)
                        formatcmd="mkswap"
                        ;;
                    *)
                        log error "Partition ${part} of ${guestname} fstype is invalid '${fstype}'"
                        return 1
                        ;;
                esac
            else
                formatcmd=""
            fi

            # Attach disk to xen
            log verbose "Attaching partition ${part}"
            log_command "xl block-attach 0 \"phy:${devname}\" xvda w"
            if [ $? -ne 0 ]; then
                log error "Attaching partition ${part} failed."
                return 1
            fi


            # Loop for 20s to wait until /dev/xvdaX appears
            i=0
            while [ ! -b /dev/xvda${part} ]; do
                ((i++))
                if [[ "$i" == '40' ]]; then
                    break;
                fi
                sleep 0.5
            done

            if [ ! -b /dev/xvda${part} ]; then
                log error "Partition ${part} creation failed."
                return 2
            fi

            log verbose "/dev/xvda${part} created"

            if [ -n "${formatcmd}" ]; then
                log info "Creating filesystem for partition '${part}'"
                log_command "${formatcmd} /dev/xvda${part}"
                if [ $? -ne 0 ]; then
                    log error "Creating filesystem for partition '${part}' failed."
                    return 2
                fi
            fi

            case ${content} in
                *.img*)
                    decompress=""
                    case ${content} in
                        *.img.gz)
                            decompress='zcat'
                            ;;
                        *.img.bz2)
                            decompress='bzcat'
                            ;;
                        *.img)
                            decompress='cat'
                            ;;
                        *)
                            # invalid/unknown compression type
                            log error "Invalid file format in disk ${content}"
                            return 2
                            ;;
                    esac
                    # dd into partition
                    log verbose "Populating partition '${part}'"
                    log_command "xenguest-mkimage extract-disk-file ${guestfile} ${content} | ${decompress} | dd of=/dev/xvda${part} "
                    if [ $? -ne 0 ]; then
                        log error "Populating partition '${part}' failed."
                        return 2
                    fi
                    ;;
                *.tar*)
                    tararg=""
                    case ${content} in
                        *.tar.gz)
                            tararg="z"
                            ;;
                        *.tar.bz2)
                            tararg="j"
                            ;;
                        *.tar.xz)
                            tararg="J"
                            ;;
                        *.tar)
                            tararg=""
                            ;;
                        *)
                            # invalid/unknown tar type
                            log error "Invalid file format in disk ${content}"
                            return 2
                            ;;
                    esac

                    # must mount the partition and extract
                    mntdir=$(mktemp -d)
                    log verbose "Mounting partition '${part}'"
                    log_command "mount /dev/xvda${part} ${mntdir}"
                    if [ $? -ne 0 ]; then
                        log error "Mounting partition '${part}' failed."
                        rm -rf ${mntdir}
                        return 2
                    fi

                    # tar and unmount
                    log_command "xenguest-mkimage extract-disk-file ${guestfile} ${content} |" \
                    "tar -C ${mntdir} -x${tararg}f - "
                    if [ $? -ne 0 ]; then
                        log error "Cannot populate partition ${part}"
                        umount ${mntdir}
                        rm -rf ${mntdir}
                        return 2
                    fi
                    log_command "umount ${mntdir}"
                    if [ $? -ne 0 ]; then
                        log error "Unmounting ${part} failed."
                        rm -rf ${mntdir}
                        return 2
                    fi
                    rm -rf ${mntdir}
                    ;;
                *)
                    #invalid content type
                    ;;
            esac

            # Detach disk
            xenguest_detach_disk
            if [ $? -ne 0 ]; then
                return 1
            fi
        fi
    done

    log info "Initialised disk for guest '${guestname}' successfully"

}

# Private
function xenguest_guest_create()
{
    # extract xenguest tar
    # put xen config in etc ?
    # if disk config file:
    #  disk init
    #  add partititions

    guestfile="$1"
    guestname="$2"

    log info "Attempting to create guest '${guestname}' using ${guestfile}"

    check_private

    log verbose "Cleaning up old directory"
    log_command verbose "rm -rf ${XENGUEST_CONF_BASE}/guests/${guestname}"
    log verbose "Creating directory for guest '${guestname}'"
    log_command "mkdir -p ${XENGUEST_CONF_BASE}/guests/${guestname}"

    log verbose "Extracting guest image"
    log_command "xenguest-mkimage extract-config ${guestfile} ${XENGUEST_CONF_BASE}/guests/${guestname}"
    if [ $? -ne 0 ]; then
        log fatal "Extracting guest image failed."
    fi

    # Set guest name inside config
    log verbose "Setting guest name"
    log_command "xenguest-mkimage update ${XENGUEST_CONF_BASE}/guests/${guestname} --xen-name=${guestname}"
    if [ $? -ne 0 ]; then
        log error "Setting guest name failed."
        xenguest_guest_remove ${guestname}
        exit 1
    fi

    xenguest_disk_init ${guestname} ${guestfile}
    disk_init_status=$?
    if [ $disk_init_status -ne 0 ]; then
        log error "Disk creation for guest '${guestname}' failed."
        if [ $disk_init_status -eq 2 ]; then
            xenguest_detach_disk
        fi
        xenguest_guest_remove ${guestname}
        exit 1
    fi

    log info "Guest '${guestname}' created successfully"

}

# Private
function xenguest_guest_remove()
{
    guestname="$1"
    log info "Attempting to remove guest '${guestname}'"

    check_private

    source ${XENGUEST_CONF_BASE}/guests/${guestname}/disk.cfg
    if [ -z "${DISK_DEVICE}" ]; then
        # By default guest is using disk defined inside xenguest-manager.conf
        diskdevice="${XENGUEST_VOLUME_DEVICE}"
        volumename="${XENGUEST_VOLUME_NAME}"
    else
        # If guest configuration contains custom disk setting,
        # overwrite default one
        diskdevice="${DISK_DEVICE}"
        volumename="vg-xen-$(basename ${diskdevice})"
    fi
    devname="/dev/${volumename}/${guestname}"

    # find and remove guest volume
    xenguest_volume_remove ${volumename} ${guestname}
    status=$?

    # remove guest files
    log info "Removing configuration files for guest '${guestname}'."
    log_command "rm -rf ${XENGUEST_CONF_BASE}/guests/${guestname}"

    if [ ${status} -ne 0 ]; then
        # Shouldn't log success message if volume removal fails
        exit 1
    fi

    log info "Removed guest '${guestname}' successfully"
}

# Private
function xenguest_call_inits()
{
    # Inputs:
    # $1 - script directory

    local scriptdir
    local guestdir
    local guestcfgfile
    local guestname

    scriptdir="${1}"
    guestdir="${2}"
    guestcfgfile="${3}"
    guestname="${4}"

    log "Attempting to call all init scripts in ${scriptdir}"

    check_private


    init_scripts="$(find ${XENGUEST_CONF_BASE}/${scriptdir} -type f 2> /dev/null | \
            sort) $(find ${guestdir}/${scriptdir} -type f 2> /dev/null | sort)"
    for f in ${init_scripts}; do
        if [ -x "$f" ]; then
            log_source $f
            if [ $? -ne 0 ]; then
                rm -f ${guestcfgfile}
                popd > /dev/null 2>&1
                log fatal "Error during init script $(basename $f) of ${guestname}"
            fi
        else
            log fatal "$f is not executable. Exiting..."
        fi
    done

    if [ "${init_scripts}" = " " ]; then
        log "No scripts found"
    else
        log "All init scripts in ${scriptdir} completed successfully"
    fi
}

# Private
function xenguest_guest_start()
{
    guestname="${1}"
    guestdir=${XENGUEST_CONF_BASE}/guests/${guestname}

    log info "Attempting to start guest '${guestname}'"

    check_private

    guestcfgfile=$(mktemp -u "${guestname}.XXXXXX" --tmpdir="${guestdir}" --suffix=".cfg")

    # Get guest configuration
    source ${guestdir}/params.cfg

    pushd ${guestdir} > /dev/null 2>&1

    # create config by merging all configurations together
    cat guest.cfg $(find guest.d -type f 2> /dev/null) > ${guestcfgfile}

    # Build init script lists (ignore non existing dirs errors,
    # sort alphabetically and run global scripts first)
    #
    # These scripts are sourced throughout the start operation if they
    # are executable
    init_pre="init.pre"
    init_d="init.d"
    init_post="init.post"

    # call pre init scripts
    xenguest_call_inits "${init_pre}" "${guestdir}" "${guestcfgfile}" "${guestname}"

    # Create non started guest
    log verbose "Initiating ${guestname}"
    log_command "xl create -p ${guestcfgfile}"
    if [ $? -ne 0 ]; then
        rm -f ${guestcfgfile}
        popd > /dev/null 2>&1
        log fatal "Initiating ${guestname} failed."
    fi

    # call init scripts
    xenguest_call_inits "${init_d}" "${guestdir}" "${guestcfgfile}" "${guestname}"

    # Start guest
    log info "Starting ${guestname}"
    log_command "xl unpause ${guestname}"
    if [ $? -ne 0 ]; then
        rm -f ${guestcfgfile}
        popd > /dev/null 2>&1
        log fatal "Starting ${guestname} failed."
    fi

    # call post init scripts
    xenguest_call_inits "${init_post}" "${guestdir}" "${guestcfgfile}" "${guestname}"

    rm -f ${guestcfgfile}
    popd > /dev/null 2>&1

    log info "Guest '${guestname}' started successfully"
}

# Private
function xenguest_guest_stop()
{
    local guestname
    local extra_arg

    guestname="${1}"
    extra_arg="${2}"

    shutdown_args=""

    log info "Attempting to stop guest '${guestname}'"

    if [[ ${extra_arg} != "--nowait" ]]; then
        shutdown_args+=" -w"
    fi

    check_private

    log_command "xl shutdown ${shutdown_args} ${guestname}"
    if [ $? -ne 0 ]; then
        if [[ ${extra_arg} == "--kill" ]]; then
            log info "Stopping '${guestname}' failed, calling kill..."
            xenguest_guest_kill "${guestname}"
        else
            log fatal "Stopping guest '${guestname}' failed."
        fi
    fi
    if [[ "${extra_arg}" != "--nowait" ]]; then
        log info "Guest '${guestname}' stopped successfully"
    else
        log info "xl shutdown exited successfully for guest '${guestname}'."
    fi
}

# Private
function xenguest_guest_kill()
{
    local guestname

    guestname="${1}"
    log "Attempting to kill guest '${guestname}'"

    check_private

    log_command "xl destroy ${guestname}"
    if [ $? -ne 0 ]; then
        log "fatal:Killing guest '${guestname}' failed."
    fi
    log "Guest '${guestname}' killed successfully"
}

# Private
function check_guest_arg()
{
    check_private

    cmd="${1}"
    guestname="${2:-}"
    if [ -z "${guestname:-}" ]; then
        log fatal "Usage ${this} ${cmd} GUESTNAME"
    fi
}

# Public
function check_guest_exist()
{
    guestname="${1}"
    if [ ! -f ${XENGUEST_CONF_BASE}/guests/${guestname}/guest.cfg -o \
        ! -f ${XENGUEST_CONF_BASE}/guests/${guestname}/params.cfg ]; then
        log fatal "Invalid guest name '${guestname}'"
    fi

    log verbose "Guest '${guestname}' found: ${XENGUEST_CONF_BASE}/guests/${guestname}/"
}

# Public
function xenguest_list_guests()
{
    guestlist=""
    if [ -d ${XENGUEST_CONF_BASE}/guests ]; then
        guestlist=$(find ${XENGUEST_CONF_BASE}/guests -mindepth 1 -maxdepth 1 -type d -exec sh -c 'if [ -f {}/guest.cfg ]; then basename {}; fi' \;)
    else
        log "Info: Guests directory ${XENGUEST_CONF_BASE}/guests not found"
    fi
}

# Public
function xl_list_contains()
{
    guestname="${1}"
    # Select first column of xl list, and find guestname exactly using regex
    running=$(xl list | awk 'NR > 1 {print $1}' | grep "^${guestname}$" || echo)
    if [ "${running}" = "${guestname}" ]; then
        log verbose "Guest '${guestname}' is running"
        return 0
    fi

    log verbose "Guest '${guestname}' is not running"

    return 1
}

# Public
function check_guest_running()
{
    guestname="${1}"
    if ! xl_list_contains $guestname; then
        log fatal "Cannot ${cmd} guest '${guestname}', already stopped"
    fi
}

# Public
function check_guest_not_running()
{
    guestname="${1}"
    if xl_list_contains $guestname; then
        log fatal "Cannot ${cmd} guest '${guestname}', already started"
    fi
}

## Entry Point ##

# Check for verbose level arguments, and shift if found
case ${1:-help} in
    -v|-V)
    LOG_LEVEL="INFO"
    shift
    ;;
    -vv|-VV)
    LOG_LEVEL="VERBOSE"
    shift
    ;;
esac

# Limit Verbose list to only those desired to be shown
LOG_LEVEL_LIST=${LOG_LEVEL_LIST//${LOG_LEVEL}*/${LOG_LEVEL}}

log ""
log "Arguments: $*"

cmd="${1:-help}"
arg1="${2:-}"
arg2="${3:-}"

case ${cmd} in
    help|--help|-h|-?)
        usage
        exit 0
        ;;
esac

# Check if we have a valid Dom0 booted with Xen
log_command "xl info"
if [ $? -ne 0 ]; then
    log error "Xen environment is not valid!!!"
    log error "Check if Xen has booted and the kernel configuration."
    log fatal "More information in the logfile: ${LOGFILE}"
fi

case ${cmd} in
    check-xen)
        log verbose "Valid Xen environment found"
        exit 0
        ;;
    create)
        guestfile="${arg1}"
        guestname="${arg2}"
        # guestfile invalid if empty
        if [ -z "${guestfile}" ]; then
            log fatal "Usage ${this} create XENGUEST_FILE [NAME]"
        fi
        # Set Guest name before resolving any symlinks
        if [ -z "${guestname}" ]; then
            guestname=$(basename ${guestfile} .xenguest)
            log info "guestname argument not provided, using '${guestname}'"
        fi
        # Check for and resolve symlink
        if [ -L "${guestfile}" ]; then
            errmsg="'${guestfile}' is a broken symlink"
            guestfile=$(readlink -e "${guestfile}")
            if [ -z "${guestfile}" ]; then
                log fatal "${errmsg}"
            else
                log info "Guestfile symlink resolved to path '${guestfile}'"
            fi
        fi
        # Check that guestfile is a valid file
        if [ ! -f "${guestfile}" ]; then
            log fatal "File '${guestfile}' not found"
        fi
        # Check if guest already exists
        if [ -f ${XENGUEST_CONF_BASE}/guests/${guestname}/guest.cfg ]; then
            log fatal "Guest '${guestname}' already exists"
        fi

        xenguest_guest_create ${guestfile} ${guestname}
        ;;
    remove)
        guestname="${arg1:-}"
        check_guest_arg ${cmd} ${guestname}
        check_guest_exist ${guestname}
        # We need to stop the guest first if it is running
        if xl_list_contains $guestname; then
            xenguest_guest_kill ${guestname}
        fi
        xenguest_guest_remove ${guestname}
        ;;
    start)
        guestname="${arg1:-}"
        check_guest_arg ${cmd} ${guestname}
        check_guest_exist ${guestname}
        check_guest_not_running ${guestname}
        xenguest_guest_start ${guestname}
        ;;
    stop|shutdown)
        guestname="${arg1:-}"
        extra_arg="${arg2:-}"
        check_guest_arg ${cmd} ${guestname}
        check_guest_exist ${guestname}
        check_guest_running ${guestname}
        xenguest_guest_stop "${guestname}" "${extra_arg}"
        ;;
    kill|destroy)
        guestname="${arg1:-}"
        check_guest_arg ${cmd} ${guestname}
        check_guest_exist ${guestname}
        check_guest_running ${guestname}
        xenguest_guest_kill ${guestname}
        ;;
    list)
        xenguest_list_guests
        echo ${guestlist} | tr " " "\n"
        ;;
    status)

        single_status() {
            guestname="${1}"
            check_guest_exist ${guestname}
            if xl_list_contains $guestname; then
                echo "${guestname} Running"
            else
                echo "${guestname} Stopped"
            fi
        }

        guestname="${arg1}"
        if [ -n "${guestname}" ]; then
            single_status ${guestname}
        else
            xenguest_list_guests
            if [ -n "${guestlist}" ]; then
                for f in ${guestlist}; do
                    single_status $f
                done
            fi
        fi
        ;;
    *)
        log fatal "Invalid argument: ${cmd}"
        ;;
esac

