]> git.ipfire.org Git - thirdparty/qemu.git/blame - python/qemu/machine/machine.py
Merge tag 'accel-20250715' of https://github.com/philmd/qemu into staging
[thirdparty/qemu.git] / python / qemu / machine / machine.py
CommitLineData
306dfcd6
JS
1"""
2QEMU machine module:
3
4The machine module primarily provides the QEMUMachine class,
5which provides facilities for managing the lifetime of a QEMU VM.
6"""
7
abf0bf99
JS
8# Copyright (C) 2015-2016 Red Hat Inc.
9# Copyright (C) 2012 IBM Corp.
10#
11# Authors:
12# Fam Zheng <famz@redhat.com>
13#
14# This work is licensed under the terms of the GNU GPL, version 2. See
15# the COPYING file in the top-level directory.
16#
17# Based on qmp.py.
18#
19
20import errno
aad3f3bb 21from itertools import chain
5690b437 22import locale
abf0bf99
JS
23import logging
24import os
abf0bf99 25import shutil
de6e08b5 26import signal
f12a282f 27import socket
932ca4bb 28import subprocess
abf0bf99 29import tempfile
1dda0404 30from types import TracebackType
aaa81ec6
JS
31from typing import (
32 Any,
f12a282f 33 BinaryIO,
aaa81ec6
JS
34 Dict,
35 List,
36 Optional,
aad3f3bb
JS
37 Sequence,
38 Tuple,
aaa81ec6 39 Type,
15c3b863 40 TypeVar,
aaa81ec6 41)
932ca4bb 42
37094b6d
JS
43from qemu.qmp import SocketAddrT
44from qemu.qmp.legacy import (
a4225303 45 QEMUMonitorProtocol,
beb6b57b
JS
46 QMPMessage,
47 QMPReturnValue,
beb6b57b
JS
48)
49
50from . import console_socket
abf0bf99 51
abf0bf99
JS
52
53LOG = logging.getLogger(__name__)
54
8dfac2ed 55
abf0bf99
JS
56class QEMUMachineError(Exception):
57 """
58 Exception called when an error in QEMUMachine happens.
59 """
60
61
62class QEMUMachineAddDeviceError(QEMUMachineError):
63 """
64 Exception raised when a request to add a device can not be fulfilled
65
66 The failures are caused by limitations, lack of information or conflicting
67 requests on the QEMUMachine methods. This exception does not represent
68 failures reported by the QEMU binary itself.
69 """
70
71
50465f94
JS
72class VMLaunchFailure(QEMUMachineError):
73 """
74 Exception raised when a VM launch was attempted, but failed.
75 """
76 def __init__(self, exitcode: Optional[int],
77 command: str, output: Optional[str]):
78 super().__init__(exitcode, command, output)
79 self.exitcode = exitcode
80 self.command = command
81 self.output = output
82
83 def __str__(self) -> str:
84 ret = ''
85 if self.__cause__ is not None:
86 name = type(self.__cause__).__name__
87 reason = str(self.__cause__)
88 if reason:
89 ret += f"{name}: {reason}"
90 else:
91 ret += f"{name}"
92 ret += '\n'
93
94 if self.exitcode is not None:
95 ret += f"\tExit code: {self.exitcode}\n"
96 ret += f"\tCommand: {self.command}\n"
97 ret += f"\tOutput: {self.output}\n"
98 return ret
99
100
193bf1c0
JS
101class AbnormalShutdown(QEMUMachineError):
102 """
103 Exception raised when a graceful shutdown was requested, but not performed.
104 """
105
106
15c3b863
VSO
107_T = TypeVar('_T', bound='QEMUMachine')
108
109
9b8ccd6d 110class QEMUMachine:
abf0bf99 111 """
f12a282f 112 A QEMU VM.
abf0bf99 113
8dfac2ed
JS
114 Use this object as a context manager to ensure
115 the QEMU process terminates::
abf0bf99
JS
116
117 with VM(binary) as vm:
118 ...
119 # vm is guaranteed to be shut down here
120 """
82e6517d 121 # pylint: disable=too-many-instance-attributes, too-many-public-methods
abf0bf99 122
aad3f3bb
JS
123 def __init__(self,
124 binary: str,
125 args: Sequence[str] = (),
126 wrapper: Sequence[str] = (),
127 name: Optional[str] = None,
2ca6e26c 128 base_temp_dir: str = "/var/tmp",
c4e6023f 129 monitor_address: Optional[SocketAddrT] = None,
f12a282f 130 drain_console: bool = False,
b306e26c 131 console_log: Optional[str] = None,
e2f948a8 132 log_dir: Optional[str] = None,
ada73a49 133 qmp_timer: Optional[float] = 30):
abf0bf99
JS
134 '''
135 Initialize a QEMUMachine
136
137 @param binary: path to the qemu binary
138 @param args: list of extra arguments
139 @param wrapper: list of arguments used as prefix to qemu binary
140 @param name: prefix for socket and log file names (default: qemu-PID)
859aeb67 141 @param base_temp_dir: default location where temp files are created
abf0bf99 142 @param monitor_address: address for QMP monitor
0fc8f660 143 @param drain_console: (optional) True to drain console socket to buffer
c5e61a6d 144 @param console_log: (optional) path to console log file
b306e26c 145 @param log_dir: where to create and keep log files
e2f948a8 146 @param qmp_timer: (optional) default QMP socket timeout
abf0bf99
JS
147 @note: Qemu process is not started until launch() is used.
148 '''
82e6517d
JS
149 # pylint: disable=too-many-arguments
150
c5e61a6d
JS
151 # Direct user configuration
152
153 self._binary = binary
c5e61a6d 154 self._args = list(args)
c5e61a6d 155 self._wrapper = wrapper
e2f948a8 156 self._qmp_timer = qmp_timer
c5e61a6d 157
f9922937 158 self._name = name or f"{id(self):x}"
bd4c0ef4 159 self._sock_pair: Optional[Tuple[socket.socket, socket.socket]] = None
1d4796cd
JS
160 self._cons_sock_pair: Optional[
161 Tuple[socket.socket, socket.socket]] = None
87bf1fe5 162 self._temp_dir: Optional[str] = None
2ca6e26c 163 self._base_temp_dir = base_temp_dir
b306e26c 164 self._log_dir = log_dir
c5e61a6d 165
bd4c0ef4 166 self._monitor_address = monitor_address
c5e61a6d
JS
167
168 self._console_log_path = console_log
169 if self._console_log_path:
170 # In order to log the console, buffering needs to be enabled.
171 self._drain_console = True
172 else:
173 self._drain_console = drain_console
174
175 # Runstate
f12a282f
JS
176 self._qemu_log_path: Optional[str] = None
177 self._qemu_log_file: Optional[BinaryIO] = None
9223fda4 178 self._popen: Optional['subprocess.Popen[bytes]'] = None
f12a282f
JS
179 self._events: List[QMPMessage] = []
180 self._iolog: Optional[str] = None
74b56bb5 181 self._qmp_set = True # Enable QMP monitor by default.
beb6b57b 182 self._qmp_connection: Optional[QEMUMonitorProtocol] = None
aad3f3bb 183 self._qemu_full_args: Tuple[str, ...] = ()
abf0bf99 184 self._launched = False
f12a282f 185 self._machine: Optional[str] = None
746f244d 186 self._console_index = 0
abf0bf99 187 self._console_set = False
f12a282f 188 self._console_device_type: Optional[str] = None
f12a282f 189 self._console_socket: Optional[socket.socket] = None
f0ec14c7 190 self._console_file: Optional[socket.SocketIO] = None
f12a282f 191 self._remove_files: List[str] = []
de6e08b5 192 self._user_killed = False
b9420e4f 193 self._quit_issued = False
abf0bf99 194
15c3b863 195 def __enter__(self: _T) -> _T:
abf0bf99
JS
196 return self
197
1dda0404
JS
198 def __exit__(self,
199 exc_type: Optional[Type[BaseException]],
200 exc_val: Optional[BaseException],
201 exc_tb: Optional[TracebackType]) -> None:
abf0bf99 202 self.shutdown()
abf0bf99 203
f12a282f 204 def add_monitor_null(self) -> None:
306dfcd6
JS
205 """
206 This can be used to add an unused monitor instance.
207 """
abf0bf99
JS
208 self._args.append('-monitor')
209 self._args.append('null')
210
15c3b863
VSO
211 def add_fd(self: _T, fd: int, fdset: int,
212 opaque: str, opts: str = '') -> _T:
abf0bf99
JS
213 """
214 Pass a file descriptor to the VM
215 """
216 options = ['fd=%d' % fd,
217 'set=%d' % fdset,
218 'opaque=%s' % opaque]
219 if opts:
220 options.append(opts)
221
222 # This did not exist before 3.4, but since then it is
223 # mandatory for our purpose
224 if hasattr(os, 'set_inheritable'):
225 os.set_inheritable(fd, True)
226
227 self._args.append('-add-fd')
228 self._args.append(','.join(options))
229 return self
230
f12a282f
JS
231 def send_fd_scm(self, fd: Optional[int] = None,
232 file_path: Optional[str] = None) -> int:
306dfcd6 233 """
514d00df 234 Send an fd or file_path to the remote via SCM_RIGHTS.
306dfcd6 235
514d00df
JS
236 Exactly one of fd and file_path must be given. If it is
237 file_path, the file will be opened read-only and the new file
238 descriptor will be sent to the remote.
306dfcd6 239 """
abf0bf99
JS
240 if file_path is not None:
241 assert fd is None
514d00df
JS
242 with open(file_path, "rb") as passfile:
243 fd = passfile.fileno()
244 self._qmp.send_fd_scm(fd)
abf0bf99
JS
245 else:
246 assert fd is not None
514d00df 247 self._qmp.send_fd_scm(fd)
abf0bf99 248
514d00df 249 return 0
abf0bf99
JS
250
251 @staticmethod
f12a282f 252 def _remove_if_exists(path: str) -> None:
abf0bf99
JS
253 """
254 Remove file object at path if it exists
255 """
256 try:
257 os.remove(path)
258 except OSError as exception:
259 if exception.errno == errno.ENOENT:
260 return
261 raise
262
f12a282f 263 def is_running(self) -> bool:
306dfcd6 264 """Returns true if the VM is running."""
abf0bf99
JS
265 return self._popen is not None and self._popen.poll() is None
266
9223fda4
JS
267 @property
268 def _subp(self) -> 'subprocess.Popen[bytes]':
269 if self._popen is None:
270 raise QEMUMachineError('Subprocess pipe not present')
271 return self._popen
272
f12a282f 273 def exitcode(self) -> Optional[int]:
306dfcd6 274 """Returns the exit code if possible, or None."""
abf0bf99
JS
275 if self._popen is None:
276 return None
277 return self._popen.poll()
278
f12a282f 279 def get_pid(self) -> Optional[int]:
306dfcd6 280 """Returns the PID of the running process, or None."""
abf0bf99
JS
281 if not self.is_running():
282 return None
9223fda4 283 return self._subp.pid
abf0bf99 284
f12a282f 285 def _load_io_log(self) -> None:
5690b437
JS
286 # Assume that the output encoding of QEMU's terminal output is
287 # defined by our locale. If indeterminate, allow open() to fall
288 # back to the platform default.
289 _, encoding = locale.getlocale()
abf0bf99 290 if self._qemu_log_path is not None:
5690b437 291 with open(self._qemu_log_path, "r", encoding=encoding) as iolog:
abf0bf99
JS
292 self._iolog = iolog.read()
293
652809df
JS
294 @property
295 def _base_args(self) -> List[str]:
74b56bb5 296 args = ['-display', 'none', '-vga', 'none']
c4e6023f 297
74b56bb5 298 if self._qmp_set:
bd4c0ef4 299 if self._sock_pair:
91e11db7 300 moncdev = f"socket,id=mon,fd={self._sock_pair[0].fileno()}"
bd4c0ef4 301 elif isinstance(self._monitor_address, tuple):
c4e6023f
JS
302 moncdev = "socket,id=mon,host={},port={}".format(
303 *self._monitor_address
304 )
74b56bb5 305 else:
c4e6023f 306 moncdev = f"socket,id=mon,path={self._monitor_address}"
74b56bb5
WSM
307 args.extend(['-chardev', moncdev, '-mon',
308 'chardev=mon,mode=control'])
c4e6023f 309
abf0bf99
JS
310 if self._machine is not None:
311 args.extend(['-machine', self._machine])
9b8ccd6d 312 for _ in range(self._console_index):
746f244d 313 args.extend(['-serial', 'null'])
abf0bf99 314 if self._console_set:
1d4796cd
JS
315 assert self._cons_sock_pair is not None
316 fd = self._cons_sock_pair[0].fileno()
317 chardev = f"socket,id=console,fd={fd}"
abf0bf99
JS
318 args.extend(['-chardev', chardev])
319 if self._console_device_type is None:
320 args.extend(['-serial', 'chardev:console'])
321 else:
322 device = '%s,chardev=console' % self._console_device_type
323 args.extend(['-device', device])
324 return args
325
555fe0c2
WSM
326 @property
327 def args(self) -> List[str]:
328 """Returns the list of arguments given to the QEMU binary."""
329 return self._args
330
33956e47
MD
331 @property
332 def binary(self) -> str:
333 """Returns path to the QEMU binary"""
334 return self._binary
335
f12a282f 336 def _pre_launch(self) -> None:
74b56bb5 337 if self._qmp_set:
84e327e8 338 sock = None
bd4c0ef4
MAL
339 if self._monitor_address is None:
340 self._sock_pair = socket.socketpair()
91e11db7 341 os.set_inheritable(self._sock_pair[0].fileno(), True)
bd4c0ef4 342 sock = self._sock_pair[1]
6eeb3de7 343 if isinstance(self._monitor_address, str):
c4e6023f 344 self._remove_files.append(self._monitor_address)
7f5f3ae7 345
5bbc5936
JS
346 sock_or_addr = self._monitor_address or sock
347 assert sock_or_addr is not None
348
beb6b57b 349 self._qmp_connection = QEMUMonitorProtocol(
5bbc5936 350 sock_or_addr,
7f5f3ae7 351 server=bool(self._monitor_address),
c4e6023f
JS
352 nickname=self._name
353 )
abf0bf99 354
1d4796cd
JS
355 if self._console_set:
356 self._cons_sock_pair = socket.socketpair()
357 os.set_inheritable(self._cons_sock_pair[0].fileno(), True)
358
63c33f3c
JS
359 # NOTE: Make sure any opened resources are *definitely* freed in
360 # _post_shutdown()!
361 # pylint: disable=consider-using-with
b306e26c 362 self._qemu_log_path = os.path.join(self.log_dir, self._name + ".log")
63c33f3c
JS
363 self._qemu_log_file = open(self._qemu_log_path, 'wb')
364
b1ca9919
JS
365 self._iolog = None
366 self._qemu_full_args = tuple(chain(
367 self._wrapper,
368 [self._binary],
369 self._base_args,
370 self._args
371 ))
372
f12a282f 373 def _post_launch(self) -> None:
bd4c0ef4
MAL
374 if self._sock_pair:
375 self._sock_pair[0].close()
1d4796cd
JS
376 if self._cons_sock_pair:
377 self._cons_sock_pair[0].close()
378
be1183e5 379 if self._qmp_connection:
7f5f3ae7
JS
380 if self._sock_pair:
381 self._qmp.connect()
382 else:
383 self._qmp.accept(self._qmp_timer)
abf0bf99 384
eb7a91d0
EGE
385 def _close_qemu_log_file(self) -> None:
386 if self._qemu_log_file is not None:
387 self._qemu_log_file.close()
388 self._qemu_log_file = None
389
f12a282f 390 def _post_shutdown(self) -> None:
a3842cb0
JS
391 """
392 Called to cleanup the VM instance after the process has exited.
393 May also be called after a failed launch.
394 """
9cccb330 395 LOG.debug("Cleaning up after VM process")
49a608b8
JS
396 try:
397 self._close_qmp_connection()
398 except Exception as err: # pylint: disable=broad-except
399 LOG.warning(
400 "Exception closing QMP connection: %s",
401 str(err) if str(err) else type(err).__name__
402 )
403 finally:
404 assert self._qmp_connection is None
671940e6 405
612b3ba2
JS
406 if self._sock_pair:
407 self._sock_pair[0].close()
408 self._sock_pair[1].close()
409 self._sock_pair = None
410
eb7a91d0 411 self._close_qemu_log_file()
abf0bf99 412
3c1e16c6
CR
413 self._load_io_log()
414
abf0bf99
JS
415 self._qemu_log_path = None
416
abf0bf99
JS
417 if self._temp_dir is not None:
418 shutil.rmtree(self._temp_dir)
419 self._temp_dir = None
420
32558ce7
HR
421 while len(self._remove_files) > 0:
422 self._remove_if_exists(self._remove_files.pop())
423
14661d93 424 exitcode = self.exitcode()
de6e08b5
JS
425 if (exitcode is not None and exitcode < 0
426 and not (self._user_killed and exitcode == -signal.SIGKILL)):
14661d93
JS
427 msg = 'qemu received signal %i; command: "%s"'
428 if self._qemu_full_args:
429 command = ' '.join(self._qemu_full_args)
430 else:
431 command = ''
432 LOG.warning(msg, -int(exitcode), command)
433
b9420e4f 434 self._quit_issued = False
de6e08b5 435 self._user_killed = False
14661d93
JS
436 self._launched = False
437
f12a282f 438 def launch(self) -> None:
abf0bf99
JS
439 """
440 Launch the VM and make sure we cleanup and expose the
441 command line/output in case of exception
442 """
443
444 if self._launched:
445 raise QEMUMachineError('VM already launched')
446
abf0bf99
JS
447 try:
448 self._launch()
50465f94 449 except BaseException as exc:
1611e6cf
JS
450 # We may have launched the process but it may
451 # have exited before we could connect via QMP.
452 # Assume the VM didn't launch or is exiting.
453 # If we don't wait for the process, exitcode() may still be
454 # 'None' by the time control is ceded back to the caller.
455 if self._launched:
456 self.wait()
457 else:
458 self._post_shutdown()
abf0bf99 459
50465f94
JS
460 if isinstance(exc, Exception):
461 raise VMLaunchFailure(
462 exitcode=self.exitcode(),
463 command=' '.join(self._qemu_full_args),
464 output=self._iolog
465 ) from exc
466
467 # Don't wrap 'BaseException'; doing so would downgrade
468 # that exception. However, we still want to clean up.
abf0bf99
JS
469 raise
470
f12a282f 471 def _launch(self) -> None:
abf0bf99
JS
472 """
473 Launch the VM and establish a QMP connection
474 """
abf0bf99 475 self._pre_launch()
abf0bf99 476 LOG.debug('VM launch command: %r', ' '.join(self._qemu_full_args))
a0eae17a
JS
477
478 # Cleaning up of this subprocess is guaranteed by _do_shutdown.
479 # pylint: disable=consider-using-with
abf0bf99 480 self._popen = subprocess.Popen(self._qemu_full_args,
07b71233 481 stdin=subprocess.DEVNULL,
abf0bf99
JS
482 stdout=self._qemu_log_file,
483 stderr=subprocess.STDOUT,
484 shell=False,
485 close_fds=False)
1611e6cf 486 self._launched = True
abf0bf99
JS
487 self._post_launch()
488
49a608b8
JS
489 def _close_qmp_connection(self) -> None:
490 """
491 Close the underlying QMP connection, if any.
492
493 Dutifully report errors that occurred while closing, but assume
494 that any error encountered indicates an abnormal termination
495 process and not a failure to close.
496 """
497 if self._qmp_connection is None:
498 return
499
500 try:
501 self._qmp.close()
502 except EOFError:
503 # EOF can occur as an Exception here when using the Async
504 # QMP backend. It indicates that the server closed the
505 # stream. If we successfully issued 'quit' at any point,
506 # then this was expected. If the remote went away without
507 # our permission, it's worth reporting that as an abnormal
508 # shutdown case.
509 if not (self._user_killed or self._quit_issued):
510 raise
511 finally:
512 self._qmp_connection = None
513
e2c97f16
JS
514 def _early_cleanup(self) -> None:
515 """
516 Perform any cleanup that needs to happen before the VM exits.
a3842cb0 517
1611e6cf
JS
518 This method may be called twice upon shutdown, once each by soft
519 and hard shutdown in failover scenarios.
e2c97f16
JS
520 """
521 # If we keep the console socket open, we may deadlock waiting
522 # for QEMU to exit, while QEMU is waiting for the socket to
9323e79f 523 # become writable.
f0ec14c7
NP
524 if self._console_file is not None:
525 LOG.debug("Closing console file")
526 self._console_file.close()
527 self._console_file = None
528
e2c97f16 529 if self._console_socket is not None:
9cccb330 530 LOG.debug("Closing console socket")
e2c97f16
JS
531 self._console_socket.close()
532 self._console_socket = None
533
1d4796cd
JS
534 if self._cons_sock_pair:
535 self._cons_sock_pair[0].close()
536 self._cons_sock_pair[1].close()
537 self._cons_sock_pair = None
538
193bf1c0
JS
539 def _hard_shutdown(self) -> None:
540 """
541 Perform early cleanup, kill the VM, and wait for it to terminate.
542
543 :raise subprocess.Timeout: When timeout is exceeds 60 seconds
544 waiting for the QEMU process to terminate.
545 """
9cccb330 546 LOG.debug("Performing hard shutdown")
193bf1c0 547 self._early_cleanup()
9223fda4
JS
548 self._subp.kill()
549 self._subp.wait(timeout=60)
193bf1c0 550
b9420e4f 551 def _soft_shutdown(self, timeout: Optional[int]) -> None:
193bf1c0
JS
552 """
553 Perform early cleanup, attempt to gracefully shut down the VM, and wait
554 for it to terminate.
555
8226a4b8
JS
556 :param timeout: Timeout in seconds for graceful shutdown.
557 A value of None is an infinite wait.
193bf1c0
JS
558
559 :raise ConnectionReset: On QMP communication errors
560 :raise subprocess.TimeoutExpired: When timeout is exceeded waiting for
561 the QEMU process to terminate.
562 """
9cccb330
JS
563 LOG.debug("Attempting graceful termination")
564
193bf1c0
JS
565 self._early_cleanup()
566
9cccb330
JS
567 if self._quit_issued:
568 LOG.debug(
569 "Anticipating QEMU termination due to prior 'quit' command, "
570 "or explicit call to wait()"
571 )
572 else:
573 LOG.debug("Politely asking QEMU to terminate")
574
be1183e5 575 if self._qmp_connection:
49a608b8
JS
576 try:
577 if not self._quit_issued:
578 # May raise ExecInterruptedError or StateError if the
579 # connection dies or has *already* died.
580 self.qmp('quit')
581 finally:
582 # Regardless, we want to quiesce the connection.
583 self._close_qmp_connection()
3c6e5e8c
JS
584 elif not self._quit_issued:
585 LOG.debug(
586 "Not anticipating QEMU quit and no QMP connection present, "
587 "issuing SIGTERM"
588 )
589 self._subp.terminate()
193bf1c0
JS
590
591 # May raise subprocess.TimeoutExpired
9cccb330
JS
592 LOG.debug(
593 "Waiting (timeout=%s) for QEMU process (pid=%s) to terminate",
594 timeout, self._subp.pid
595 )
9223fda4 596 self._subp.wait(timeout=timeout)
193bf1c0 597
b9420e4f 598 def _do_shutdown(self, timeout: Optional[int]) -> None:
193bf1c0
JS
599 """
600 Attempt to shutdown the VM gracefully; fallback to a hard shutdown.
601
8226a4b8
JS
602 :param timeout: Timeout in seconds for graceful shutdown.
603 A value of None is an infinite wait.
193bf1c0
JS
604
605 :raise AbnormalShutdown: When the VM could not be shut down gracefully.
606 The inner exception will likely be ConnectionReset or
607 subprocess.TimeoutExpired. In rare cases, non-graceful termination
608 may result in its own exceptions, likely subprocess.TimeoutExpired.
609 """
610 try:
b9420e4f 611 self._soft_shutdown(timeout)
193bf1c0 612 except Exception as exc:
9cccb330
JS
613 if isinstance(exc, subprocess.TimeoutExpired):
614 LOG.debug("Timed out waiting for QEMU process to exit")
615 LOG.debug("Graceful shutdown failed", exc_info=True)
616 LOG.debug("Falling back to hard shutdown")
193bf1c0
JS
617 self._hard_shutdown()
618 raise AbnormalShutdown("Could not perform graceful shutdown") \
619 from exc
620
b9420e4f 621 def shutdown(self,
c9b3045b 622 hard: bool = False,
8226a4b8 623 timeout: Optional[int] = 30) -> None:
abf0bf99 624 """
193bf1c0
JS
625 Terminate the VM (gracefully if possible) and perform cleanup.
626 Cleanup will always be performed.
627
628 If the VM has not yet been launched, or shutdown(), wait(), or kill()
629 have already been called, this method does nothing.
630
193bf1c0
JS
631 :param hard: When true, do not attempt graceful shutdown, and
632 suppress the SIGKILL warning log message.
633 :param timeout: Optional timeout in seconds for graceful shutdown.
8226a4b8 634 Default 30 seconds, A `None` value is an infinite wait.
abf0bf99 635 """
a3842cb0
JS
636 if not self._launched:
637 return
638
9cccb330
JS
639 LOG.debug("Shutting down VM appliance; timeout=%s", timeout)
640 if hard:
641 LOG.debug("Caller requests immediate termination of QEMU process.")
642
193bf1c0 643 try:
e0e925a6 644 if hard:
de6e08b5 645 self._user_killed = True
193bf1c0
JS
646 self._hard_shutdown()
647 else:
b9420e4f 648 self._do_shutdown(timeout)
193bf1c0
JS
649 finally:
650 self._post_shutdown()
abf0bf99 651
f12a282f 652 def kill(self) -> None:
193bf1c0
JS
653 """
654 Terminate the VM forcefully, wait for it to exit, and perform cleanup.
655 """
e0e925a6
VSO
656 self.shutdown(hard=True)
657
8226a4b8 658 def wait(self, timeout: Optional[int] = 30) -> None:
89528059
JS
659 """
660 Wait for the VM to power off and perform post-shutdown cleanup.
661
8226a4b8
JS
662 :param timeout: Optional timeout in seconds. Default 30 seconds.
663 A value of `None` is an infinite wait.
89528059 664 """
b9420e4f
JS
665 self._quit_issued = True
666 self.shutdown(timeout=timeout)
89528059 667
f12a282f 668 def set_qmp_monitor(self, enabled: bool = True) -> None:
74b56bb5
WSM
669 """
670 Set the QMP monitor.
671
672 @param enabled: if False, qmp monitor options will be removed from
673 the base arguments of the resulting QEMU command
674 line. Default is True.
5c02c865
JS
675
676 .. note:: Call this function before launch().
74b56bb5 677 """
be1183e5
JS
678 self._qmp_set = enabled
679
680 @property
beb6b57b 681 def _qmp(self) -> QEMUMonitorProtocol:
be1183e5
JS
682 if self._qmp_connection is None:
683 raise QEMUMachineError("Attempt to access QMP with no connection")
684 return self._qmp_connection
74b56bb5 685
aaa81ec6 686 @classmethod
c7daa57e
VSO
687 def _qmp_args(cls, conv_keys: bool,
688 args: Dict[str, Any]) -> Dict[str, object]:
689 if conv_keys:
690 return {k.replace('_', '-'): v for k, v in args.items()}
691
692 return args
abf0bf99 693
aaa81ec6 694 def qmp(self, cmd: str,
3f3c9b4c
VSO
695 args_dict: Optional[Dict[str, object]] = None,
696 conv_keys: Optional[bool] = None,
aaa81ec6
JS
697 **args: Any) -> QMPMessage:
698 """
699 Invoke a QMP command and return the response dict
700 """
3f3c9b4c
VSO
701 if args_dict is not None:
702 assert not args
703 assert conv_keys is None
704 args = args_dict
705 conv_keys = False
706
707 if conv_keys is None:
708 conv_keys = True
709
c7daa57e 710 qmp_args = self._qmp_args(conv_keys, args)
37274707 711 ret = self._qmp.cmd_raw(cmd, args=qmp_args)
b9420e4f
JS
712 if cmd == 'quit' and 'error' not in ret and 'return' in ret:
713 self._quit_issued = True
714 return ret
abf0bf99 715
684750ab 716 def cmd(self, cmd: str,
4e620ff4
VSO
717 args_dict: Optional[Dict[str, object]] = None,
718 conv_keys: Optional[bool] = None,
684750ab 719 **args: Any) -> QMPReturnValue:
abf0bf99
JS
720 """
721 Invoke a QMP command.
722 On success return the response dict.
723 On failure raise an exception.
724 """
4e620ff4
VSO
725 if args_dict is not None:
726 assert not args
727 assert conv_keys is None
728 args = args_dict
729 conv_keys = False
730
731 if conv_keys is None:
732 conv_keys = True
733
c7daa57e 734 qmp_args = self._qmp_args(conv_keys, args)
684750ab 735 ret = self._qmp.cmd(cmd, **qmp_args)
b9420e4f
JS
736 if cmd == 'quit':
737 self._quit_issued = True
738 return ret
abf0bf99 739
f12a282f 740 def get_qmp_event(self, wait: bool = False) -> Optional[QMPMessage]:
abf0bf99
JS
741 """
742 Poll for one queued QMP events and return it
743 """
306dfcd6 744 if self._events:
abf0bf99
JS
745 return self._events.pop(0)
746 return self._qmp.pull_event(wait=wait)
747
f12a282f 748 def get_qmp_events(self, wait: bool = False) -> List[QMPMessage]:
abf0bf99
JS
749 """
750 Poll for queued QMP events and return a list of dicts
751 """
752 events = self._qmp.get_events(wait=wait)
753 events.extend(self._events)
754 del self._events[:]
abf0bf99
JS
755 return events
756
757 @staticmethod
f12a282f 758 def event_match(event: Any, match: Optional[Any]) -> bool:
abf0bf99
JS
759 """
760 Check if an event matches optional match criteria.
761
762 The match criteria takes the form of a matching subdict. The event is
763 checked to be a superset of the subdict, recursively, with matching
764 values whenever the subdict values are not None.
765
766 This has a limitation that you cannot explicitly check for None values.
767
768 Examples, with the subdict queries on the left:
769 - None matches any object.
770 - {"foo": None} matches {"foo": {"bar": 1}}
771 - {"foo": None} matches {"foo": 5}
772 - {"foo": {"abc": None}} does not match {"foo": {"bar": 1}}
773 - {"foo": {"rab": 2}} matches {"foo": {"bar": 1, "rab": 2}}
774 """
775 if match is None:
776 return True
777
778 try:
779 for key in match:
780 if key in event:
781 if not QEMUMachine.event_match(event[key], match[key]):
782 return False
783 else:
784 return False
785 return True
786 except TypeError:
787 # either match or event wasn't iterable (not a dict)
f12a282f 788 return bool(match == event)
abf0bf99 789
f12a282f
JS
790 def event_wait(self, name: str,
791 timeout: float = 60.0,
792 match: Optional[QMPMessage] = None) -> Optional[QMPMessage]:
abf0bf99
JS
793 """
794 event_wait waits for and returns a named event from QMP with a timeout.
795
796 name: The event to wait for.
797 timeout: QEMUMonitorProtocol.pull_event timeout parameter.
798 match: Optional match criteria. See event_match for details.
799 """
800 return self.events_wait([(name, match)], timeout)
801
f12a282f
JS
802 def events_wait(self,
803 events: Sequence[Tuple[str, Any]],
804 timeout: float = 60.0) -> Optional[QMPMessage]:
abf0bf99 805 """
1847a4a8
JS
806 events_wait waits for and returns a single named event from QMP.
807 In the case of multiple qualifying events, this function returns the
808 first one.
abf0bf99 809
1847a4a8
JS
810 :param events: A sequence of (name, match_criteria) tuples.
811 The match criteria are optional and may be None.
812 See event_match for details.
813 :param timeout: Optional timeout, in seconds.
814 See QEMUMonitorProtocol.pull_event.
815
a4225303
JS
816 :raise asyncio.TimeoutError:
817 If timeout was non-zero and no matching events were found.
818
1847a4a8
JS
819 :return: A QMP event matching the filter criteria.
820 If timeout was 0 and no event matched, None.
abf0bf99 821 """
f12a282f 822 def _match(event: QMPMessage) -> bool:
abf0bf99 823 for name, match in events:
306dfcd6 824 if event['event'] == name and self.event_match(event, match):
abf0bf99
JS
825 return True
826 return False
827
1847a4a8
JS
828 event: Optional[QMPMessage]
829
abf0bf99
JS
830 # Search cached events
831 for event in self._events:
832 if _match(event):
833 self._events.remove(event)
834 return event
835
836 # Poll for new events
837 while True:
838 event = self._qmp.pull_event(wait=timeout)
1847a4a8
JS
839 if event is None:
840 # NB: None is only returned when timeout is false-ish.
a4225303 841 # Timeouts raise asyncio.TimeoutError instead!
1847a4a8 842 break
abf0bf99
JS
843 if _match(event):
844 return event
845 self._events.append(event)
846
847 return None
848
f12a282f 849 def get_log(self) -> Optional[str]:
abf0bf99
JS
850 """
851 After self.shutdown or failed qemu execution, this returns the output
852 of the qemu process.
853 """
854 return self._iolog
855
f12a282f 856 def add_args(self, *args: str) -> None:
abf0bf99
JS
857 """
858 Adds to the list of extra arguments to be given to the QEMU binary
859 """
860 self._args.extend(args)
861
f12a282f 862 def set_machine(self, machine_type: str) -> None:
abf0bf99
JS
863 """
864 Sets the machine type
865
866 If set, the machine type will be added to the base arguments
867 of the resulting QEMU command line.
868 """
869 self._machine = machine_type
870
f12a282f
JS
871 def set_console(self,
872 device_type: Optional[str] = None,
873 console_index: int = 0) -> None:
abf0bf99
JS
874 """
875 Sets the device type for a console device
876
877 If set, the console device and a backing character device will
878 be added to the base arguments of the resulting QEMU command
879 line.
880
881 This is a convenience method that will either use the provided
882 device type, or default to a "-serial chardev:console" command
883 line argument.
884
885 The actual setting of command line arguments will be be done at
886 machine launch time, as it depends on the temporary directory
887 to be created.
888
889 @param device_type: the device type, such as "isa-serial". If
890 None is given (the default value) a "-serial
891 chardev:console" command line argument will
892 be used instead, resorting to the machine's
893 default device type.
746f244d
PMD
894 @param console_index: the index of the console device to use.
895 If not zero, the command line will create
896 'index - 1' consoles and connect them to
897 the 'null' backing character device.
abf0bf99
JS
898 """
899 self._console_set = True
900 self._console_device_type = device_type
746f244d 901 self._console_index = console_index
abf0bf99
JS
902
903 @property
f12a282f 904 def console_socket(self) -> socket.socket:
abf0bf99
JS
905 """
906 Returns a socket connected to the console
907 """
908 if self._console_socket is None:
f0ec14c7 909 LOG.debug("Opening console socket")
1d4796cd
JS
910 if not self._console_set:
911 raise QEMUMachineError(
912 "Attempt to access console socket with no connection")
913 assert self._cons_sock_pair is not None
914 # os.dup() is used here for sock_fd because otherwise we'd
915 # have two rich python socket objects that would each try to
916 # close the same underlying fd when either one gets garbage
917 # collected.
80ded8e9 918 self._console_socket = console_socket.ConsoleSocket(
1d4796cd 919 sock_fd=os.dup(self._cons_sock_pair[1].fileno()),
80ded8e9
RF
920 file=self._console_log_path,
921 drain=self._drain_console)
1d4796cd 922 self._cons_sock_pair[1].close()
abf0bf99 923 return self._console_socket
2ca6e26c 924
f0ec14c7
NP
925 @property
926 def console_file(self) -> socket.SocketIO:
927 """
928 Returns a file associated with the console socket
929 """
930 if self._console_file is None:
931 LOG.debug("Opening console file")
932 self._console_file = self.console_socket.makefile(mode='rb',
933 buffering=0,
934 encoding='utf-8')
935 return self._console_file
936
2ca6e26c
CR
937 @property
938 def temp_dir(self) -> str:
939 """
940 Returns a temporary directory to be used for this machine
941 """
942 if self._temp_dir is None:
943 self._temp_dir = tempfile.mkdtemp(prefix="qemu-machine-",
944 dir=self._base_temp_dir)
945 return self._temp_dir
b306e26c
CR
946
947 @property
948 def log_dir(self) -> str:
949 """
950 Returns a directory to be used for writing logs
951 """
952 if self._log_dir is None:
953 return self.temp_dir
954 return self._log_dir