diff mbox

[KVM-AUTOTEST,13/14] KVM test: kvm_monitor.py: add QMP interface

Message ID 1276439625-32472-13-git-send-email-mgoldish@redhat.com (mailing list archive)
State New, archived
Headers show

Commit Message

Michael Goldish June 13, 2010, 2:33 p.m. UTC
None
diff mbox

Patch

diff --git a/client/tests/kvm/kvm_monitor.py b/client/tests/kvm/kvm_monitor.py
index c5cf9c3..76a1a83 100644
--- a/client/tests/kvm/kvm_monitor.py
+++ b/client/tests/kvm/kvm_monitor.py
@@ -6,6 +6,11 @@  Interfaces to the QEMU monitor.
 
 import socket, time, threading, logging
 import kvm_utils
+try:
+    import json
+except ImportError:
+    logging.warning("Could not import json module. "
+                    "QMP monitor functionality disabled.")
 
 
 class MonitorError(Exception):
@@ -28,6 +33,10 @@  class MonitorProtocolError(MonitorError):
     pass
 
 
+class QMPCmdError(MonitorError):
+    pass
+
+
 class Monitor:
     """
     Common code for monitor classes.
@@ -114,6 +123,8 @@  class HumanMonitor(Monitor):
                 suppress_exceptions is False
         @raise MonitorProtocolError: Raised if the initial (qemu) prompt isn't
                 found and suppress_exceptions is False
+        @note: Other exceptions may be raised.  See _get_command_output's
+                docstring.
         """
         try:
             Monitor.__init__(self, filename)
@@ -354,3 +365,267 @@  class HumanMonitor(Monitor):
         @return: The command's output
         """
         return self._get_command_output("mouse_button %d" % state)
+
+
+class QMPMonitor(Monitor):
+    """
+    Wraps QMP monitor commands.
+    """
+
+    def __init__(self, filename, suppress_exceptions=False):
+        """
+        Connect to the monitor socket and issue the qmp_capabilities command
+
+        @param filename: Monitor socket filename
+        @raise MonitorConnectError: Raised if the connection fails and
+                suppress_exceptions is False
+        @note: Other exceptions may be raised if the qmp_capabilities command
+                fails.  See _get_command_output's docstring.
+        """
+        try:
+            Monitor.__init__(self, filename)
+
+            self.protocol = "qmp"
+            self.events = []
+
+            # Issue qmp_capabilities
+            self._get_command_output("qmp_capabilities")
+
+        except MonitorError, e:
+            if suppress_exceptions:
+                logging.warn(e)
+            else:
+                raise
+
+
+    # Private methods
+
+    def _build_cmd(self, cmd, args=None):
+        obj = {"execute": cmd}
+        if args:
+            obj["arguments"] = args
+        return obj
+
+
+    def _read_objects(self, timeout=5):
+        """
+        Read lines from monitor and try to decode them.
+        Stop when all available lines have been successfully decoded, or when
+        timeout expires.  If any decoded objects are asynchronous events, store
+        them in self.events.  Return all decoded objects.
+
+        @param timeout: Time to wait for all lines to decode successfully
+        @return: A list of objects
+        """
+        s = ""
+        objs = []
+        end_time = time.time() + timeout
+        while time.time() < end_time:
+            s += self._recvall()
+            for line in s.splitlines():
+                if not line:
+                    continue
+                try:
+                    obj = json.loads(line)
+                except:
+                    # Found an incomplete or broken line -- keep reading
+                    break
+                objs += [obj]
+            else:
+                # All lines are OK -- stop reading
+                break
+            time.sleep(0.1)
+        # Keep track of asynchronous events
+        self.events += [obj for obj in objs if "event" in obj]
+        return objs
+
+
+    def _send_command(self, cmd, args=None):
+        """
+        Send command without waiting for response.
+
+        @param cmd: Command to send
+        @param args: A dict containing command arguments, or None
+        @raise MonitorLockError: Raised if the lock cannot be acquired
+        @raise MonitorSendError: Raised if the command cannot be sent
+        """
+        if not self._acquire_lock(20):
+            raise MonitorLockError("Could not acquire exclusive lock to send "
+                                   "QMP command '%s'" % cmd)
+
+        try:
+            cmdobj = self._build_cmd(cmd, args)
+            try:
+                self.socket.sendall(json.dumps(cmdobj) + "\n")
+            except socket.error:
+                raise MonitorSendError("Could not send QMP command '%s'" % cmd)
+
+        finally:
+            self.lock.release()
+
+
+    def _get_command_output(self, cmd, args=None, timeout=20):
+        """
+        Send monitor command and wait for response.
+
+        @param cmd: Command to send
+        @param args: A dict containing command arguments, or None
+        @param timeout: Time duration to wait for response
+        @return: The response received
+        @raise MonitorLockError: Raised if the lock cannot be acquired
+        @raise MonitorSendError: Raised if the command cannot be sent
+        @raise MonitorProtocolError: Raised if no response is received
+        @raise QMPCmdError: Raised if the response is an error message
+                (the exception's args are (msg, data) where msg is a string and
+                data is the error data)
+        """
+        if not self._acquire_lock(20):
+            raise MonitorLockError("Could not acquire exclusive lock to send "
+                                   "QMP command '%s'" % cmd)
+
+        try:
+            # Read any data that might be available
+            self._read_objects()
+            # Send command
+            self._send_command(cmd, args)
+            # Read response
+            end_time = time.time() + timeout
+            while time.time() < end_time:
+                for obj in self._read_objects():
+                    if "return" in obj:
+                        return obj["return"]
+                    elif "error" in obj:
+                        raise QMPCmdError("QMP command '%s' failed" % cmd,
+                                          obj["error"])
+                time.sleep(0.1)
+            # No response found
+            raise MonitorProtocolError("Received no response to QMP command "
+                                       "'%s'" % cmd)
+
+        finally:
+            self.lock.release()
+
+
+    # Public methods
+
+    def is_responsive(self):
+        """
+        Make sure the monitor is responsive by sending a command.
+
+        @return: True if responsive, False otherwise
+        """
+        try:
+            self._get_command_output("query-version")
+            return True
+        except MonitorError:
+            return False
+
+
+    def get_events(self):
+        """
+        Return a list of the asynchronous events received since the last
+        clear_events() call.
+
+        @return: A list of events (the objects returned have an "event" key)
+        @raise MonitorLockError: Raised if the lock cannot be acquired
+        """
+        if not self._acquire_lock(20):
+            raise MonitorLockError("Could not acquire exclusive lock to read "
+                                   "events from monitor")
+        try:
+            self._read_objects()
+            return self.events[:]
+        finally:
+            self.lock.release()
+
+
+    def clear_events(self):
+        """
+        Clear the list of asynchronous events.
+
+        @raise MonitorLockError: Raised if the lock cannot be acquired
+        """
+        if not self._acquire_lock(20):
+            raise MonitorLockError("Could not acquire exclusive lock to clear "
+                                   "event list")
+        self.events = []
+        self.lock.release()
+
+
+    # Command wrappers
+    # Note: all of the following functions raise exceptions in a similar manner
+    # to cmd() and _get_command_output().
+
+    def cmd(self, command, timeout=20):
+        """
+        Send a simple command with no parameters and return its output.
+        Should only be used for commands that take no parameters and are
+        implemented under the same name for both the human and QMP monitors.
+
+        @param command: Command to send
+        @param timeout: Time duration to wait for response
+        @return: The response to the command
+        @raise MonitorLockError: Raised if the lock cannot be acquired
+        @raise MonitorSendError: Raised if the command cannot be sent
+        @raise MonitorProtocolError: Raised if no response is received
+        """
+        return self._get_command_output(command, timeout=timeout)
+
+
+    def quit(self):
+        """
+        Send "quit" and return the response.
+        """
+        return self._get_command_output("quit")
+
+
+    def info(self, what):
+        """
+        Request info about something and return the response.
+        """
+        return self._get_command_output("query-%s" % what)
+
+
+    def query(self, what):
+        """
+        Alias for info.
+        """
+        return self.info(what)
+
+
+    def screendump(self, filename):
+        """
+        Request a screendump.
+
+        @param filename: Location for the screendump
+        @return: The response to the command
+        """
+        args = {"filename": filename}
+        return self._get_command_output("screendump", args)
+
+
+    def migrate(self, uri, full_copy=False, incremental_copy=False, wait=False):
+        """
+        Migrate.
+
+        @param uri: destination URI
+        @param full_copy: If true, migrate with full disk copy
+        @param incremental_copy: If true, migrate with incremental disk copy
+        @param wait: If true, wait for completion
+        @return: The response to the command
+        """
+        args = {"uri": uri,
+                "blk": full_copy,
+                "inc": incremental_copy}
+        return self._get_command_output("migrate", args)
+
+
+    def migrate_set_speed(self, value):
+        """
+        Set maximum speed (in bytes/sec) for migrations.
+
+        @param value: Speed in bytes/sec
+        @return: The response to the command
+        """
+        args = {"value": value}
+        return self._get_command_output("migrate_set_speed", args)
diff --git a/client/tests/kvm/kvm_vm.py b/client/tests/kvm/kvm_vm.py
index 6aae053..a77cb9c 100755
--- a/client/tests/kvm/kvm_vm.py
+++ b/client/tests/kvm/kvm_vm.py
@@ -584,7 +584,8 @@  class VM:
             # Establish monitor connections
             self.monitors = []
             self.monitor = None
-            for monitor_name in kvm_utils.get_sub_dict_names(params, "monitors"):
+            for monitor_name in kvm_utils.get_sub_dict_names(params,
+                                                             "monitors"):
                 monitor_params = kvm_utils.get_sub_dict(params, monitor_name)
                 # Wait for monitor connection to succeed
                 end_time = time.time() + timeout
@@ -592,7 +593,8 @@  class VM:
                     try:
                         if monitor_params.get("monitor_type") == "qmp":
                             # Add a QMP monitor: not implemented yet
-                            monitor = None
+                            monitor = kvm_monitor.QMPMonitor(
+                                self.get_monitor_filename(monitor_name))
                         else:
                             # Add a "human" monitor
                             monitor = kvm_monitor.HumanMonitor(