libvirt/tools/libvirt-guests.sh.in
Benjamin Taubmann a39dd25715 Extend libvirt-guests to shutdown only persistent VMs
At the moment, there is no configuration option for the libvirt-guests
service that allows users to define that only persistent virtual machines
should be shutdown on host shutdown.

Currently, the service config allows to choose between two ON_SHUTDOWN
actions that are executed on running virtual machines when the host goes
down: shutdown, suspend.
The ON_SHUTDOWN action should be orthogonal to the type of the virtual
machine. However, the existing implementation, does not suspend
transient virtual machines.
This is the matrix of actions that is executed on virtual machines based
on the configured ON_SHUTDOWN action and the type of a virtual machine.

         | persistent | transient
shutdown | shutdown   | shutdown (what we want to change)
suspend  | suspend    | nothing

Add config option PERSISTENT_ONLY to libvirt-guests config that allows
users to define if the ON_SHUTDOWN action should be applied only on
persistent virtual machines. PERSISTENT_ONLY can be set to true, false,
default. The default option will implement the already existing logic.

Case 1: PERSISTENT_ONLY=default
         | persistent | transient
shutdown | shutdown   | shutdown
suspend  | suspend    | nothing

Case 2: PERSISTENT_ONLY=true
         | persistent | transient
shutdown | shutdown   | nothing
suspend  | suspend    | nothing

Case 3: PERSISTENT_ONLY=false
         | persistent | transient
shutdown | shutdown   | shutdown
suspend  | suspend    | suspend

Signed-off-by: Benjamin Taubmann <benjamin.taubmann@nutanix.com>
Reviewed-by: Martin Kletzander <mkletzan@redhat.com>
2024-04-04 09:10:00 +02:00

646 lines
17 KiB
Bash

#!/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="@sysconfdir@"
localstatedir="@localstatedir@"
initconfdir="@initconfdir@"
# 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
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/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() {
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