Replace cold migration to use Nova migration API
Since Nova API v2.56, Nova migrate Server(migrate Action) API has host option. This patch replaces cold migration implementation to use the API. Change-Id: Idd6ebc94f81ad5d65256c80885f2addc1aaeaae1 Implements: blueprint replace-cold-migrate-to-use-nova-migration-api
This commit is contained in:
parent
b0bdeea7cf
commit
4179c3527c
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue