#!/bin/sh

# Copyright (C) 2011-2013 Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.  If not, see
# <http://www.gnu.org/licenses/>.

sysconfdir="@sysconfdir@"
localstatedir="@localstatedir@"
libvirtd="@sbindir@"/libvirtd

# Source function library.
test ! -r "$sysconfdir"/rc.d/init.d/functions ||
    . "$sysconfdir"/rc.d/init.d/functions

# Source gettext library.
# Make sure this file is recognized as having translations: _("dummy")
. "@bindir@"/gettext.sh

export TEXTDOMAIN="@PACKAGE@" TEXTDOMAINDIR="@localedir@"

URIS=default
ON_BOOT=start
ON_SHUTDOWN=suspend
SHUTDOWN_TIMEOUT=300
PARALLEL_SHUTDOWN=0
START_DELAY=0
BYPASS_CACHE=0
CONNECT_RETRIES=10
RETRIES_SLEEP=1

test -f "$sysconfdir"/sysconfig/libvirt-guests &&
    . "$sysconfdir"/sysconfig/libvirt-guests

LISTFILE="$localstatedir"/lib/libvirt/libvirt-guests
VAR_SUBSYS_LIBVIRT_GUESTS="$localstatedir"/lock/subsys/libvirt-guests

RETVAL=0

# retval COMMAND ARGUMENTS...
# run command with arguments and convert non-zero return value to 1 and set
# the global return variable
retval() {
    "$@"
    if [ $? -ne 0 ]; then
        RETVAL=1
        return 1
    else
        return 0
    fi
}

# run_virsh URI ARGUMENTS...
# start virsh and let it execute ARGUMENTS on URI
# If URI is "default" virsh is called without the "-c" argument
# (using libvirt's default connection)
run_virsh() {
    uri=$1
    shift

    if [ "x$uri" = xdefault ]; then
        virsh "$@" </dev/null
    else
        virsh -c "$uri" "$@" </dev/null
    fi
}

# run_virsh_c URI ARGUMENTS
# Same as "run_virsh" but the "C" locale is used instead of
# the system's locale.
run_virsh_c() {
    ( export LC_ALL=C; run_virsh "$@" )
}

# test_connect URI
# check if URI is reachable
test_connect()
{
    uri=$1

    for ((i = 0; i < ${CONNECT_RETRIES}; i++)); do
        run_virsh "$uri" connect 2>/dev/null
        if [ $? -eq 0 ]; then
            return 0;
        fi
        sleep ${RETRIES_SLEEP}
        eval_gettext "Unable to connect to libvirt currently. Retrying .. \$i"
    done
    eval_gettext "Can't connect to \$uri. Skipping."
    echo
    return 1
}

# list_guests URI PERSISTENT
# List running guests on URI.
# PERSISTENT argument options:
# --persistent: list only persistent guests
# --transient: list only transient guests
# [none]: list both persistent and transient guests
list_guests() {
    uri=$1
    persistent=$2

    list=$(run_virsh_c "$uri" list --uuid $persistent)
    if [ $? -ne 0 ]; then
        RETVAL=1
        return 1
    fi

    echo $list
}

# guest_name URI UUID
# return name of guest UUID on URI
guest_name() {
    uri=$1
    uuid=$2

    run_virsh "$uri" domname "$uuid" 2>/dev/null
}

# guest_is_on URI UUID
# check if guest UUID on URI is running
# Result is returned by variable "guest_running"
guest_is_on() {
    uri=$1
    uuid=$2

    guest_running=false
    id=$(run_virsh "$uri" domid "$uuid")
    if [ $? -ne 0 ]; then
        RETVAL=1
        return 1
    fi

    [ -n "$id" ] && [ "x$id" != x- ] && guest_running=true
    return 0
}

# started
# Create the startup lock file
started() {
    touch "$VAR_SUBSYS_LIBVIRT_GUESTS"
}

# start
# Start or resume the guests
start() {
    [ -f "$LISTFILE" ] || { started; return 0; }

    if [ "x$ON_BOOT" != xstart ]; then
        gettext "libvirt-guests is configured not to start any guests on boot"
        echo
        rm -f "$LISTFILE"
        started
        return 0
    fi

    isfirst=true
    bypass=
    test "x$BYPASS_CACHE" = x0 || bypass=--bypass-cache
    while read uri list; do
        configured=false
        set -f
        for confuri in $URIS; do
            set +f
            if [ "x$confuri" = "x$uri" ]; then
                configured=true
                break
            fi
        done
        set +f
        if ! "$configured"; then
            eval_gettext "Ignoring guests on \$uri URI"; echo
            continue
        fi

        test_connect "$uri" || continue

        eval_gettext "Resuming guests on \$uri URI..."; echo
        for guest in $list; do
            name=$(guest_name "$uri" "$guest")
            eval_gettext "Resuming guest \$name: "
            if guest_is_on "$uri" "$guest"; then
                if "$guest_running"; then
                    gettext "already active"; echo
                else
                    if "$isfirst"; then
                        isfirst=false
                    else
                        sleep $START_DELAY
                    fi
                    retval run_virsh "$uri" start $bypass "$name" \
                        >/dev/null && \
                    gettext "done"; echo
                fi
            fi
        done
    done <"$LISTFILE"

    rm -f "$LISTFILE"
    started
}

# suspend_guest URI GUEST
# Do a managed save on a GUEST on URI. This function returns after the guest
# was saved.
suspend_guest()
{
    uri=$1
    guest=$2

    name=$(guest_name "$uri" "$guest")
    label=$(eval_gettext "Suspending \$name: ")
    bypass=
    slept=0
    test "x$BYPASS_CACHE" = x0 || bypass=--bypass-cache
    printf '%s...\n' "$label"
    run_virsh "$uri" managedsave $bypass "$guest" >/dev/null &
    virsh_pid=$!
    while true; do
        sleep 1
        kill -0 "$virsh_pid" >/dev/null 2>&1 || break

        slept=$(($slept + 1))
        if [ $(($slept % 5)) -eq 0 ]; then
            progress=$(run_virsh_c "$uri" domjobinfo "$guest" 2>/dev/null | \
                    awk '/^Data processed:/{print $3, $4}')
            if [ -n "$progress" ]; then
                printf '%s%s\n' "$label" "$progress"
            else
                printf '%s%s\n' "$label" "..."
            fi
        fi
    done
    retval wait "$virsh_pid" && printf '%s%s\n' "$label" "$(gettext "done")"
}

# shutdown_guest URI GUEST
# Start a ACPI shutdown of GUEST on URI. This function return after the quest
# was successfully shutdown or the timeout defined by $SHUTDOWN_TIMEOUT expires.
shutdown_guest()
{
    uri=$1
    guest=$2

    name=$(guest_name "$uri" "$guest")
    eval_gettext "Starting shutdown on guest: \$name"
    echo
    retval run_virsh "$uri" shutdown "$guest" >/dev/null || return
    timeout=$SHUTDOWN_TIMEOUT
    check_timeout=false
    if [ $timeout -gt 0 ]; then
        check_timeout=true
        format=$(eval_gettext "Waiting for guest %s to shut down, %d seconds left\n")
    else
        slept=0
        format=$(eval_gettext "Waiting for guest %s to shut down\n")
    fi
    while ! $check_timeout || [ "$timeout" -gt 0 ]; do
        sleep 1
        guest_is_on "$uri" "$guest" || return
        "$guest_running" || break

        if $check_timeout; then
            if [ $(($timeout % 5)) -eq 0 ]; then
                printf "$format" "$name" "$timeout"
            fi
            timeout=$(($timeout - 1))
        else
            slept=$(($slept + 1))
            if [ $(($slept % 5)) -eq 0 ]; then
                printf "$format" "$name"
            fi
        fi
    done

    if guest_is_on "$uri" "$guest"; then
        if "$guest_running"; then
            eval_gettext "Shutdown of guest \$name failed to complete in time."
        else
            eval_gettext "Shutdown of guest \$name complete."
        fi
        echo
    fi
}

# shutdown_guest_async URI GUEST
# Start a ACPI shutdown of GUEST on URI. This function returns after the command
# was issued to libvirt to allow parallel shutdown.
shutdown_guest_async()
{
    uri=$1
    guest=$2

    name=$(guest_name "$uri" "$guest")
    eval_gettext "Starting shutdown on guest: \$name"
    echo
    retval run_virsh "$uri" shutdown "$guest" > /dev/null
}

# guest_count GUEST_LIST
# Returns number of guests in GUEST_LIST
guest_count()
{
    set -- $1
    echo $#
}

# check_guests_shutdown URI GUESTS
# check if shutdown is complete on guests in "GUESTS" and returns only
# guests that are still shutting down
check_guests_shutdown()
{
    uri=$1
    guests=$2

    guests_up=
    for guest in $guests; do
        if ! guest_is_on "$uri" "$guest" >/dev/null 2>&1; then
            eval_gettext "Failed to determine state of guest: \$guest. Not tracking it anymore."
            echo
            continue
        fi
        if "$guest_running"; then
            guests_up="$guests_up $guest"
        fi
    done
    echo "$guests_up"
}

# print_guests_shutdown URI BEFORE AFTER
# Checks for differences in the lists BEFORE and AFTER and prints
# a shutdown complete notice for guests that have finished
print_guests_shutdown()
{
    uri=$1
    before=$2
    after=$3

    for guest in $before; do
        case " $after " in
            *" $guest "*) continue;;
        esac

        name=$(guest_name "$uri" "$guest")
        eval_gettext "Shutdown of guest \$name complete."
        echo
    done
}

# shutdown_guests_parallel URI GUESTS
# Shutdown guests GUESTS on machine URI in parallel
shutdown_guests_parallel()
{
    uri=$1
    guests=$2

    on_shutdown=
    check_timeout=false
    timeout=$SHUTDOWN_TIMEOUT
    if [ $timeout -gt 0 ]; then
        check_timeout=true
        format=$(eval_gettext "Waiting for %d guests to shut down, %d seconds left\n")
    else
        slept=0
        format=$(eval_gettext "Waiting for %d guests to shut down\n")
    fi
    while [ -n "$on_shutdown" ] || [ -n "$guests" ]; do
        while [ -n "$guests" ] &&
              [ $(guest_count "$on_shutdown") -lt "$PARALLEL_SHUTDOWN" ]; do
            set -- $guests
            guest=$1
            shift
            guests=$*
            shutdown_guest_async "$uri" "$guest"
            on_shutdown="$on_shutdown $guest"
        done
        sleep 1

        set -- $guests
        guestcount=$#
        set -- $on_shutdown
        shutdowncount=$#

        if $check_timeout; then
            if [ $(($timeout % 5)) -eq 0 ]; then
                printf "$format" $(($guestcount + $shutdowncount)) "$timeout"
            fi
            timeout=$(($timeout - 1))
            if [ $timeout -le 0 ]; then
                eval_gettext "Timeout expired while shutting down domains"; echo
                RETVAL=1
                return
            fi
        else
            slept=$(($slept + 1))
            if [ $(($slept % 5)) -eq 0 ]; then
                printf "$format" $(($guestcount + $shutdowncount))
            fi
        fi

        on_shutdown_prev=$on_shutdown
        on_shutdown=$(check_guests_shutdown "$uri" "$on_shutdown")
        print_guests_shutdown "$uri" "$on_shutdown_prev" "$on_shutdown"
    done
}

# stop
# Shutdown or save guests on the configured uris
stop() {
    # last stop was not followed by start
    [ -f "$LISTFILE" ] && return 0

    suspending=true
    if [ "x$ON_SHUTDOWN" = xshutdown ]; then
        suspending=false
        if [ $SHUTDOWN_TIMEOUT -lt 0 ]; then
            gettext "SHUTDOWN_TIMEOUT must be equal or greater than 0"
            echo
            RETVAL=6
            return
        fi
    fi

    : >"$LISTFILE"
    set -f
    for uri in $URIS; do
        set +f

        test_connect "$uri" || continue

        eval_gettext "Running guests on \$uri URI: "

        list=$(list_guests "$uri")
        if [ $? -eq 0 ]; then
            empty=true
            for uuid in $list; do
                "$empty" || printf ", "
                printf %s "$(guest_name "$uri" "$uuid")"
                empty=false
            done

            if "$empty"; then
                gettext "no running guests."
            fi
            echo
        fi

        if "$suspending"; then
            transient=$(list_guests "$uri" "--transient")
            if [ $? -eq 0 ]; then
                empty=true
                for uuid in $transient; do
                    if "$empty"; then
                        eval_gettext "Not suspending transient guests on URI: \$uri: "
                        empty=false
                    else
                        printf ", "
                    fi
                    printf %s "$(guest_name "$uri" "$uuid")"
                done
                echo
                # reload domain list to contain only persistent guests
                list=$(list_guests "$uri" "--persistent")
                if [ $? -ne 0 ]; then
                    eval_gettext "Failed to list persistent guests on \$uri"
                    echo
                    RETVAL=1
                    set +f
                    return
                fi
            else
                gettext "Failed to list transient guests"
                echo
                RETVAL=1
                set +f
                return
            fi
        fi

        if [ -n "$list" ]; then
            echo "$uri" "$list" >>"$LISTFILE"
        fi
    done
    set +f

    if [ -s "$LISTFILE" ]; then
        while read uri list; do
            if "$suspending"; then
                eval_gettext "Suspending guests on \$uri URI..."; echo
            else
                eval_gettext "Shutting down guests on \$uri URI..."; echo
            fi

            if [ "$PARALLEL_SHUTDOWN" -gt 1 ] &&
               ! "$suspending"; then
                shutdown_guests_parallel "$uri" "$list"
            else
                for guest in $list; do
                    if "$suspending"; then
                        suspend_guest "$uri" "$guest"
                    else
                        shutdown_guest "$uri" "$guest"
                    fi
                done
            fi
        done <"$LISTFILE"
    else
        rm -f "$LISTFILE"
    fi

    rm -f "$VAR_SUBSYS_LIBVIRT_GUESTS"
}

# gueststatus
# List status of guests
gueststatus() {
    set -f
    for uri in $URIS; do
        set +f
        echo "* $uri URI:"
        retval run_virsh "$uri" list || echo
    done
    set +f
}

# rh_status
# Display current status: whether saved state exists, and whether start
# has been executed.  We cannot use status() from the functions library,
# since there is no external daemon process matching this init script.
rh_status() {
    if [ -f "$LISTFILE" ]; then
        gettext "stopped, with saved guests"; echo
        RETVAL=3
    else
        if [ -f "$VAR_SUBSYS_LIBVIRT_GUESTS" ]; then
            gettext "started"; echo
            RETVAL=0
        else
            gettext "stopped, with no saved guests"; echo
            RETVAL=3
        fi
    fi
}

# usage [val]
# Display usage string, then exit with VAL (defaults to 2).
usage() {
    program_name=$0
    eval_gettext "Usage: \$program_name {start|stop|status|restart|"\
"condrestart|try-restart|reload|force-reload|gueststatus|shutdown}"; echo
    exit ${1-2}
}

# See how we were called.
if test $# != 1; then
    usage
fi
case "$1" in
    --help)
        usage 0
        ;;
    start|stop|gueststatus)
        "$1"
        ;;
    restart)
        stop && start
        ;;
    condrestart|try-restart)
        [ -f "$VAR_SUBSYS_LIBVIRT_GUESTS" ] && stop && start
        ;;
    reload|force-reload)
        # Nothing to do; we reread configuration on each invocation
        ;;
    status)
        rh_status
        ;;
    shutdown)
        ON_SHUTDOWN=shutdown
        stop
        ;;
    *)
        usage
        ;;
esac
exit $RETVAL
