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