From patchwork Sun Jun 13 14:33:44 2010 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Michael Goldish X-Patchwork-Id: 105817 Received: from vger.kernel.org (vger.kernel.org [209.132.180.67]) by demeter.kernel.org (8.14.3/8.14.3) with ESMTP id o5DEYRr9026976 for ; Sun, 13 Jun 2010 14:34:32 GMT Received: (majordomo@vger.kernel.org) by vger.kernel.org via listexpand id S1753925Ab0FMOe2 (ORCPT ); Sun, 13 Jun 2010 10:34:28 -0400 Received: from mx1.redhat.com ([209.132.183.28]:19380 "EHLO mx1.redhat.com" rhost-flags-OK-OK-OK-OK) by vger.kernel.org with ESMTP id S1753895Ab0FMOeZ (ORCPT ); Sun, 13 Jun 2010 10:34:25 -0400 Received: from int-mx01.intmail.prod.int.phx2.redhat.com (int-mx01.intmail.prod.int.phx2.redhat.com [10.5.11.11]) by mx1.redhat.com (8.13.8/8.13.8) with ESMTP id o5DEYO4N006911 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-SHA bits=256 verify=OK); Sun, 13 Jun 2010 10:34:24 -0400 Received: from ns3.rdu.redhat.com (ns3.rdu.redhat.com [10.11.255.199]) by int-mx01.intmail.prod.int.phx2.redhat.com (8.13.8/8.13.8) with ESMTP id o5DEYNjr004578; Sun, 13 Jun 2010 10:34:24 -0400 Received: from localhost.localdomain (dhcp-1-188.tlv.redhat.com [10.35.1.188]) by ns3.rdu.redhat.com (8.13.8/8.13.8) with ESMTP id o5DEY6v8030030; Sun, 13 Jun 2010 10:34:22 -0400 From: Michael Goldish To: autotest@test.kernel.org, kvm@vger.kernel.org Cc: Michael Goldish Subject: [KVM-AUTOTEST PATCH 13/14] KVM test: kvm_monitor.py: add QMP interface Date: Sun, 13 Jun 2010 17:33:44 +0300 Message-Id: <1276439625-32472-13-git-send-email-mgoldish@redhat.com> In-Reply-To: <1276439625-32472-12-git-send-email-mgoldish@redhat.com> References: <1276439625-32472-1-git-send-email-mgoldish@redhat.com> <1276439625-32472-2-git-send-email-mgoldish@redhat.com> <1276439625-32472-3-git-send-email-mgoldish@redhat.com> <1276439625-32472-4-git-send-email-mgoldish@redhat.com> <1276439625-32472-5-git-send-email-mgoldish@redhat.com> <1276439625-32472-6-git-send-email-mgoldish@redhat.com> <1276439625-32472-7-git-send-email-mgoldish@redhat.com> <1276439625-32472-8-git-send-email-mgoldish@redhat.com> <1276439625-32472-9-git-send-email-mgoldish@redhat.com> <1276439625-32472-10-git-send-email-mgoldish@redhat.com> <1276439625-32472-11-git-send-email-mgoldish@redhat.com> <1276439625-32472-12-git-send-email-mgoldish@redhat.com> X-Scanned-By: MIMEDefang 2.67 on 10.5.11.11 Sender: kvm-owner@vger.kernel.org Precedence: bulk List-ID: X-Mailing-List: kvm@vger.kernel.org X-Greylist: IP, sender and recipient auto-whitelisted, not delayed by milter-greylist-4.2.3 (demeter.kernel.org [140.211.167.41]); Sun, 13 Jun 2010 14:34:32 +0000 (UTC) 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(