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 <sahid.ferdjaoui@industrialdiscipline.com>
Change-Id: I141b6f057cc4eb9c541c2bc6eddae27270ede08d
This commit is contained in:
Sahid Orentino Ferdjaoui 2022-10-27 09:59:46 +02:00
parent 8c2e765989
commit d732ee38a1
27 changed files with 252 additions and 27 deletions

View File

@ -0,0 +1,5 @@
{
"evacuate": {
"targetState": "stopped"
}
}

View File

@ -0,0 +1,6 @@
{
"evacuate": {
"host": "testHost",
"targetState": "stopped"
}
}

View File

@ -19,7 +19,7 @@
}
],
"status": "CURRENT",
"version": "2.94",
"version": "2.95",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -22,7 +22,7 @@
}
],
"status": "CURRENT",
"version": "2.94",
"version": "2.95",
"min_version": "2.1",
"updated": "2013-07-23T11:33:21Z"
}

View File

@ -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

View File

@ -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):

View File

@ -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.

View File

@ -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)

View File

@ -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")

View File

@ -0,0 +1,5 @@
{
"evacuate": {
"adminPass": "%(adminPass)s"
}
}

View File

@ -0,0 +1,5 @@
{
"evacuate": {
"adminPass": "%(adminPass)s"
}
}

View File

@ -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")

View File

@ -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)

View File

@ -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']}]},

View File

@ -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',

View File

@ -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(

View File

@ -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()

View File

@ -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()

View File

@ -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')

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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'})

View File

@ -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):

View File

@ -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.