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