#!/bin/sh

# Copyright (C) 2011-2014 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="/etc"
localstatedir="/var"
initconfdir="/etc/conf.d"

# 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")
. "/usr/bin"/gettext.sh

export TEXTDOMAIN="libvirt" TEXTDOMAINDIR="/usr/share/locale"

URIS="default"
ON_BOOT="start"
ON_SHUTDOWN="suspend"
SHUTDOWN_TIMEOUT=300
PARALLEL_SHUTDOWN=0
START_DELAY=0
BYPASS_CACHE=0
SYNC_TIME=0
PERSISTENT_ONLY="default"

test -f "$initconfdir"/libvirt-guests &&
    . "$initconfdir"/libvirt-guests

LISTFILE="$localstatedir"/lib/libvirt/libvirt-guests
VAR_SUBSYS_LIBVIRT_GUESTS="$localstatedir"/lock/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() {
    local 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()
{
    local uri="$1"

    if run_virsh "$uri" connect 2>/dev/null; then
        return 0;
    else
        eval_gettext "Can't connect to \$uri. Skipping."
        return 1
    fi
}

# 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() {
    local uri="$1"
    local persistent="$2"
    local list="$(run_virsh_c "$uri" list --uuid $persistent)"

    if [ $? -ne 0 ]; then
        RETVAL=1
        return 1
    fi

    echo "$list" | sed "/00000000-0000-0000-0000-000000000000/d"
}

# guest_name URI UUID
# return name of guest UUID on URI
guest_name() {
    local uri="$1"
    local 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() {
    local uri="$1"
    local uuid="$2"
    local id="$(run_virsh "$uri" domid "$uuid")"

    guest_running="false"
    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() {
    local isfirst="true"
    local bypass=
    local sync_time="false"
    local uri=
    local list=

    [ -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

    test "x$BYPASS_CACHE" = x0 || bypass="--bypass-cache"
    test "x$SYNC_TIME" = x0 || sync_time="true"
    while read uri list; do
        local configured="false"
        local confuri=
        local guest=

        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
            local 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
                if "$sync_time"; then
                    run_virsh "$uri" domtime --sync "$name" >/dev/null
                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()
{
    local uri="$1"
    local guest="$2"
    local name="$(guest_name "$uri" "$guest")"
    local label="$(eval_gettext "Suspending \$name: ")"
    local bypass=
    local slept=0

    test "x$BYPASS_CACHE" = x0 || bypass="--bypass-cache"
    printf '%s...\n' "$label"
    run_virsh "$uri" managedsave $bypass "$guest" >/dev/null &
    local 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
            local 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 an ACPI shutdown of GUEST on URI. This function returns after the guest
# was successfully shutdown or the timeout defined by $SHUTDOWN_TIMEOUT expired.
shutdown_guest()
{
    local uri="$1"
    local guest="$2"
    local name="$(guest_name "$uri" "$guest")"
    local timeout="$SHUTDOWN_TIMEOUT"
    local check_timeout="false"
    local format=
    local slept=

    eval_gettext "Starting shutdown on guest: \$name"
    echo
    retval run_virsh "$uri" shutdown "$guest" >/dev/null || return

    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()
{
    local uri="$1"
    local guest="$2"
    local 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
# Result is returned in "guests_shutting_down"
check_guests_shutdown()
{
    local uri="$1"
    local guests_to_check="$2"
    local guest=

    guests_shutting_down=
    for guest in $guests_to_check; 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_shutting_down="$guests_shutting_down $guest"
        fi
    done
}

# 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()
{
    local uri="$1"
    local before="$2"
    local after="$3"
    local guest=

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

        local name="$(guest_name "$uri" "$guest")"
        if [ -n "$name" ]; then
            eval_gettext "Shutdown of guest \$name complete."
            echo
        fi
    done
}

# shutdown_guests_parallel URI GUESTS
# Shutdown guests GUESTS on machine URI in parallel
shutdown_guests_parallel()
{
    local uri="$1"
    local guests="$2"
    local on_shutdown=
    local check_timeout="false"
    local timeout="$SHUTDOWN_TIMEOUT"
    local slept=
    local format=

    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
            local guest="$1"
            shift
            guests="$*"
            if [ -z "$(echo $on_shutdown | grep $guest)" ] &&
               [ -n "$(guest_name "$uri" "$guest")" ]; then
                shutdown_guest_async "$uri" "$guest"
                on_shutdown="$on_shutdown $guest"
            fi
        done
        sleep 1

        set -- $guests
        local guestcount=$#
        set -- $on_shutdown
        local 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

        local on_shutdown_prev="$on_shutdown"
        check_guests_shutdown "$uri" "$on_shutdown"
        on_shutdown="$guests_shutting_down"
        print_guests_shutdown "$uri" "$on_shutdown_prev" "$on_shutdown"
    done
}

# stop
# Shutdown or save guests on the configured uris
stop() {
    local uri=
    local action="suspend"
    local persistent_only="default"


    # last stop was not followed by start
    [ -f "$LISTFILE" ] && return 0

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

    case "x$PERSISTENT_ONLY" in
        xtrue)
            persistent_only="true"
            ;;
        xfalse)
            persistent_only="false"
            ;;
        *)
            if [ "x$action" = xshutdown ]; then
                persistent_only="false"
            elif [ "x$action" = xsuspend ]; then
                persistent_only="true"
            fi
            ;;
    esac

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

        test_connect "$uri" || continue

        eval_gettext "Running guests on \$uri URI: "

        local list="$(list_guests "$uri")"
        if [ $? -eq 0 ]; then
            local 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 "$persistent_only"; then
            local transient="$(list_guests "$uri" "--transient")"
            if [ $? -eq 0 ]; then
                local empty="true"
                local uuid=

                for uuid in $transient; do
                    if "$empty"; then
                        if [ "x$action" = xsuspend ]; then
                            eval_gettext "Not suspending transient guests on URI: \$uri: "
                        else
                            eval_gettext "Not shutting down transient guests on URI: \$uri: "
                        fi
                        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 [ "x$action" = xsuspend ]; 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 ] &&
                [ "x$action" = xshutdown ]; then
                shutdown_guests_parallel "$uri" "$list"
            else
                local guest=
                for guest in $list; do
                    if [ "x$action" = xsuspend ]; 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() {
    local uri=
    set -f
    for uri in $URIS; do
        set +f
        echo "* $uri URI:"
        retval run_virsh "$uri" list | grep -v "Domain-0" || 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() {
    local 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
