From d732ee38a1c596ab2720de51e91f83d6dbc727cf Mon Sep 17 00:00:00 2001 From: Sahid Orentino Ferdjaoui Date: Thu, 27 Oct 2022 09:59:46 +0200 Subject: api: extend evacuate instance to support target state Start to v2.95 any evacuated instances will be stopped a destination Implements: bp/allowing-target-state-for-evacuate Signed-off-by: Sahid Orentino Ferdjaoui Change-Id: I141b6f057cc4eb9c541c2bc6eddae27270ede08d --- .../v2.95/server-evacuate-find-host-req.json | 5 ++ .../os-evacuate/v2.95/server-evacuate-req.json | 6 +++ doc/api_samples/versions/v21-version-get-resp.json | 2 +- doc/api_samples/versions/versions-get-resp.json | 2 +- nova/api/openstack/api_version_request.py | 3 +- nova/api/openstack/compute/evacuate.py | 25 +++++++++- .../openstack/compute/rest_api_version_history.rst | 8 +++ nova/api/openstack/compute/schemas/evacuate.py | 4 ++ nova/exception.py | 7 +++ .../v2.95/server-evacuate-find-host-req.json.tpl | 5 ++ .../os-evacuate/v2.95/server-evacuate-req.json.tpl | 5 ++ .../functional/api_sample_tests/test_evacuate.py | 38 +++++++++++++++ nova/tests/functional/integrated_helpers.py | 2 +- .../notification_sample_tests/test_compute_task.py | 7 +++ .../notification_sample_tests/test_instance.py | 20 +++++--- .../functional/regressions/test_bug_1669054.py | 3 +- .../functional/regressions/test_bug_1713783.py | 6 +++ .../functional/regressions/test_bug_1764883.py | 3 +- .../functional/regressions/test_bug_1823370.py | 3 +- .../functional/regressions/test_bug_1896463.py | 2 +- .../functional/regressions/test_bug_1922053.py | 6 ++- nova/tests/functional/test_server_group.py | 57 ++++++++++++++++++++-- nova/tests/functional/test_servers.py | 12 +++-- .../functional/test_servers_resource_request.py | 4 +- .../unit/api/openstack/compute/test_evacuate.py | 29 +++++++++++ nova/tests/unit/policies/test_evacuate.py | 2 +- ...target-state-for-evacuate-d4c1912c481973d6.yaml | 13 +++++ 27 files changed, 252 insertions(+), 27 deletions(-) create mode 100644 doc/api_samples/os-evacuate/v2.95/server-evacuate-find-host-req.json create mode 100644 doc/api_samples/os-evacuate/v2.95/server-evacuate-req.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-evacuate/v2.95/server-evacuate-find-host-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-evacuate/v2.95/server-evacuate-req.json.tpl create mode 100644 releasenotes/notes/allowing-target-state-for-evacuate-d4c1912c481973d6.yaml diff --git a/doc/api_samples/os-evacuate/v2.95/server-evacuate-find-host-req.json b/doc/api_samples/os-evacuate/v2.95/server-evacuate-find-host-req.json new file mode 100644 index 0000000000..ae9fb0a67b --- /dev/null +++ b/doc/api_samples/os-evacuate/v2.95/server-evacuate-find-host-req.json @@ -0,0 +1,5 @@ +{ + "evacuate": { + "targetState": "stopped" + } +} diff --git a/doc/api_samples/os-evacuate/v2.95/server-evacuate-req.json b/doc/api_samples/os-evacuate/v2.95/server-evacuate-req.json new file mode 100644 index 0000000000..a9f809c830 --- /dev/null +++ b/doc/api_samples/os-evacuate/v2.95/server-evacuate-req.json @@ -0,0 +1,6 @@ +{ + "evacuate": { + "host": "testHost", + "targetState": "stopped" + } +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index f2ed6a83b0..3f285e6017 100644 --- a/doc/api_samples/versions/v21-version-get-resp.json +++ b/doc/api_samples/versions/v21-version-get-resp.json @@ -19,7 +19,7 @@ } ], "status": "CURRENT", - "version": "2.94", + "version": "2.95", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/api_samples/versions/versions-get-resp.json b/doc/api_samples/versions/versions-get-resp.json index 7b39c94ef7..749fd4674f 100644 --- a/doc/api_samples/versions/versions-get-resp.json +++ b/doc/api_samples/versions/versions-get-resp.json @@ -22,7 +22,7 @@ } ], "status": "CURRENT", - "version": "2.94", + "version": "2.95", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index dda3cbbc74..718ac7e8e6 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -254,6 +254,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: in keypair name. * 2.93 - Add support for volume backed server rebuild. * 2.94 - Allow FQDN in server hostname. + * 2.95 - Evacuate will now stop instance at destination. """ # The minimum and maximum versions of the API supported @@ -262,7 +263,7 @@ REST_API_VERSION_HISTORY = """REST API Version History: # Note(cyeoh): This only applies for the v2.1 API once microversions # support is fully merged. It does not affect the V2 API. _MIN_API_VERSION = '2.1' -_MAX_API_VERSION = '2.94' +_MAX_API_VERSION = '2.95' DEFAULT_API_VERSION = _MIN_API_VERSION # Almost all proxy APIs which are related to network, images and baremetal diff --git a/nova/api/openstack/compute/evacuate.py b/nova/api/openstack/compute/evacuate.py index aa35812759..a6602be079 100644 --- a/nova/api/openstack/compute/evacuate.py +++ b/nova/api/openstack/compute/evacuate.py @@ -23,9 +23,11 @@ from nova.api.openstack.compute.schemas import evacuate from nova.api.openstack import wsgi from nova.api import validation from nova.compute import api as compute +from nova.compute import vm_states import nova.conf from nova import exception from nova.i18n import _ +from nova import objects from nova.policies import evacuate as evac_policies from nova import utils @@ -33,6 +35,8 @@ CONF = nova.conf.CONF LOG = logging.getLogger(__name__) +MIN_VER_NOVA_COMPUTE_EVACUATE_STOPPED = 62 + class EvacuateController(wsgi.Controller): def __init__(self): @@ -77,7 +81,8 @@ class EvacuateController(wsgi.Controller): @validation.schema(evacuate.evacuate, "2.0", "2.13") @validation.schema(evacuate.evacuate_v214, "2.14", "2.28") @validation.schema(evacuate.evacuate_v2_29, "2.29", "2.67") - @validation.schema(evacuate.evacuate_v2_68, "2.68") + @validation.schema(evacuate.evacuate_v2_68, "2.68", "2.94") + @validation.schema(evacuate.evacuate_v2_95, "2.95") def _evacuate(self, req, id, body): """Permit admins to evacuate a server from a failed host to a new one. @@ -92,6 +97,19 @@ class EvacuateController(wsgi.Controller): host = evacuate_body.get("host") force = None + target_state = None + if api_version_request.is_supported(req, min_version='2.95'): + min_ver = objects.service.get_minimum_version_all_cells( + context, ['nova-compute']) + if min_ver < MIN_VER_NOVA_COMPUTE_EVACUATE_STOPPED: + raise exception.NotSupportedComputeForEvacuateV295( + {'currently': min_ver, + 'expected': MIN_VER_NOVA_COMPUTE_EVACUATE_STOPPED}) + # Starts to 2.95 any evacuated instances will be stopped at + # destination. Previously an active or stopped instance would have + # kept its state. + target_state = vm_states.STOPPED + on_shared_storage = self._get_on_shared_storage(req, evacuate_body) if api_version_request.is_supported(req, min_version='2.29'): @@ -120,7 +138,8 @@ class EvacuateController(wsgi.Controller): try: self.compute_api.evacuate(context, instance, host, - on_shared_storage, password, force) + on_shared_storage, password, force, + target_state) except exception.InstanceInvalidState as state_error: common.raise_http_conflict_for_instance_invalid_state(state_error, 'evacuate', id) @@ -130,6 +149,8 @@ class EvacuateController(wsgi.Controller): exception.ExtendedResourceRequestOldCompute, ) as e: raise exc.HTTPBadRequest(explanation=e.format_message()) + except exception.UnsupportedRPCVersion as e: + raise exc.HTTPConflict(explanation=e.format_message()) if (not api_version_request.is_supported(req, min_version='2.14') and CONF.api.enable_instance_password): diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index d4df0e81ed..b34510be5c 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -1237,3 +1237,11 @@ The ``hostname`` parameter to the ``POST /servers`` (create server), ``PUT /servers/{id}`` (update server) and ``POST /servers/{server_id}/action (rebuild)`` (rebuild server) APIs is now allowed to be a Fully Qualified Domain Name (FQDN). + + +2.95 +--------------------- + +Any evacuated instances will be now stopped at destination. This +requires minimun compute version 27.0.0 (antelope 2023.1). Operators +can still use previous microversion for older behavior. diff --git a/nova/api/openstack/compute/schemas/evacuate.py b/nova/api/openstack/compute/schemas/evacuate.py index a415a97f89..c7b84a655e 100644 --- a/nova/api/openstack/compute/schemas/evacuate.py +++ b/nova/api/openstack/compute/schemas/evacuate.py @@ -46,3 +46,7 @@ evacuate_v2_29['properties']['evacuate']['properties'][ # v2.68 removes the 'force' parameter added in v2.29, meaning it is identical # to v2.14 evacuate_v2_68 = copy.deepcopy(evacuate_v214) + +# v2.95 keeps the same schema, evacuating an instance will now result its state +# to be stopped at destination. +evacuate_v2_95 = copy.deepcopy(evacuate_v2_68) diff --git a/nova/exception.py b/nova/exception.py index 2c4bd77d34..20c112b628 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2510,3 +2510,10 @@ class ReimageException(NovaException): class InvalidNodeConfiguration(NovaException): msg_fmt = _('Invalid node identity configuration: %(reason)s') + + +class NotSupportedComputeForEvacuateV295(NotSupported): + msg_fmt = _("Starting to microversion 2.95, evacuate API will stop " + "instance on destination. To evacuate before upgrades are " + "complete please use an older microversion. Required version " + "for compute %(expected), current version %(currently)s") diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-evacuate/v2.95/server-evacuate-find-host-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-evacuate/v2.95/server-evacuate-find-host-req.json.tpl new file mode 100644 index 0000000000..8abf0b4e18 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-evacuate/v2.95/server-evacuate-find-host-req.json.tpl @@ -0,0 +1,5 @@ +{ + "evacuate": { + "adminPass": "%(adminPass)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-evacuate/v2.95/server-evacuate-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-evacuate/v2.95/server-evacuate-req.json.tpl new file mode 100644 index 0000000000..8abf0b4e18 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-evacuate/v2.95/server-evacuate-req.json.tpl @@ -0,0 +1,5 @@ +{ + "evacuate": { + "adminPass": "%(adminPass)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/test_evacuate.py b/nova/tests/functional/api_sample_tests/test_evacuate.py index a92d942715..15efb39d44 100644 --- a/nova/tests/functional/api_sample_tests/test_evacuate.py +++ b/nova/tests/functional/api_sample_tests/test_evacuate.py @@ -216,3 +216,41 @@ class EvacuateJsonTestV268(EvacuateJsonTestV229): def test_server_evacuate_with_force(self): # doesn't apply to v2.68+, which removed the ability to force migrate pass + + +class EvacuateJsonTestV295(EvacuateJsonTestV268): + microversion = '2.95' + scenarios = [('v2_95', {'api_major_version': 'v2.1'})] + + @mock.patch('nova.conductor.manager.ComputeTaskManager.rebuild_instance') + def test_server_evacuate(self, rebuild_mock): + req_subs = { + "adminPass": "MySecretPass", + } + self._test_evacuate(req_subs, 'server-evacuate-req', + server_resp=None, expected_resp_code=200) + + rebuild_mock.assert_called_once_with(mock.ANY, instance=mock.ANY, + orig_image_ref=mock.ANY, image_ref=mock.ANY, + injected_files=mock.ANY, new_pass="MySecretPass", + orig_sys_metadata=mock.ANY, bdms=mock.ANY, recreate=mock.ANY, + on_shared_storage=None, preserve_ephemeral=mock.ANY, + host=None, request_spec=mock.ANY, + reimage_boot_volume=False, target_state="stopped") + + @mock.patch('nova.conductor.manager.ComputeTaskManager.rebuild_instance') + def test_server_evacuate_find_host(self, rebuild_mock): + req_subs = { + 'host': 'testHost', + "adminPass": "MySecretPass", + } + self._test_evacuate(req_subs, 'server-evacuate-find-host-req', + server_resp=None, expected_resp_code=200) + + rebuild_mock.assert_called_once_with(mock.ANY, instance=mock.ANY, + orig_image_ref=mock.ANY, image_ref=mock.ANY, + injected_files=mock.ANY, new_pass="MySecretPass", + orig_sys_metadata=mock.ANY, bdms=mock.ANY, recreate=mock.ANY, + on_shared_storage=None, preserve_ephemeral=mock.ANY, + host=None, request_spec=mock.ANY, + reimage_boot_volume=False, target_state="stopped") diff --git a/nova/tests/functional/integrated_helpers.py b/nova/tests/functional/integrated_helpers.py index 2dd840259f..f8ff572c59 100644 --- a/nova/tests/functional/integrated_helpers.py +++ b/nova/tests/functional/integrated_helpers.py @@ -598,7 +598,7 @@ class InstanceHelperMixin: def _evacuate_server( self, server, extra_post_args=None, expected_host=None, - expected_state='ACTIVE', expected_task_state=NOT_SPECIFIED, + expected_state='SHUTOFF', expected_task_state=NOT_SPECIFIED, expected_migration_status='done'): """Evacuate a server.""" api = getattr(self, 'admin_api', self.api) diff --git a/nova/tests/functional/notification_sample_tests/test_compute_task.py b/nova/tests/functional/notification_sample_tests/test_compute_task.py index 3de1c7d4e1..05d2d32fde 100644 --- a/nova/tests/functional/notification_sample_tests/test_compute_task.py +++ b/nova/tests/functional/notification_sample_tests/test_compute_task.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + +from nova import objects from nova.tests import fixtures from nova.tests.functional.notification_sample_tests \ import notification_sample_base @@ -53,6 +56,10 @@ class TestComputeTaskNotificationSample( }, actual=self.notifier.versioned_notifications[1]) + @mock.patch.object( + objects.service, 'get_minimum_version_all_cells', + new=mock.Mock(return_value=62) + ) def test_rebuild_fault(self): server = self._boot_a_server( extra_params={'networks': [{'port': self.neutron.port_1['id']}]}, diff --git a/nova/tests/functional/notification_sample_tests/test_instance.py b/nova/tests/functional/notification_sample_tests/test_instance.py index f671a8abca..5a52c2dad6 100644 --- a/nova/tests/functional/notification_sample_tests/test_instance.py +++ b/nova/tests/functional/notification_sample_tests/test_instance.py @@ -46,18 +46,18 @@ class TestInstanceNotificationSampleWithMultipleCompute( self.compute2 = self.start_service('compute', host='host2') actions = [ - self._test_live_migration_rollback, - self._test_live_migration_abort, - self._test_live_migration_success, - self._test_evacuate_server, - self._test_live_migration_force_complete + (self._test_live_migration_rollback, 'ACTIVE'), + (self._test_live_migration_abort, 'ACTIVE'), + (self._test_live_migration_success, 'ACTIVE'), + (self._test_evacuate_server, 'SHUTOFF'), + (self._test_live_migration_force_complete, 'ACTIVE'), ] - for action in actions: + for action, expected_state in actions: self.notifier.reset() action(server) # Ensure that instance is in active state after an action - self._wait_for_state_change(server, 'ACTIVE') + self._wait_for_state_change(server, expected_state) @mock.patch('nova.compute.manager.ComputeManager.' '_live_migration_cleanup_flags', return_value=[True, False]) @@ -275,6 +275,12 @@ class TestInstanceNotificationSampleWithMultipleCompute( self.admin_api.put_service(service_id, {'forced_down': False}) def _test_live_migration_force_complete(self, server): + # In the scenario evacuate happened before which stopped the + # server. + self._start_server(server) + self._wait_for_state_change(server, 'ACTIVE') + self.notifier.reset() + post = { 'os-migrateLive': { 'host': 'host2', diff --git a/nova/tests/functional/regressions/test_bug_1669054.py b/nova/tests/functional/regressions/test_bug_1669054.py index 6180dbfbaa..b20e1530cc 100644 --- a/nova/tests/functional/regressions/test_bug_1669054.py +++ b/nova/tests/functional/regressions/test_bug_1669054.py @@ -59,7 +59,8 @@ class ResizeEvacuateTestCase(integrated_helpers._IntegratedTestBase): # Now try to evacuate the server back to the original source compute. server = self._evacuate_server( server, {'onSharedStorage': 'False'}, - expected_host=self.compute.host, expected_migration_status='done') + expected_host=self.compute.host, expected_migration_status='done', + expected_state='ACTIVE') # Assert the RequestSpec.ignore_hosts field is not populated. reqspec = objects.RequestSpec.get_by_instance_uuid( diff --git a/nova/tests/functional/regressions/test_bug_1713783.py b/nova/tests/functional/regressions/test_bug_1713783.py index 9a6a79d7a2..8088ccfe06 100644 --- a/nova/tests/functional/regressions/test_bug_1713783.py +++ b/nova/tests/functional/regressions/test_bug_1713783.py @@ -13,9 +13,11 @@ # limitations under the License. import time +from unittest import mock from oslo_log import log as logging +from nova import objects from nova import test from nova.tests import fixtures as nova_fixtures from nova.tests.functional import fixtures as func_fixtures @@ -81,6 +83,10 @@ class FailedEvacuateStateTests(test.TestCase, created_server = self.api.post_server({'server': server_req}) return self._wait_for_state_change(created_server, 'ACTIVE') + @mock.patch.object( + objects.service, 'get_minimum_version_all_cells', + new=mock.Mock(return_value=62) + ) def test_evacuate_no_valid_host(self): # Boot a server server = self._boot_a_server() diff --git a/nova/tests/functional/regressions/test_bug_1764883.py b/nova/tests/functional/regressions/test_bug_1764883.py index aa86770584..59bbed4f46 100644 --- a/nova/tests/functional/regressions/test_bug_1764883.py +++ b/nova/tests/functional/regressions/test_bug_1764883.py @@ -95,7 +95,8 @@ class TestEvacuationWithSourceReturningDuringRebuild( # Evacuate the instance from the source_host server = self._evacuate_server( - server, expected_migration_status='done') + server, expected_migration_status='done', + expected_state='ACTIVE') host = server['OS-EXT-SRV-ATTR:host'] migrations = self.api.get_migrations() diff --git a/nova/tests/functional/regressions/test_bug_1823370.py b/nova/tests/functional/regressions/test_bug_1823370.py index 5e69905f5f..af134070cd 100644 --- a/nova/tests/functional/regressions/test_bug_1823370.py +++ b/nova/tests/functional/regressions/test_bug_1823370.py @@ -66,4 +66,5 @@ class MultiCellEvacuateTestCase(integrated_helpers._IntegratedTestBase): # higher than host3. self._evacuate_server( server, {'onSharedStorage': 'False'}, expected_host='host3', - expected_migration_status='done') + expected_migration_status='done', + expected_state='ACTIVE') diff --git a/nova/tests/functional/regressions/test_bug_1896463.py b/nova/tests/functional/regressions/test_bug_1896463.py index dc74791e0e..3cfece8d36 100644 --- a/nova/tests/functional/regressions/test_bug_1896463.py +++ b/nova/tests/functional/regressions/test_bug_1896463.py @@ -216,7 +216,7 @@ class TestEvacuateResourceTrackerRace( self._run_periodics() self._wait_for_server_parameter( - server, {'OS-EXT-SRV-ATTR:host': 'host2', 'status': 'ACTIVE'}) + server, {'OS-EXT-SRV-ATTR:host': 'host2', 'status': 'SHUTOFF'}) self._assert_pci_device_allocated(server['id'], self.compute1_id) self._assert_pci_device_allocated(server['id'], self.compute2_id) diff --git a/nova/tests/functional/regressions/test_bug_1922053.py b/nova/tests/functional/regressions/test_bug_1922053.py index 612be27b2b..70bb3d4cab 100644 --- a/nova/tests/functional/regressions/test_bug_1922053.py +++ b/nova/tests/functional/regressions/test_bug_1922053.py @@ -1,3 +1,4 @@ + # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -27,6 +28,7 @@ class ForceUpWithDoneEvacuations(integrated_helpers._IntegratedTestBase): ADMIN_API = True microversion = 'latest' + expected_state = 'SHUTOFF' def _create_test_server(self, compute_host): return self._create_server(host=compute_host, networks='none') @@ -59,7 +61,8 @@ class ForceUpWithDoneEvacuations(integrated_helpers._IntegratedTestBase): server = self._evacuate_server( server, expected_host='compute2', - expected_migration_status='done' + expected_migration_status='done', + expected_state=self.expected_state ) # Assert that the request to force up the host is rejected @@ -97,6 +100,7 @@ class ForceUpWithDoneEvacuationsv252(ForceUpWithDoneEvacuations): """ microversion = '2.52' + expected_state = 'ACTIVE' def _create_test_server(self, compute_host): return self._create_server(az='nova:compute', networks='none') diff --git a/nova/tests/functional/test_server_group.py b/nova/tests/functional/test_server_group.py index 7cbe8bdb67..01e3547f7e 100644 --- a/nova/tests/functional/test_server_group.py +++ b/nova/tests/functional/test_server_group.py @@ -444,7 +444,8 @@ class ServerGroupTestV21(ServerGroupTestBase): evacuated_server = self._evacuate_server( servers[1], {'onSharedStorage': 'False'}, - expected_migration_status='done') + expected_migration_status='done', + expected_state='ACTIVE') # check that the server is evacuated to another host self.assertNotEqual(evacuated_server['OS-EXT-SRV-ATTR:host'], @@ -621,7 +622,8 @@ class ServerGroupTestV215(ServerGroupTestV21): compute3 = self.start_service('compute', host='host3') evacuated_server = self._evacuate_server( - servers[1], expected_migration_status='done') + servers[1], expected_migration_status='done', + expected_state='ACTIVE') # check that the server is evacuated self.assertNotEqual(evacuated_server['OS-EXT-SRV-ATTR:host'], @@ -800,7 +802,8 @@ class ServerGroupTestV215(ServerGroupTestV21): self._set_forced_down(host, True) evacuated_server = self._evacuate_server( - servers[1], expected_migration_status='done') + servers[1], expected_migration_status='done', + expected_state='ACTIVE') # Note(gibi): need to get the server again as the state of the instance # goes to ACTIVE first then the host of the instance changes to the @@ -870,6 +873,54 @@ class ServerGroupTestV264(ServerGroupTestV215): self.assertEqual(2, hosts.count(host)) +class ServerGroupTestV295(ServerGroupTestV264): + microversion = '2.95' + + def _evacuate_with_soft_anti_affinity_policies(self, group): + created_group = self.api.post_server_groups(group) + servers = self._boot_servers_to_group(created_group) + + host = self._get_compute_service_by_host_name( + servers[1]['OS-EXT-SRV-ATTR:host']) + # Set forced_down on the host to ensure nova considers the host down. + self._set_forced_down(host, True) + + evacuated_server = self._evacuate_server( + servers[1], expected_migration_status='done') + + # Note(gibi): need to get the server again as the state of the instance + # goes to ACTIVE first then the host of the instance changes to the + # new host later + evacuated_server = self.admin_api.get_server(evacuated_server['id']) + + return [evacuated_server['OS-EXT-SRV-ATTR:host'], + servers[0]['OS-EXT-SRV-ATTR:host']] + + def test_evacuate_with_anti_affinity(self): + created_group = self.api.post_server_groups(self.anti_affinity) + servers = self._boot_servers_to_group(created_group) + + host = self._get_compute_service_by_host_name( + servers[1]['OS-EXT-SRV-ATTR:host']) + # Set forced_down on the host to ensure nova considers the host down. + self._set_forced_down(host, True) + + # Start additional host to test evacuation + compute3 = self.start_service('compute', host='host3') + + evacuated_server = self._evacuate_server( + servers[1], expected_migration_status='done') + + # check that the server is evacuated + self.assertNotEqual(evacuated_server['OS-EXT-SRV-ATTR:host'], + servers[1]['OS-EXT-SRV-ATTR:host']) + # check that policy is kept + self.assertNotEqual(evacuated_server['OS-EXT-SRV-ATTR:host'], + servers[0]['OS-EXT-SRV-ATTR:host']) + + compute3.kill() + + class ServerGroupTestMultiCell(ServerGroupTestBase): NUMBER_OF_CELLS = 2 diff --git a/nova/tests/functional/test_servers.py b/nova/tests/functional/test_servers.py index d1ab84aa7b..43208aa812 100644 --- a/nova/tests/functional/test_servers.py +++ b/nova/tests/functional/test_servers.py @@ -2260,7 +2260,8 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase): } server = self._evacuate_server( - server, extra_post_args=post, expected_host=dest_hostname) + server, extra_post_args=post, expected_host=dest_hostname, + expected_state='ACTIVE') # Run the periodics to show those don't modify allocations. self._run_periodics() @@ -2437,7 +2438,8 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase): # stay ACTIVE and task_state will be set to None. server = self._evacuate_server( server, expected_task_state=None, - expected_migration_status='failed') + expected_migration_status='failed', + expected_state='ACTIVE') # Run the periodics to show those don't modify allocations. self._run_periodics() @@ -5324,7 +5326,8 @@ class ServerMovingTestsWithNestedResourceRequests( server = self._evacuate_server( server, extra_post_args=post, expected_migration_status='error', - expected_host=source_hostname) + expected_host=source_hostname, + expected_state='ACTIVE') self.assertIn('Unable to move instance %s to host host2. The instance ' 'has complex allocations on the source host so move ' @@ -5530,7 +5533,8 @@ class ServerMovingTestsFromFlatToNested( self._evacuate_server( server, extra_post_args=post, expected_host='host1', - expected_migration_status='error') + expected_migration_status='error', + expected_state='ACTIVE') # We expect that the evacuation will fail as force evacuate tries to # blindly copy the source allocation to the destination but on the diff --git a/nova/tests/functional/test_servers_resource_request.py b/nova/tests/functional/test_servers_resource_request.py index f881fc0323..9c91af7218 100644 --- a/nova/tests/functional/test_servers_resource_request.py +++ b/nova/tests/functional/test_servers_resource_request.py @@ -2162,7 +2162,8 @@ class ServerMoveWithPortResourceRequestTest( # simply fail and the server remains on the source host server = self._evacuate_server( server, expected_host='host1', expected_task_state=None, - expected_migration_status='failed') + expected_migration_status='failed', + expected_state="ACTIVE") # As evacuation failed the resource allocation should be untouched self._check_allocation( @@ -2979,6 +2980,7 @@ class ExtendedResourceRequestOldCompute( super().setUp() self.neutron = self.useFixture( ExtendedResourceRequestNeutronFixture(self)) + self.api.microversion = '2.72' @mock.patch.object( objects.service, 'get_minimum_version_all_cells', diff --git a/nova/tests/unit/api/openstack/compute/test_evacuate.py b/nova/tests/unit/api/openstack/compute/test_evacuate.py index fb7f7662d8..bd88bb8d6e 100644 --- a/nova/tests/unit/api/openstack/compute/test_evacuate.py +++ b/nova/tests/unit/api/openstack/compute/test_evacuate.py @@ -416,3 +416,32 @@ class EvacuateTestV268(EvacuateTestV229): def test_forced_evacuate_with_no_host_provided(self): # not applicable for v2.68, which removed the 'force' parameter pass + + +class EvacuateTestV295(EvacuateTestV268): + def setUp(self): + super(EvacuateTestV268, self).setUp() + self.admin_req = fakes.HTTPRequest.blank('', use_admin_context=True, + version='2.95') + self.req = fakes.HTTPRequest.blank('', version='2.95') + self.mock_get_min_ver = self.useFixture(fixtures.MockPatch( + 'nova.objects.service.get_minimum_version_all_cells', + return_value=62)).mock + + def test_evacuate_version_error(self): + self.mock_get_min_ver.return_value = 61 + self.assertRaises(webob.exc.HTTPBadRequest, + self._get_evacuate_response, + {'host': 'my-host', 'adminPass': 'foo'}) + + def test_evacuate_unsupported_rpc(self): + def fake_evacuate(*args, **kwargs): + raise exception.UnsupportedRPCVersion( + api="fakeapi", + required="x.xx") + + self.stub_out('nova.compute.api.API.evacuate', fake_evacuate) + self._check_evacuate_failure(webob.exc.HTTPConflict, + {'host': 'my-host', + 'onSharedStorage': 'False', + 'adminPass': 'MyNewPass'}) diff --git a/nova/tests/unit/policies/test_evacuate.py b/nova/tests/unit/policies/test_evacuate.py index ddc8241003..b9e4c29dba 100644 --- a/nova/tests/unit/policies/test_evacuate.py +++ b/nova/tests/unit/policies/test_evacuate.py @@ -103,7 +103,7 @@ class EvacuatePolicyTest(base.BasePolicyTest): evacuate_mock.assert_called_once_with( self.user_req.environ['nova.context'], mock.ANY, 'my-host', False, - 'MyNewPass', None) + 'MyNewPass', None, None) class EvacuateNoLegacyNoScopePolicyTest(EvacuatePolicyTest): diff --git a/releasenotes/notes/allowing-target-state-for-evacuate-d4c1912c481973d6.yaml b/releasenotes/notes/allowing-target-state-for-evacuate-d4c1912c481973d6.yaml new file mode 100644 index 0000000000..6c5bc98046 --- /dev/null +++ b/releasenotes/notes/allowing-target-state-for-evacuate-d4c1912c481973d6.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Starting with v2.95 any evacuated instance will be stopped at + destination. The required minimum version for Nova computes is + 27.0.0 (antelope 2023.1). Operator can still continue using + previous behavior by selecting microversion below v2.95. +upgrade: + - | + Operators will have to consider upgrading compute hosts to Nova + 27.0.0 (antelope 2023.1) in order to take advantage of the new + (microversion v2.95) evacuate API behavior. An exception will be + raised for older versions. -- cgit v1.2.1