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