Merge "Replace cold migration to use Nova migration API"

This commit is contained in:
Zuul
2018-04-18 13:36:53 +00:00
committed by Gerrit Code Review
5 changed files with 65 additions and 266 deletions

View File

@@ -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.

View File

@@ -50,6 +50,12 @@ class Migrate(base.BaseAction):
source and the destination compute hostname (list of available compute source and the destination compute hostname (list of available compute
hosts is returned by this command: ``nova service-list --binary hosts is returned by this command: ``nova service-list --binary
nova-compute``). nova-compute``).
.. note::
Nova API version must be 2.56 or above if `destination_node` parameter
is given.
""" """
# input parameters constants # input parameters constants

View File

@@ -17,9 +17,9 @@
# limitations under the License. # limitations under the License.
# #
import random
import time import time
from novaclient import api_versions
from oslo_log import log from oslo_log import log
import cinderclient.exceptions as ciexceptions import cinderclient.exceptions as ciexceptions
@@ -133,31 +133,24 @@ class NovaHelper(object):
return volume.status == status return volume.status == status
def watcher_non_live_migrate_instance(self, instance_id, dest_hostname, def watcher_non_live_migrate_instance(self, instance_id, dest_hostname,
keep_original_image_name=True,
retry=120): retry=120):
"""This method migrates a given instance """This method migrates a given instance
using an image of this instance and creating a new instance This method uses the Nova built-in migrate()
from this image. It saves some configuration information action to do a migration of a given instance.
about the original instance : security group, list of networks, For migrating a given dest_hostname, Nova API version
list of attached volumes, floating IP, ... must be 2.56 or higher.
in order to apply the same settings to the new instance.
At the end of the process the original instance is deleted.
It returns True if the migration was successful, It returns True if the migration was successful,
False otherwise. 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 instance_id: the unique id of the instance to migrate.
:param keep_original_image_name: flag indicating whether the :param dest_hostname: the name of the destination compute node, if
image name from which the original instance was built must be destination_node is None, nova scheduler choose
used as the name of the intermediate image used for migration. the destination host
If this flag is False, a temporary image name is built
""" """
new_image_name = ""
LOG.debug( 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 # Looking for the instance to migrate
instance = self.find_instance(instance_id) instance = self.find_instance(instance_id)
@@ -165,215 +158,43 @@ class NovaHelper(object):
LOG.debug("Instance %s not found !", instance_id) LOG.debug("Instance %s not found !", instance_id)
return False return False
else: else:
# NOTE: If destination node is None call Nova API to migrate
# instance
host_name = getattr(instance, "OS-EXT-SRV-ATTR:host") host_name = getattr(instance, "OS-EXT-SRV-ATTR:host")
LOG.debug( LOG.debug(
"Instance %(instance)s found on host '%(host)s'.", "Instance %(instance)s found on host '%(host)s'.",
{'instance': instance_id, 'host': host_name}) {'instance': instance_id, 'host': host_name})
if dest_hostname is None: previous_status = getattr(instance, 'status')
previous_status = getattr(instance, 'status')
instance.migrate() if (dest_hostname and
instance = self.nova.servers.get(instance_id) not self._check_nova_api_version(self.nova, "2.56")):
while (getattr(instance, 'status') not in LOG.error("For migrating a given dest_hostname,"
["VERIFY_RESIZE", "ERROR"] and retry): "Nova API version must be 2.56 or higher")
instance = self.nova.servers.get(instance.id) return False
time.sleep(2)
retry -= 1
new_hostname = getattr(instance, 'OS-EXT-SRV-ATTR:host')
if (host_name != new_hostname and instance.migrate(host=dest_hostname)
instance.status == 'VERIFY_RESIZE'): instance = self.nova.servers.get(instance_id)
if not self.confirm_resize(instance, previous_status):
return False while (getattr(instance, 'status') not in
LOG.debug( ["VERIFY_RESIZE", "ERROR"] and retry):
"cold migration succeeded : " instance = self.nova.servers.get(instance.id)
"instance %s is now on host '%s'.", ( time.sleep(2)
instance_id, new_hostname)) retry -= 1
return True new_hostname = getattr(instance, 'OS-EXT-SRV-ATTR:host')
else:
LOG.debug( if (host_name != new_hostname and
"cold migration for instance %s failed", instance_id) instance.status == 'VERIFY_RESIZE'):
if not self.confirm_resize(instance, previous_status):
return False return False
LOG.debug(
if not keep_original_image_name: "cold migration succeeded : "
# randrange gives you an integral value "instance %(instance)s is now on host '%(host)s'.",
irand = random.randint(0, 1000) {'instance': instance_id, 'host': new_hostname})
return True
# Building the temporary image name
# which will be used for the migration
new_image_name = "tmp-migrate-%s-%s" % (instance_id, irand)
else: 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( LOG.debug(
"Extracting network configuration for network '%s'", "cold migration for instance %s failed", instance_id)
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)
return False 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): def resize_instance(self, instance_id, flavor, retry=120):
"""This method resizes given instance with specified flavor. """This method resizes given instance with specified flavor.
@@ -936,3 +757,12 @@ class NovaHelper(object):
"Volume %s is now on host '%s'.", "Volume %s is now on host '%s'.",
(new_volume.id, host_name)) (new_volume.id, host_name))
return True 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

View File

@@ -23,7 +23,7 @@ nova_client = cfg.OptGroup(name='nova_client',
NOVA_CLIENT_OPTS = [ NOVA_CLIENT_OPTS = [
cfg.StrOpt('api_version', cfg.StrOpt('api_version',
default='2.53', default='2.56',
help='Version of Nova API to use in novaclient.'), help='Version of Nova API to use in novaclient.'),
cfg.StrOpt('endpoint_type', cfg.StrOpt('endpoint_type',
default='publicURL', default='publicURL',

View File

@@ -210,57 +210,6 @@ class TestNovaHelper(base.TestCase):
self.assertFalse(is_success) 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()) @mock.patch.object(time, 'sleep', mock.Mock())
def test_abort_live_migrate_instance(self, mock_glance, mock_cinder, def test_abort_live_migrate_instance(self, mock_glance, mock_cinder,
mock_neutron, mock_nova): mock_neutron, mock_nova):