Added cold VM migration support
Cold migration enables migrating some of the VMs which are not in active state (e.g. stopped). Cold migration can also be used for migrating active VM, although VM is shut down and hence unaccessible while migrating. Change-Id: I89ad0a04d41282431c9773f6ae7feb41573368e3 Closes-Bug: #1564297
This commit is contained in:
@@ -31,27 +31,24 @@ LOG = log.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Migrate(base.BaseAction):
|
class Migrate(base.BaseAction):
|
||||||
"""Live-Migrates a server to a destination nova-compute host
|
"""Migrates a server to a destination nova-compute host
|
||||||
|
|
||||||
This action will allow you to migrate a server to another compute
|
This action will allow you to migrate a server to another compute
|
||||||
destination host. As of now, only live migration can be performed using
|
destination host.
|
||||||
this action.
|
Migration type 'live' can only be used for migrating active VMs.
|
||||||
.. If either host uses shared storage, you can use ``live``
|
Migration type 'cold' can be used for migrating non-active VMs
|
||||||
.. as ``migration_type``. If both source and destination hosts provide
|
as well active VMs, which will be shut down while migrating.
|
||||||
.. local disks, you can set the block_migration parameter to True (not
|
|
||||||
.. supported for yet).
|
|
||||||
|
|
||||||
The action schema is::
|
The action schema is::
|
||||||
|
|
||||||
schema = Schema({
|
schema = Schema({
|
||||||
'resource_id': str, # should be a UUID
|
'resource_id': str, # should be a UUID
|
||||||
'migration_type': str, # choices -> "live" only
|
'migration_type': str, # choices -> "live", "cold"
|
||||||
'dst_hypervisor': str,
|
'dst_hypervisor': str,
|
||||||
'src_hypervisor': str,
|
'src_hypervisor': str,
|
||||||
})
|
})
|
||||||
|
|
||||||
The `resource_id` is the UUID of the server to migrate. Only live migration
|
The `resource_id` is the UUID of the server to migrate.
|
||||||
is supported.
|
|
||||||
The `src_hypervisor` and `dst_hypervisor` parameters are respectively the
|
The `src_hypervisor` and `dst_hypervisor` parameters are respectively the
|
||||||
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
|
||||||
@@ -61,6 +58,7 @@ class Migrate(base.BaseAction):
|
|||||||
# input parameters constants
|
# input parameters constants
|
||||||
MIGRATION_TYPE = 'migration_type'
|
MIGRATION_TYPE = 'migration_type'
|
||||||
LIVE_MIGRATION = 'live'
|
LIVE_MIGRATION = 'live'
|
||||||
|
COLD_MIGRATION = 'cold'
|
||||||
DST_HYPERVISOR = 'dst_hypervisor'
|
DST_HYPERVISOR = 'dst_hypervisor'
|
||||||
SRC_HYPERVISOR = 'src_hypervisor'
|
SRC_HYPERVISOR = 'src_hypervisor'
|
||||||
|
|
||||||
@@ -77,7 +75,8 @@ class Migrate(base.BaseAction):
|
|||||||
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
|
voluptuous.Required(self.RESOURCE_ID): self.check_resource_id,
|
||||||
voluptuous.Required(self.MIGRATION_TYPE,
|
voluptuous.Required(self.MIGRATION_TYPE,
|
||||||
default=self.LIVE_MIGRATION):
|
default=self.LIVE_MIGRATION):
|
||||||
voluptuous.Any(*[self.LIVE_MIGRATION]),
|
voluptuous.Any(*[self.LIVE_MIGRATION,
|
||||||
|
self.COLD_MIGRATION]),
|
||||||
voluptuous.Required(self.DST_HYPERVISOR):
|
voluptuous.Required(self.DST_HYPERVISOR):
|
||||||
voluptuous.All(voluptuous.Any(*six.string_types),
|
voluptuous.All(voluptuous.Any(*six.string_types),
|
||||||
voluptuous.Length(min=1)),
|
voluptuous.Length(min=1)),
|
||||||
@@ -127,14 +126,30 @@ class Migrate(base.BaseAction):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def _cold_migrate_instance(self, nova, destination):
|
||||||
|
result = None
|
||||||
|
try:
|
||||||
|
result = nova.watcher_non_live_migrate_instance(
|
||||||
|
instance_id=self.instance_uuid,
|
||||||
|
dest_hostname=destination)
|
||||||
|
except Exception as exc:
|
||||||
|
LOG.exception(exc)
|
||||||
|
LOG.critical(_LC("Unexpected error occured. Migration failed for"
|
||||||
|
"instance %s. Leaving instance on previous "
|
||||||
|
"host."), self.instance_uuid)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
def migrate(self, destination):
|
def migrate(self, destination):
|
||||||
nova = nova_helper.NovaHelper(osc=self.osc)
|
nova = nova_helper.NovaHelper(osc=self.osc)
|
||||||
LOG.debug("Migrate instance %s to %s", self.instance_uuid,
|
LOG.debug("Migrate instance %s to %s", self.instance_uuid,
|
||||||
destination)
|
destination)
|
||||||
instance = nova.find_instance(self.instance_uuid)
|
instance = nova.find_instance(self.instance_uuid)
|
||||||
if instance:
|
if instance:
|
||||||
if self.migration_type == 'live':
|
if self.migration_type == self.LIVE_MIGRATION:
|
||||||
return self._live_migrate_instance(nova, destination)
|
return self._live_migrate_instance(nova, destination)
|
||||||
|
elif self.migration_type == self.COLD_MIGRATION:
|
||||||
|
return self._cold_migrate_instance(nova, destination)
|
||||||
else:
|
else:
|
||||||
raise exception.Invalid(
|
raise exception.Invalid(
|
||||||
message=(_('Migration of type %(migration_type)s is not '
|
message=(_('Migration of type %(migration_type)s is not '
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ class TestMigration(base.TestCase):
|
|||||||
self.action = migration.Migrate()
|
self.action = migration.Migrate()
|
||||||
self.action.input_parameters = self.input_parameters
|
self.action.input_parameters = self.input_parameters
|
||||||
|
|
||||||
|
self.input_parameters_cold = {
|
||||||
|
"migration_type": "cold",
|
||||||
|
"src_hypervisor": "hypervisor1-hostname",
|
||||||
|
"dst_hypervisor": "hypervisor2-hostname",
|
||||||
|
baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID,
|
||||||
|
}
|
||||||
|
self.action_cold = migration.Migrate()
|
||||||
|
self.action_cold.input_parameters = self.input_parameters_cold
|
||||||
|
|
||||||
def test_parameters(self):
|
def test_parameters(self):
|
||||||
params = {baction.BaseAction.RESOURCE_ID:
|
params = {baction.BaseAction.RESOURCE_ID:
|
||||||
self.INSTANCE_UUID,
|
self.INSTANCE_UUID,
|
||||||
@@ -70,6 +79,15 @@ class TestMigration(base.TestCase):
|
|||||||
self.action.input_parameters = params
|
self.action.input_parameters = params
|
||||||
self.assertEqual(True, self.action.validate_parameters())
|
self.assertEqual(True, self.action.validate_parameters())
|
||||||
|
|
||||||
|
def test_parameters_cold(self):
|
||||||
|
params = {baction.BaseAction.RESOURCE_ID:
|
||||||
|
self.INSTANCE_UUID,
|
||||||
|
self.action.MIGRATION_TYPE: 'cold',
|
||||||
|
self.action.DST_HYPERVISOR: 'compute-2',
|
||||||
|
self.action.SRC_HYPERVISOR: 'compute-3'}
|
||||||
|
self.action_cold.input_parameters = params
|
||||||
|
self.assertEqual(True, self.action_cold.validate_parameters())
|
||||||
|
|
||||||
def test_parameters_exception_empty_fields(self):
|
def test_parameters_exception_empty_fields(self):
|
||||||
parameters = {baction.BaseAction.RESOURCE_ID: None,
|
parameters = {baction.BaseAction.RESOURCE_ID: None,
|
||||||
'migration_type': None,
|
'migration_type': None,
|
||||||
@@ -87,7 +105,7 @@ class TestMigration(base.TestCase):
|
|||||||
def test_parameters_exception_migration_type(self):
|
def test_parameters_exception_migration_type(self):
|
||||||
parameters = {baction.BaseAction.RESOURCE_ID:
|
parameters = {baction.BaseAction.RESOURCE_ID:
|
||||||
self.INSTANCE_UUID,
|
self.INSTANCE_UUID,
|
||||||
'migration_type': 'cold',
|
'migration_type': 'unknown',
|
||||||
'src_hypervisor': 'compute-2',
|
'src_hypervisor': 'compute-2',
|
||||||
'dst_hypervisor': 'compute-3'}
|
'dst_hypervisor': 'compute-3'}
|
||||||
self.action.input_parameters = parameters
|
self.action.input_parameters = parameters
|
||||||
@@ -154,6 +172,13 @@ class TestMigration(base.TestCase):
|
|||||||
self.m_helper.find_instance.assert_called_once_with(self.INSTANCE_UUID)
|
self.m_helper.find_instance.assert_called_once_with(self.INSTANCE_UUID)
|
||||||
self.assertEqual(self.INSTANCE_UUID, exc.kwargs["name"])
|
self.assertEqual(self.INSTANCE_UUID, exc.kwargs["name"])
|
||||||
|
|
||||||
|
def test_execute_cold_migration_invalid_instance(self):
|
||||||
|
self.m_helper.find_instance.return_value = None
|
||||||
|
exc = self.assertRaises(
|
||||||
|
exception.InstanceNotFound, self.action_cold.execute)
|
||||||
|
self.m_helper.find_instance.assert_called_once_with(self.INSTANCE_UUID)
|
||||||
|
self.assertEqual(self.INSTANCE_UUID, exc.kwargs["name"])
|
||||||
|
|
||||||
def test_execute_live_migration(self):
|
def test_execute_live_migration(self):
|
||||||
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
|
||||||
@@ -166,6 +191,20 @@ class TestMigration(base.TestCase):
|
|||||||
instance_id=self.INSTANCE_UUID,
|
instance_id=self.INSTANCE_UUID,
|
||||||
dest_hostname="hypervisor2-hostname")
|
dest_hostname="hypervisor2-hostname")
|
||||||
|
|
||||||
|
def test_execute_cold_migration(self):
|
||||||
|
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.action_cold.execute()
|
||||||
|
except Exception as exc:
|
||||||
|
self.fail(exc)
|
||||||
|
|
||||||
|
self.m_helper.watcher_non_live_migrate_instance.\
|
||||||
|
assert_called_once_with(
|
||||||
|
instance_id=self.INSTANCE_UUID,
|
||||||
|
dest_hostname="hypervisor2-hostname"
|
||||||
|
)
|
||||||
|
|
||||||
def test_revert_live_migration(self):
|
def test_revert_live_migration(self):
|
||||||
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
|
||||||
@@ -177,6 +216,18 @@ class TestMigration(base.TestCase):
|
|||||||
dest_hostname="hypervisor1-hostname"
|
dest_hostname="hypervisor1-hostname"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_revert_cold_migration(self):
|
||||||
|
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
|
||||||
|
self.action_cold.revert()
|
||||||
|
|
||||||
|
self.m_helper_cls.assert_called_once_with(osc=self.m_osc)
|
||||||
|
self.m_helper.watcher_non_live_migrate_instance.\
|
||||||
|
assert_called_once_with(
|
||||||
|
instance_id=self.INSTANCE_UUID,
|
||||||
|
dest_hostname="hypervisor1-hostname"
|
||||||
|
)
|
||||||
|
|
||||||
def test_live_migrate_non_shared_storage_instance(self):
|
def test_live_migrate_non_shared_storage_instance(self):
|
||||||
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
self.m_helper.find_instance.return_value = self.INSTANCE_UUID
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user