diff --git a/releasenotes/notes/replace-cold-migrate-to-use-nova-migration-api-cecd9a39ddd3bc58.yaml b/releasenotes/notes/replace-cold-migrate-to-use-nova-migration-api-cecd9a39ddd3bc58.yaml new file mode 100644 index 000000000..4df325966 --- /dev/null +++ b/releasenotes/notes/replace-cold-migrate-to-use-nova-migration-api-cecd9a39ddd3bc58.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Instance cold migration logic is now replaced with using Nova migrate + Server(migrate Action) API which has host option since v2.56. +upgrade: + - | + Nova API version is now set to 2.56 by default. This needs the migrate + action of migration type cold with destination_node parameter to work. +fixes: + - | + The migrate action of migration type cold with destination_node parameter + was fixed. Before fixing, it booted an instance in the service project + as a migrated instance. diff --git a/watcher/applier/actions/migration.py b/watcher/applier/actions/migration.py index b1679b409..2fe547460 100644 --- a/watcher/applier/actions/migration.py +++ b/watcher/applier/actions/migration.py @@ -50,6 +50,12 @@ class Migrate(base.BaseAction): source and the destination compute hostname (list of available compute hosts is returned by this command: ``nova service-list --binary nova-compute``). + + .. note:: + + Nova API version must be 2.56 or above if `destination_node` parameter + is given. + """ # input parameters constants diff --git a/watcher/common/nova_helper.py b/watcher/common/nova_helper.py index a1853fca8..2937d0dfc 100644 --- a/watcher/common/nova_helper.py +++ b/watcher/common/nova_helper.py @@ -17,9 +17,9 @@ # limitations under the License. # -import random import time +from novaclient import api_versions from oslo_log import log import cinderclient.exceptions as ciexceptions @@ -133,31 +133,24 @@ class NovaHelper(object): return volume.status == status def watcher_non_live_migrate_instance(self, instance_id, dest_hostname, - keep_original_image_name=True, retry=120): """This method migrates a given instance - using an image of this instance and creating a new instance - from this image. It saves some configuration information - about the original instance : security group, list of networks, - list of attached volumes, floating IP, ... - in order to apply the same settings to the new instance. - At the end of the process the original instance is deleted. + This method uses the Nova built-in migrate() + action to do a migration of a given instance. + For migrating a given dest_hostname, Nova API version + must be 2.56 or higher. + It returns True if the migration was successful, False otherwise. - if destination hostname not given, this method calls nova api - to migrate the instance. - :param instance_id: the unique id of the instance to migrate. - :param keep_original_image_name: flag indicating whether the - image name from which the original instance was built must be - used as the name of the intermediate image used for migration. - If this flag is False, a temporary image name is built + :param dest_hostname: the name of the destination compute node, if + destination_node is None, nova scheduler choose + the destination host """ - new_image_name = "" LOG.debug( - "Trying a non-live migrate of instance '%s' ", instance_id) + "Trying a cold migrate of instance '%s' ", instance_id) # Looking for the instance to migrate instance = self.find_instance(instance_id) @@ -165,215 +158,43 @@ class NovaHelper(object): LOG.debug("Instance %s not found !", instance_id) return False else: - # NOTE: If destination node is None call Nova API to migrate - # instance host_name = getattr(instance, "OS-EXT-SRV-ATTR:host") LOG.debug( "Instance %(instance)s found on host '%(host)s'.", {'instance': instance_id, 'host': host_name}) - if dest_hostname is None: - previous_status = getattr(instance, 'status') + previous_status = getattr(instance, 'status') - instance.migrate() - instance = self.nova.servers.get(instance_id) - while (getattr(instance, 'status') not in - ["VERIFY_RESIZE", "ERROR"] and retry): - instance = self.nova.servers.get(instance.id) - time.sleep(2) - retry -= 1 - new_hostname = getattr(instance, 'OS-EXT-SRV-ATTR:host') + if (dest_hostname and + not self._check_nova_api_version(self.nova, "2.56")): + LOG.error("For migrating a given dest_hostname," + "Nova API version must be 2.56 or higher") + return False - if (host_name != new_hostname and - instance.status == 'VERIFY_RESIZE'): - if not self.confirm_resize(instance, previous_status): - return False - LOG.debug( - "cold migration succeeded : " - "instance %s is now on host '%s'.", ( - instance_id, new_hostname)) - return True - else: - LOG.debug( - "cold migration for instance %s failed", instance_id) + instance.migrate(host=dest_hostname) + instance = self.nova.servers.get(instance_id) + + while (getattr(instance, 'status') not in + ["VERIFY_RESIZE", "ERROR"] and retry): + instance = self.nova.servers.get(instance.id) + time.sleep(2) + retry -= 1 + new_hostname = getattr(instance, 'OS-EXT-SRV-ATTR:host') + + if (host_name != new_hostname and + instance.status == 'VERIFY_RESIZE'): + if not self.confirm_resize(instance, previous_status): return False - - if not keep_original_image_name: - # randrange gives you an integral value - irand = random.randint(0, 1000) - - # Building the temporary image name - # which will be used for the migration - new_image_name = "tmp-migrate-%s-%s" % (instance_id, irand) + LOG.debug( + "cold migration succeeded : " + "instance %(instance)s is now on host '%(host)s'.", + {'instance': instance_id, 'host': new_hostname}) + return True else: - # Get the image name of the current instance. - # We'll use the same name for the new instance. - imagedict = getattr(instance, "image") - image_id = imagedict["id"] - image = self.glance.images.get(image_id) - new_image_name = getattr(image, "name") - - instance_name = getattr(instance, "name") - flavor_name = instance.flavor.get('original_name') - keypair_name = getattr(instance, "key_name") - - addresses = getattr(instance, "addresses") - - floating_ip = "" - network_names_list = [] - - for network_name, network_conf_obj in addresses.items(): LOG.debug( - "Extracting network configuration for network '%s'", - network_name) - - network_names_list.append(network_name) - - for net_conf_item in network_conf_obj: - if net_conf_item['OS-EXT-IPS:type'] == "floating": - floating_ip = net_conf_item['addr'] - break - - sec_groups_list = getattr(instance, "security_groups") - sec_groups = [] - - for sec_group_dict in sec_groups_list: - sec_groups.append(sec_group_dict['name']) - - # Stopping the old instance properly so - # that no new data is sent to it and to its attached volumes - stopped_ok = self.stop_instance(instance_id) - - if not stopped_ok: - LOG.debug("Could not stop instance: %s", instance_id) + "cold migration for instance %s failed", instance_id) return False - # Building the temporary image which will be used - # to re-build the same instance on another target host - image_uuid = self.create_image_from_instance(instance_id, - new_image_name) - - if not image_uuid: - LOG.debug( - "Could not build temporary image of instance: %s", - instance_id) - return False - - # - # We need to get the list of attached volumes and detach - # them from the instance in order to attache them later - # to the new instance - # - blocks = [] - - # Looks like this : - # os-extended-volumes:volumes_attached | - # [{u'id': u'c5c3245f-dd59-4d4f-8d3a-89d80135859a'}] - attached_volumes = getattr(instance, - "os-extended-volumes:volumes_attached") - - for attached_volume in attached_volumes: - volume_id = attached_volume['id'] - - try: - volume = self.cinder.volumes.get(volume_id) - - attachments_list = getattr(volume, "attachments") - - device_name = attachments_list[0]['device'] - # When a volume is attached to an instance - # it contains the following property : - # attachments = [{u'device': u'/dev/vdb', - # u'server_id': u'742cc508-a2f2-4769-a794-bcdad777e814', - # u'id': u'f6d62785-04b8-400d-9626-88640610f65e', - # u'host_name': None, u'volume_id': - # u'f6d62785-04b8-400d-9626-88640610f65e'}] - - # boot_index indicates a number - # designating the boot order of the device. - # Use -1 for the boot volume, - # choose 0 for an attached volume. - block_device_mapping_v2_item = {"device_name": device_name, - "source_type": "volume", - "destination_type": - "volume", - "uuid": volume_id, - "boot_index": "0"} - - blocks.append( - block_device_mapping_v2_item) - - LOG.debug( - "Detaching volume %(volume)s from " - "instance: %(instance)s", - {'volume': volume_id, 'instance': instance_id}) - # volume.detach() - self.nova.volumes.delete_server_volume(instance_id, - volume_id) - - if not self.wait_for_volume_status(volume, "available", 5, - 10): - LOG.debug( - "Could not detach volume %(volume)s " - "from instance: %(instance)s", - {'volume': volume_id, 'instance': instance_id}) - return False - except ciexceptions.NotFound: - LOG.debug("Volume '%s' not found ", image_id) - return False - - # We create the new instance from - # the intermediate image of the original instance - new_instance = self. \ - create_instance(dest_hostname, - instance_name, - image_uuid, - flavor_name, - sec_groups, - network_names_list=network_names_list, - keypair_name=keypair_name, - create_new_floating_ip=False, - block_device_mapping_v2=blocks) - - if not new_instance: - LOG.debug( - "Could not create new instance " - "for non-live migration of instance %s", instance_id) - return False - - try: - LOG.debug( - "Detaching floating ip '%(floating_ip)s' " - "from instance %(instance)s", - {'floating_ip': floating_ip, 'instance': instance_id}) - # We detach the floating ip from the current instance - instance.remove_floating_ip(floating_ip) - - LOG.debug( - "Attaching floating ip '%(ip)s' to the new " - "instance %(id)s", - {'ip': floating_ip, 'id': new_instance.id}) - - # We attach the same floating ip to the new instance - new_instance.add_floating_ip(floating_ip) - except Exception as e: - LOG.debug(e) - - new_host_name = getattr(new_instance, "OS-EXT-SRV-ATTR:host") - - # Deleting the old instance (because no more useful) - delete_ok = self.delete_instance(instance_id) - if not delete_ok: - LOG.debug("Could not delete instance: %s", instance_id) - return False - - LOG.debug( - "Instance %s has been successfully migrated " - "to new host '%s' and its new id is %s.", ( - instance_id, new_host_name, new_instance.id)) - - return True - def resize_instance(self, instance_id, flavor, retry=120): """This method resizes given instance with specified flavor. @@ -936,3 +757,12 @@ class NovaHelper(object): "Volume %s is now on host '%s'.", (new_volume.id, host_name)) return True + + def _check_nova_api_version(self, client, version): + api_version = api_versions.APIVersion(version_str=version) + try: + api_versions.discover_version(client, api_version) + return True + except nvexceptions.UnsupportedVersion as e: + LOG.exception(e) + return False diff --git a/watcher/conf/nova_client.py b/watcher/conf/nova_client.py index 5ae78ae0c..14c43a2f7 100755 --- a/watcher/conf/nova_client.py +++ b/watcher/conf/nova_client.py @@ -23,7 +23,7 @@ nova_client = cfg.OptGroup(name='nova_client', NOVA_CLIENT_OPTS = [ cfg.StrOpt('api_version', - default='2.53', + default='2.56', help='Version of Nova API to use in novaclient.'), cfg.StrOpt('endpoint_type', default='publicURL', diff --git a/watcher/tests/common/test_nova_helper.py b/watcher/tests/common/test_nova_helper.py index 500e4f549..d27b28ab1 100644 --- a/watcher/tests/common/test_nova_helper.py +++ b/watcher/tests/common/test_nova_helper.py @@ -210,57 +210,6 @@ class TestNovaHelper(base.TestCase): self.assertFalse(is_success) - @mock.patch.object(time, 'sleep', mock.Mock()) - def test_watcher_non_live_migrate_instance_volume( - self, mock_glance, mock_cinder, mock_neutron, mock_nova): - nova_util = nova_helper.NovaHelper() - nova_servers = nova_util.nova.servers - instance = self.fake_server(self.instance_uuid) - setattr(instance, 'OS-EXT-SRV-ATTR:host', - self.source_node) - setattr(instance, 'OS-EXT-STS:vm_state', "stopped") - attached_volumes = [{'id': str(utils.generate_uuid())}] - setattr(instance, "os-extended-volumes:volumes_attached", - attached_volumes) - self.fake_nova_find_list(nova_util, find=instance, list=instance) - nova_servers.create_image.return_value = utils.generate_uuid() - nova_util.glance.images.get.return_value = mock.MagicMock( - status='active') - nova_util.cinder.volumes.get.return_value = mock.MagicMock( - status='available') - - is_success = nova_util.watcher_non_live_migrate_instance( - self.instance_uuid, - self.destination_node) - self.assertTrue(is_success) - - @mock.patch.object(time, 'sleep', mock.Mock()) - def test_watcher_non_live_migrate_keep_image( - self, mock_glance, mock_cinder, mock_neutron, mock_nova): - nova_util = nova_helper.NovaHelper() - nova_servers = nova_util.nova.servers - instance = self.fake_server(self.instance_uuid) - setattr(instance, 'OS-EXT-SRV-ATTR:host', - self.source_node) - setattr(instance, 'OS-EXT-STS:vm_state', "stopped") - addresses = mock.MagicMock() - network_type = mock.MagicMock() - networks = [] - networks.append(("lan", network_type)) - addresses.items.return_value = networks - attached_volumes = mock.MagicMock() - setattr(instance, 'addresses', addresses) - setattr(instance, "os-extended-volumes:volumes_attached", - attached_volumes) - self.fake_nova_find_list(nova_util, find=instance, list=instance) - nova_servers.create_image.return_value = utils.generate_uuid() - nova_util.glance.images.get.return_value = mock.MagicMock( - status='active') - is_success = nova_util.watcher_non_live_migrate_instance( - self.instance_uuid, - self.destination_node, keep_original_image_name=False) - self.assertTrue(is_success) - @mock.patch.object(time, 'sleep', mock.Mock()) def test_abort_live_migrate_instance(self, mock_glance, mock_cinder, mock_neutron, mock_nova):