Index: client/tests/kvm/kvm_vm.py |
diff --git a/client/tests/kvm/kvm_vm.py b/client/tests/kvm/kvm_vm.py |
index a860437f6ea41e92d8f069b8bdc6e3a61d45e7f6..969558b92b8f52ecba07db4e6faf4adb59904ecf 100755 |
--- a/client/tests/kvm/kvm_vm.py |
+++ b/client/tests/kvm/kvm_vm.py |
@@ -5,12 +5,179 @@ Utility classes and functions to handle Virtual Machine creation using qemu. |
@copyright: 2008-2009 Red Hat Inc. |
""" |
-import time, socket, os, logging, fcntl, re, commands, shelve, glob |
-import kvm_utils, kvm_subprocess, kvm_monitor, rss_file_transfer |
+import time, os, logging, fcntl, re, commands, glob |
+import kvm_utils, kvm_subprocess, kvm_monitor |
from autotest_lib.client.common_lib import error |
from autotest_lib.client.bin import 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, status, output): |
+ VMError.__init__(self, status, output) |
+ self.status = status |
+ self.output = output |
+ |
+ def __str__(self): |
+ return ("VM process is dead (status: %s, output: %r)" % |
+ (self.status, self.output)) |
+ |
+ |
+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 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 |
+ |
+ |
def get_image_filename(params, root_dir): |
""" |
Generate an image path from params and root_dir. |
@@ -24,6 +191,8 @@ def get_image_filename(params, root_dir): |
""" |
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 = kvm_utils.get_path(root_dir, image_filename) |
return image_filename |
@@ -55,19 +224,8 @@ def create_image(params, root_dir): |
size = params.get("image_size", "10G") |
qemu_img_cmd += " %s" % size |
- try: |
- utils.system(qemu_img_cmd) |
- except error.CmdError, e: |
- logging.error("Could not create image; qemu-img command failed:\n%s", |
- str(e)) |
- return None |
- |
- if not os.path.exists(image_filename): |
- logging.error("Image could not be created for some reason; " |
- "qemu-img command:\n%s" % qemu_img_cmd) |
- return None |
- |
- logging.info("Image created in %s" % image_filename) |
+ utils.system(qemu_img_cmd) |
+ logging.info("Image created in %r", image_filename) |
return image_filename |
@@ -83,19 +241,70 @@ def remove_image(params, root_dir): |
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) |
+ 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 qemu-img. |
+ |
+ @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. |
+ """ |
+ image_filename = get_image_filename(params, root_dir) |
+ logging.debug("Checking image file %s...", image_filename) |
+ qemu_img_cmd = kvm_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) |
+ try: |
+ utils.system("%s check %s" % (qemu_img_cmd, image_filename)) |
+ except error.CmdError: |
+ raise VMImageCheckError(image_filename) |
+ |
+ 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 VM: |
""" |
This class handles all basic VM operations. |
""" |
- def __init__(self, name, params, root_dir, address_cache): |
+ def __init__(self, name, params, root_dir, address_cache, state=None): |
""" |
Initialize the object and set a few attributes. |
@@ -104,30 +313,35 @@ class VM: |
(see method make_qemu_command for a full description) |
@param root_dir: Base directory for relative filenames |
@param address_cache: A dict that maps MAC addresses to IP addresses |
+ @param state: If provided, use this as self.__dict__ |
""" |
- self.process = None |
- self.serial_console = None |
- self.redirs = {} |
- self.vnc_port = 5900 |
- self.monitors = [] |
- self.pci_assignable = None |
- self.netdev_id = [] |
- self.uuid = None |
+ if state: |
+ self.__dict__ = state |
+ else: |
+ self.process = None |
+ self.serial_console = None |
+ self.redirs = {} |
+ self.vnc_port = 5900 |
+ self.monitors = [] |
+ self.pci_assignable = None |
+ self.netdev_id = [] |
+ self.uuid = None |
+ |
+ # Find a unique identifier for this VM |
+ while True: |
+ self.instance = (time.strftime("%Y%m%d-%H%M%S-") + |
+ kvm_utils.generate_random_string(4)) |
+ if not glob.glob("/tmp/*%s" % self.instance): |
+ break |
self.name = name |
self.params = params |
self.root_dir = root_dir |
self.address_cache = address_cache |
- # Find a unique identifier for this VM |
- while True: |
- self.instance = (time.strftime("%Y%m%d-%H%M%S-") + |
- kvm_utils.generate_random_string(4)) |
- if not glob.glob("/tmp/*%s" % self.instance): |
- break |
- |
- def clone(self, name=None, params=None, root_dir=None, address_cache=None): |
+ def clone(self, name=None, params=None, root_dir=None, address_cache=None, |
+ copy_state=False): |
""" |
Return a clone of the VM object with optionally modified parameters. |
The clone is initially not alive and needs to be started using create(). |
@@ -138,6 +352,8 @@ class VM: |
@param params: Optional new VM creation parameters |
@param root_dir: Optional new base directory for relative filenames |
@param address_cache: A dict that maps MAC addresses to IP addresses |
+ @param copy_state: If True, copy the original VM's state to the clone. |
+ Mainly useful for make_qemu_command(). |
""" |
if name is None: |
name = self.name |
@@ -147,7 +363,11 @@ class VM: |
root_dir = self.root_dir |
if address_cache is None: |
address_cache = self.address_cache |
- return VM(name, params, root_dir, address_cache) |
+ if copy_state: |
+ state = self.__dict__.copy() |
+ else: |
+ state = None |
+ return VM(name, params, root_dir, address_cache, state) |
def make_qemu_command(self, name=None, params=None, root_dir=None): |
@@ -225,36 +445,40 @@ class VM: |
def add_drive(help, filename, index=None, format=None, cache=None, |
werror=None, serial=None, snapshot=False, boot=False): |
cmd = " -drive file='%s'" % filename |
- if index is not None: cmd += ",index=%s" % index |
- if format: cmd += ",if=%s" % format |
- if cache: cmd += ",cache=%s" % cache |
- if werror: cmd += ",werror=%s" % werror |
- if serial: cmd += ",serial='%s'" % serial |
- if snapshot: cmd += ",snapshot=on" |
- if boot: cmd += ",boot=on" |
+ if index is not None: |
+ cmd += ",index=%s" % index |
+ if format: |
+ cmd += ",if=%s" % format |
+ if cache: |
+ cmd += ",cache=%s" % cache |
+ if werror: |
+ cmd += ",werror=%s" % werror |
+ if serial: |
+ cmd += ",serial='%s'" % serial |
+ if snapshot: |
+ cmd += ",snapshot=on" |
+ if boot: |
+ cmd += ",boot=on" |
return cmd |
def add_nic(help, vlan, model=None, mac=None, netdev_id=None, |
nic_extra_params=None): |
+ if has_option(help, "netdev"): |
+ netdev_vlan_str = ",netdev=%s" % netdev_id |
+ else: |
+ netdev_vlan_str = ",vlan=%d" % vlan |
if has_option(help, "device"): |
- if model == "virtio": |
- model="virtio-net-pci" |
if not model: |
- model= "rtl8139" |
- cmd = " -device %s" % model |
+ model = "rtl8139" |
+ elif model == "virtio": |
+ model = "virtio-net-pci" |
+ cmd = " -device %s" % model + netdev_vlan_str |
if mac: |
- cmd += ",mac=%s" % mac |
- if has_option(help, "netdev"): |
- cmd += ",netdev=%s" % netdev_id |
- else: |
- cmd += "vlan=%d," % vlan |
+ cmd += ",mac='%s'" % mac |
if nic_extra_params: |
cmd += ",%s" % nic_extra_params |
else: |
- if has_option(help, "netdev"): |
- cmd = " -net nic,netdev=%s" % netdev_id |
- else: |
- cmd = " -net nic,vlan=%d" % vlan |
+ cmd = " -net nic" + netdev_vlan_str |
if model: |
cmd += ",model=%s" % model |
if mac: |
@@ -263,11 +487,11 @@ class VM: |
def add_net(help, vlan, mode, ifname=None, script=None, |
downscript=None, tftp=None, bootfile=None, hostfwd=[], |
- netdev_id=None, vhost=False): |
+ netdev_id=None, netdev_extra_params=None): |
if has_option(help, "netdev"): |
cmd = " -netdev %s,id=%s" % (mode, netdev_id) |
- if vhost: |
- cmd +=",vhost=on" |
+ if netdev_extra_params: |
+ cmd += ",%s" % netdev_extra_params |
else: |
cmd = " -net %s,vlan=%d" % (mode, vlan) |
if mode == "tap": |
@@ -347,9 +571,15 @@ class VM: |
# End of command line option wrappers |
- if name is None: name = self.name |
- if params is None: params = self.params |
- if root_dir is None: root_dir = self.root_dir |
+ if name is None: |
+ name = self.name |
+ if params is None: |
+ params = self.params |
+ if root_dir is None: |
+ root_dir = self.root_dir |
+ |
+ # Clone this VM using the new params |
+ vm = self.clone(name, params, root_dir, copy_state=True) |
qemu_binary = kvm_utils.get_path(root_dir, params.get("qemu_binary", |
"qemu")) |
@@ -368,19 +598,19 @@ class VM: |
# Add the VM's name |
qemu_cmd += add_name(help, name) |
# Add monitors |
- for monitor_name in kvm_utils.get_sub_dict_names(params, "monitors"): |
- monitor_params = kvm_utils.get_sub_dict(params, monitor_name) |
- monitor_filename = self.get_monitor_filename(monitor_name) |
+ for monitor_name in params.objects("monitors"): |
+ monitor_params = params.object_params(monitor_name) |
+ monitor_filename = vm.get_monitor_filename(monitor_name) |
if monitor_params.get("monitor_type") == "qmp": |
qemu_cmd += add_qmp_monitor(help, monitor_filename) |
else: |
qemu_cmd += add_human_monitor(help, monitor_filename) |
# Add serial console redirection |
- qemu_cmd += add_serial(help, self.get_serial_console_filename()) |
+ qemu_cmd += add_serial(help, vm.get_serial_console_filename()) |
- for image_name in kvm_utils.get_sub_dict_names(params, "images"): |
- image_params = kvm_utils.get_sub_dict(params, image_name) |
+ for image_name in params.objects("images"): |
+ image_params = params.object_params(image_name) |
if image_params.get("boot_drive") == "no": |
continue |
qemu_cmd += add_drive(help, |
@@ -394,20 +624,23 @@ class VM: |
image_params.get("image_boot") == "yes") |
redirs = [] |
- for redir_name in kvm_utils.get_sub_dict_names(params, "redirs"): |
- redir_params = kvm_utils.get_sub_dict(params, redir_name) |
+ for redir_name in params.objects("redirs"): |
+ redir_params = params.object_params(redir_name) |
guest_port = int(redir_params.get("guest_port")) |
- host_port = self.redirs.get(guest_port) |
+ host_port = vm.redirs.get(guest_port) |
redirs += [(host_port, guest_port)] |
vlan = 0 |
- for nic_name in kvm_utils.get_sub_dict_names(params, "nics"): |
- nic_params = kvm_utils.get_sub_dict(params, nic_name) |
+ for nic_name in params.objects("nics"): |
+ nic_params = params.object_params(nic_name) |
+ try: |
+ netdev_id = vm.netdev_id[vlan] |
+ except IndexError: |
+ netdev_id = None |
# Handle the '-net nic' part |
- mac = self.get_mac_address(vlan) |
+ mac = vm.get_mac_address(vlan) |
qemu_cmd += add_nic(help, vlan, nic_params.get("nic_model"), mac, |
- self.netdev_id[vlan], |
- nic_params.get("nic_extra_params")) |
+ netdev_id, nic_params.get("nic_extra_params")) |
# Handle the '-net tap' or '-net user' part |
script = nic_params.get("nic_script") |
downscript = nic_params.get("nic_downscript") |
@@ -419,11 +652,10 @@ class VM: |
if tftp: |
tftp = kvm_utils.get_path(root_dir, tftp) |
qemu_cmd += add_net(help, vlan, nic_params.get("nic_mode", "user"), |
- self.get_ifname(vlan), |
+ vm.get_ifname(vlan), |
script, downscript, tftp, |
- nic_params.get("bootp"), redirs, |
- self.netdev_id[vlan], |
- nic_params.get("vhost")=="yes") |
+ nic_params.get("bootp"), redirs, netdev_id, |
+ nic_params.get("netdev_extra_params")) |
# Proceed to next NIC |
vlan += 1 |
@@ -435,9 +667,8 @@ class VM: |
if smp: |
qemu_cmd += add_smp(help, smp) |
- cdroms = kvm_utils.get_sub_dict_names(params, "cdroms") |
- for cdrom in cdroms: |
- cdrom_params = kvm_utils.get_sub_dict(params, cdrom) |
+ for cdrom in params.objects("cdroms"): |
+ cdrom_params = params.object_params(cdrom) |
iso = cdrom_params.get("cdrom") |
if iso: |
qemu_cmd += add_cdrom(help, kvm_utils.get_path(root_dir, iso), |
@@ -477,27 +708,27 @@ class VM: |
qemu_cmd += add_tcp_redir(help, host_port, guest_port) |
if params.get("display") == "vnc": |
- qemu_cmd += add_vnc(help, self.vnc_port) |
+ qemu_cmd += add_vnc(help, vm.vnc_port) |
elif params.get("display") == "sdl": |
qemu_cmd += add_sdl(help) |
elif params.get("display") == "nographic": |
qemu_cmd += add_nographic(help) |
if params.get("uuid") == "random": |
- qemu_cmd += add_uuid(help, self.uuid) |
+ qemu_cmd += add_uuid(help, vm.uuid) |
elif params.get("uuid"): |
qemu_cmd += add_uuid(help, params.get("uuid")) |
if params.get("testdev") == "yes": |
- qemu_cmd += add_testdev(help, self.get_testlog_filename()) |
+ qemu_cmd += add_testdev(help, vm.get_testlog_filename()) |
if params.get("disable_hpet") == "yes": |
qemu_cmd += add_no_hpet(help) |
# If the PCI assignment step went OK, add each one of the PCI assigned |
# devices to the qemu command line. |
- if self.pci_assignable: |
- for pci_id in self.pa_pci_ids: |
+ if vm.pci_assignable: |
+ for pci_id in vm.pa_pci_ids: |
qemu_cmd += add_pcidevice(help, pci_id) |
extra_params = params.get("extra_params") |
@@ -507,8 +738,9 @@ class VM: |
return qemu_cmd |
+ @error.context_aware |
def create(self, name=None, params=None, root_dir=None, timeout=5.0, |
- migration_mode=None, migration_exec_cmd=None, mac_source=None): |
+ migration_mode=None, mac_source=None): |
""" |
Start the VM by running a qemu command. |
All parameters are optional. If name, params or root_dir are not |
@@ -523,8 +755,19 @@ class VM: |
(e.g. 'gzip -c -d filename') if migration_mode is 'exec' |
@param mac_source: A VM object from which to copy MAC addresses. If not |
specified, new addresses will be generated. |
+ |
+ @raise VMCreateError: If qemu terminates unexpectedly |
+ @raise VMKVMInitError: If KVM initialization fails |
+ @raise VMHugePageError: If hugepage initialization fails |
+ @raise VMImageMissingError: If a CD image is missing |
+ @raise VMHashMismatchError: If a CD image hash has doesn't match the |
+ expected hash |
+ @raise VMBadPATypeError: If an unsupported PCI assignment type is |
+ requested |
+ @raise VMPAError: If no PCI assignable devices could be assigned |
""" |
- self.destroy() |
+ error.context("creating '%s'" % self.name) |
+ self.destroy(free_mac_addresses=False) |
if name is not None: |
self.name = name |
@@ -536,38 +779,38 @@ class VM: |
params = self.params |
root_dir = self.root_dir |
- # Verify the md5sum of the ISO image |
- iso = params.get("cdrom") |
- if iso: |
- iso = kvm_utils.get_path(root_dir, iso) |
- if not os.path.exists(iso): |
- logging.error("ISO file not found: %s" % iso) |
- return False |
- compare = False |
- if params.get("md5sum_1m"): |
- logging.debug("Comparing expected MD5 sum with MD5 sum of " |
- "first MB of ISO file...") |
- actual_hash = utils.hash_file(iso, 1048576, method="md5") |
- expected_hash = params.get("md5sum_1m") |
- compare = True |
- elif params.get("md5sum"): |
- logging.debug("Comparing expected MD5 sum with MD5 sum of ISO " |
- "file...") |
- actual_hash = utils.hash_file(iso, method="md5") |
- expected_hash = params.get("md5sum") |
- compare = True |
- elif params.get("sha1sum"): |
- logging.debug("Comparing expected SHA1 sum with SHA1 sum of " |
- "ISO file...") |
- actual_hash = utils.hash_file(iso, method="sha1") |
- expected_hash = params.get("sha1sum") |
- compare = True |
- if compare: |
- if actual_hash == expected_hash: |
- logging.debug("Hashes match") |
- else: |
- logging.error("Actual hash differs from expected one") |
- return False |
+ # Verify the md5sum of the ISO images |
+ for cdrom in params.objects("cdroms"): |
+ cdrom_params = params.object_params(cdrom) |
+ iso = cdrom_params.get("cdrom") |
+ if iso: |
+ iso = kvm_utils.get_path(root_dir, iso) |
+ if not os.path.exists(iso): |
+ raise VMImageMissingError(iso) |
+ compare = False |
+ if cdrom_params.get("md5sum_1m"): |
+ logging.debug("Comparing expected MD5 sum with MD5 sum of " |
+ "first MB of ISO file...") |
+ actual_hash = utils.hash_file(iso, 1048576, method="md5") |
+ expected_hash = cdrom_params.get("md5sum_1m") |
+ compare = True |
+ elif cdrom_params.get("md5sum"): |
+ logging.debug("Comparing expected MD5 sum with MD5 sum of " |
+ "ISO file...") |
+ actual_hash = utils.hash_file(iso, method="md5") |
+ expected_hash = cdrom_params.get("md5sum") |
+ compare = True |
+ elif cdrom_params.get("sha1sum"): |
+ logging.debug("Comparing expected SHA1 sum with SHA1 sum " |
+ "of ISO file...") |
+ actual_hash = utils.hash_file(iso, method="sha1") |
+ expected_hash = cdrom_params.get("sha1sum") |
+ compare = True |
+ if compare: |
+ if actual_hash == expected_hash: |
+ logging.debug("Hashes match") |
+ else: |
+ raise VMHashMismatchError(actual_hash, expected_hash) |
# Make sure the following code is not executed by more than one thread |
# at the same time |
@@ -576,15 +819,17 @@ class VM: |
try: |
# Handle port redirections |
- redir_names = kvm_utils.get_sub_dict_names(params, "redirs") |
+ redir_names = params.objects("redirs") |
host_ports = kvm_utils.find_free_ports(5000, 6000, len(redir_names)) |
self.redirs = {} |
for i in range(len(redir_names)): |
- redir_params = kvm_utils.get_sub_dict(params, redir_names[i]) |
+ redir_params = params.object_params(redir_names[i]) |
guest_port = int(redir_params.get("guest_port")) |
self.redirs[guest_port] = host_ports[i] |
- for nic in kvm_utils.get_sub_dict_names(params, "nics"): |
+ # Generate netdev IDs for all NICs |
+ self.netdev_id = [] |
+ for nic in params.objects("nics"): |
self.netdev_id.append(kvm_utils.generate_random_id()) |
# Find available VNC port, if needed |
@@ -598,18 +843,24 @@ class VM: |
f.close() |
# Generate or copy MAC addresses for all NICs |
- num_nics = len(kvm_utils.get_sub_dict_names(params, "nics")) |
+ num_nics = len(params.objects("nics")) |
for vlan in range(num_nics): |
- mac = mac_source and mac_source.get_mac_address(vlan) |
- if mac: |
+ nic_name = params.objects("nics")[vlan] |
+ nic_params = params.object_params(nic_name) |
+ if nic_params.get("nic_mac", None): |
+ mac = nic_params.get("nic_mac") |
kvm_utils.set_mac_address(self.instance, vlan, mac) |
else: |
- kvm_utils.generate_mac_address(self.instance, vlan) |
+ mac = mac_source and mac_source.get_mac_address(vlan) |
+ if mac: |
+ kvm_utils.set_mac_address(self.instance, vlan, mac) |
+ else: |
+ kvm_utils.generate_mac_address(self.instance, vlan) |
# Assign a PCI assignable device |
self.pci_assignable = None |
pa_type = params.get("pci_assignable") |
- if pa_type in ["vf", "pf", "mixed"]: |
+ if pa_type and pa_type != "no": |
pa_devices_requested = params.get("devices_requested") |
# Virtual Functions (VF) assignable devices |
@@ -633,6 +884,8 @@ class VM: |
driver_option=params.get("driver_option"), |
names=params.get("device_names"), |
devices_requested=pa_devices_requested) |
+ else: |
+ raise VMBadPATypeError(pa_type) |
self.pa_pci_ids = self.pci_assignable.request_devs() |
@@ -640,14 +893,7 @@ class VM: |
logging.debug("Successfuly assigned devices: %s", |
self.pa_pci_ids) |
else: |
- logging.error("No PCI assignable devices were assigned " |
- "and 'pci_assignable' is defined to %s " |
- "on your config file. Aborting VM creation.", |
- pa_type) |
- return False |
- |
- elif pa_type and pa_type != "no": |
- logging.warn("Unsupported pci_assignable type: %s", pa_type) |
+ raise VMPAError(pa_type) |
# Make qemu command |
qemu_command = self.make_qemu_command() |
@@ -660,27 +906,26 @@ class VM: |
self.migration_file = "/tmp/migration-unix-%s" % self.instance |
qemu_command += " -incoming unix:%s" % self.migration_file |
elif migration_mode == "exec": |
- qemu_command += ' -incoming "exec:%s"' % migration_exec_cmd |
+ self.migration_port = kvm_utils.find_free_port(5200, 6000) |
+ qemu_command += (' -incoming "exec:nc -l %s"' % |
+ self.migration_port) |
- logging.debug("Running qemu command:\n%s", qemu_command) |
+ logging.info("Running qemu command:\n%s", qemu_command) |
self.process = kvm_subprocess.run_bg(qemu_command, None, |
- logging.debug, "(qemu) ") |
+ logging.info, "(qemu) ") |
# Make sure the process was started successfully |
if not self.process.is_alive(): |
- logging.error("VM could not be created; " |
- "qemu command failed:\n%s" % qemu_command) |
- logging.error("Status: %s" % self.process.get_status()) |
- logging.error("Output:" + kvm_utils.format_str_for_message( |
- self.process.get_output())) |
+ e = VMCreateError(qemu_command, |
+ self.process.get_status(), |
+ self.process.get_output()) |
self.destroy() |
- return False |
+ raise e |
# Establish monitor connections |
self.monitors = [] |
- for monitor_name in kvm_utils.get_sub_dict_names(params, |
- "monitors"): |
- monitor_params = kvm_utils.get_sub_dict(params, monitor_name) |
+ for monitor_name in params.objects("monitors"): |
+ monitor_params = params.object_params(monitor_name) |
# Wait for monitor connection to succeed |
end_time = time.time() + timeout |
while time.time() < end_time: |
@@ -695,17 +940,14 @@ class VM: |
monitor = kvm_monitor.HumanMonitor( |
monitor_name, |
self.get_monitor_filename(monitor_name)) |
+ monitor.verify_responsive() |
+ break |
except kvm_monitor.MonitorError, e: |
logging.warn(e) |
- else: |
- if monitor.is_responsive(): |
- break |
- time.sleep(1) |
+ time.sleep(1) |
else: |
- logging.error("Could not connect to monitor '%s'" % |
- monitor_name) |
self.destroy() |
- return False |
+ raise e |
# Add this monitor to the list |
self.monitors += [monitor] |
@@ -714,39 +956,31 @@ class VM: |
output = self.process.get_output() |
if re.search("Could not initialize KVM", output, re.IGNORECASE): |
- logging.error("Could not initialize KVM; " |
- "qemu command:\n%s" % qemu_command) |
- logging.error("Output:" + kvm_utils.format_str_for_message( |
- self.process.get_output())) |
+ e = VMKVMInitError(qemu_command, self.process.get_output()) |
self.destroy() |
- return False |
+ raise e |
if "alloc_mem_area" in output: |
- logging.error("Could not allocate hugepage memory; " |
- "qemu command:\n%s" % qemu_command) |
- logging.error("Output:" + kvm_utils.format_str_for_message( |
- self.process.get_output())) |
+ e = VMHugePageError(qemu_command, self.process.get_output()) |
self.destroy() |
- return False |
+ raise e |
logging.debug("VM appears to be alive with PID %s", self.get_pid()) |
# Establish a session with the serial console -- requires a version |
# of netcat that supports -U |
- self.serial_console = kvm_subprocess.kvm_shell_session( |
+ self.serial_console = kvm_subprocess.ShellSession( |
"nc -U %s" % self.get_serial_console_filename(), |
auto_close=False, |
output_func=kvm_utils.log_line, |
output_params=("serial-%s.log" % name,)) |
- return True |
- |
finally: |
fcntl.lockf(lockfile, fcntl.LOCK_UN) |
lockfile.close() |
- def destroy(self, gracefully=True): |
+ def destroy(self, gracefully=True, free_mac_addresses=True): |
""" |
Destroy the VM. |
@@ -754,14 +988,15 @@ class VM: |
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: Whether an attempt will be made to end the VM |
+ @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. |
""" |
try: |
# Is it already dead? |
if self.is_dead(): |
- logging.debug("VM is already down") |
return |
logging.debug("Destroying VM with PID %s...", self.get_pid()) |
@@ -769,15 +1004,18 @@ class VM: |
if gracefully and self.params.get("shutdown_command"): |
# Try to destroy with shell command |
logging.debug("Trying to shutdown VM with shell command...") |
- session = self.remote_login() |
- if session: |
+ try: |
+ session = self.login() |
+ except (kvm_utils.LoginError, VMError), e: |
+ logging.debug(e) |
+ else: |
try: |
# Send the shutdown command |
session.sendline(self.params.get("shutdown_command")) |
logging.debug("Shutdown command sent; waiting for VM " |
"to go down...") |
if kvm_utils.wait_for(self.is_dead, 60, 1, 1): |
- logging.debug("VM is down, freeing mac address.") |
+ logging.debug("VM is down") |
return |
finally: |
session.close() |
@@ -804,7 +1042,7 @@ class VM: |
logging.debug("VM is down") |
return |
- logging.error("Process %s is a zombie!" % self.process.get_pid()) |
+ logging.error("Process %s is a zombie!", self.process.get_pid()) |
finally: |
self.monitors = [] |
@@ -826,9 +1064,10 @@ class VM: |
os.unlink(self.migration_file) |
except OSError: |
pass |
- num_nics = len(kvm_utils.get_sub_dict_names(self.params, "nics")) |
- for vlan in range(num_nics): |
- self.free_mac_address(vlan) |
+ if free_mac_addresses: |
+ num_nics = len(self.params.objects("nics")) |
+ for vlan in range(num_nics): |
+ self.free_mac_address(vlan) |
@property |
@@ -846,15 +1085,26 @@ class VM: |
return self.monitors[0] |
+ def verify_alive(self): |
+ """ |
+ Make sure the VM is alive and that the main monitor is responsive. |
+ |
+ @raise VMDeadError: If the VM is dead |
+ @raise: Various monitor exceptions if the monitor is unresponsive |
+ """ |
+ if self.is_dead(): |
+ raise VMDeadError(self.process.get_status(), |
+ self.process.get_output()) |
+ if self.monitors: |
+ self.monitor.verify_responsive() |
+ |
+ |
def is_alive(self): |
""" |
Return True if the VM is alive and its monitor is responsive. |
""" |
- # Check if the process is running |
- if self.is_dead(): |
- return False |
- # Try sending a monitor command |
- return bool(self.monitor) and self.monitor.is_responsive() |
+ return not self.is_dead() and (not self.monitors or |
+ self.monitor.is_responsive()) |
def is_dead(self): |
@@ -885,7 +1135,7 @@ class VM: |
params). |
""" |
return [self.get_monitor_filename(m) for m in |
- kvm_utils.get_sub_dict_names(self.params, "monitors")] |
+ self.params.objects("monitors")] |
def get_serial_console_filename(self): |
@@ -910,28 +1160,26 @@ class VM: |
address of its own). Otherwise return the NIC's IP address. |
@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) |
""" |
- nics = kvm_utils.get_sub_dict_names(self.params, "nics") |
+ nics = self.params.objects("nics") |
nic_name = nics[index] |
- nic_params = kvm_utils.get_sub_dict(self.params, nic_name) |
+ nic_params = self.params.object_params(nic_name) |
if nic_params.get("nic_mode") == "tap": |
- mac = self.get_mac_address(index) |
- if not mac: |
- logging.debug("MAC address unavailable") |
- return None |
- mac = mac.lower() |
+ mac = self.get_mac_address(index).lower() |
# Get the IP address from the cache |
ip = self.address_cache.get(mac) |
if not ip: |
- logging.debug("Could not find IP address for MAC address: %s" % |
- mac) |
- return None |
+ raise VMIPAddressMissingError(mac) |
# Make sure the IP address is assigned to this guest |
macs = [self.get_mac_address(i) for i in range(len(nics))] |
if not kvm_utils.verify_ip_address_ownership(ip, macs): |
- logging.debug("Could not verify MAC-IP address mapping: " |
- "%s ---> %s" % (mac, ip)) |
- return None |
+ raise VMAddressVerificationError(mac, ip) |
return ip |
else: |
return "localhost" |
@@ -945,16 +1193,18 @@ class VM: |
@param nic_index: Index of the NIC. |
@return: If port redirection is used, return the host port redirected |
to guest port port. Otherwise return port. |
+ @raise VMPortNotRedirectedError: If an unredirected port is requested |
+ in user mode |
""" |
- nic_name = kvm_utils.get_sub_dict_names(self.params, "nics")[nic_index] |
- nic_params = kvm_utils.get_sub_dict(self.params, nic_name) |
+ nic_name = self.params.objects("nics")[nic_index] |
+ nic_params = self.params.object_params(nic_name) |
if nic_params.get("nic_mode") == "tap": |
return port |
else: |
- if not self.redirs.has_key(port): |
- logging.warn("Warning: guest port %s requested but not " |
- "redirected" % port) |
- return self.redirs.get(port) |
+ try: |
+ return self.redirs[port] |
+ except KeyError: |
+ raise VMPortNotRedirectedError(port) |
def get_ifname(self, nic_index=0): |
@@ -963,9 +1213,9 @@ class VM: |
@param nic_index: Index of the NIC |
""" |
- nics = kvm_utils.get_sub_dict_names(self.params, "nics") |
+ nics = self.params.objects("nics") |
nic_name = nics[nic_index] |
- nic_params = kvm_utils.get_sub_dict(self.params, nic_name) |
+ nic_params = self.params.object_params(nic_name) |
if nic_params.get("nic_ifname"): |
return nic_params.get("nic_ifname") |
else: |
@@ -977,8 +1227,13 @@ class VM: |
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 |
""" |
- return kvm_utils.get_mac_address(self.instance, nic_index) |
+ mac = kvm_utils.get_mac_address(self.instance, nic_index) |
+ if not mac: |
+ raise VMMACAddressMissingError(nic_index) |
+ return mac |
def free_mac_address(self, nic_index=0): |
@@ -1031,7 +1286,8 @@ class VM: |
return shm * 4.0 / 1024 |
- def remote_login(self, nic_index=0, timeout=10): |
+ @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 |
@@ -1040,8 +1296,9 @@ class VM: |
@param nic_index: The index of the NIC to connect to. |
@param timeout: Time (seconds) before giving up logging into the |
guest. |
- @return: kvm_spawn object on success and None on failure. |
+ @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", "[\#\$]") |
@@ -1051,87 +1308,98 @@ class VM: |
port = self.get_port(int(self.params.get("shell_port"))) |
log_filename = ("session-%s-%s.log" % |
(self.name, kvm_utils.generate_random_string(4))) |
- |
- if not address or not port: |
- logging.debug("IP address or port unavailable") |
- return None |
- |
session = kvm_utils.remote_login(client, address, port, username, |
password, prompt, linesep, |
log_filename, timeout) |
- |
- if session: |
- session.set_status_test_command(self.params.get("status_test_" |
- "command", "")) |
+ session.set_status_test_command(self.params.get("status_test_command", |
+ "")) |
return session |
- def copy_files_to(self, local_path, remote_path, nic_index=0, timeout=600): |
+ 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 (kvm_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 guest. |
+ Transfer files to the remote host(guest). |
- @param local_path: Host path |
- @param remote_path: Guest path |
+ @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"))) |
- |
- if not address or not port: |
- logging.debug("IP address or port unavailable") |
- return None |
- |
- if client == "scp": |
- log_filename = ("scp-%s-%s.log" % |
- (self.name, kvm_utils.generate_random_string(4))) |
- return kvm_utils.scp_to_remote(address, port, username, password, |
- local_path, remote_path, |
- log_filename, timeout) |
- elif client == "rss": |
- c = rss_file_transfer.FileUploadClient(address, port) |
- c.upload(local_path, remote_path, timeout) |
- c.close() |
- return True |
+ log_filename = ("transfer-%s-to-%s-%s.log" % |
+ (self.name, address, |
+ kvm_utils.generate_random_string(4))) |
+ kvm_utils.copy_files_to(address, client, username, password, port, |
+ host_path, guest_path, log_filename, verbose, |
+ timeout) |
- def copy_files_from(self, remote_path, local_path, nic_index=0, timeout=600): |
+ @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 local_path: Guest path |
- @param remote_path: Host path |
+ @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"))) |
- |
- if not address or not port: |
- logging.debug("IP address or port unavailable") |
- return None |
- |
- if client == "scp": |
- log_filename = ("scp-%s-%s.log" % |
- (self.name, kvm_utils.generate_random_string(4))) |
- return kvm_utils.scp_from_remote(address, port, username, password, |
- remote_path, local_path, |
- log_filename, timeout) |
- elif client == "rss": |
- c = rss_file_transfer.FileDownloadClient(address, port) |
- c.download(remote_path, local_path, timeout) |
- c.close() |
- return True |
+ log_filename = ("transfer-%s-from-%s-%s.log" % |
+ (self.name, address, |
+ kvm_utils.generate_random_string(4))) |
+ kvm_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. |
@@ -1139,26 +1407,247 @@ class VM: |
password prompt or a shell prompt) -- fail. |
@param timeout: Time (seconds) before giving up logging into the guest. |
- @return: kvm_spawn object on success and None on failure. |
+ @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", "") |
- if self.serial_console: |
- self.serial_console.set_linesep(linesep) |
- self.serial_console.set_status_test_command(status_test_command) |
- else: |
- return None |
+ self.serial_console.set_linesep(linesep) |
+ self.serial_console.set_status_test_command(status_test_command) |
- # Make sure we get a login prompt |
+ # Try to get a login prompt |
self.serial_console.sendline() |
- if kvm_utils._remote_login(self.serial_console, username, password, |
- prompt, timeout): |
- return self.serial_console |
+ kvm_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 kvm_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) |
+ |
+ |
+ @error.context_aware |
+ 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. |
+ """ |
+ error.base_context("migrating '%s'" % self.name) |
+ |
+ def mig_finished(): |
+ o = self.monitor.info("migrate") |
+ if isinstance(o, str): |
+ return "status: active" not in o |
+ else: |
+ return o.get("status") != "active" |
+ |
+ def mig_succeeded(): |
+ o = self.monitor.info("migrate") |
+ if isinstance(o, str): |
+ return "status: completed" in o |
+ else: |
+ return o.get("status") == "completed" |
+ |
+ def mig_failed(): |
+ o = self.monitor.info("migrate") |
+ if isinstance(o, str): |
+ return "status: failed" in o |
+ else: |
+ return o.get("status") == "failed" |
+ |
+ def mig_cancelled(): |
+ o = self.monitor.info("migrate") |
+ if isinstance(o, str): |
+ return ("Migration status: cancelled" in o or |
+ "Migration status: canceled" in o) |
+ else: |
+ return (o.get("status") == "cancelled" or |
+ o.get("status") == "canceled") |
+ |
+ def wait_for_migration(): |
+ if not kvm_utils.wait_for(mig_finished, timeout, 2, 2, |
+ "Waiting for migration to complete"): |
+ raise VMMigrateTimeoutError("Timeout expired while waiting " |
+ "for migration to finish") |
+ |
+ local = dest_host == "localhost" |
+ |
+ clone = self.clone() |
+ if local: |
+ error.context("creating destination VM") |
+ if stable_check: |
+ # Pause the dest vm after creation |
+ extra_params = clone.params.get("extra_params", "") + " -S" |
+ clone.params["extra_params"] = extra_params |
+ clone.create(migration_mode=protocol, mac_source=self) |
+ error.context() |
+ |
+ try: |
+ if protocol == "tcp": |
+ if local: |
+ uri = "tcp:localhost:%d" % clone.migration_port |
+ else: |
+ uri = "tcp:%s:%d" % (dest_host, remote_port) |
+ elif protocol == "unix": |
+ uri = "unix:%s" % clone.migration_file |
+ elif protocol == "exec": |
+ uri = '"exec:nc localhost %s"' % clone.migration_port |
+ |
+ if offline: |
+ self.monitor.cmd("stop") |
+ |
+ logging.info("Migrating to %s", uri) |
+ self.monitor.migrate(uri) |
+ |
+ if cancel_delay: |
+ time.sleep(cancel_delay) |
+ self.monitor.cmd("migrate_cancel") |
+ if not kvm_utils.wait_for(mig_cancelled, 60, 2, 2, |
+ "Waiting for migration " |
+ "cancellation"): |
+ raise VMMigrateCancelError("Cannot cancel migration") |
+ return |
+ |
+ wait_for_migration() |
+ |
+ # Report migration status |
+ if mig_succeeded(): |
+ logging.info("Migration completed successfully") |
+ elif mig_failed(): |
+ raise VMMigrateFailedError("Migration failed") |
+ else: |
+ raise VMMigrateFailedError("Migration ended with unknown " |
+ "status") |
+ |
+ # Switch self <-> clone |
+ temp = self.clone(copy_state=True) |
+ self.__dict__ = clone.__dict__ |
+ clone = temp |
+ |
+ # From now on, clone is the source VM that will soon be destroyed |
+ # and self is the destination VM that will remain alive. If this |
+ # is remote migration, self is a dead VM object. |
+ |
+ error.context("after migration") |
+ if local: |
+ time.sleep(1) |
+ self.verify_alive() |
+ |
+ if local and stable_check: |
+ try: |
+ save1 = os.path.join(save_path, "src-" + clone.instance) |
+ save2 = os.path.join(save_path, "dst-" + self.instance) |
+ clone.save_to_file(save1) |
+ self.save_to_file(save2) |
+ # Fail if we see deltas |
+ md5_save1 = utils.hash_file(save1) |
+ md5_save2 = utils.hash_file(save2) |
+ if md5_save1 != md5_save2: |
+ raise VMMigrateStateMismatchError(md5_save1, md5_save2) |
+ finally: |
+ if clean: |
+ if os.path.isfile(save1): |
+ os.remove(save1) |
+ if os.path.isfile(save2): |
+ os.remove(save2) |
+ |
+ finally: |
+ # If we're doing remote migration and it's completed successfully, |
+ # self points to a dead VM object |
+ if self.is_alive(): |
+ self.monitor.cmd("cont") |
+ clone.destroy(gracefully=False) |
+ |
+ |
+ @error.context_aware |
+ 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. |
+ """ |
+ error.base_context("rebooting '%s'" % self.name, logging.info) |
+ error.context("before reboot") |
+ session = session or self.login() |
+ error.context() |
+ |
+ if method == "shell": |
+ session.sendline(self.params.get("reboot_command")) |
+ elif method == "system_reset": |
+ # Clear the event list of all QMP monitors |
+ qmp_monitors = [m for m in self.monitors if m.protocol == "qmp"] |
+ for m in qmp_monitors: |
+ m.clear_events() |
+ # Send a system_reset monitor command |
+ self.monitor.cmd("system_reset") |
+ # Look for RESET QMP events |
+ time.sleep(1) |
+ for m in qmp_monitors: |
+ if m.get_event("RESET"): |
+ logging.info("RESET QMP event received") |
+ else: |
+ raise VMRebootError("RESET QMP event not received after " |
+ "system_reset (monitor '%s')" % m.name) |
+ else: |
+ raise VMRebootError("Unknown reboot method: %s" % method) |
+ |
+ error.context("waiting for guest to go down", logging.info) |
+ if not kvm_utils.wait_for(lambda: |
+ not session.is_responsive(timeout=30), |
+ 120, 0, 1): |
+ raise VMRebootError("Guest refuses to go down") |
+ session.close() |
+ |
+ error.context("logging in after reboot", logging.info) |
+ return self.wait_for_login(nic_index, timeout=timeout) |
def send_key(self, keystr): |
@@ -1209,15 +1698,9 @@ class VM: |
""" |
Get the cpu count of the VM. |
""" |
- session = self.remote_login() |
- if not session: |
- return None |
+ session = self.login() |
try: |
- cmd = self.params.get("cpu_chk_cmd") |
- s, count = session.get_command_status_output(cmd) |
- if s == 0: |
- return int(count) |
- return None |
+ return int(session.cmd(self.params.get("cpu_chk_cmd"))) |
finally: |
session.close() |
@@ -1229,15 +1712,11 @@ class VM: |
@param check_cmd: Command used to check memory. If not provided, |
self.params.get("mem_chk_cmd") will be used. |
""" |
- session = self.remote_login() |
- if not session: |
- return None |
+ session = self.login() |
try: |
if not cmd: |
cmd = self.params.get("mem_chk_cmd") |
- s, mem_str = session.get_command_status_output(cmd) |
- if s != 0: |
- return None |
+ mem_str = session.cmd(cmd) |
mem = re.findall("([0-9]+)", mem_str) |
mem_size = 0 |
for m in mem: |
@@ -1259,3 +1738,17 @@ class VM: |
""" |
cmd = self.params.get("mem_chk_cur_cmd") |
return self.get_memory_size(cmd) |
+ |
+ |
+ def save_to_file(self, path): |
+ """ |
+ Save the state of virtual machine to a file through migrate to |
+ exec |
+ """ |
+ # Make sure we only get one iteration |
+ self.monitor.cmd("migrate_set_speed 1000g") |
+ self.monitor.cmd("migrate_set_downtime 100000000") |
+ self.monitor.migrate('"exec:cat>%s"' % path) |
+ # Restore the speed and downtime of migration |
+ self.monitor.cmd("migrate_set_speed %d" % (32<<20)) |
+ self.monitor.cmd("migrate_set_downtime 0.03") |