1beb6b57bSJohn Snow""" 2beb6b57bSJohn SnowQEMU machine module: 3beb6b57bSJohn Snow 4beb6b57bSJohn SnowThe machine module primarily provides the QEMUMachine class, 5beb6b57bSJohn Snowwhich provides facilities for managing the lifetime of a QEMU VM. 6beb6b57bSJohn Snow""" 7beb6b57bSJohn Snow 8beb6b57bSJohn Snow# Copyright (C) 2015-2016 Red Hat Inc. 9beb6b57bSJohn Snow# Copyright (C) 2012 IBM Corp. 10beb6b57bSJohn Snow# 11beb6b57bSJohn Snow# Authors: 12beb6b57bSJohn Snow# Fam Zheng <famz@redhat.com> 13beb6b57bSJohn Snow# 14beb6b57bSJohn Snow# This work is licensed under the terms of the GNU GPL, version 2. See 15beb6b57bSJohn Snow# the COPYING file in the top-level directory. 16beb6b57bSJohn Snow# 17beb6b57bSJohn Snow# Based on qmp.py. 18beb6b57bSJohn Snow# 19beb6b57bSJohn Snow 20beb6b57bSJohn Snowimport errno 21beb6b57bSJohn Snowfrom itertools import chain 22beb6b57bSJohn Snowimport logging 23beb6b57bSJohn Snowimport os 24beb6b57bSJohn Snowimport shutil 25beb6b57bSJohn Snowimport signal 26beb6b57bSJohn Snowimport socket 27beb6b57bSJohn Snowimport subprocess 28beb6b57bSJohn Snowimport tempfile 29beb6b57bSJohn Snowfrom types import TracebackType 30beb6b57bSJohn Snowfrom typing import ( 31beb6b57bSJohn Snow Any, 32beb6b57bSJohn Snow BinaryIO, 33beb6b57bSJohn Snow Dict, 34beb6b57bSJohn Snow List, 35beb6b57bSJohn Snow Optional, 36beb6b57bSJohn Snow Sequence, 37beb6b57bSJohn Snow Tuple, 38beb6b57bSJohn Snow Type, 39beb6b57bSJohn Snow) 40beb6b57bSJohn Snow 41d1e04769SJohn Snowfrom qemu.qmp import ( # pylint: disable=import-error 42beb6b57bSJohn Snow QEMUMonitorProtocol, 43beb6b57bSJohn Snow QMPMessage, 44beb6b57bSJohn Snow QMPReturnValue, 45beb6b57bSJohn Snow SocketAddrT, 46beb6b57bSJohn Snow) 47beb6b57bSJohn Snow 48beb6b57bSJohn Snowfrom . import console_socket 49beb6b57bSJohn Snow 50beb6b57bSJohn Snow 51beb6b57bSJohn SnowLOG = logging.getLogger(__name__) 52beb6b57bSJohn Snow 53beb6b57bSJohn Snow 54beb6b57bSJohn Snowclass QEMUMachineError(Exception): 55beb6b57bSJohn Snow """ 56beb6b57bSJohn Snow Exception called when an error in QEMUMachine happens. 57beb6b57bSJohn Snow """ 58beb6b57bSJohn Snow 59beb6b57bSJohn Snow 60beb6b57bSJohn Snowclass QEMUMachineAddDeviceError(QEMUMachineError): 61beb6b57bSJohn Snow """ 62beb6b57bSJohn Snow Exception raised when a request to add a device can not be fulfilled 63beb6b57bSJohn Snow 64beb6b57bSJohn Snow The failures are caused by limitations, lack of information or conflicting 65beb6b57bSJohn Snow requests on the QEMUMachine methods. This exception does not represent 66beb6b57bSJohn Snow failures reported by the QEMU binary itself. 67beb6b57bSJohn Snow """ 68beb6b57bSJohn Snow 69beb6b57bSJohn Snow 70beb6b57bSJohn Snowclass AbnormalShutdown(QEMUMachineError): 71beb6b57bSJohn Snow """ 72beb6b57bSJohn Snow Exception raised when a graceful shutdown was requested, but not performed. 73beb6b57bSJohn Snow """ 74beb6b57bSJohn Snow 75beb6b57bSJohn Snow 76beb6b57bSJohn Snowclass QEMUMachine: 77beb6b57bSJohn Snow """ 78beb6b57bSJohn Snow A QEMU VM. 79beb6b57bSJohn Snow 80beb6b57bSJohn Snow Use this object as a context manager to ensure 81beb6b57bSJohn Snow the QEMU process terminates:: 82beb6b57bSJohn Snow 83beb6b57bSJohn Snow with VM(binary) as vm: 84beb6b57bSJohn Snow ... 85beb6b57bSJohn Snow # vm is guaranteed to be shut down here 86beb6b57bSJohn Snow """ 8782e6517dSJohn Snow # pylint: disable=too-many-instance-attributes, too-many-public-methods 88beb6b57bSJohn Snow 89beb6b57bSJohn Snow def __init__(self, 90beb6b57bSJohn Snow binary: str, 91beb6b57bSJohn Snow args: Sequence[str] = (), 92beb6b57bSJohn Snow wrapper: Sequence[str] = (), 93beb6b57bSJohn Snow name: Optional[str] = None, 94beb6b57bSJohn Snow base_temp_dir: str = "/var/tmp", 95beb6b57bSJohn Snow monitor_address: Optional[SocketAddrT] = None, 96beb6b57bSJohn Snow socket_scm_helper: Optional[str] = None, 97beb6b57bSJohn Snow sock_dir: Optional[str] = None, 98beb6b57bSJohn Snow drain_console: bool = False, 99b306e26cSCleber Rosa console_log: Optional[str] = None, 100e2f948a8SEmanuele Giuseppe Esposito log_dir: Optional[str] = None, 101e2f948a8SEmanuele Giuseppe Esposito qmp_timer: Optional[float] = None): 102beb6b57bSJohn Snow ''' 103beb6b57bSJohn Snow Initialize a QEMUMachine 104beb6b57bSJohn Snow 105beb6b57bSJohn Snow @param binary: path to the qemu binary 106beb6b57bSJohn Snow @param args: list of extra arguments 107beb6b57bSJohn Snow @param wrapper: list of arguments used as prefix to qemu binary 108beb6b57bSJohn Snow @param name: prefix for socket and log file names (default: qemu-PID) 109beb6b57bSJohn Snow @param base_temp_dir: default location where temp files are created 110beb6b57bSJohn Snow @param monitor_address: address for QMP monitor 111beb6b57bSJohn Snow @param socket_scm_helper: helper program, required for send_fd_scm() 112beb6b57bSJohn Snow @param sock_dir: where to create socket (defaults to base_temp_dir) 113beb6b57bSJohn Snow @param drain_console: (optional) True to drain console socket to buffer 114beb6b57bSJohn Snow @param console_log: (optional) path to console log file 115b306e26cSCleber Rosa @param log_dir: where to create and keep log files 116e2f948a8SEmanuele Giuseppe Esposito @param qmp_timer: (optional) default QMP socket timeout 117beb6b57bSJohn Snow @note: Qemu process is not started until launch() is used. 118beb6b57bSJohn Snow ''' 11982e6517dSJohn Snow # pylint: disable=too-many-arguments 12082e6517dSJohn Snow 121beb6b57bSJohn Snow # Direct user configuration 122beb6b57bSJohn Snow 123beb6b57bSJohn Snow self._binary = binary 124beb6b57bSJohn Snow self._args = list(args) 125beb6b57bSJohn Snow self._wrapper = wrapper 126e2f948a8SEmanuele Giuseppe Esposito self._qmp_timer = qmp_timer 127beb6b57bSJohn Snow 128beb6b57bSJohn Snow self._name = name or "qemu-%d" % os.getpid() 129beb6b57bSJohn Snow self._base_temp_dir = base_temp_dir 130beb6b57bSJohn Snow self._sock_dir = sock_dir or self._base_temp_dir 131b306e26cSCleber Rosa self._log_dir = log_dir 132beb6b57bSJohn Snow self._socket_scm_helper = socket_scm_helper 133beb6b57bSJohn Snow 134beb6b57bSJohn Snow if monitor_address is not None: 135beb6b57bSJohn Snow self._monitor_address = monitor_address 136beb6b57bSJohn Snow self._remove_monitor_sockfile = False 137beb6b57bSJohn Snow else: 138beb6b57bSJohn Snow self._monitor_address = os.path.join( 139beb6b57bSJohn Snow self._sock_dir, f"{self._name}-monitor.sock" 140beb6b57bSJohn Snow ) 141beb6b57bSJohn Snow self._remove_monitor_sockfile = True 142beb6b57bSJohn Snow 143beb6b57bSJohn Snow self._console_log_path = console_log 144beb6b57bSJohn Snow if self._console_log_path: 145beb6b57bSJohn Snow # In order to log the console, buffering needs to be enabled. 146beb6b57bSJohn Snow self._drain_console = True 147beb6b57bSJohn Snow else: 148beb6b57bSJohn Snow self._drain_console = drain_console 149beb6b57bSJohn Snow 150beb6b57bSJohn Snow # Runstate 151beb6b57bSJohn Snow self._qemu_log_path: Optional[str] = None 152beb6b57bSJohn Snow self._qemu_log_file: Optional[BinaryIO] = None 153beb6b57bSJohn Snow self._popen: Optional['subprocess.Popen[bytes]'] = None 154beb6b57bSJohn Snow self._events: List[QMPMessage] = [] 155beb6b57bSJohn Snow self._iolog: Optional[str] = None 156beb6b57bSJohn Snow self._qmp_set = True # Enable QMP monitor by default. 157beb6b57bSJohn Snow self._qmp_connection: Optional[QEMUMonitorProtocol] = None 158beb6b57bSJohn Snow self._qemu_full_args: Tuple[str, ...] = () 159beb6b57bSJohn Snow self._temp_dir: Optional[str] = None 160beb6b57bSJohn Snow self._launched = False 161beb6b57bSJohn Snow self._machine: Optional[str] = None 162beb6b57bSJohn Snow self._console_index = 0 163beb6b57bSJohn Snow self._console_set = False 164beb6b57bSJohn Snow self._console_device_type: Optional[str] = None 165beb6b57bSJohn Snow self._console_address = os.path.join( 166beb6b57bSJohn Snow self._sock_dir, f"{self._name}-console.sock" 167beb6b57bSJohn Snow ) 168beb6b57bSJohn Snow self._console_socket: Optional[socket.socket] = None 169beb6b57bSJohn Snow self._remove_files: List[str] = [] 170beb6b57bSJohn Snow self._user_killed = False 171beb6b57bSJohn Snow 172beb6b57bSJohn Snow def __enter__(self) -> 'QEMUMachine': 173beb6b57bSJohn Snow return self 174beb6b57bSJohn Snow 175beb6b57bSJohn Snow def __exit__(self, 176beb6b57bSJohn Snow exc_type: Optional[Type[BaseException]], 177beb6b57bSJohn Snow exc_val: Optional[BaseException], 178beb6b57bSJohn Snow exc_tb: Optional[TracebackType]) -> None: 179beb6b57bSJohn Snow self.shutdown() 180beb6b57bSJohn Snow 181beb6b57bSJohn Snow def add_monitor_null(self) -> None: 182beb6b57bSJohn Snow """ 183beb6b57bSJohn Snow This can be used to add an unused monitor instance. 184beb6b57bSJohn Snow """ 185beb6b57bSJohn Snow self._args.append('-monitor') 186beb6b57bSJohn Snow self._args.append('null') 187beb6b57bSJohn Snow 188beb6b57bSJohn Snow def add_fd(self, fd: int, fdset: int, 189beb6b57bSJohn Snow opaque: str, opts: str = '') -> 'QEMUMachine': 190beb6b57bSJohn Snow """ 191beb6b57bSJohn Snow Pass a file descriptor to the VM 192beb6b57bSJohn Snow """ 193beb6b57bSJohn Snow options = ['fd=%d' % fd, 194beb6b57bSJohn Snow 'set=%d' % fdset, 195beb6b57bSJohn Snow 'opaque=%s' % opaque] 196beb6b57bSJohn Snow if opts: 197beb6b57bSJohn Snow options.append(opts) 198beb6b57bSJohn Snow 199beb6b57bSJohn Snow # This did not exist before 3.4, but since then it is 200beb6b57bSJohn Snow # mandatory for our purpose 201beb6b57bSJohn Snow if hasattr(os, 'set_inheritable'): 202beb6b57bSJohn Snow os.set_inheritable(fd, True) 203beb6b57bSJohn Snow 204beb6b57bSJohn Snow self._args.append('-add-fd') 205beb6b57bSJohn Snow self._args.append(','.join(options)) 206beb6b57bSJohn Snow return self 207beb6b57bSJohn Snow 208beb6b57bSJohn Snow def send_fd_scm(self, fd: Optional[int] = None, 209beb6b57bSJohn Snow file_path: Optional[str] = None) -> int: 210beb6b57bSJohn Snow """ 211beb6b57bSJohn Snow Send an fd or file_path to socket_scm_helper. 212beb6b57bSJohn Snow 213beb6b57bSJohn Snow Exactly one of fd and file_path must be given. 214beb6b57bSJohn Snow If it is file_path, the helper will open that file and pass its own fd. 215beb6b57bSJohn Snow """ 216beb6b57bSJohn Snow # In iotest.py, the qmp should always use unix socket. 217beb6b57bSJohn Snow assert self._qmp.is_scm_available() 218beb6b57bSJohn Snow if self._socket_scm_helper is None: 219beb6b57bSJohn Snow raise QEMUMachineError("No path to socket_scm_helper set") 220beb6b57bSJohn Snow if not os.path.exists(self._socket_scm_helper): 221beb6b57bSJohn Snow raise QEMUMachineError("%s does not exist" % 222beb6b57bSJohn Snow self._socket_scm_helper) 223beb6b57bSJohn Snow 224beb6b57bSJohn Snow # This did not exist before 3.4, but since then it is 225beb6b57bSJohn Snow # mandatory for our purpose 226beb6b57bSJohn Snow if hasattr(os, 'set_inheritable'): 227beb6b57bSJohn Snow os.set_inheritable(self._qmp.get_sock_fd(), True) 228beb6b57bSJohn Snow if fd is not None: 229beb6b57bSJohn Snow os.set_inheritable(fd, True) 230beb6b57bSJohn Snow 231beb6b57bSJohn Snow fd_param = ["%s" % self._socket_scm_helper, 232beb6b57bSJohn Snow "%d" % self._qmp.get_sock_fd()] 233beb6b57bSJohn Snow 234beb6b57bSJohn Snow if file_path is not None: 235beb6b57bSJohn Snow assert fd is None 236beb6b57bSJohn Snow fd_param.append(file_path) 237beb6b57bSJohn Snow else: 238beb6b57bSJohn Snow assert fd is not None 239beb6b57bSJohn Snow fd_param.append(str(fd)) 240beb6b57bSJohn Snow 241beb6b57bSJohn Snow proc = subprocess.run( 242beb6b57bSJohn Snow fd_param, 243beb6b57bSJohn Snow stdin=subprocess.DEVNULL, 244beb6b57bSJohn Snow stdout=subprocess.PIPE, 245beb6b57bSJohn Snow stderr=subprocess.STDOUT, 246beb6b57bSJohn Snow check=False, 247beb6b57bSJohn Snow close_fds=False, 248beb6b57bSJohn Snow ) 249beb6b57bSJohn Snow if proc.stdout: 250beb6b57bSJohn Snow LOG.debug(proc.stdout) 251beb6b57bSJohn Snow 252beb6b57bSJohn Snow return proc.returncode 253beb6b57bSJohn Snow 254beb6b57bSJohn Snow @staticmethod 255beb6b57bSJohn Snow def _remove_if_exists(path: str) -> None: 256beb6b57bSJohn Snow """ 257beb6b57bSJohn Snow Remove file object at path if it exists 258beb6b57bSJohn Snow """ 259beb6b57bSJohn Snow try: 260beb6b57bSJohn Snow os.remove(path) 261beb6b57bSJohn Snow except OSError as exception: 262beb6b57bSJohn Snow if exception.errno == errno.ENOENT: 263beb6b57bSJohn Snow return 264beb6b57bSJohn Snow raise 265beb6b57bSJohn Snow 266beb6b57bSJohn Snow def is_running(self) -> bool: 267beb6b57bSJohn Snow """Returns true if the VM is running.""" 268beb6b57bSJohn Snow return self._popen is not None and self._popen.poll() is None 269beb6b57bSJohn Snow 270beb6b57bSJohn Snow @property 271beb6b57bSJohn Snow def _subp(self) -> 'subprocess.Popen[bytes]': 272beb6b57bSJohn Snow if self._popen is None: 273beb6b57bSJohn Snow raise QEMUMachineError('Subprocess pipe not present') 274beb6b57bSJohn Snow return self._popen 275beb6b57bSJohn Snow 276beb6b57bSJohn Snow def exitcode(self) -> Optional[int]: 277beb6b57bSJohn Snow """Returns the exit code if possible, or None.""" 278beb6b57bSJohn Snow if self._popen is None: 279beb6b57bSJohn Snow return None 280beb6b57bSJohn Snow return self._popen.poll() 281beb6b57bSJohn Snow 282beb6b57bSJohn Snow def get_pid(self) -> Optional[int]: 283beb6b57bSJohn Snow """Returns the PID of the running process, or None.""" 284beb6b57bSJohn Snow if not self.is_running(): 285beb6b57bSJohn Snow return None 286beb6b57bSJohn Snow return self._subp.pid 287beb6b57bSJohn Snow 288beb6b57bSJohn Snow def _load_io_log(self) -> None: 289beb6b57bSJohn Snow if self._qemu_log_path is not None: 290beb6b57bSJohn Snow with open(self._qemu_log_path, "r") as iolog: 291beb6b57bSJohn Snow self._iolog = iolog.read() 292beb6b57bSJohn Snow 293beb6b57bSJohn Snow @property 294beb6b57bSJohn Snow def _base_args(self) -> List[str]: 295beb6b57bSJohn Snow args = ['-display', 'none', '-vga', 'none'] 296beb6b57bSJohn Snow 297beb6b57bSJohn Snow if self._qmp_set: 298beb6b57bSJohn Snow if isinstance(self._monitor_address, tuple): 299beb6b57bSJohn Snow moncdev = "socket,id=mon,host={},port={}".format( 300beb6b57bSJohn Snow *self._monitor_address 301beb6b57bSJohn Snow ) 302beb6b57bSJohn Snow else: 303beb6b57bSJohn Snow moncdev = f"socket,id=mon,path={self._monitor_address}" 304beb6b57bSJohn Snow args.extend(['-chardev', moncdev, '-mon', 305beb6b57bSJohn Snow 'chardev=mon,mode=control']) 306beb6b57bSJohn Snow 307beb6b57bSJohn Snow if self._machine is not None: 308beb6b57bSJohn Snow args.extend(['-machine', self._machine]) 309beb6b57bSJohn Snow for _ in range(self._console_index): 310beb6b57bSJohn Snow args.extend(['-serial', 'null']) 311beb6b57bSJohn Snow if self._console_set: 312beb6b57bSJohn Snow chardev = ('socket,id=console,path=%s,server=on,wait=off' % 313beb6b57bSJohn Snow self._console_address) 314beb6b57bSJohn Snow args.extend(['-chardev', chardev]) 315beb6b57bSJohn Snow if self._console_device_type is None: 316beb6b57bSJohn Snow args.extend(['-serial', 'chardev:console']) 317beb6b57bSJohn Snow else: 318beb6b57bSJohn Snow device = '%s,chardev=console' % self._console_device_type 319beb6b57bSJohn Snow args.extend(['-device', device]) 320beb6b57bSJohn Snow return args 321beb6b57bSJohn Snow 322555fe0c2SWainer dos Santos Moschetta @property 323555fe0c2SWainer dos Santos Moschetta def args(self) -> List[str]: 324555fe0c2SWainer dos Santos Moschetta """Returns the list of arguments given to the QEMU binary.""" 325555fe0c2SWainer dos Santos Moschetta return self._args 326555fe0c2SWainer dos Santos Moschetta 327beb6b57bSJohn Snow def _pre_launch(self) -> None: 328beb6b57bSJohn Snow if self._console_set: 329beb6b57bSJohn Snow self._remove_files.append(self._console_address) 330beb6b57bSJohn Snow 331beb6b57bSJohn Snow if self._qmp_set: 332beb6b57bSJohn Snow if self._remove_monitor_sockfile: 333beb6b57bSJohn Snow assert isinstance(self._monitor_address, str) 334beb6b57bSJohn Snow self._remove_files.append(self._monitor_address) 335beb6b57bSJohn Snow self._qmp_connection = QEMUMonitorProtocol( 336beb6b57bSJohn Snow self._monitor_address, 337beb6b57bSJohn Snow server=True, 338beb6b57bSJohn Snow nickname=self._name 339beb6b57bSJohn Snow ) 340beb6b57bSJohn Snow 341beb6b57bSJohn Snow # NOTE: Make sure any opened resources are *definitely* freed in 342beb6b57bSJohn Snow # _post_shutdown()! 343beb6b57bSJohn Snow # pylint: disable=consider-using-with 344b306e26cSCleber Rosa self._qemu_log_path = os.path.join(self.log_dir, self._name + ".log") 345beb6b57bSJohn Snow self._qemu_log_file = open(self._qemu_log_path, 'wb') 346beb6b57bSJohn Snow 347beb6b57bSJohn Snow def _post_launch(self) -> None: 348beb6b57bSJohn Snow if self._qmp_connection: 349e2f948a8SEmanuele Giuseppe Esposito self._qmp.accept(self._qmp_timer) 350beb6b57bSJohn Snow 351*eb7a91d0SEmanuele Giuseppe Esposito def _close_qemu_log_file(self) -> None: 352*eb7a91d0SEmanuele Giuseppe Esposito if self._qemu_log_file is not None: 353*eb7a91d0SEmanuele Giuseppe Esposito self._qemu_log_file.close() 354*eb7a91d0SEmanuele Giuseppe Esposito self._qemu_log_file = None 355*eb7a91d0SEmanuele Giuseppe Esposito 356beb6b57bSJohn Snow def _post_shutdown(self) -> None: 357beb6b57bSJohn Snow """ 358beb6b57bSJohn Snow Called to cleanup the VM instance after the process has exited. 359beb6b57bSJohn Snow May also be called after a failed launch. 360beb6b57bSJohn Snow """ 361beb6b57bSJohn Snow # Comprehensive reset for the failed launch case: 362beb6b57bSJohn Snow self._early_cleanup() 363beb6b57bSJohn Snow 364beb6b57bSJohn Snow if self._qmp_connection: 365beb6b57bSJohn Snow self._qmp.close() 366beb6b57bSJohn Snow self._qmp_connection = None 367beb6b57bSJohn Snow 368*eb7a91d0SEmanuele Giuseppe Esposito self._close_qemu_log_file() 369beb6b57bSJohn Snow 370beb6b57bSJohn Snow self._load_io_log() 371beb6b57bSJohn Snow 372beb6b57bSJohn Snow self._qemu_log_path = None 373beb6b57bSJohn Snow 374beb6b57bSJohn Snow if self._temp_dir is not None: 375beb6b57bSJohn Snow shutil.rmtree(self._temp_dir) 376beb6b57bSJohn Snow self._temp_dir = None 377beb6b57bSJohn Snow 378beb6b57bSJohn Snow while len(self._remove_files) > 0: 379beb6b57bSJohn Snow self._remove_if_exists(self._remove_files.pop()) 380beb6b57bSJohn Snow 381beb6b57bSJohn Snow exitcode = self.exitcode() 382beb6b57bSJohn Snow if (exitcode is not None and exitcode < 0 383beb6b57bSJohn Snow and not (self._user_killed and exitcode == -signal.SIGKILL)): 384beb6b57bSJohn Snow msg = 'qemu received signal %i; command: "%s"' 385beb6b57bSJohn Snow if self._qemu_full_args: 386beb6b57bSJohn Snow command = ' '.join(self._qemu_full_args) 387beb6b57bSJohn Snow else: 388beb6b57bSJohn Snow command = '' 389beb6b57bSJohn Snow LOG.warning(msg, -int(exitcode), command) 390beb6b57bSJohn Snow 391beb6b57bSJohn Snow self._user_killed = False 392beb6b57bSJohn Snow self._launched = False 393beb6b57bSJohn Snow 394beb6b57bSJohn Snow def launch(self) -> None: 395beb6b57bSJohn Snow """ 396beb6b57bSJohn Snow Launch the VM and make sure we cleanup and expose the 397beb6b57bSJohn Snow command line/output in case of exception 398beb6b57bSJohn Snow """ 399beb6b57bSJohn Snow 400beb6b57bSJohn Snow if self._launched: 401beb6b57bSJohn Snow raise QEMUMachineError('VM already launched') 402beb6b57bSJohn Snow 403beb6b57bSJohn Snow self._iolog = None 404beb6b57bSJohn Snow self._qemu_full_args = () 405beb6b57bSJohn Snow try: 406beb6b57bSJohn Snow self._launch() 407beb6b57bSJohn Snow self._launched = True 408beb6b57bSJohn Snow except: 409beb6b57bSJohn Snow self._post_shutdown() 410beb6b57bSJohn Snow 411beb6b57bSJohn Snow LOG.debug('Error launching VM') 412beb6b57bSJohn Snow if self._qemu_full_args: 413beb6b57bSJohn Snow LOG.debug('Command: %r', ' '.join(self._qemu_full_args)) 414beb6b57bSJohn Snow if self._iolog: 415beb6b57bSJohn Snow LOG.debug('Output: %r', self._iolog) 416beb6b57bSJohn Snow raise 417beb6b57bSJohn Snow 418beb6b57bSJohn Snow def _launch(self) -> None: 419beb6b57bSJohn Snow """ 420beb6b57bSJohn Snow Launch the VM and establish a QMP connection 421beb6b57bSJohn Snow """ 422beb6b57bSJohn Snow self._pre_launch() 423beb6b57bSJohn Snow self._qemu_full_args = tuple( 424beb6b57bSJohn Snow chain(self._wrapper, 425beb6b57bSJohn Snow [self._binary], 426beb6b57bSJohn Snow self._base_args, 427beb6b57bSJohn Snow self._args) 428beb6b57bSJohn Snow ) 429beb6b57bSJohn Snow LOG.debug('VM launch command: %r', ' '.join(self._qemu_full_args)) 430beb6b57bSJohn Snow 431beb6b57bSJohn Snow # Cleaning up of this subprocess is guaranteed by _do_shutdown. 432beb6b57bSJohn Snow # pylint: disable=consider-using-with 433beb6b57bSJohn Snow self._popen = subprocess.Popen(self._qemu_full_args, 434beb6b57bSJohn Snow stdin=subprocess.DEVNULL, 435beb6b57bSJohn Snow stdout=self._qemu_log_file, 436beb6b57bSJohn Snow stderr=subprocess.STDOUT, 437beb6b57bSJohn Snow shell=False, 438beb6b57bSJohn Snow close_fds=False) 439beb6b57bSJohn Snow self._post_launch() 440beb6b57bSJohn Snow 441beb6b57bSJohn Snow def _early_cleanup(self) -> None: 442beb6b57bSJohn Snow """ 443beb6b57bSJohn Snow Perform any cleanup that needs to happen before the VM exits. 444beb6b57bSJohn Snow 445beb6b57bSJohn Snow May be invoked by both soft and hard shutdown in failover scenarios. 446beb6b57bSJohn Snow Called additionally by _post_shutdown for comprehensive cleanup. 447beb6b57bSJohn Snow """ 448beb6b57bSJohn Snow # If we keep the console socket open, we may deadlock waiting 449beb6b57bSJohn Snow # for QEMU to exit, while QEMU is waiting for the socket to 450beb6b57bSJohn Snow # become writeable. 451beb6b57bSJohn Snow if self._console_socket is not None: 452beb6b57bSJohn Snow self._console_socket.close() 453beb6b57bSJohn Snow self._console_socket = None 454beb6b57bSJohn Snow 455beb6b57bSJohn Snow def _hard_shutdown(self) -> None: 456beb6b57bSJohn Snow """ 457beb6b57bSJohn Snow Perform early cleanup, kill the VM, and wait for it to terminate. 458beb6b57bSJohn Snow 459beb6b57bSJohn Snow :raise subprocess.Timeout: When timeout is exceeds 60 seconds 460beb6b57bSJohn Snow waiting for the QEMU process to terminate. 461beb6b57bSJohn Snow """ 462beb6b57bSJohn Snow self._early_cleanup() 463beb6b57bSJohn Snow self._subp.kill() 464beb6b57bSJohn Snow self._subp.wait(timeout=60) 465beb6b57bSJohn Snow 466beb6b57bSJohn Snow def _soft_shutdown(self, timeout: Optional[int], 467beb6b57bSJohn Snow has_quit: bool = False) -> None: 468beb6b57bSJohn Snow """ 469beb6b57bSJohn Snow Perform early cleanup, attempt to gracefully shut down the VM, and wait 470beb6b57bSJohn Snow for it to terminate. 471beb6b57bSJohn Snow 472beb6b57bSJohn Snow :param timeout: Timeout in seconds for graceful shutdown. 473beb6b57bSJohn Snow A value of None is an infinite wait. 474beb6b57bSJohn Snow :param has_quit: When True, don't attempt to issue 'quit' QMP command 475beb6b57bSJohn Snow 476beb6b57bSJohn Snow :raise ConnectionReset: On QMP communication errors 477beb6b57bSJohn Snow :raise subprocess.TimeoutExpired: When timeout is exceeded waiting for 478beb6b57bSJohn Snow the QEMU process to terminate. 479beb6b57bSJohn Snow """ 480beb6b57bSJohn Snow self._early_cleanup() 481beb6b57bSJohn Snow 482beb6b57bSJohn Snow if self._qmp_connection: 483beb6b57bSJohn Snow if not has_quit: 484beb6b57bSJohn Snow # Might raise ConnectionReset 485beb6b57bSJohn Snow self._qmp.cmd('quit') 486beb6b57bSJohn Snow 487beb6b57bSJohn Snow # May raise subprocess.TimeoutExpired 488beb6b57bSJohn Snow self._subp.wait(timeout=timeout) 489beb6b57bSJohn Snow 490beb6b57bSJohn Snow def _do_shutdown(self, timeout: Optional[int], 491beb6b57bSJohn Snow has_quit: bool = False) -> None: 492beb6b57bSJohn Snow """ 493beb6b57bSJohn Snow Attempt to shutdown the VM gracefully; fallback to a hard shutdown. 494beb6b57bSJohn Snow 495beb6b57bSJohn Snow :param timeout: Timeout in seconds for graceful shutdown. 496beb6b57bSJohn Snow A value of None is an infinite wait. 497beb6b57bSJohn Snow :param has_quit: When True, don't attempt to issue 'quit' QMP command 498beb6b57bSJohn Snow 499beb6b57bSJohn Snow :raise AbnormalShutdown: When the VM could not be shut down gracefully. 500beb6b57bSJohn Snow The inner exception will likely be ConnectionReset or 501beb6b57bSJohn Snow subprocess.TimeoutExpired. In rare cases, non-graceful termination 502beb6b57bSJohn Snow may result in its own exceptions, likely subprocess.TimeoutExpired. 503beb6b57bSJohn Snow """ 504beb6b57bSJohn Snow try: 505beb6b57bSJohn Snow self._soft_shutdown(timeout, has_quit) 506beb6b57bSJohn Snow except Exception as exc: 507beb6b57bSJohn Snow self._hard_shutdown() 508beb6b57bSJohn Snow raise AbnormalShutdown("Could not perform graceful shutdown") \ 509beb6b57bSJohn Snow from exc 510beb6b57bSJohn Snow 511beb6b57bSJohn Snow def shutdown(self, has_quit: bool = False, 512beb6b57bSJohn Snow hard: bool = False, 513beb6b57bSJohn Snow timeout: Optional[int] = 30) -> None: 514beb6b57bSJohn Snow """ 515beb6b57bSJohn Snow Terminate the VM (gracefully if possible) and perform cleanup. 516beb6b57bSJohn Snow Cleanup will always be performed. 517beb6b57bSJohn Snow 518beb6b57bSJohn Snow If the VM has not yet been launched, or shutdown(), wait(), or kill() 519beb6b57bSJohn Snow have already been called, this method does nothing. 520beb6b57bSJohn Snow 521beb6b57bSJohn Snow :param has_quit: When true, do not attempt to issue 'quit' QMP command. 522beb6b57bSJohn Snow :param hard: When true, do not attempt graceful shutdown, and 523beb6b57bSJohn Snow suppress the SIGKILL warning log message. 524beb6b57bSJohn Snow :param timeout: Optional timeout in seconds for graceful shutdown. 525beb6b57bSJohn Snow Default 30 seconds, A `None` value is an infinite wait. 526beb6b57bSJohn Snow """ 527beb6b57bSJohn Snow if not self._launched: 528beb6b57bSJohn Snow return 529beb6b57bSJohn Snow 530beb6b57bSJohn Snow try: 531beb6b57bSJohn Snow if hard: 532beb6b57bSJohn Snow self._user_killed = True 533beb6b57bSJohn Snow self._hard_shutdown() 534beb6b57bSJohn Snow else: 535beb6b57bSJohn Snow self._do_shutdown(timeout, has_quit) 536beb6b57bSJohn Snow finally: 537beb6b57bSJohn Snow self._post_shutdown() 538beb6b57bSJohn Snow 539beb6b57bSJohn Snow def kill(self) -> None: 540beb6b57bSJohn Snow """ 541beb6b57bSJohn Snow Terminate the VM forcefully, wait for it to exit, and perform cleanup. 542beb6b57bSJohn Snow """ 543beb6b57bSJohn Snow self.shutdown(hard=True) 544beb6b57bSJohn Snow 545beb6b57bSJohn Snow def wait(self, timeout: Optional[int] = 30) -> None: 546beb6b57bSJohn Snow """ 547beb6b57bSJohn Snow Wait for the VM to power off and perform post-shutdown cleanup. 548beb6b57bSJohn Snow 549beb6b57bSJohn Snow :param timeout: Optional timeout in seconds. Default 30 seconds. 550beb6b57bSJohn Snow A value of `None` is an infinite wait. 551beb6b57bSJohn Snow """ 552beb6b57bSJohn Snow self.shutdown(has_quit=True, timeout=timeout) 553beb6b57bSJohn Snow 554beb6b57bSJohn Snow def set_qmp_monitor(self, enabled: bool = True) -> None: 555beb6b57bSJohn Snow """ 556beb6b57bSJohn Snow Set the QMP monitor. 557beb6b57bSJohn Snow 558beb6b57bSJohn Snow @param enabled: if False, qmp monitor options will be removed from 559beb6b57bSJohn Snow the base arguments of the resulting QEMU command 560beb6b57bSJohn Snow line. Default is True. 5615c02c865SJohn Snow 5625c02c865SJohn Snow .. note:: Call this function before launch(). 563beb6b57bSJohn Snow """ 564beb6b57bSJohn Snow self._qmp_set = enabled 565beb6b57bSJohn Snow 566beb6b57bSJohn Snow @property 567beb6b57bSJohn Snow def _qmp(self) -> QEMUMonitorProtocol: 568beb6b57bSJohn Snow if self._qmp_connection is None: 569beb6b57bSJohn Snow raise QEMUMachineError("Attempt to access QMP with no connection") 570beb6b57bSJohn Snow return self._qmp_connection 571beb6b57bSJohn Snow 572beb6b57bSJohn Snow @classmethod 573beb6b57bSJohn Snow def _qmp_args(cls, _conv_keys: bool = True, **args: Any) -> Dict[str, Any]: 574beb6b57bSJohn Snow qmp_args = dict() 575beb6b57bSJohn Snow for key, value in args.items(): 576beb6b57bSJohn Snow if _conv_keys: 577beb6b57bSJohn Snow qmp_args[key.replace('_', '-')] = value 578beb6b57bSJohn Snow else: 579beb6b57bSJohn Snow qmp_args[key] = value 580beb6b57bSJohn Snow return qmp_args 581beb6b57bSJohn Snow 582beb6b57bSJohn Snow def qmp(self, cmd: str, 583beb6b57bSJohn Snow conv_keys: bool = True, 584beb6b57bSJohn Snow **args: Any) -> QMPMessage: 585beb6b57bSJohn Snow """ 586beb6b57bSJohn Snow Invoke a QMP command and return the response dict 587beb6b57bSJohn Snow """ 588beb6b57bSJohn Snow qmp_args = self._qmp_args(conv_keys, **args) 589beb6b57bSJohn Snow return self._qmp.cmd(cmd, args=qmp_args) 590beb6b57bSJohn Snow 591beb6b57bSJohn Snow def command(self, cmd: str, 592beb6b57bSJohn Snow conv_keys: bool = True, 593beb6b57bSJohn Snow **args: Any) -> QMPReturnValue: 594beb6b57bSJohn Snow """ 595beb6b57bSJohn Snow Invoke a QMP command. 596beb6b57bSJohn Snow On success return the response dict. 597beb6b57bSJohn Snow On failure raise an exception. 598beb6b57bSJohn Snow """ 599beb6b57bSJohn Snow qmp_args = self._qmp_args(conv_keys, **args) 600beb6b57bSJohn Snow return self._qmp.command(cmd, **qmp_args) 601beb6b57bSJohn Snow 602beb6b57bSJohn Snow def get_qmp_event(self, wait: bool = False) -> Optional[QMPMessage]: 603beb6b57bSJohn Snow """ 604beb6b57bSJohn Snow Poll for one queued QMP events and return it 605beb6b57bSJohn Snow """ 606beb6b57bSJohn Snow if self._events: 607beb6b57bSJohn Snow return self._events.pop(0) 608beb6b57bSJohn Snow return self._qmp.pull_event(wait=wait) 609beb6b57bSJohn Snow 610beb6b57bSJohn Snow def get_qmp_events(self, wait: bool = False) -> List[QMPMessage]: 611beb6b57bSJohn Snow """ 612beb6b57bSJohn Snow Poll for queued QMP events and return a list of dicts 613beb6b57bSJohn Snow """ 614beb6b57bSJohn Snow events = self._qmp.get_events(wait=wait) 615beb6b57bSJohn Snow events.extend(self._events) 616beb6b57bSJohn Snow del self._events[:] 617beb6b57bSJohn Snow self._qmp.clear_events() 618beb6b57bSJohn Snow return events 619beb6b57bSJohn Snow 620beb6b57bSJohn Snow @staticmethod 621beb6b57bSJohn Snow def event_match(event: Any, match: Optional[Any]) -> bool: 622beb6b57bSJohn Snow """ 623beb6b57bSJohn Snow Check if an event matches optional match criteria. 624beb6b57bSJohn Snow 625beb6b57bSJohn Snow The match criteria takes the form of a matching subdict. The event is 626beb6b57bSJohn Snow checked to be a superset of the subdict, recursively, with matching 627beb6b57bSJohn Snow values whenever the subdict values are not None. 628beb6b57bSJohn Snow 629beb6b57bSJohn Snow This has a limitation that you cannot explicitly check for None values. 630beb6b57bSJohn Snow 631beb6b57bSJohn Snow Examples, with the subdict queries on the left: 632beb6b57bSJohn Snow - None matches any object. 633beb6b57bSJohn Snow - {"foo": None} matches {"foo": {"bar": 1}} 634beb6b57bSJohn Snow - {"foo": None} matches {"foo": 5} 635beb6b57bSJohn Snow - {"foo": {"abc": None}} does not match {"foo": {"bar": 1}} 636beb6b57bSJohn Snow - {"foo": {"rab": 2}} matches {"foo": {"bar": 1, "rab": 2}} 637beb6b57bSJohn Snow """ 638beb6b57bSJohn Snow if match is None: 639beb6b57bSJohn Snow return True 640beb6b57bSJohn Snow 641beb6b57bSJohn Snow try: 642beb6b57bSJohn Snow for key in match: 643beb6b57bSJohn Snow if key in event: 644beb6b57bSJohn Snow if not QEMUMachine.event_match(event[key], match[key]): 645beb6b57bSJohn Snow return False 646beb6b57bSJohn Snow else: 647beb6b57bSJohn Snow return False 648beb6b57bSJohn Snow return True 649beb6b57bSJohn Snow except TypeError: 650beb6b57bSJohn Snow # either match or event wasn't iterable (not a dict) 651beb6b57bSJohn Snow return bool(match == event) 652beb6b57bSJohn Snow 653beb6b57bSJohn Snow def event_wait(self, name: str, 654beb6b57bSJohn Snow timeout: float = 60.0, 655beb6b57bSJohn Snow match: Optional[QMPMessage] = None) -> Optional[QMPMessage]: 656beb6b57bSJohn Snow """ 657beb6b57bSJohn Snow event_wait waits for and returns a named event from QMP with a timeout. 658beb6b57bSJohn Snow 659beb6b57bSJohn Snow name: The event to wait for. 660beb6b57bSJohn Snow timeout: QEMUMonitorProtocol.pull_event timeout parameter. 661beb6b57bSJohn Snow match: Optional match criteria. See event_match for details. 662beb6b57bSJohn Snow """ 663beb6b57bSJohn Snow return self.events_wait([(name, match)], timeout) 664beb6b57bSJohn Snow 665beb6b57bSJohn Snow def events_wait(self, 666beb6b57bSJohn Snow events: Sequence[Tuple[str, Any]], 667beb6b57bSJohn Snow timeout: float = 60.0) -> Optional[QMPMessage]: 668beb6b57bSJohn Snow """ 669beb6b57bSJohn Snow events_wait waits for and returns a single named event from QMP. 670beb6b57bSJohn Snow In the case of multiple qualifying events, this function returns the 671beb6b57bSJohn Snow first one. 672beb6b57bSJohn Snow 673beb6b57bSJohn Snow :param events: A sequence of (name, match_criteria) tuples. 674beb6b57bSJohn Snow The match criteria are optional and may be None. 675beb6b57bSJohn Snow See event_match for details. 676beb6b57bSJohn Snow :param timeout: Optional timeout, in seconds. 677beb6b57bSJohn Snow See QEMUMonitorProtocol.pull_event. 678beb6b57bSJohn Snow 679beb6b57bSJohn Snow :raise QMPTimeoutError: If timeout was non-zero and no matching events 680beb6b57bSJohn Snow were found. 681beb6b57bSJohn Snow :return: A QMP event matching the filter criteria. 682beb6b57bSJohn Snow If timeout was 0 and no event matched, None. 683beb6b57bSJohn Snow """ 684beb6b57bSJohn Snow def _match(event: QMPMessage) -> bool: 685beb6b57bSJohn Snow for name, match in events: 686beb6b57bSJohn Snow if event['event'] == name and self.event_match(event, match): 687beb6b57bSJohn Snow return True 688beb6b57bSJohn Snow return False 689beb6b57bSJohn Snow 690beb6b57bSJohn Snow event: Optional[QMPMessage] 691beb6b57bSJohn Snow 692beb6b57bSJohn Snow # Search cached events 693beb6b57bSJohn Snow for event in self._events: 694beb6b57bSJohn Snow if _match(event): 695beb6b57bSJohn Snow self._events.remove(event) 696beb6b57bSJohn Snow return event 697beb6b57bSJohn Snow 698beb6b57bSJohn Snow # Poll for new events 699beb6b57bSJohn Snow while True: 700beb6b57bSJohn Snow event = self._qmp.pull_event(wait=timeout) 701beb6b57bSJohn Snow if event is None: 702beb6b57bSJohn Snow # NB: None is only returned when timeout is false-ish. 703beb6b57bSJohn Snow # Timeouts raise QMPTimeoutError instead! 704beb6b57bSJohn Snow break 705beb6b57bSJohn Snow if _match(event): 706beb6b57bSJohn Snow return event 707beb6b57bSJohn Snow self._events.append(event) 708beb6b57bSJohn Snow 709beb6b57bSJohn Snow return None 710beb6b57bSJohn Snow 711beb6b57bSJohn Snow def get_log(self) -> Optional[str]: 712beb6b57bSJohn Snow """ 713beb6b57bSJohn Snow After self.shutdown or failed qemu execution, this returns the output 714beb6b57bSJohn Snow of the qemu process. 715beb6b57bSJohn Snow """ 716beb6b57bSJohn Snow return self._iolog 717beb6b57bSJohn Snow 718beb6b57bSJohn Snow def add_args(self, *args: str) -> None: 719beb6b57bSJohn Snow """ 720beb6b57bSJohn Snow Adds to the list of extra arguments to be given to the QEMU binary 721beb6b57bSJohn Snow """ 722beb6b57bSJohn Snow self._args.extend(args) 723beb6b57bSJohn Snow 724beb6b57bSJohn Snow def set_machine(self, machine_type: str) -> None: 725beb6b57bSJohn Snow """ 726beb6b57bSJohn Snow Sets the machine type 727beb6b57bSJohn Snow 728beb6b57bSJohn Snow If set, the machine type will be added to the base arguments 729beb6b57bSJohn Snow of the resulting QEMU command line. 730beb6b57bSJohn Snow """ 731beb6b57bSJohn Snow self._machine = machine_type 732beb6b57bSJohn Snow 733beb6b57bSJohn Snow def set_console(self, 734beb6b57bSJohn Snow device_type: Optional[str] = None, 735beb6b57bSJohn Snow console_index: int = 0) -> None: 736beb6b57bSJohn Snow """ 737beb6b57bSJohn Snow Sets the device type for a console device 738beb6b57bSJohn Snow 739beb6b57bSJohn Snow If set, the console device and a backing character device will 740beb6b57bSJohn Snow be added to the base arguments of the resulting QEMU command 741beb6b57bSJohn Snow line. 742beb6b57bSJohn Snow 743beb6b57bSJohn Snow This is a convenience method that will either use the provided 744beb6b57bSJohn Snow device type, or default to a "-serial chardev:console" command 745beb6b57bSJohn Snow line argument. 746beb6b57bSJohn Snow 747beb6b57bSJohn Snow The actual setting of command line arguments will be be done at 748beb6b57bSJohn Snow machine launch time, as it depends on the temporary directory 749beb6b57bSJohn Snow to be created. 750beb6b57bSJohn Snow 751beb6b57bSJohn Snow @param device_type: the device type, such as "isa-serial". If 752beb6b57bSJohn Snow None is given (the default value) a "-serial 753beb6b57bSJohn Snow chardev:console" command line argument will 754beb6b57bSJohn Snow be used instead, resorting to the machine's 755beb6b57bSJohn Snow default device type. 756beb6b57bSJohn Snow @param console_index: the index of the console device to use. 757beb6b57bSJohn Snow If not zero, the command line will create 758beb6b57bSJohn Snow 'index - 1' consoles and connect them to 759beb6b57bSJohn Snow the 'null' backing character device. 760beb6b57bSJohn Snow """ 761beb6b57bSJohn Snow self._console_set = True 762beb6b57bSJohn Snow self._console_device_type = device_type 763beb6b57bSJohn Snow self._console_index = console_index 764beb6b57bSJohn Snow 765beb6b57bSJohn Snow @property 766beb6b57bSJohn Snow def console_socket(self) -> socket.socket: 767beb6b57bSJohn Snow """ 768beb6b57bSJohn Snow Returns a socket connected to the console 769beb6b57bSJohn Snow """ 770beb6b57bSJohn Snow if self._console_socket is None: 771beb6b57bSJohn Snow self._console_socket = console_socket.ConsoleSocket( 772beb6b57bSJohn Snow self._console_address, 773beb6b57bSJohn Snow file=self._console_log_path, 774beb6b57bSJohn Snow drain=self._drain_console) 775beb6b57bSJohn Snow return self._console_socket 776beb6b57bSJohn Snow 777beb6b57bSJohn Snow @property 778beb6b57bSJohn Snow def temp_dir(self) -> str: 779beb6b57bSJohn Snow """ 780beb6b57bSJohn Snow Returns a temporary directory to be used for this machine 781beb6b57bSJohn Snow """ 782beb6b57bSJohn Snow if self._temp_dir is None: 783beb6b57bSJohn Snow self._temp_dir = tempfile.mkdtemp(prefix="qemu-machine-", 784beb6b57bSJohn Snow dir=self._base_temp_dir) 785beb6b57bSJohn Snow return self._temp_dir 786b306e26cSCleber Rosa 787b306e26cSCleber Rosa @property 788b306e26cSCleber Rosa def log_dir(self) -> str: 789b306e26cSCleber Rosa """ 790b306e26cSCleber Rosa Returns a directory to be used for writing logs 791b306e26cSCleber Rosa """ 792b306e26cSCleber Rosa if self._log_dir is None: 793b306e26cSCleber Rosa return self.temp_dir 794b306e26cSCleber Rosa return self._log_dir 795