Gracefully shut down QEMU/KVM on host shutdown

14,437

With the help of @Serg's answer, I crafted this set of three scripts (Python 3 and Bash) which listens for Unity Shutdown/Logout dialogs, checks for running VMs, blocks the Unity dialog, displays a nice progress bar, waits until all VMs are off or the timeout is reached, asks if remaining VMs should be forcibly killed and finally displays a custom shutdown/logout dialog.

Here are the scripts. Place them in a location contained in the $PATH variable, like /usr/local/bin/. Make sure they're owned by root and have all execution bits set (chmod +x).

vm-terminator (in Bash, the GUI):

#! /bin/bash

# Use first command-line argument as timeout, if given and numeric, else 30 sec
if [ "$1" -eq "$1" ] 2> /dev/null
    then timeout=$1
    else timeout=30
fi


# Define function to ask whether to shut down / log out / reboot later.
function end_session () {
    action=$(zenity --list --title="VM Terminator" --text="All VMs are shut down. What to do now?" --radiolist --hide-header --column="" --column="" TRUE "Log out" FALSE "Reboot" FALSE "Shut down")

    case $action in
        "Log out")
          gnome-session-quit --logout --no-prompt
          ;;
        "Reboot")
          systemctl reboot
          ;;
        "Shut down")
          systemctl poweroff
          ;;
        *)
          echo "Not ending current session."
          ;;
    esac
}


# Try to shut down VMs with
(
    set -o pipefail
    shutdown-all-vms -i 0.5 -t $timeout -z |
      zenity --progress --title="VM Terminator" --auto-close --auto-kill --width=400
) &> /dev/null
succeeded=$?

# Evaluate whether the task was successful and show host shutdown/logout dialog or kill/manual dialog or error message.
case $succeeded in
    0)
      end_session
      ;;
    1)
      zenity --question --title="VM Terminator" --text="The timeout was reached.\n\nWould you like to forcibly power off all remaining VMs\nor abort and take care of them yourself?" --ok-label="Kill them!" --cancel-label="I'll do it myself" --default-cancel
      if [ $? == 0 ]
        then shutdown-all-vms -t 0 -k
            end_session
        else exit 1
      fi
      ;;
    129)
      zenity --question --title="VM Terminator" --text="You cancelled the timeout.\n\nWould you like to forcibly power off all remaining VMs\nor abort and take care of them yourself?" --ok-label="Kill them!" --cancel-label="I'll do it myself" --default-cancel
      if [ $? == 0 ]
        then shutdown-all-vms -t 0 -k
            end_session
        else exit 1
      fi
      ;;
    *)
      zenity --error --title="VM Terminator" --text="An error occured while trying to shut down some VMs. Please review them manualy!"
      exit 2
      ;;
esac

shutdown-all-vms (in Python 3, the core):

#! /usr/bin/env python3

# Script to gracefully shut down all running virtual machines accessible to the 'virtsh' command.
# It was initially designed for QEMU/KVM machines, but might work with more hypervisors.

# The VMs are tried to be shut down by triggering a "power-button-pressed" event in each machine.
# Each guest OS is responsible to shut down when detecting one. By default, some systems may just show
# an user dialog prompt instead and do nothing. If configured, this script can turn them off forcibly.
# That would be similar to holding the power button or pulling the AC plug on a real machine.

# This script exits with code 0 when all VMs could be shut down or were forced off at timeout.
# If the 'virsh shutdown VM_NAME' command returned an error, this script will exit with error code 1.
# On timeout with KILL_ON_TIMEOUT set to False, the script will exit with error code 2.
# If KILL_ON_TIMEOUT is active and the timeout was reached, but one of the 'virsh destroy VM_NAME' commands
# returned an error, this script exits with error code 3.


import subprocess
import time
from optparse import OptionParser

# Function to get a list of running VM names:
def list_running_vms():
    as_string = subprocess.check_output(["virsh", "list", "--state-running", "--name"], universal_newlines=True).strip()
    return [] if not as_string else as_string.split("\n")

# Evaluate command-line arguments:
parser = OptionParser(version="%prog 1.0")
parser.add_option("-i", "--interval", type="float", dest="interval", default=1,
                  help="Interval to use for polling the VM state after sending the shutdown command. (default: %default)")
parser.add_option("-t", "--timeout", type="float", dest="timeout", default=30,
                  help="Time to wait for all VMs to shut down. (default: %default)")
parser.add_option("-k", "--kill-on-timeout", action="store_true", dest="kill", default=False,
                  help="Kill (power cut) all remaining VMs when the timeout is reached. "
                       "Otherwise exit with error code 1. (default: %default)")
parser.add_option("-v", "--verbose", action="store_true", dest="verbose", default=False,
                  help="Print verbose status output. (default: %default)")
parser.add_option("-z", "--zenity", action="store_true", dest="zenity", default=False,
                  help="Print progress lines for 'zenity --progress' GUI progress dialog. (default: %default)")
(options, args) = parser.parse_args()

# List all running VMs:
running_vms = list_running_vms()

# Print summary of what will happen:
print("Shutting down all running VMs (currently {}) within {} seconds. {} remaining VMs.".format(
       len(running_vms), options.timeout, "Kill all" if options.kill else "Do not kill any"))

# Send shutdown command ("power-button-pressed" event) to all running VMs:
any_errors = False
if options.zenity:
    print("# Sending shutdown signals...", flush=True)
for vm in running_vms:
    if options.verbose:
        ok = subprocess.call(["virsh", "shutdown", vm])
    else:
        ok = subprocess.call(["virsh", "shutdown", vm], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    if ok != 0:
        print("Error trying to shut down VM '{}' (code {})!".format(vm, ok))
        any_errors = True

# Don't start waiting if there was any error sending the shutdown command, exit with error:
if any_errors:
    print("ERROR: could not successfully send all shutdown commands!")
    exit(3)

# Wait for all VMs to shut down, but at most MAX_WAIT seconds. Poll every INTERVAL seconds::
t0 = time.time()
while running_vms:
    num_of_vms = len(running_vms)
    t = time.time() - t0
    if options.zenity:
        print("# Waiting for {} VM{} to shut down... ({} seconds left)".format(
               num_of_vms, "" if num_of_vms == 1 else "s", int(options.timeout - t)), flush=True)
        print(int(100 * t/options.timeout) if t < options.timeout else 99, flush=True)
    if options.verbose or t > options.timeout:
        print("\n[{:5.1f}s] Still waiting for {} VMs to shut down:".format(t, num_of_vms))
        print(" > " + "\n > ".join(running_vms))
    if t > options.timeout:
        if options.kill:
            print("\nTimeout of {} seconds reached! Killing all remaining VMs now!".format(options.timeout))
            if options.zenity:
                print("# Timeout reached! Have to kill the remaining {}.".format(
                       "VM" if num_of_vms == 1 else "{} VMs".format(num_of_vms)), flush=True)
            for vm in running_vms:
                if options.verbose:
                    ok = subprocess.call(["virsh", "destroy", vm])
                else:
                    ok = subprocess.call(["virsh", "destroy", vm], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
                if ok != 0:
                    if options.verbose:
                        print("Error trying to forcibly kill VM '{}' (code {})!".format(vm, ok))
                    any_errors = True
            if any_errors:
                print("ERROR: could not successfully send all destroy commands!")
                exit(3)
        else:
            print("ERROR: Timeout of {} seconds reached!".format(options.timeout))
            exit(1)
        break
    time.sleep(options.interval)
    running_vms = list_running_vms()

print("#" if options.zenity else "" + " All VMs were shut down successfully.", flush=True)
if options.zenity:
    print(100, flush=True)
exit(0)

shutdown-dialog-listener (in Bash, the Unity shutdown/logout watchdog):

#!/bin/bash

DISPLAY=:0
dbus-monitor --session "interface='com.canonical.Unity.Session'" | \
  while read LINE;do \
  if grep -qi 'reboot\|shutdown\|logout' <<< "$LINE" ;then \
    VAR="$(virsh list --state-running --name)"
    if [ $(wc -w <<<$VAR) -gt 0 ]; then
      qdbus com.canonical.Unity /org/gnome/SessionManager/EndSessionDialog \
      org.gnome.SessionManager.EndSessionDialog.Close

      vm-terminator
    fi
  fi ;done

All three scripts are directly callable, the core script shutdown-all-vms even has a nice command-line help:

$ shutdown-all-vms --help
Usage: shutdown-all-vms [options]

Options:
  --version             show program's version number and exit
  -h, --help            show this help message and exit
  -i INTERVAL, --interval=INTERVAL
                        Interval to use for polling the VM state after sending
                        the shutdown command. (default: 1)
  -t TIMEOUT, --timeout=TIMEOUT
                        Time to wait for all VMs to shut down. (default: 30)
  -k, --kill-on-timeout
                        Kill (power cut) all remaining VMs when the timeout is
                        reached. Otherwise exit with error code 1. (default:
                        False)
  -v, --verbose         Print verbose status output. (default: False)
  -z, --zenity          Print progress lines for 'zenity --progress' GUI
                        progress dialog. (default: False)

Additionally, you may place shutdown-dialog-listener into your user account's startup applications.

Share:
14,437

Related videos on Youtube

Byte Commander
Author by

Byte Commander

Ask Ubuntu moderator♦, IT student and DevOps engineer. I love Ubuntu, Python, good music and coffee, although not necessarily in that order. You can easily contact me in the Ask Ubuntu General Room most of the time, or on Discord as @ByteCommander#2800. I'd also love to invite you to my Ubuntu Hideout Discord Server btw. PS: My profile picture is derived from "Wolf Tribals" by user HaskDitex (DeviantArt.com), which is under creative Commons 3.0 License. Currently I'm using the character "Dregg Morriss" from the game "Medieval Cop" by Vasant Jahav ("Gemini Gamer"). It can be found here.

Updated on September 18, 2022

Comments

  • Byte Commander
    Byte Commander over 1 year

    I start a QEMU/KVM Ubuntu 15.10 virtual machine on boot and let it run in the background (as webserver).

    What happens now if I shut down the host (also 15.10)?
    Will it kill the VM and result in a virtual power cut or even worse?
    Or will it trigger a "power-button-pressed" event in the VM and wait for it to shut down cleanly?

    The guest system is set up to shut down properly when such a "power-button-pressed" event occurs. It's off after less than 5-10 seconds usually.

    If the default behaviour on host shutdown is to kill the VM, how can I change this to a clean shutdown of the guest and waiting until it's off?

  • Byte Commander
    Byte Commander about 8 years
    I wrote my own answer below.
  • Byte Commander
    Byte Commander about 8 years
    Screenshots of the GUI might follow.