blob: 8815bf40cd2f835393339f894d3e625edad111e6 [file] [log] [blame]
import os, logging, time, glob, re
from autotest_lib.client.common_lib import error
from autotest_lib.client.bin import utils
import virt_utils
class VMError(Exception):
pass
class VMCreateError(VMError):
def __init__(self, cmd, status, output):
VMError.__init__(self, cmd, status, output)
self.cmd = cmd
self.status = status
self.output = output
def __str__(self):
return ("VM creation command failed: %r (status: %s, "
"output: %r)" % (self.cmd, self.status, self.output))
class VMHashMismatchError(VMError):
def __init__(self, actual, expected):
VMError.__init__(self, actual, expected)
self.actual_hash = actual
self.expected_hash = expected
def __str__(self):
return ("CD image hash (%s) differs from expected one (%s)" %
(self.actual_hash, self.expected_hash))
class VMImageMissingError(VMError):
def __init__(self, filename):
VMError.__init__(self, filename)
self.filename = filename
def __str__(self):
return "CD image file not found: %r" % self.filename
class VMImageCheckError(VMError):
def __init__(self, filename):
VMError.__init__(self, filename)
self.filename = filename
def __str__(self):
return "Errors found on image: %r" % self.filename
class VMBadPATypeError(VMError):
def __init__(self, pa_type):
VMError.__init__(self, pa_type)
self.pa_type = pa_type
def __str__(self):
return "Unsupported PCI assignable type: %r" % self.pa_type
class VMPAError(VMError):
def __init__(self, pa_type):
VMError.__init__(self, pa_type)
self.pa_type = pa_type
def __str__(self):
return ("No PCI assignable devices could be assigned "
"(pci_assignable=%r)" % self.pa_type)
class VMPostCreateError(VMError):
def __init__(self, cmd, output):
VMError.__init__(self, cmd, output)
self.cmd = cmd
self.output = output
class VMHugePageError(VMPostCreateError):
def __str__(self):
return ("Cannot allocate hugepage memory (command: %r, "
"output: %r)" % (self.cmd, self.output))
class VMKVMInitError(VMPostCreateError):
def __str__(self):
return ("Cannot initialize KVM (command: %r, output: %r)" %
(self.cmd, self.output))
class VMDeadError(VMError):
def __init__(self, reason='', detail=''):
VMError.__init__(self)
self.reason = reason
self.detail = detail
def __str__(self):
msg = "VM is dead"
if self.reason:
msg += " reason: %s" % self.reason
if self.detail:
msg += " detail: %r" % self.detail
return (msg)
class VMDeadKernelCrashError(VMError):
def __init__(self, kernel_crash):
VMError.__init__(self, kernel_crash)
self.kernel_crash = kernel_crash
def __str__(self):
return ("VM is dead due to a kernel crash:\n%s" % self.kernel_crash)
class VMAddressError(VMError):
pass
class VMPortNotRedirectedError(VMAddressError):
def __init__(self, port):
VMAddressError.__init__(self, port)
self.port = port
def __str__(self):
return "Port not redirected: %s" % self.port
class VMAddressVerificationError(VMAddressError):
def __init__(self, mac, ip):
VMAddressError.__init__(self, mac, ip)
self.mac = mac
self.ip = ip
def __str__(self):
return ("Cannot verify MAC-IP address mapping using arping: "
"%s ---> %s" % (self.mac, self.ip))
class VMMACAddressMissingError(VMAddressError):
def __init__(self, nic_index):
VMAddressError.__init__(self, nic_index)
self.nic_index = nic_index
def __str__(self):
return "No MAC address defined for NIC #%s" % self.nic_index
class VMIPAddressMissingError(VMAddressError):
def __init__(self, mac):
VMAddressError.__init__(self, mac)
self.mac = mac
def __str__(self):
return "Cannot find IP address for MAC address %s" % self.mac
class VMMigrateError(VMError):
pass
class VMMigrateTimeoutError(VMMigrateError):
pass
class VMMigrateCancelError(VMMigrateError):
pass
class VMMigrateFailedError(VMMigrateError):
pass
class VMMigrateProtoUnsupportedError(VMMigrateError):
pass
class VMMigrateStateMismatchError(VMMigrateError):
def __init__(self, src_hash, dst_hash):
VMMigrateError.__init__(self, src_hash, dst_hash)
self.src_hash = src_hash
self.dst_hash = dst_hash
def __str__(self):
return ("Mismatch of VM state before and after migration (%s != %s)" %
(self.src_hash, self.dst_hash))
class VMRebootError(VMError):
pass
class VMStatusError(VMError):
pass
def get_image_filename(params, root_dir):
"""
Generate an image path from params and root_dir.
@param params: Dictionary containing the test parameters.
@param root_dir: Base directory for relative filenames.
@note: params should contain:
image_name -- the name of the image file, without extension
image_format -- the format of the image (qcow2, raw etc)
"""
image_name = params.get("image_name", "image")
image_format = params.get("image_format", "qcow2")
if params.get("image_raw_device") == "yes":
return image_name
image_filename = "%s.%s" % (image_name, image_format)
image_filename = virt_utils.get_path(root_dir, image_filename)
return image_filename
def create_image(params, root_dir):
"""
Create an image using qemu_image.
@param params: Dictionary containing the test parameters.
@param root_dir: Base directory for relative filenames.
@note: params should contain:
image_name -- the name of the image file, without extension
image_format -- the format of the image (qcow2, raw etc)
image_cluster_size (optional) -- the cluster size for the image
image_size -- the requested size of the image (a string
qemu-img can understand, such as '10G')
"""
qemu_img_cmd = virt_utils.get_path(root_dir, params.get("qemu_img_binary",
"qemu-img"))
qemu_img_cmd += " create"
format = params.get("image_format", "qcow2")
qemu_img_cmd += " -f %s" % format
image_cluster_size = params.get("image_cluster_size", None)
if image_cluster_size is not None:
qemu_img_cmd += " -o cluster_size=%s" % image_cluster_size
image_filename = get_image_filename(params, root_dir)
qemu_img_cmd += " %s" % image_filename
size = params.get("image_size", "10G")
qemu_img_cmd += " %s" % size
utils.system(qemu_img_cmd)
return image_filename
def remove_image(params, root_dir):
"""
Remove an image file.
@param params: A dict
@param root_dir: Base directory for relative filenames.
@note: params should contain:
image_name -- the name of the image file, without extension
image_format -- the format of the image (qcow2, raw etc)
"""
image_filename = get_image_filename(params, root_dir)
logging.debug("Removing image file %s", image_filename)
if os.path.exists(image_filename):
os.unlink(image_filename)
else:
logging.debug("Image file %s not found")
def check_image(params, root_dir):
"""
Check an image using the appropriate tools for each virt backend.
@param params: Dictionary containing the test parameters.
@param root_dir: Base directory for relative filenames.
@note: params should contain:
image_name -- the name of the image file, without extension
image_format -- the format of the image (qcow2, raw etc)
@raise VMImageCheckError: In case qemu-img check fails on the image.
"""
vm_type = params.get("vm_type")
if vm_type == 'kvm':
image_filename = get_image_filename(params, root_dir)
logging.debug("Checking image file %s", image_filename)
qemu_img_cmd = virt_utils.get_path(root_dir,
params.get("qemu_img_binary", "qemu-img"))
image_is_qcow2 = params.get("image_format") == 'qcow2'
if os.path.exists(image_filename) and image_is_qcow2:
# Verifying if qemu-img supports 'check'
q_result = utils.run(qemu_img_cmd, ignore_status=True)
q_output = q_result.stdout
check_img = True
if not "check" in q_output:
logging.error("qemu-img does not support 'check', "
"skipping check")
check_img = False
if not "info" in q_output:
logging.error("qemu-img does not support 'info', "
"skipping check")
check_img = False
if check_img:
try:
utils.system("%s info %s" % (qemu_img_cmd, image_filename))
except error.CmdError:
logging.error("Error getting info from image %s",
image_filename)
cmd_result = utils.run("%s check %s" %
(qemu_img_cmd, image_filename),
ignore_status=True)
# Error check, large chances of a non-fatal problem.
# There are chances that bad data was skipped though
if cmd_result.exit_status == 1:
for e_line in cmd_result.stdout.splitlines():
logging.error("[stdout] %s", e_line)
for e_line in cmd_result.stderr.splitlines():
logging.error("[stderr] %s", e_line)
raise error.TestWarn("qemu-img check error. Some bad data "
"in the image may have gone unnoticed")
# Exit status 2 is data corruption for sure, so fail the test
elif cmd_result.exit_status == 2:
for e_line in cmd_result.stdout.splitlines():
logging.error("[stdout] %s", e_line)
for e_line in cmd_result.stderr.splitlines():
logging.error("[stderr] %s", e_line)
raise VMImageCheckError(image_filename)
# Leaked clusters, they are known to be harmless to data
# integrity
elif cmd_result.exit_status == 3:
raise error.TestWarn("Leaked clusters were noticed during "
"image check. No data integrity "
"problem was found though.")
else:
if not os.path.exists(image_filename):
logging.debug("Image file %s not found, skipping check",
image_filename)
elif not image_is_qcow2:
logging.debug("Image file %s not qcow2, skipping check",
image_filename)
class BaseVM(object):
"""
Base class for all hypervisor specific VM subclasses.
This class should not be used directly, that is, do not attempt to
instantiate and use this class. Instead, one should implement a subclass
that implements, at the very least, all methods defined right after the
the comment blocks that are marked with:
"Public API - *must* be reimplemented with virt specific code"
and
"Protected API - *must* be reimplemented with virt specific classes"
The current proposal regarding methods naming convention is:
- Public API methods: named in the usual way, consumed by tests
- Protected API methods: name begins with a single underline, to be
consumed only by BaseVM and subclasses
- Private API methods: name begins with double underline, to be consumed
only by the VM subclass itself (usually implements virt specific
functionality: example: __make_qemu_command())
So called "protected" methods are intended to be used only by VM classes,
and not be consumed by tests. Theses should respect a naming convention
and always be preceeded by a single underline.
Currently most (if not all) methods are public and appears to be consumed
by tests. It is a ongoing task to determine whether methods should be
"public" or "protected".
"""
#
# Assuming that all low-level hypervisor have at least migration via tcp
# (true for xen & kvm). Also true for libvirt (using xen and kvm drivers)
#
MIGRATION_PROTOS = ['tcp', ]
def __init__(self, name, params):
self.name = name
self.params = params
#
# Assuming all low-level hypervisors will have a serial (like) console
# connection to the guest. libvirt also supports serial (like) consoles
# (virDomainOpenConsole). subclasses should set this to an object that
# is or behaves like aexpect.ShellSession.
#
self.serial_console = None
self._generate_unique_id()
def _generate_unique_id(self):
"""
Generate a unique identifier for this VM
"""
while True:
self.instance = (time.strftime("%Y%m%d-%H%M%S-") +
virt_utils.generate_random_string(4))
if not glob.glob("/tmp/*%s" % self.instance):
break
#
# Public API - could be reimplemented with virt specific code
#
def verify_alive(self):
"""
Make sure the VM is alive and that the main monitor is responsive.
Can be subclassed to provide better information on why the VM is
not alive (reason, detail)
@raise VMDeadError: If the VM is dead
@raise: Various monitor exceptions if the monitor is unresponsive
"""
if self.is_dead():
raise VMDeadError
def get_mac_address(self, nic_index=0):
"""
Return the MAC address of a NIC.
@param nic_index: Index of the NIC
@raise VMMACAddressMissingError: If no MAC address is defined for the
requested NIC
"""
nic_name = self.params.objects("nics")[nic_index]
nic_params = self.params.object_params(nic_name)
mac = (nic_params.get("nic_mac") or
virt_utils.get_mac_address(self.instance, nic_index))
if not mac:
raise VMMACAddressMissingError(nic_index)
return mac
def verify_kernel_crash(self):
"""
Find kernel crash message on the VM serial console.
@raise: VMDeadKernelCrashError, in case a kernel crash message was
found.
"""
if self.serial_console is not None:
data = self.serial_console.get_output()
match = re.search(r"BUG:.*---\[ end trace .* \]---", data,
re.DOTALL|re.MULTILINE)
if match is not None:
raise VMDeadKernelCrashError(match.group(0))
def get_params(self):
"""
Return the VM's params dict. Most modified params take effect only
upon VM.create().
"""
return self.params
def get_serial_console_filename(self):
"""
Return the serial console filename.
"""
return "/tmp/serial-%s" % self.instance
def get_testlog_filename(self):
"""
Return the testlog filename.
"""
return "/tmp/testlog-%s" % self.instance
@error.context_aware
def login(self, nic_index=0, timeout=10):
"""
Log into the guest via SSH/Telnet/Netcat.
If timeout expires while waiting for output from the guest (e.g. a
password prompt or a shell prompt) -- fail.
@param nic_index: The index of the NIC to connect to.
@param timeout: Time (seconds) before giving up logging into the
guest.
@return: A ShellSession object.
"""
error.context("logging into '%s'" % self.name)
username = self.params.get("username", "")
password = self.params.get("password", "")
prompt = self.params.get("shell_prompt", "[\#\$]")
linesep = eval("'%s'" % self.params.get("shell_linesep", r"\n"))
client = self.params.get("shell_client")
address = self.get_address(nic_index)
port = self.get_port(int(self.params.get("shell_port")))
log_filename = ("session-%s-%s.log" %
(self.name, virt_utils.generate_random_string(4)))
session = virt_utils.remote_login(client, address, port, username,
password, prompt, linesep,
log_filename, timeout)
session.set_status_test_command(self.params.get("status_test_command",
""))
return session
def remote_login(self, nic_index=0, timeout=10):
"""
Alias for login() for backward compatibility.
"""
return self.login(nic_index, timeout)
def wait_for_login(self, nic_index=0, timeout=240, internal_timeout=10):
"""
Make multiple attempts to log into the guest via SSH/Telnet/Netcat.
@param nic_index: The index of the NIC to connect to.
@param timeout: Time (seconds) to keep trying to log in.
@param internal_timeout: Timeout to pass to login().
@return: A ShellSession object.
"""
logging.debug("Attempting to log into '%s' (timeout %ds)", self.name,
timeout)
end_time = time.time() + timeout
while time.time() < end_time:
try:
return self.login(nic_index, internal_timeout)
except (virt_utils.LoginError, VMError), e:
logging.debug(e)
time.sleep(2)
# Timeout expired; try one more time but don't catch exceptions
return self.login(nic_index, internal_timeout)
@error.context_aware
def copy_files_to(self, host_path, guest_path, nic_index=0, verbose=False,
timeout=600):
"""
Transfer files to the remote host(guest).
@param host_path: Host path
@param guest_path: Guest path
@param nic_index: The index of the NIC to connect to.
@param verbose: If True, log some stats using logging.debug (RSS only)
@param timeout: Time (seconds) before giving up on doing the remote
copy.
"""
error.context("sending file(s) to '%s'" % self.name)
username = self.params.get("username", "")
password = self.params.get("password", "")
client = self.params.get("file_transfer_client")
address = self.get_address(nic_index)
port = self.get_port(int(self.params.get("file_transfer_port")))
log_filename = ("transfer-%s-to-%s-%s.log" %
(self.name, address,
virt_utils.generate_random_string(4)))
virt_utils.copy_files_to(address, client, username, password, port,
host_path, guest_path, log_filename, verbose,
timeout)
@error.context_aware
def copy_files_from(self, guest_path, host_path, nic_index=0,
verbose=False, timeout=600):
"""
Transfer files from the guest.
@param host_path: Guest path
@param guest_path: Host path
@param nic_index: The index of the NIC to connect to.
@param verbose: If True, log some stats using logging.debug (RSS only)
@param timeout: Time (seconds) before giving up on doing the remote
copy.
"""
error.context("receiving file(s) from '%s'" % self.name)
username = self.params.get("username", "")
password = self.params.get("password", "")
client = self.params.get("file_transfer_client")
address = self.get_address(nic_index)
port = self.get_port(int(self.params.get("file_transfer_port")))
log_filename = ("transfer-%s-from-%s-%s.log" %
(self.name, address,
virt_utils.generate_random_string(4)))
virt_utils.copy_files_from(address, client, username, password, port,
guest_path, host_path, log_filename,
verbose, timeout)
@error.context_aware
def serial_login(self, timeout=10):
"""
Log into the guest via the serial console.
If timeout expires while waiting for output from the guest (e.g. a
password prompt or a shell prompt) -- fail.
@param timeout: Time (seconds) before giving up logging into the guest.
@return: ShellSession object on success and None on failure.
"""
error.context("logging into '%s' via serial console" % self.name)
username = self.params.get("username", "")
password = self.params.get("password", "")
prompt = self.params.get("shell_prompt", "[\#\$]")
linesep = eval("'%s'" % self.params.get("shell_linesep", r"\n"))
status_test_command = self.params.get("status_test_command", "")
self.serial_console.set_linesep(linesep)
self.serial_console.set_status_test_command(status_test_command)
# Try to get a login prompt
self.serial_console.sendline()
virt_utils._remote_login(self.serial_console, username, password,
prompt, timeout)
return self.serial_console
def wait_for_serial_login(self, timeout=240, internal_timeout=10):
"""
Make multiple attempts to log into the guest via serial console.
@param timeout: Time (seconds) to keep trying to log in.
@param internal_timeout: Timeout to pass to serial_login().
@return: A ShellSession object.
"""
logging.debug("Attempting to log into '%s' via serial console "
"(timeout %ds)", self.name, timeout)
end_time = time.time() + timeout
while time.time() < end_time:
try:
return self.serial_login(internal_timeout)
except virt_utils.LoginError, e:
logging.debug(e)
time.sleep(2)
# Timeout expired; try one more time but don't catch exceptions
return self.serial_login(internal_timeout)
def get_uuid(self):
"""
Catch UUID of the VM.
@return: None,if not specified in config file
"""
if self.params.get("uuid") == "random":
return self.uuid
else:
return self.params.get("uuid", None)
def send_string(self, str):
"""
Send a string to the VM.
@param str: String, that must consist of alphanumeric characters only.
Capital letters are allowed.
"""
for char in str:
if char.isupper():
self.send_key("shift-%s" % char.lower())
else:
self.send_key(char)
def get_cpu_count(self):
"""
Get the cpu count of the VM.
"""
session = self.login()
try:
return int(session.cmd(self.params.get("cpu_chk_cmd")))
finally:
session.close()
def get_memory_size(self, cmd=None):
"""
Get bootup memory size of the VM.
@param check_cmd: Command used to check memory. If not provided,
self.params.get("mem_chk_cmd") will be used.
"""
session = self.login()
try:
if not cmd:
cmd = self.params.get("mem_chk_cmd")
mem_str = session.cmd(cmd)
mem = re.findall("([0-9]+)", mem_str)
mem_size = 0
for m in mem:
mem_size += int(m)
if "GB" in mem_str:
mem_size *= 1024
elif "MB" in mem_str:
pass
else:
mem_size /= 1024
return int(mem_size)
finally:
session.close()
def get_current_memory_size(self):
"""
Get current memory size of the VM, rather than bootup memory.
"""
cmd = self.params.get("mem_chk_cur_cmd")
return self.get_memory_size(cmd)
#
# Public API - *must* be reimplemented with virt specific code
#
def is_alive(self):
"""
Return True if the VM is alive and the management interface is responsive.
"""
raise NotImplementedError
def is_dead(self):
"""
Return True if the the VM is dead.
"""
raise NotImplementedError
def get_address(self, index=0):
"""
Return the IP address of a NIC of the guest
@param index: Index of the NIC whose address is requested.
@raise VMMACAddressMissingError: If no MAC address is defined for the
requested NIC
@raise VMIPAddressMissingError: If no IP address is found for the the
NIC's MAC address
@raise VMAddressVerificationError: If the MAC-IP address mapping cannot
be verified (using arping)
"""
raise NotImplementedError
def clone(self, name, **params):
"""
Return a clone of the VM object with optionally modified parameters.
This method should be implemented by
"""
raise NotImplementedError
def destroy(self, gracefully=True, free_mac_addresses=True):
"""
Destroy the VM.
If gracefully is True, first attempt to shutdown the VM with a shell
command. Then, attempt to destroy the VM via the monitor with a 'quit'
command. If that fails, send SIGKILL to the qemu process.
@param gracefully: If True, an attempt will be made to end the VM
using a shell command before trying to end the qemu process
with a 'quit' or a kill signal.
@param free_mac_addresses: If True, the MAC addresses used by the VM
will be freed.
"""
raise NotImplementedError
def migrate(self, timeout=3600, protocol="tcp", cancel_delay=None,
offline=False, stable_check=False, clean=True,
save_path="/tmp", dest_host="localhost", remote_port=None):
"""
Migrate the VM.
If the migration is local, the VM object's state is switched with that
of the destination VM. Otherwise, the state is switched with that of
a dead VM (returned by self.clone()).
@param timeout: Time to wait for migration to complete.
@param protocol: Migration protocol ('tcp', 'unix' or 'exec').
@param cancel_delay: If provided, specifies a time duration after which
migration will be canceled. Used for testing migrate_cancel.
@param offline: If True, pause the source VM before migration.
@param stable_check: If True, compare the VM's state after migration to
its state before migration and raise an exception if they
differ.
@param clean: If True, delete the saved state files (relevant only if
stable_check is also True).
@save_path: The path for state files.
@param dest_host: Destination host (defaults to 'localhost').
@param remote_port: Port to use for remote migration.
"""
raise NotImplementedError
def reboot(self, session=None, method="shell", nic_index=0, timeout=240):
"""
Reboot the VM and wait for it to come back up by trying to log in until
timeout expires.
@param session: A shell session object or None.
@param method: Reboot method. Can be "shell" (send a shell reboot
command) or "system_reset" (send a system_reset monitor command).
@param nic_index: Index of NIC to access in the VM, when logging in
after rebooting.
@param timeout: Time to wait for login to succeed (after rebooting).
@return: A new shell session object.
"""
raise NotImplementedError
# should this really be expected from VMs of all hypervisor types?
def send_key(self, keystr):
"""
Send a key event to the VM.
@param: keystr: A key event string (e.g. "ctrl-alt-delete")
"""
raise NotImplementedError
def save_to_file(self, path):
"""
Save the state of virtual machine to a file through migrate to
exec
"""
raise NotImplementedError
def needs_restart(self, name, params, basedir):
"""
Based on virt preprocessing information, decide whether the VM needs
a restart.
"""
raise NotImplementedError