xref: /openbmc/qemu/python/qemu/machine/machine.py (revision 500eb21c)
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