In the case that we receive a reply but are unable to understand it, use this exception name to indicate that case. Signed-off-by: John Snow <jsnow@redhat.com> Reviewed-by: Philippe Mathieu-Daudé <philmd@redhat.com> Reviewed-by: Kevin Wolf <kwolf@redhat.com> Message-Id: <20200710052220.3306-7-jsnow@redhat.com> Signed-off-by: Philippe Mathieu-Daudé <philmd@redhat.com>
		
			
				
	
	
		
			357 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			357 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
""" QEMU Monitor Protocol Python class """
 | 
						|
# Copyright (C) 2009, 2010 Red Hat Inc.
 | 
						|
#
 | 
						|
# Authors:
 | 
						|
#  Luiz Capitulino <lcapitulino@redhat.com>
 | 
						|
#
 | 
						|
# This work is licensed under the terms of the GNU GPL, version 2.  See
 | 
						|
# the COPYING file in the top-level directory.
 | 
						|
 | 
						|
import json
 | 
						|
import errno
 | 
						|
import socket
 | 
						|
import logging
 | 
						|
from typing import (
 | 
						|
    Any,
 | 
						|
    cast,
 | 
						|
    Dict,
 | 
						|
    Optional,
 | 
						|
    TextIO,
 | 
						|
    Type,
 | 
						|
    Tuple,
 | 
						|
    Union,
 | 
						|
)
 | 
						|
from types import TracebackType
 | 
						|
 | 
						|
 | 
						|
# QMPMessage is a QMP Message of any kind.
 | 
						|
# e.g. {'yee': 'haw'}
 | 
						|
#
 | 
						|
# QMPReturnValue is the inner value of return values only.
 | 
						|
# {'return': {}} is the QMPMessage,
 | 
						|
# {} is the QMPReturnValue.
 | 
						|
QMPMessage = Dict[str, Any]
 | 
						|
QMPReturnValue = Dict[str, Any]
 | 
						|
 | 
						|
InternetAddrT = Tuple[str, str]
 | 
						|
UnixAddrT = str
 | 
						|
SocketAddrT = Union[InternetAddrT, UnixAddrT]
 | 
						|
 | 
						|
 | 
						|
class QMPError(Exception):
 | 
						|
    """
 | 
						|
    QMP base exception
 | 
						|
    """
 | 
						|
 | 
						|
 | 
						|
class QMPConnectError(QMPError):
 | 
						|
    """
 | 
						|
    QMP connection exception
 | 
						|
    """
 | 
						|
 | 
						|
 | 
						|
class QMPCapabilitiesError(QMPError):
 | 
						|
    """
 | 
						|
    QMP negotiate capabilities exception
 | 
						|
    """
 | 
						|
 | 
						|
 | 
						|
class QMPTimeoutError(QMPError):
 | 
						|
    """
 | 
						|
    QMP timeout exception
 | 
						|
    """
 | 
						|
 | 
						|
 | 
						|
class QMPProtocolError(QMPError):
 | 
						|
    """
 | 
						|
    QMP protocol error; unexpected response
 | 
						|
    """
 | 
						|
 | 
						|
 | 
						|
class QMPResponseError(QMPError):
 | 
						|
    """
 | 
						|
    Represents erroneous QMP monitor reply
 | 
						|
    """
 | 
						|
    def __init__(self, reply: QMPMessage):
 | 
						|
        try:
 | 
						|
            desc = reply['error']['desc']
 | 
						|
        except KeyError:
 | 
						|
            desc = reply
 | 
						|
        super().__init__(desc)
 | 
						|
        self.reply = reply
 | 
						|
 | 
						|
 | 
						|
class QEMUMonitorProtocol:
 | 
						|
    """
 | 
						|
    Provide an API to connect to QEMU via QEMU Monitor Protocol (QMP) and then
 | 
						|
    allow to handle commands and events.
 | 
						|
    """
 | 
						|
 | 
						|
    #: Logger object for debugging messages
 | 
						|
    logger = logging.getLogger('QMP')
 | 
						|
 | 
						|
    def __init__(self, address, server=False, nickname=None):
 | 
						|
        """
 | 
						|
        Create a QEMUMonitorProtocol class.
 | 
						|
 | 
						|
        @param address: QEMU address, can be either a unix socket path (string)
 | 
						|
                        or a tuple in the form ( address, port ) for a TCP
 | 
						|
                        connection
 | 
						|
        @param server: server mode listens on the socket (bool)
 | 
						|
        @raise OSError on socket connection errors
 | 
						|
        @note No connection is established, this is done by the connect() or
 | 
						|
              accept() methods
 | 
						|
        """
 | 
						|
        self.__events = []
 | 
						|
        self.__address = address
 | 
						|
        self.__sock = self.__get_sock()
 | 
						|
        self.__sockfile: Optional[TextIO] = None
 | 
						|
        self._nickname = nickname
 | 
						|
        if self._nickname:
 | 
						|
            self.logger = logging.getLogger('QMP').getChild(self._nickname)
 | 
						|
        if server:
 | 
						|
            self.__sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 | 
						|
            self.__sock.bind(self.__address)
 | 
						|
            self.__sock.listen(1)
 | 
						|
 | 
						|
    def __get_sock(self):
 | 
						|
        if isinstance(self.__address, tuple):
 | 
						|
            family = socket.AF_INET
 | 
						|
        else:
 | 
						|
            family = socket.AF_UNIX
 | 
						|
        return socket.socket(family, socket.SOCK_STREAM)
 | 
						|
 | 
						|
    def __negotiate_capabilities(self):
 | 
						|
        greeting = self.__json_read()
 | 
						|
        if greeting is None or "QMP" not in greeting:
 | 
						|
            raise QMPConnectError
 | 
						|
        # Greeting seems ok, negotiate capabilities
 | 
						|
        resp = self.cmd('qmp_capabilities')
 | 
						|
        if resp and "return" in resp:
 | 
						|
            return greeting
 | 
						|
        raise QMPCapabilitiesError
 | 
						|
 | 
						|
    def __json_read(self, only_event=False):
 | 
						|
        assert self.__sockfile is not None
 | 
						|
        while True:
 | 
						|
            data = self.__sockfile.readline()
 | 
						|
            if not data:
 | 
						|
                return None
 | 
						|
            # By definition, any JSON received from QMP is a QMPMessage,
 | 
						|
            # and we are asserting only at static analysis time that it
 | 
						|
            # has a particular shape.
 | 
						|
            resp: QMPMessage = json.loads(data)
 | 
						|
            if 'event' in resp:
 | 
						|
                self.logger.debug("<<< %s", resp)
 | 
						|
                self.__events.append(resp)
 | 
						|
                if not only_event:
 | 
						|
                    continue
 | 
						|
            return resp
 | 
						|
 | 
						|
    def __get_events(self, wait=False):
 | 
						|
        """
 | 
						|
        Check for new events in the stream and cache them in __events.
 | 
						|
 | 
						|
        @param wait (bool): block until an event is available.
 | 
						|
        @param wait (float): If wait is a float, treat it as a timeout value.
 | 
						|
 | 
						|
        @raise QMPTimeoutError: If a timeout float is provided and the timeout
 | 
						|
                                period elapses.
 | 
						|
        @raise QMPConnectError: If wait is True but no events could be
 | 
						|
                                retrieved or if some other error occurred.
 | 
						|
        """
 | 
						|
 | 
						|
        # Check for new events regardless and pull them into the cache:
 | 
						|
        self.__sock.setblocking(False)
 | 
						|
        try:
 | 
						|
            self.__json_read()
 | 
						|
        except OSError as err:
 | 
						|
            if err.errno == errno.EAGAIN:
 | 
						|
                # No data available
 | 
						|
                pass
 | 
						|
        self.__sock.setblocking(True)
 | 
						|
 | 
						|
        # Wait for new events, if needed.
 | 
						|
        # if wait is 0.0, this means "no wait" and is also implicitly false.
 | 
						|
        if not self.__events and wait:
 | 
						|
            if isinstance(wait, float):
 | 
						|
                self.__sock.settimeout(wait)
 | 
						|
            try:
 | 
						|
                ret = self.__json_read(only_event=True)
 | 
						|
            except socket.timeout:
 | 
						|
                raise QMPTimeoutError("Timeout waiting for event")
 | 
						|
            except:
 | 
						|
                raise QMPConnectError("Error while reading from socket")
 | 
						|
            if ret is None:
 | 
						|
                raise QMPConnectError("Error while reading from socket")
 | 
						|
            self.__sock.settimeout(None)
 | 
						|
 | 
						|
    def __enter__(self):
 | 
						|
        # Implement context manager enter function.
 | 
						|
        return self
 | 
						|
 | 
						|
    def __exit__(self,
 | 
						|
                 # pylint: disable=duplicate-code
 | 
						|
                 # see https://github.com/PyCQA/pylint/issues/3619
 | 
						|
                 exc_type: Optional[Type[BaseException]],
 | 
						|
                 exc_val: Optional[BaseException],
 | 
						|
                 exc_tb: Optional[TracebackType]) -> None:
 | 
						|
        # Implement context manager exit function.
 | 
						|
        self.close()
 | 
						|
 | 
						|
    def connect(self, negotiate=True):
 | 
						|
        """
 | 
						|
        Connect to the QMP Monitor and perform capabilities negotiation.
 | 
						|
 | 
						|
        @return QMP greeting dict, or None if negotiate is false
 | 
						|
        @raise OSError on socket connection errors
 | 
						|
        @raise QMPConnectError if the greeting is not received
 | 
						|
        @raise QMPCapabilitiesError if fails to negotiate capabilities
 | 
						|
        """
 | 
						|
        self.__sock.connect(self.__address)
 | 
						|
        self.__sockfile = self.__sock.makefile(mode='r')
 | 
						|
        if negotiate:
 | 
						|
            return self.__negotiate_capabilities()
 | 
						|
        return None
 | 
						|
 | 
						|
    def accept(self, timeout=15.0):
 | 
						|
        """
 | 
						|
        Await connection from QMP Monitor and perform capabilities negotiation.
 | 
						|
 | 
						|
        @param timeout: timeout in seconds (nonnegative float number, or
 | 
						|
                        None). The value passed will set the behavior of the
 | 
						|
                        underneath QMP socket as described in [1].
 | 
						|
                        Default value is set to 15.0.
 | 
						|
        @return QMP greeting dict
 | 
						|
        @raise OSError on socket connection errors
 | 
						|
        @raise QMPConnectError if the greeting is not received
 | 
						|
        @raise QMPCapabilitiesError if fails to negotiate capabilities
 | 
						|
 | 
						|
        [1]
 | 
						|
        https://docs.python.org/3/library/socket.html#socket.socket.settimeout
 | 
						|
        """
 | 
						|
        self.__sock.settimeout(timeout)
 | 
						|
        self.__sock, _ = self.__sock.accept()
 | 
						|
        self.__sockfile = self.__sock.makefile(mode='r')
 | 
						|
        return self.__negotiate_capabilities()
 | 
						|
 | 
						|
    def cmd_obj(self, qmp_cmd: QMPMessage) -> QMPMessage:
 | 
						|
        """
 | 
						|
        Send a QMP command to the QMP Monitor.
 | 
						|
 | 
						|
        @param qmp_cmd: QMP command to be sent as a Python dict
 | 
						|
        @return QMP response as a Python dict
 | 
						|
        """
 | 
						|
        self.logger.debug(">>> %s", qmp_cmd)
 | 
						|
        self.__sock.sendall(json.dumps(qmp_cmd).encode('utf-8'))
 | 
						|
        resp = self.__json_read()
 | 
						|
        if resp is None:
 | 
						|
            raise QMPConnectError("Unexpected empty reply from server")
 | 
						|
        self.logger.debug("<<< %s", resp)
 | 
						|
        return resp
 | 
						|
 | 
						|
    def cmd(self, name, args=None, cmd_id=None):
 | 
						|
        """
 | 
						|
        Build a QMP command and send it to the QMP Monitor.
 | 
						|
 | 
						|
        @param name: command name (string)
 | 
						|
        @param args: command arguments (dict)
 | 
						|
        @param cmd_id: command id (dict, list, string or int)
 | 
						|
        """
 | 
						|
        qmp_cmd = {'execute': name}
 | 
						|
        if args:
 | 
						|
            qmp_cmd['arguments'] = args
 | 
						|
        if cmd_id:
 | 
						|
            qmp_cmd['id'] = cmd_id
 | 
						|
        return self.cmd_obj(qmp_cmd)
 | 
						|
 | 
						|
    def command(self, cmd, **kwds):
 | 
						|
        """
 | 
						|
        Build and send a QMP command to the monitor, report errors if any
 | 
						|
        """
 | 
						|
        ret = self.cmd(cmd, kwds)
 | 
						|
        if 'error' in ret:
 | 
						|
            raise QMPResponseError(ret)
 | 
						|
        if 'return' not in ret:
 | 
						|
            raise QMPProtocolError(
 | 
						|
                "'return' key not found in QMP response '{}'".format(str(ret))
 | 
						|
            )
 | 
						|
        return cast(QMPReturnValue, ret['return'])
 | 
						|
 | 
						|
    def pull_event(self, wait=False):
 | 
						|
        """
 | 
						|
        Pulls a single event.
 | 
						|
 | 
						|
        @param wait (bool): block until an event is available.
 | 
						|
        @param wait (float): If wait is a float, treat it as a timeout value.
 | 
						|
 | 
						|
        @raise QMPTimeoutError: If a timeout float is provided and the timeout
 | 
						|
                                period elapses.
 | 
						|
        @raise QMPConnectError: If wait is True but no events could be
 | 
						|
                                retrieved or if some other error occurred.
 | 
						|
 | 
						|
        @return The first available QMP event, or None.
 | 
						|
        """
 | 
						|
        self.__get_events(wait)
 | 
						|
 | 
						|
        if self.__events:
 | 
						|
            return self.__events.pop(0)
 | 
						|
        return None
 | 
						|
 | 
						|
    def get_events(self, wait=False):
 | 
						|
        """
 | 
						|
        Get a list of available QMP events.
 | 
						|
 | 
						|
        @param wait (bool): block until an event is available.
 | 
						|
        @param wait (float): If wait is a float, treat it as a timeout value.
 | 
						|
 | 
						|
        @raise QMPTimeoutError: If a timeout float is provided and the timeout
 | 
						|
                                period elapses.
 | 
						|
        @raise QMPConnectError: If wait is True but no events could be
 | 
						|
                                retrieved or if some other error occurred.
 | 
						|
 | 
						|
        @return The list of available QMP events.
 | 
						|
        """
 | 
						|
        self.__get_events(wait)
 | 
						|
        return self.__events
 | 
						|
 | 
						|
    def clear_events(self):
 | 
						|
        """
 | 
						|
        Clear current list of pending events.
 | 
						|
        """
 | 
						|
        self.__events = []
 | 
						|
 | 
						|
    def close(self):
 | 
						|
        """
 | 
						|
        Close the socket and socket file.
 | 
						|
        """
 | 
						|
        if self.__sock:
 | 
						|
            self.__sock.close()
 | 
						|
        if self.__sockfile:
 | 
						|
            self.__sockfile.close()
 | 
						|
 | 
						|
    def settimeout(self, timeout):
 | 
						|
        """
 | 
						|
        Set the socket timeout.
 | 
						|
 | 
						|
        @param timeout (float): timeout in seconds, or None.
 | 
						|
        @note This is a wrap around socket.settimeout
 | 
						|
        """
 | 
						|
        self.__sock.settimeout(timeout)
 | 
						|
 | 
						|
    def get_sock_fd(self):
 | 
						|
        """
 | 
						|
        Get the socket file descriptor.
 | 
						|
 | 
						|
        @return The file descriptor number.
 | 
						|
        """
 | 
						|
        return self.__sock.fileno()
 | 
						|
 | 
						|
    def is_scm_available(self):
 | 
						|
        """
 | 
						|
        Check if the socket allows for SCM_RIGHTS.
 | 
						|
 | 
						|
        @return True if SCM_RIGHTS is available, otherwise False.
 | 
						|
        """
 | 
						|
        return self.__sock.family == socket.AF_UNIX
 |