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