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