Merge "Added cold VM migration support"
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