On Sun, Jun 13, 2010 at 05:33:44PM +0300, Michael Goldish wrote: > 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. > > Signed-off-by: Michael Goldish <mgoldish@xxxxxxxxxx> > --- > client/tests/kvm/kvm_monitor.py | 275 +++++++++++++++++++++++++++++++++++++++ > client/tests/kvm/kvm_vm.py | 6 +- > 2 files changed, 279 insertions(+), 2 deletions(-) > > 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) Fail to execute get a screendump. qmp hasn't support this cmd ? 09:55:14 WARNI| ("QMP command 'screendump' failed", {u'data': {u'name': u'screendump'}, u'class': u'CommandNotFound', u'desc': u'The command screendump has not been found'}) > + > + 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 -- To unsubscribe from this list: send the line "unsubscribe kvm" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html