[KVM-AUTOTEST PATCH v2 3/3] KVM test: kvm_monitor.py: add QMP interface

Michael Goldish
2010-06-14T16:55:27Z

An initial QMP client implementation.
Should be fully functional and supports asynchronous events.
However, most tests must be modified to support it, because it returns output
in a different format from the human monitor (the human monitor returns strings
and the QMP one returns dicts or lists).

To enable QMP, set main_monitor to a monitor whose monitor_type is "qmp".

For example (a single QMP monitor):

monitors = monitor1
monitor_type_monitor1 = qmp
main_monitor = monitor1

Another example (multiple monitors, both human and QMP):

monitors = MyMonitor SomeOtherMonitor YetAnotherMonitor # defines 3 monitors
monitor_type = human # default for all monitors
monitor_type_SomeOtherMonitor = qmp # applies only to SomeOtherMonitor
monitor_type_YetAnotherMonitor = qmp # applies only to YetAnotherMonitor
main_monitor = SomeOtherMonitor # the main monitor is a QMP one, so
# the test will use QMP

Note:
Monitor methods now raise exceptions such as MonitorLockError and QMPCmdError.
If this turns out to be a bad idea, it shouldn't be hard to revert to the old
convention of returning a (status, output) tuple.

Changes from v1:
- Upon connection make sure the QMP greeting is received (otherwise raise an
exception)
- Upon connection make sure the json module is available
- Rename the events attribute to _events to indicate private use
- Use _lock instead of lock
- Use _socket instead of socket

Signed-off-by: Michael Goldish <mgoldish@redhat.com>
---
client/tests/kvm/kvm_monitor.py | 305 +++++++++++++++++++++++++++++++++++++++
client/tests/kvm/kvm_vm.py | 8 +-
2 files changed, 310 insertions(+), 3 deletions(-)

diff --git a/client/tests/kvm/kvm_monitor.py b/client/tests/kvm/kvm_monitor.py
index 27045a4..12cf773 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,14 @@ class MonitorProtocolError(MonitorError):
pass

+class MonitorNotSupportedError(MonitorError):
+ pass
+
+
+class QMPCmdError(MonitorError):
+ pass
+
+
class Monitor:
"""
Common code for monitor classes.
@@ -352,3 +365,295 @@ 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, name, filename, suppress_exceptions=False):
+ """
+ Connect to the monitor socket, read the greeting message and issue the
+ qmp_capabilities command. Also make sure the json module is available.
+
+ @param name: Monitor identifier (a string)
+ @param filename: Monitor socket filename
+ @raise MonitorConnectError: Raised if the connection fails and
+ suppress_exceptions is False
+ @raise MonitorProtocolError: Raised if the no QMP greeting message is
+ received and suppress_exceptions is False
+ @raise MonitorNotSupportedError: Raised if json isn't available 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, name, filename)
+
+ self.protocol = "qmp"
+ self._greeting = None
+ self._events = []
+
+ # Make sure json is available
+ try:
+ json
+ except NameError:
+ raise MonitorNotSupportedError("QMP requires the json module "
+ "(Python 2.6 and up)")
+
+ # Read greeting message
+ end_time = time.time() + 20
+ while time.time() < end_time:
+ for obj in self._read_objects():
+ if "QMP" in obj:
+ self._greeting = obj["QMP"]
+ break
+ if self._greeting:
+ break
+ time.sleep(0.1)
+ else:
+ raise MonitorProtocolError("No QMP greeting message received")
+
+ # 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 "
+ "QMP events")
+ 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 "
+ "QMP 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 db5f8e0..8113a45 100755
--- a/client/tests/kvm/kvm_vm.py
+++ b/client/tests/kvm/kvm_vm.py
@@ -590,8 +590,10 @@ class VM:
while time.time() < end_time:
try:
if monitor_params.get("monitor_type") == "qmp":
- # Add a QMP monitor: not implemented yet
- monitor = None
+ # Add a QMP monitor
+ monitor = kvm_monitor.QMPMonitor(
+ monitor_name,
+ self.get_monitor_filename(monitor_name))
else:
# Add a "human" monitor
monitor = kvm_monitor.HumanMonitor(
@@ -600,7 +602,7 @@ class VM:
except kvm_monitor.MonitorError, e:
logging.warn(e)
else:
- if monitor and monitor.is_responsive():
+ if monitor.is_responsive():
break
time.sleep(1)
else:
--
1.5.4.1