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

Lucas Meneghel Rodrigues
2010-06-17T20:40:33Z

On Mon, 2010-06-14 at 19:54 +0300, Michael Goldish wrote:
> 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.

Other than some very small comments with regards to the use of new style
classes and super() below, this code looks good, the improvements from
v1 are very welcome.

I am a bit afraid that both monitors have different sets of commands, so
we can't guarantee a monitor public interface. What I mean by this is
that list of public methods of QmpMonitor and HumanMonitor will differ.
But there's not much we can do about it right now, perhaps have a fixed
set of 'allowed' common functions to be used, that have to be
implemented for every monitor, and have only those functions available
for test writers.

Just loud thinking here. Your approach is fine, due to some changes on
the migration test I am rebasing the 2nd patch of the series and will
re-send.

> 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:

^ Small thing here - let's make this a new-style py class by making it
subclass (object).

> + """
> + 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)

^ Here, let's use

super(HumanMonitor, self).__init__(name, filename)

since we'll use new style classes and, if by some reason we change the
name of the parent class we don't have to change this statement.

> + 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)