[KVM-AUTOTEST PATCH v2 1/3] KVM test: add kvm_monitor.py, an interface to QEMU monitors

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

This module should replace vm.send_monitor_cmd(). Instead of connecting to the
monitor each time a command is issued, this module maintains a continuous
connection to the monitor. It disconnects when a test terminates and
reconnects as soon as the next test begins (upon unpickling).

It currently contains only an interface to the human monitor. A QMP interface
will be introduced in a future patch.

Changes from v1:
- Add name parameter to __init__()
- Remove help() method
- Rename help attribute to _help_str to indicate private use
- Rename lock to _lock
- Rename socket to _socket

Signed-off-by: Michael Goldish <mgoldish@redhat.com>
---
client/tests/kvm/kvm_monitor.py | 354 +++++++++++++++++++++++++++++++++++++++
1 files changed, 354 insertions(+), 0 deletions(-)
create mode 100644 client/tests/kvm/kvm_monitor.py

diff --git a/client/tests/kvm/kvm_monitor.py b/client/tests/kvm/kvm_monitor.py
new file mode 100644
index 0000000..27045a4
--- /dev/null
+++ b/client/tests/kvm/kvm_monitor.py
@@ -0,0 +1,354 @@
+"""
+Interfaces to the QEMU monitor.
+
+@copyright: 2008-2010 Red Hat Inc.
+"""
+
+import socket, time, threading, logging
+import kvm_utils
+
+
+class MonitorError(Exception):
+ pass
+
+
+class MonitorConnectError(MonitorError):
+ pass
+
+
+class MonitorSendError(MonitorError):
+ pass
+
+
+class MonitorLockError(MonitorError):
+ pass
+
+
+class MonitorProtocolError(MonitorError):
+ pass
+
+
+class Monitor:
+ """
+ Common code for monitor classes.
+ """
+
+ def __init__(self, name, filename):
+ """
+ Initialize the instance.
+
+ @param name: Monitor identifier (a string)
+ @param filename: Monitor socket filename
+ @raise MonitorConnectError: Raised if the connection fails
+ """
+ self.name = name
+ self.filename = filename
+ self._lock = threading.RLock()
+ self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self._socket.setblocking(False)
+
+ try:
+ self._socket.connect(filename)
+ except socket.error:
+ raise MonitorConnectError("Could not connect to monitor socket")
+
+
+ def __del__(self):
+ # Automatically close the connection when the instance is garbage
+ # collected
+ try:
+ self._socket.shutdown(socket.SHUT_RDWR)
+ except socket.error:
+ pass
+ self._socket.close()
+
+
+ # The following two functions are defined to make sure the state is set
+ # exclusively by the constructor call as specified in __getinitargs__().
+
+ def __getstate__(self):
+ pass
+
+
+ def __setstate__(self, state):
+ pass
+
+
+ def __getinitargs__(self):
+ # Save some information when pickling -- will be passed to the
+ # constructor upon unpickling
+ return self.name, self.filename, True
+
+
+ def _acquire_lock(self, timeout=20):
+ end_time = time.time() + timeout
+ while time.time() < end_time:
+ if self._lock.acquire(False):
+ return True
+ time.sleep(0.05)
+ return False
+
+
+ def _recvall(self):
+ s = ""
+ while True:
+ try:
+ data = self._socket.recv(1024)
+ except socket.error:
+ break
+ if not data:
+ break
+ s += data
+ return s
+
+
+class HumanMonitor(Monitor):
+ """
+ Wraps "human monitor" commands.
+ """
+
+ def __init__(self, name, filename, suppress_exceptions=False):
+ """
+ Connect to the monitor socket and find the (qemu) prompt.
+
+ @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 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, name, filename)
+
+ self.protocol = "human"
+
+ # Find the initial (qemu) prompt
+ s, o = self._read_up_to_qemu_prompt(20)
+ if not s:
+ raise MonitorProtocolError("Could not find (qemu) prompt "
+ "after connecting to monitor. "
+ "Output so far: %r" % o)
+
+ # Save the output of 'help' for future use
+ self._help_str = self._get_command_output("help")
+
+ except MonitorError, e:
+ if suppress_exceptions:
+ logging.warn(e)
+ else:
+ raise
+
+
+ # Private methods
+
+ def _read_up_to_qemu_prompt(self, timeout=20):
+ o = ""
+ end_time = time.time() + timeout
+ while time.time() < end_time:
+ try:
+ data = self._socket.recv(1024)
+ if not data:
+ break
+ o += data
+ if o.splitlines()[-1].split()[-1] == "(qemu)":
+ return True, "\n".join(o.splitlines()[:-1])
+ except (socket.error, IndexError):
+ time.sleep(0.01)
+ return False, "\n".join(o.splitlines())
+
+
+ def _send_command(self, command):
+ """
+ Send a command without waiting for output.
+
+ @param command: Command to send
+ @return: True if successful, False otherwise
+ @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 "
+ "monitor command '%s'" % command)
+
+ try:
+ try:
+ self._socket.sendall(command + "\n")
+ except socket.error:
+ raise MonitorSendError("Could not send monitor command '%s'" %
+ command)
+
+ finally:
+ self._lock.release()
+
+
+ def _get_command_output(self, command, timeout=20):
+ """
+ Send command to the monitor.
+
+ @param command: Command to send to the monitor
+ @param timeout: Time duration to wait for the (qemu) prompt to return
+ @return: Output received from the monitor
+ @raise MonitorLockError: Raised if the lock cannot be acquired
+ @raise MonitorSendError: Raised if the command cannot be sent
+ @raise MonitorProtocolError: Raised if the (qemu) prompt cannot be
+ found after sending the command
+ """
+ if not self._acquire_lock(20):
+ raise MonitorLockError("Could not acquire exclusive lock to send "
+ "monitor command '%s'" % command)
+
+ try:
+ # Read any data that might be available
+ self._recvall()
+ # Send command
+ self._send_command(command)
+ # Read output
+ s, o = self._read_up_to_qemu_prompt(timeout)
+ # Remove command echo from output
+ o = "\n".join(o.splitlines()[1:])
+ # Report success/failure
+ if s:
+ return o
+ else:
+ msg = ("Could not find (qemu) prompt after command '%s'. "
+ "Output so far: %r" % (command, o))
+ raise MonitorProtocolError(msg)
+
+ 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("help")
+ return True
+ except MonitorError:
+ return False
+
+
+ # Command wrappers
+ # Notes:
+ # - All of the following commands raise exceptions in a similar manner to
+ # cmd() and _get_command_output().
+ # - A command wrapper should use self._help_str if it requires information
+ # about the monitor's capabilities.
+
+ 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 (qemu) prompt after command
+ @return: The output of the command
+ @raise MonitorLockError: Raised if the lock cannot be acquired
+ @raise MonitorSendError: Raised if the command cannot be sent
+ @raise MonitorProtocolError: Raised if the (qemu) prompt cannot be
+ found after sending the command
+ """
+ return self._get_command_output(command, timeout)
+
+
+ def quit(self):
+ """
+ Send "quit" without waiting for output.
+ """
+ self._send_command("quit")
+
+
+ def info(self, what):
+ """
+ Request info about something and return the output.
+ """
+ return self._get_command_output("info %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 command's output
+ """
+ return self._get_command_output("screendump %s" % filename)
+
+
+ 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 command's output
+ """
+ cmd = "migrate"
+ if not wait:
+ cmd += " -d"
+ if full_copy:
+ cmd += " -b"
+ if incremental_copy:
+ cmd += " -i"
+ cmd += " %s" % uri
+ return self._get_command_output(cmd)
+
+
+ def migrate_set_speed(self, value):
+ """
+ Set maximum speed (in bytes/sec) for migrations.
+
+ @param value: Speed in bytes/sec
+ @return: The command's output
+ """
+ return self._get_command_output("migrate_set_speed %s" % value)
+
+
+ def sendkey(self, keystr, hold_time=1):
+ """
+ Send key combination to VM.
+
+ @param keystr: Key combination string
+ @param hold_time: Hold time in ms (should normally stay 1 ms)
+ @return: The command's output
+ """
+ return self._get_command_output("sendkey %s %s" % (keystr, hold_time))
+
+
+ def mouse_move(self, dx, dy):
+ """
+ Move mouse.
+
+ @param dx: X amount
+ @param dy: Y amount
+ @return: The command's output
+ """
+ return self._get_command_output("mouse_move %d %d" % (dx, dy))
+
+
+ def mouse_button(self, state):
+ """
+ Set mouse button state.
+
+ @param state: Button state (1=L, 2=M, 4=R)
+ @return: The command's output
+ """
+ return self._get_command_output("mouse_button %d" % state)
--
1.5.4.1