diff options
Diffstat (limited to 'python/qemu/machine.py')
| -rw-r--r-- | python/qemu/machine.py | 174 |
1 files changed, 128 insertions, 46 deletions
diff --git a/python/qemu/machine.py b/python/qemu/machine.py index c25f0b42cf..80c4d4a8b6 100644 --- a/python/qemu/machine.py +++ b/python/qemu/machine.py @@ -22,6 +22,7 @@ import logging import os import subprocess import shutil +import signal import socket import tempfile from typing import Optional, Type @@ -49,17 +50,10 @@ class QEMUMachineAddDeviceError(QEMUMachineError): """ -class MonitorResponseError(qmp.QMPError): +class AbnormalShutdown(QEMUMachineError): """ - Represents erroneous QMP monitor reply + Exception raised when a graceful shutdown was requested, but not performed. """ - def __init__(self, reply): - try: - desc = reply["error"]["desc"] - except KeyError: - desc = reply - super().__init__(desc) - self.reply = reply class QEMUMachine: @@ -127,6 +121,7 @@ class QEMUMachine: self._console_address = None self._console_socket = None self._remove_files = [] + self._user_killed = False self._console_log_path = console_log if self._console_log_path: # In order to log the console, buffering needs to be enabled. @@ -294,6 +289,19 @@ class QEMUMachine: self._qmp.accept() def _post_shutdown(self): + """ + Called to cleanup the VM instance after the process has exited. + May also be called after a failed launch. + """ + # Comprehensive reset for the failed launch case: + self._early_cleanup() + + if self._qmp: + self._qmp.close() + self._qmp = None + + self._load_io_log() + if self._qemu_log_file is not None: self._qemu_log_file.close() self._qemu_log_file = None @@ -307,6 +315,19 @@ class QEMUMachine: while len(self._remove_files) > 0: self._remove_if_exists(self._remove_files.pop()) + exitcode = self.exitcode() + if (exitcode is not None and exitcode < 0 + and not (self._user_killed and exitcode == -signal.SIGKILL)): + msg = 'qemu received signal %i; command: "%s"' + if self._qemu_full_args: + command = ' '.join(self._qemu_full_args) + else: + command = '' + LOG.warning(msg, -int(exitcode), command) + + self._user_killed = False + self._launched = False + def launch(self): """ Launch the VM and make sure we cleanup and expose the @@ -322,7 +343,7 @@ class QEMUMachine: self._launch() self._launched = True except: - self.shutdown() + self._post_shutdown() LOG.debug('Error launching VM') if self._qemu_full_args: @@ -348,19 +369,12 @@ class QEMUMachine: close_fds=False) self._post_launch() - def wait(self): - """ - Wait for the VM to power off + def _early_cleanup(self) -> None: """ - self._popen.wait() - if self._qmp: - self._qmp.close() - self._load_io_log() - self._post_shutdown() + Perform any cleanup that needs to happen before the VM exits. - def shutdown(self, has_quit=False, hard=False): - """ - Terminate the VM and clean up + May be invoked by both soft and hard shutdown in failover scenarios. + Called additionally by _post_shutdown for comprehensive cleanup. """ # If we keep the console socket open, we may deadlock waiting # for QEMU to exit, while QEMU is waiting for the socket to @@ -369,37 +383,105 @@ class QEMUMachine: self._console_socket.close() self._console_socket = None - if self.is_running(): - if hard: - self._popen.kill() - elif self._qmp: - try: - if not has_quit: - self._qmp.cmd('quit') - self._qmp.close() - self._popen.wait(timeout=3) - except: - self._popen.kill() - self._popen.wait() + def _hard_shutdown(self) -> None: + """ + Perform early cleanup, kill the VM, and wait for it to terminate. - self._load_io_log() - self._post_shutdown() + :raise subprocess.Timeout: When timeout is exceeds 60 seconds + waiting for the QEMU process to terminate. + """ + self._early_cleanup() + self._popen.kill() + self._popen.wait(timeout=60) - exitcode = self.exitcode() - if exitcode is not None and exitcode < 0 and \ - not (exitcode == -9 and hard): - msg = 'qemu received signal %i: %s' - if self._qemu_full_args: - command = ' '.join(self._qemu_full_args) - else: - command = '' - LOG.warning(msg, -int(exitcode), command) + def _soft_shutdown(self, has_quit: bool = False, + timeout: Optional[int] = 3) -> None: + """ + Perform early cleanup, attempt to gracefully shut down the VM, and wait + for it to terminate. - self._launched = False + :param has_quit: When True, don't attempt to issue 'quit' QMP command + :param timeout: Optional timeout in seconds for graceful shutdown. + Default 3 seconds, A value of None is an infinite wait. + + :raise ConnectionReset: On QMP communication errors + :raise subprocess.TimeoutExpired: When timeout is exceeded waiting for + the QEMU process to terminate. + """ + self._early_cleanup() + + if self._qmp is not None: + if not has_quit: + # Might raise ConnectionReset + self._qmp.cmd('quit') + + # May raise subprocess.TimeoutExpired + self._popen.wait(timeout=timeout) + + def _do_shutdown(self, has_quit: bool = False, + timeout: Optional[int] = 3) -> None: + """ + Attempt to shutdown the VM gracefully; fallback to a hard shutdown. + + :param has_quit: When True, don't attempt to issue 'quit' QMP command + :param timeout: Optional timeout in seconds for graceful shutdown. + Default 3 seconds, A value of None is an infinite wait. + + :raise AbnormalShutdown: When the VM could not be shut down gracefully. + The inner exception will likely be ConnectionReset or + subprocess.TimeoutExpired. In rare cases, non-graceful termination + may result in its own exceptions, likely subprocess.TimeoutExpired. + """ + try: + self._soft_shutdown(has_quit, timeout) + except Exception as exc: + self._hard_shutdown() + raise AbnormalShutdown("Could not perform graceful shutdown") \ + from exc + + def shutdown(self, has_quit: bool = False, + hard: bool = False, + timeout: Optional[int] = 3) -> None: + """ + Terminate the VM (gracefully if possible) and perform cleanup. + Cleanup will always be performed. + + If the VM has not yet been launched, or shutdown(), wait(), or kill() + have already been called, this method does nothing. + + :param has_quit: When true, do not attempt to issue 'quit' QMP command. + :param hard: When true, do not attempt graceful shutdown, and + suppress the SIGKILL warning log message. + :param timeout: Optional timeout in seconds for graceful shutdown. + Default 3 seconds, A value of None is an infinite wait. + """ + if not self._launched: + return + + try: + if hard: + self._user_killed = True + self._hard_shutdown() + else: + self._do_shutdown(has_quit, timeout=timeout) + finally: + self._post_shutdown() def kill(self): + """ + Terminate the VM forcefully, wait for it to exit, and perform cleanup. + """ self.shutdown(hard=True) + def wait(self, timeout: Optional[int] = 3) -> None: + """ + Wait for the VM to power off and perform post-shutdown cleanup. + + :param timeout: Optional timeout in seconds. + Default 3 seconds, A value of None is an infinite wait. + """ + self.shutdown(has_quit=True, timeout=timeout) + def set_qmp_monitor(self, enabled=True): """ Set the QMP monitor. @@ -438,7 +520,7 @@ class QEMUMachine: if reply is None: raise qmp.QMPError("Monitor is closed") if "error" in reply: - raise MonitorResponseError(reply) + raise qmp.QMPResponseError(reply) return reply["return"] def get_qmp_event(self, wait=False): |