Source code for virttest.guest_agent

"""
Interfaces to the virt agent.

:copyright: 2008-2012 Red Hat Inc.
"""

import socket
import time
import logging
import random
from autotest.client.shared import error
from qemu_monitor import Monitor, MonitorError

try:
    import json
except ImportError:
    logging.warning("Could not import json module. "
                    "virt agent functionality disabled.")


[docs]class VAgentError(MonitorError): pass
[docs]class VAgentConnectError(VAgentError): pass
[docs]class VAgentSocketError(VAgentError): def __init__(self, msg, e): VAgentError.__init__(self) self.msg = msg self.e = e def __str__(self): return "%s (%s)" % (self.msg, self.e)
[docs]class VAgentLockError(VAgentError): pass
[docs]class VAgentProtocolError(VAgentError): pass
[docs]class VAgentNotSupportedError(VAgentError): pass
[docs]class VAgentCmdError(VAgentError): def __init__(self, cmd, args, data): VAgentError.__init__(self) self.ecmd = cmd self.eargs = args self.edata = data def __str__(self): return ("Virt Agent command %r failed (arguments: %r, " "error message: %r)" % (self.ecmd, self.eargs, self.edata))
[docs]class VAgentSyncError(VAgentError): def __init__(self, vm_name): VAgentError.__init__(self) self.vm_name = vm_name def __str__(self): return "Could not sync with guest agent in vm '%s'" % self.vm_name
[docs]class VAgentSuspendError(VAgentError): pass
[docs]class VAgentSuspendUnknownModeError(VAgentSuspendError): def __init__(self, mode): VAgentSuspendError.__init__(self) self.mode = mode def __str__(self): return "Not supported suspend mode '%s'" % self.mode
[docs]class VAgentFreezeStatusError(VAgentError): def __init__(self, vm_name, status, expected): VAgentError.__init__(self) self.vm_name = vm_name self.status = status self.expected = expected def __str__(self): return ("Unexpected guest FS status '%s' (expected '%s') in vm " "'%s'" % (self.status, self.expected, self.vm_name))
[docs]class QemuAgent(Monitor): """ Wraps qemu guest agent commands. """ READ_OBJECTS_TIMEOUT = 5 CMD_TIMEOUT = 20 RESPONSE_TIMEOUT = 20 PROMPT_TIMEOUT = 20 SERIAL_TYPE_VIRTIO = "virtio" SERIAL_TYPE_ISA = "isa" SUPPORTED_SERIAL_TYPE = [SERIAL_TYPE_VIRTIO, SERIAL_TYPE_ISA] SHUTDOWN_MODE_POWERDOWN = "powerdown" SHUTDOWN_MODE_REBOOT = "reboot" SHUTDOWN_MODE_HALT = "halt" SUSPEND_MODE_DISK = "disk" SUSPEND_MODE_RAM = "ram" SUSPEND_MODE_HYBRID = "hybrid" FSFREEZE_STATUS_FROZEN = "frozen" FSFREEZE_STATUS_THAWED = "thawed" def __init__(self, vm, name, serial_type, serial_filename, get_supported_cmds=False, suppress_exceptions=False): """ Connect to the guest agent socket, Also make sure the json module is available. :param vm: The VM object who has this GuestAgent. :param name: Guest agent identifier. :param serial_type: Specific which serial type (firtio or isa) guest agent will use. :param serial_filename: Guest agent socket filename. :param get_supported_cmds: Try to get supported cmd list when initiation. :param suppress_exceptions: If True, ignore VAgentError exception. :raise VAgentConnectError: Raised if the connection fails and suppress_exceptions is False :raise VAgentNotSupportedError: Raised if the serial type is neither 'virtio' nor 'isa' and suppress_exceptions is False :raise VAgentNotSupportedError: Raised if json isn't available and suppress_exceptions is False """ try: if serial_type not in self.SUPPORTED_SERIAL_TYPE: raise VAgentNotSupportedError("Not supported serial type: " "'%s'" % serial_type) Monitor.__init__(self, vm, name, serial_filename) # Make sure json is available try: json except NameError: raise VAgentNotSupportedError("guest agent requires the json" " module (Python 2.6 and up)") # Set a reference to the VM object that has this GuestAgent. self.vm = vm if get_supported_cmds: self._get_supported_cmds() # pylint: disable=E0712 except VAgentError, e: self._close_sock() if suppress_exceptions: logging.warn(e) else: raise # Methods only used inside this class def _build_cmd(self, cmd, args=None): obj = {"execute": cmd} if args is not None: obj["arguments"] = args return obj def _read_objects(self, timeout=READ_OBJECTS_TIMEOUT): """ Read lines from the guest agent socket and try to decode them. Stop when all available lines have been successfully decoded, or when timeout expires. Return all decoded objects. :param timeout: Time to wait for all lines to decode successfully :return: A list of objects """ if not self._data_available(): return [] s = "" end_time = time.time() + timeout while self._data_available(end_time - time.time()): s += self._recvall() # Make sure all lines are decodable for line in s.splitlines(): if line: try: json.loads(line) except Exception: # Found an incomplete or broken line -- keep reading break else: # All lines are OK -- stop reading break # Decode all decodable lines objs = [] for line in s.splitlines(): try: objs += [json.loads(line)] self._log_lines(line) except Exception: pass return objs def _send(self, data): """ Send raw data without waiting for response. :param data: Data to send :raise VAgentSocketError: Raised if a socket error occurs """ try: self._socket.sendall(data) self._log_lines(str(data)) except socket.error, e: raise VAgentSocketError("Could not send data: %r" % data, e) def _get_response(self, timeout=RESPONSE_TIMEOUT): """ Read a response from the guest agent socket. :param id: If not None, look for a response with this id :param timeout: Time duration to wait for response :return: The response dict """ end_time = time.time() + timeout while self._data_available(end_time - time.time()): for obj in self._read_objects(): if isinstance(obj, dict): if "return" in obj or "error" in obj: return obj # Return empty dict when timeout. return {} def _sync(self, timeout=RESPONSE_TIMEOUT * 3): """ Helper for guest agent socket sync. The guest agent doesn't provide a command id in its response, so we have to send 'guest-sync' cmd by ourselves to keep the socket synced. :param timeout: Time duration to wait for response :return: True if socket is synced. """ def check_result(response): if response: self._log_response(cmd, r) if "return" in response: return response["return"] if "error" in response: raise VAgentError("Get an error message when waiting for sync" " with qemu guest agent, check the debug log" " for the future message," " detail: '%s'" % r["error"]) cmd = "guest-sync" rnd_num = random.randint(1000, 9999) args = {"id": rnd_num} self._log_command(cmd) cmdobj = self._build_cmd(cmd, args) data = json.dumps(cmdobj) + "\n" # Send command r = self.cmd_raw(data) if check_result(r) == rnd_num: return True # We don't get the correct response of 'guest-sync' cmd, # thus wait for the response until timeout. start_time = time.time() while (time.time() - start_time) < timeout: r = self._get_response() if check_result(r) == rnd_num: return True return False def _get_supported_cmds(self): """ Get supported qmp cmds list. """ self._sync() cmds = self.cmd("guest-info", debug=False) if cmds and cmds.has_key("supported_commands"): cmd_list = cmds["supported_commands"] self._supported_cmds = [n["name"] for n in cmd_list if isinstance(n, dict) and n.has_key("name")] if not self._supported_cmds: # If initiation fails, set supported list to a None-only list. self._supported_cmds = [None] logging.warn("Could not get supported guest agent cmds list") def _has_command(self, cmd): """ Check wheter guest agent support 'cmd'. :param cmd: command string which will be checked. :return: True if cmd is supported, False if not supported. """ # Initiate supported cmds list if it's empty. if not self._supported_cmds: self.get_supported_cmds() # If the first element in supported cmd list is 'None', it means # autotest fails to get the cmd list, so bypass cmd checking. if self._supported_cmds[0] is None: return True if cmd and cmd in self._supported_cmds: return True return False def _log_command(self, cmd, debug=True, extra_str=""): """ Print log message beening sent. :param cmd: Command string. :param debug: Whether to print the commands. :param extra_str: Extra string would be printed in log. """ if self.debug_log or debug: logging.debug("(vagent %s) Sending command '%s' %s", self.name, cmd, extra_str) def _log_response(self, cmd, resp, debug=True): """ Print log message for guest agent cmd's response. :param cmd: Command string. :param resp: Response from guest agent command. :param debug: Whether to print the commands. """ def _log_output(o, indent=0): logging.debug("(vagent %s) %s%s", self.name, " " * indent, o) def _dump_list(li, indent=0): for l in li: if isinstance(l, dict): _dump_dict(l, indent + 2) else: _log_output(str(l), indent) def _dump_dict(di, indent=0): for k, v in di.iteritems(): o = "%s%s: " % (" " * indent, k) if isinstance(v, dict): _log_output(o, indent) _dump_dict(v, indent + 2) elif isinstance(v, list): _log_output(o, indent) _dump_list(v, indent + 2) else: o += str(v) _log_output(o, indent) if self.debug_log or debug: logging.debug("(vagent %s) Response to '%s' " "(re-formated)", self.name, cmd) if isinstance(resp, dict): _dump_dict(resp) elif isinstance(resp, list): _dump_list(resp) else: for l in str(resp).splitlines(): _log_output(l) # Public methods
[docs] def cmd(self, cmd, args=None, timeout=CMD_TIMEOUT, debug=True, success_resp=True): """ Send a guest agent command and return the response if success_resp. :param cmd: Command to send :param args: A dict containing command arguments, or None :param timeout: Time duration to wait for response :param debug: Whether to print the commands being sent and responses :param fd: file object or file descriptor to pass :return: The response received :raise VAgentLockError: Raised if the lock cannot be acquired :raise VAgentSocketError: Raised if a socket error occurs :raise VAgentProtocolError: Raised if no response is received :raise VAgentCmdError: Raised if the response is an error message """ self._log_command(cmd, debug) # Send command cmdobj = self._build_cmd(cmd, args) data = json.dumps(cmdobj) + "\n" r = self.cmd_raw(data, timeout, success_resp) if not success_resp: return "" if "return" in r: ret = r["return"] if ret: self._log_response(cmd, ret, debug) return ret if "error" in r: raise VAgentCmdError(cmd, args, r["error"])
[docs] def cmd_raw(self, data, timeout=CMD_TIMEOUT, success_resp=True): """ Send a raw string to the guest agent and return the response. Unlike cmd(), return the raw response dict without performing any checks on it. :param data: The data to send :param timeout: Time duration to wait for response :return: The response received :raise VAgentLockError: Raised if the lock cannot be acquired :raise VAgentSocketError: Raised if a socket error occurs :raise VAgentProtocolError: Raised if no response is received """ if not self._acquire_lock(): raise VAgentLockError("Could not acquire exclusive lock to send " "data: %r" % data) try: self._read_objects() self._send(data) # Return directly for some cmd without any response. if not success_resp: return {} # Read response r = self._get_response(timeout) finally: self._lock.release() if r is None: raise VAgentProtocolError( "Received no response to data: %r" % data) return r
[docs] def cmd_obj(self, obj, timeout=CMD_TIMEOUT): """ Transform a Python object to JSON, send the resulting string to the guest agent, and return the response. Unlike cmd(), return the raw response dict without performing any checks on it. :param obj: The object to send :param timeout: Time duration to wait for response :return: The response received :raise VAgentLockError: Raised if the lock cannot be acquired :raise VAgentSocketError: Raised if a socket error occurs :raise VAgentProtocolError: Raised if no response is received """ return self.cmd_raw(json.dumps(obj) + "\n", timeout)
[docs] def verify_responsive(self): """ Make sure the guest agent is responsive by sending a command. """ cmd = "guest-ping" if self._has_command(cmd): self.cmd(cmd=cmd, debug=False)
@error.context_aware def shutdown(self, mode=SHUTDOWN_MODE_POWERDOWN): """ Send "guest-shutdown", this cmd would not return any response. :param mode: Speicfy shutdown mode, now qemu guest agent supports 'powerdown', 'reboot', 'halt' 3 modes. :return: True if shutdown cmd is sent successfully, False if 'shutdown' is unsupported. """ cmd = "guest-shutdown" if not self._has_command(cmd): return False args = None if mode in [self.SHUTDOWN_MODE_POWERDOWN, self.SHUTDOWN_MODE_REBOOT, self.SHUTDOWN_MODE_HALT]: args = {"mode": mode} self.cmd(cmd=cmd, args=args, success_resp=False) return True @error.context_aware def sync(self): """ Sync guest agent with cmd 'guest-sync'. """ cmd = "guest-sync" if not self._has_command(cmd): return synced = self._sync() if not synced: raise VAgentSyncError(self.vm.name) @error.context_aware def suspend(self, mode=SUSPEND_MODE_RAM): """ This function tries to execute the scripts provided by the pm-utils package via guest agent interface. If it's not available, the suspend operation will be performed by manually writing to a sysfs file. Notes: #. For the best results it's strongly recommended to have the ``pm-utils`` package installed in the guest. #. The ``ram`` and 'hybrid' mode require QEMU to support the ``system_wakeup`` command. Thus, it's *required* to query QEMU for the presence of the ``system_wakeup`` command before issuing guest agent command. :param mode: Specify suspend mode, could be one of ``disk``, ``ram``, ``hybrid``. :return: True if shutdown cmd is sent successfully, False if ``suspend`` is unsupported. :raise VAgentSuspendUnknownModeError: Raise if mode is not supported. """ error.context("Suspend guest '%s' to '%s'" % (self.vm.name, mode)) if mode not in [self.SUSPEND_MODE_DISK, self.SUSPEND_MODE_RAM, self.SUSPEND_MODE_HYBRID]: raise VAgentSuspendUnknownModeError("Not supported suspend" " mode '%s'" % mode) cmd = "guest-suspend-%s" % mode if not self._has_command(cmd): return False # First, sync with guest. self.sync() # Then send suspend cmd. self.cmd(cmd=cmd, success_resp=False) return True
[docs] def get_fsfreeze_status(self): """ Get guest 'fsfreeze' status. The status could be 'frozen' or 'thawed'. """ cmd = "guest-fsfreeze-status" if self._has_command(cmd): return self.cmd(cmd=cmd)
[docs] def verify_fsfreeze_status(self, expected): """ Verify the guest agent fsfreeze status is same as expected, if not, raise a VAgentFreezeStatusError. :param expected: The expected status. :raise VAgentFreezeStatusError: Raise if the guest fsfreeze status is unexpected. """ status = self.get_fsfreeze_status() if status != expected: raise VAgentFreezeStatusError(self.vm.name, status, expected)
@error.context_aware def fsfreeze(self, check_status=True): """ Freeze File system on guest. :param check_status: Force this function to check the fsreeze status before/after sending cmd. :return: Frozen FS number if cmd succeed, -1 if guest agent doesn't support fsfreeze cmd. """ error.context("Freeze all FS in guest '%s'" % self.vm.name) if check_status: self.verify_fsfreeze_status(self.FSFREEZE_STATUS_THAWED) cmd = "guest-fsfreeze-freeze" if self._has_command(cmd): ret = self.cmd(cmd=cmd) if check_status: try: self.verify_fsfreeze_status(self.FSFREEZE_STATUS_FROZEN) # pylint: disable=E0712 except VAgentFreezeStatusError: # When the status is incorrect, reset fsfreeze status to # 'thawed'. self.cmd(cmd="guest-fsreeze-thaw") raise return ret return -1 @error.context_aware def fsthaw(self, check_status=True): """ Thaw File system on guest. :param check_status: Force this function to check the fsreeze status before/after sending cmd. :return: Thaw FS number if cmd succeed, -1 if guest agent doesn't support fsfreeze cmd. """ error.context("thaw all FS in guest '%s'" % self.vm.name) if check_status: self.verify_fsfreeze_status(self.FSFREEZE_STATUS_FROZEN) cmd = "guest-fsfreeze-thaw" if self._has_command(cmd): ret = self.cmd(cmd=cmd) if check_status: try: self.verify_fsfreeze_status(self.FSFREEZE_STATUS_THAWED) # pylint: disable=E0712 except VAgentFreezeStatusError: # When the status is incorrect, reset fsfreeze status to # 'thawed'. self.cmd(cmd=cmd) raise return ret return -1