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