diff --git a/watcher/applier/actions/migration.py b/watcher/applier/actions/migration.py index 91fd9859c..59e8a86e0 100644 --- a/watcher/applier/actions/migration.py +++ b/watcher/applier/actions/migration.py @@ -31,27 +31,24 @@ LOG = log.getLogger(__name__) 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 - destination host. As of now, only live migration can be performed using - this action. - .. If either host uses shared storage, you can use ``live`` - .. as ``migration_type``. If both source and destination hosts provide - .. local disks, you can set the block_migration parameter to True (not - .. supported for yet). + destination host. + Migration type 'live' can only be used for migrating active VMs. + Migration type 'cold' can be used for migrating non-active VMs + as well active VMs, which will be shut down while migrating. The action schema is:: schema = Schema({ 'resource_id': str, # should be a UUID - 'migration_type': str, # choices -> "live" only + 'migration_type': str, # choices -> "live", "cold" 'dst_hypervisor': str, 'src_hypervisor': str, }) - The `resource_id` is the UUID of the server to migrate. Only live migration - is supported. + The `resource_id` is the UUID of the server to migrate. The `src_hypervisor` and `dst_hypervisor` parameters are respectively the source and the destination compute hostname (list of available compute hosts is returned by this command: ``nova service-list --binary @@ -61,6 +58,7 @@ class Migrate(base.BaseAction): # input parameters constants MIGRATION_TYPE = 'migration_type' LIVE_MIGRATION = 'live' + COLD_MIGRATION = 'cold' DST_HYPERVISOR = 'dst_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.MIGRATION_TYPE, default=self.LIVE_MIGRATION): - voluptuous.Any(*[self.LIVE_MIGRATION]), + voluptuous.Any(*[self.LIVE_MIGRATION, + self.COLD_MIGRATION]), voluptuous.Required(self.DST_HYPERVISOR): voluptuous.All(voluptuous.Any(*six.string_types), voluptuous.Length(min=1)), @@ -127,14 +126,30 @@ class Migrate(base.BaseAction): 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): nova = nova_helper.NovaHelper(osc=self.osc) LOG.debug("Migrate instance %s to %s", self.instance_uuid, destination) instance = nova.find_instance(self.instance_uuid) if instance: - if self.migration_type == 'live': + if self.migration_type == self.LIVE_MIGRATION: return self._live_migrate_instance(nova, destination) + elif self.migration_type == self.COLD_MIGRATION: + return self._cold_migrate_instance(nova, destination) else: raise exception.Invalid( message=(_('Migration of type %(migration_type)s is not ' diff --git a/watcher/tests/applier/actions/test_migration.py b/watcher/tests/applier/actions/test_migration.py index 766250fb2..d8e6ab8f4 100644 --- a/watcher/tests/applier/actions/test_migration.py +++ b/watcher/tests/applier/actions/test_migration.py @@ -61,6 +61,15 @@ class TestMigration(base.TestCase): self.action = migration.Migrate() 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): params = {baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID, @@ -70,6 +79,15 @@ class TestMigration(base.TestCase): self.action.input_parameters = params 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): parameters = {baction.BaseAction.RESOURCE_ID: None, 'migration_type': None, @@ -87,7 +105,7 @@ class TestMigration(base.TestCase): def test_parameters_exception_migration_type(self): parameters = {baction.BaseAction.RESOURCE_ID: self.INSTANCE_UUID, - 'migration_type': 'cold', + 'migration_type': 'unknown', 'src_hypervisor': 'compute-2', 'dst_hypervisor': 'compute-3'} 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.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): self.m_helper.find_instance.return_value = self.INSTANCE_UUID @@ -166,6 +191,20 @@ class TestMigration(base.TestCase): instance_id=self.INSTANCE_UUID, 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): self.m_helper.find_instance.return_value = self.INSTANCE_UUID @@ -177,6 +216,18 @@ class TestMigration(base.TestCase): 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): self.m_helper.find_instance.return_value = self.INSTANCE_UUID