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