Merge "Replace cold migration to use Nova migration API"
This commit is contained in:
@@ -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
|
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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user