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