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