API change for verifying the scheduler when evacuating

Adding a new microversion for changing the evacuate action behaviour
to call the scheduler anyway unless the admin user provides a force
flag that then keeps the previous behaviour by forcing the conductor
to call the destination without verifying it.

Implements: blueprint check-destination-on-migrations

APIImpact

Change-Id: I9ecbe3d481bf17b12072511da4bb106ff1b6404e
This commit is contained in:
Sylvain Bauza 2016-05-18 21:58:06 +02:00
parent cfd64c976b
commit 86706785ff
17 changed files with 225 additions and 15 deletions

View File

@ -1405,6 +1405,14 @@ force:
in: body in: body
required: false required: false
type: boolean type: boolean
force_evacuate:
description: |
Force an evacuation by not verifying the provided destination host by the
scheduler.
in: body
required: false
type: boolean
min_version: 2.29
forced_down: forced_down:
in: body in: body
required: true required: true

View File

@ -27,6 +27,7 @@ Request
- host: host - host: host
- adminPass: adminPass_evacuate_rebuild_request - adminPass: adminPass_evacuate_rebuild_request
- onSharedStorage: on_shared_storage - onSharedStorage: on_shared_storage
- force: force_evacuate
| |

View File

@ -0,0 +1,7 @@
{
"evacuate": {
"host": "testHost",
"adminPass": "MySecretPass",
"force": false
}
}

View File

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

View File

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

View File

@ -75,6 +75,8 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 2.27 - Adds support for new-style microversion headers while * 2.27 - Adds support for new-style microversion headers while
keeping support for the original style. keeping support for the original style.
* 2.28 - Changes compute_node.cpu_info from string to object * 2.28 - Changes compute_node.cpu_info from string to object
* 2.29 - Add a force flag in evacuate request body and change the
behaviour for the host flag by calling the scheduler.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
@ -83,7 +85,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1" _MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.28" _MAX_API_VERSION = "2.29"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -75,7 +75,8 @@ class EvacuateController(wsgi.Controller):
@extensions.expected_errors((400, 404, 409)) @extensions.expected_errors((400, 404, 409))
@wsgi.action('evacuate') @wsgi.action('evacuate')
@validation.schema(evacuate.evacuate, "2.1", "2.12") @validation.schema(evacuate.evacuate, "2.1", "2.12")
@validation.schema(evacuate.evacuate_v214, "2.14") @validation.schema(evacuate.evacuate_v214, "2.14", "2.28")
@validation.schema(evacuate.evacuate_v2_29, "2.29")
def _evacuate(self, req, id, body): def _evacuate(self, req, id, body):
"""Permit admins to evacuate a server from a failed host """Permit admins to evacuate a server from a failed host
to a new one. to a new one.
@ -85,9 +86,16 @@ class EvacuateController(wsgi.Controller):
evacuate_body = body["evacuate"] evacuate_body = body["evacuate"]
host = evacuate_body.get("host") host = evacuate_body.get("host")
force = None
on_shared_storage = self._get_on_shared_storage(req, evacuate_body) on_shared_storage = self._get_on_shared_storage(req, evacuate_body)
if api_version_request.is_supported(req, min_version='2.29'):
force = body["evacuate"].get("force", False)
force = strutils.bool_from_string(force, strict=True)
if force is True and not host:
message = _("Can't force to a non-provided destination")
raise exc.HTTPBadRequest(explanation=message)
if api_version_request.is_supported(req, min_version='2.14'): if api_version_request.is_supported(req, min_version='2.14'):
password = self._get_password_v214(req, evacuate_body) password = self._get_password_v214(req, evacuate_body)
else: else:
@ -108,7 +116,7 @@ class EvacuateController(wsgi.Controller):
try: try:
self.compute_api.evacuate(context, instance, host, self.compute_api.evacuate(context, instance, host,
on_shared_storage, password) on_shared_storage, password, force)
except exception.InstanceUnknownCell as e: except exception.InstanceUnknownCell as e:
raise exc.HTTPNotFound(explanation=e.format_message()) raise exc.HTTPNotFound(explanation=e.format_message())
except exception.InstanceInvalidState as state_error: except exception.InstanceInvalidState as state_error:

View File

@ -38,3 +38,7 @@ evacuate = {
evacuate_v214 = copy.deepcopy(evacuate) evacuate_v214 = copy.deepcopy(evacuate)
del evacuate_v214['properties']['evacuate']['properties']['onSharedStorage'] del evacuate_v214['properties']['evacuate']['properties']['onSharedStorage']
del evacuate_v214['properties']['evacuate']['required'] del evacuate_v214['properties']['evacuate']['required']
evacuate_v2_29 = copy.deepcopy(evacuate_v214)
evacuate_v2_29['properties']['evacuate']['properties'][
'force'] = parameter_types.boolean

View File

@ -300,3 +300,12 @@ user documentation.
From this version of the API the hypervisor's 'cpu_info' field will be From this version of the API the hypervisor's 'cpu_info' field will be
will returned as JSON object (not string) by sending GET request will returned as JSON object (not string) by sending GET request
to the /v2.1/os-hypervisors/{hypervisor_id}. to the /v2.1/os-hypervisors/{hypervisor_id}.
2.29
----
Updates the POST request body for the ``evacuate`` action to include the
optional ``force`` boolean field defaulted to False.
Also changes the evacuate action behaviour when providing a ``host`` string
field by calling the nova scheduler to verify the provided host unless the
``force`` attribute is set.

View File

@ -3445,7 +3445,7 @@ class API(base.Base):
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED,
vm_states.ERROR]) vm_states.ERROR])
def evacuate(self, context, instance, host, on_shared_storage, def evacuate(self, context, instance, host, on_shared_storage,
admin_password=None): admin_password=None, force=None):
"""Running evacuate to target host. """Running evacuate to target host.
Checking vm compute host state, if the host not in expected_state, Checking vm compute host state, if the host not in expected_state,
@ -3455,6 +3455,7 @@ class API(base.Base):
:param host: Target host. if not set, the scheduler will pick up one :param host: Target host. if not set, the scheduler will pick up one
:param on_shared_storage: True if instance files on shared storage :param on_shared_storage: True if instance files on shared storage
:param admin_password: password to set on rebuilt instance :param admin_password: password to set on rebuilt instance
:param force: Force the evacuation to the specific host target
""" """
LOG.debug('vm evacuation scheduled', instance=instance) LOG.debug('vm evacuation scheduled', instance=instance)
@ -3491,6 +3492,30 @@ class API(base.Base):
# Some old instances can still have no RequestSpec object attached # Some old instances can still have no RequestSpec object attached
# to them, we need to support the old way # to them, we need to support the old way
request_spec = None request_spec = None
# NOTE(sbauza): Force is a boolean by the new related API version
if force is False and host:
nodes = objects.ComputeNodeList.get_all_by_host(context, host)
if not nodes:
raise exception.ComputeHostNotFound(host=host)
# NOTE(sbauza): Unset the host to make sure we call the scheduler
host = None
# FIXME(sbauza): Since only Ironic driver uses more than one
# compute per service but doesn't support evacuations,
# let's provide the first one.
target = nodes[0]
if request_spec:
# TODO(sbauza): Hydrate a fake spec for old instances not yet
# having a request spec attached to them (particularly true for
# cells v1). For the moment, let's keep the same behaviour for
# all the instances but provide the destination only if a spec
# is found.
destination = objects.Destination(
host=target.host,
node=target.hypervisor_hostname
)
request_spec.requested_destination = destination
return self.compute_task_api.rebuild_instance(context, return self.compute_task_api.rebuild_instance(context,
instance=instance, instance=instance,
new_pass=admin_password, new_pass=admin_password,

View File

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

View File

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

View File

@ -16,6 +16,7 @@
import mock import mock
import nova.conf import nova.conf
from nova import objects
from nova.tests.functional.api_sample_tests import test_servers from nova.tests.functional.api_sample_tests import test_servers
CONF = nova.conf.CONF CONF = nova.conf.CONF
@ -144,3 +145,50 @@ class EvacuateJsonTestV214(EvacuateJsonTest):
orig_sys_metadata=mock.ANY, bdms=mock.ANY, recreate=mock.ANY, orig_sys_metadata=mock.ANY, bdms=mock.ANY, recreate=mock.ANY,
on_shared_storage=None, preserve_ephemeral=mock.ANY, on_shared_storage=None, preserve_ephemeral=mock.ANY,
host=None, request_spec=mock.ANY) host=None, request_spec=mock.ANY)
class EvacuateJsonTestV229(EvacuateJsonTestV214):
microversion = '2.29'
scenarios = [('v2_29', {'api_major_version': 'v2.1'})]
@mock.patch('nova.conductor.manager.ComputeTaskManager.rebuild_instance')
@mock.patch('nova.objects.ComputeNodeList.get_all_by_host')
def test_server_evacuate(self, compute_node_get_all_by_host, rebuild_mock):
# Note (wingwj): The host can't be the same one
req_subs = {
'host': 'testHost',
"adminPass": "MySecretPass",
"force": "false",
}
fake_computes = objects.ComputeNodeList(
objects=[objects.ComputeNode(host='testHost',
hypervisor_hostname='host')])
compute_node_get_all_by_host.return_value = fake_computes
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)
@mock.patch('nova.conductor.manager.ComputeTaskManager.rebuild_instance')
@mock.patch('nova.objects.ComputeNodeList.get_all_by_host')
def test_server_evacuate_with_force(self, compute_node_get_all_by_host,
rebuild_mock):
# Note (wingwj): The host can't be the same one
req_subs = {
'host': 'testHost',
"adminPass": "MySecretPass",
"force": "True",
}
self._test_evacuate(req_subs, 'server-evacuate-req',
server_resp=None, expected_resp_code=200)
self.assertEqual(0, compute_node_get_all_by_host.call_count)
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='testHost', request_spec=mock.ANY)

View File

@ -351,3 +351,39 @@ class EvacuateTestV214(EvacuateTestV21):
# underscores in hostnames. However, we should test that it # underscores in hostnames. However, we should test that it
# is supported because it sometimes occurs in real systems. # is supported because it sometimes occurs in real systems.
self._get_evacuate_response({'host': 'underscore_hostname'}) self._get_evacuate_response({'host': 'underscore_hostname'})
class EvacuateTestV229(EvacuateTestV214):
def setUp(self):
super(EvacuateTestV229, self).setUp()
self.admin_req = fakes.HTTPRequest.blank('', use_admin_context=True,
version='2.29')
self.req = fakes.HTTPRequest.blank('', version='2.29')
@mock.patch.object(compute_api.API, 'evacuate')
def test_evacuate_instance(self, mock_evacuate):
self._get_evacuate_response({})
admin_pass = mock_evacuate.call_args_list[0][0][4]
on_shared_storage = mock_evacuate.call_args_list[0][0][3]
force = mock_evacuate.call_args_list[0][0][5]
self.assertEqual(CONF.password_length, len(admin_pass))
self.assertIsNone(on_shared_storage)
self.assertEqual(False, force)
def test_evacuate_with_valid_instance(self):
admin_pass = 'MyNewPass'
res = self._get_evacuate_response({'host': 'my-host',
'adminPass': admin_pass,
'force': 'false'})
self.assertIsNone(res)
@mock.patch.object(compute_api.API, 'evacuate')
def test_evacuate_instance_with_forced_host(self, mock_evacuate):
self._get_evacuate_response({'host': 'my-host',
'force': 'true'})
force = mock_evacuate.call_args_list[0][0][5]
self.assertEqual(True, force)
def test_forced_evacuate_with_no_host_provided(self):
self._check_evacuate_failure(webob.exc.HTTPBadRequest,
{'force': 'true'})

View File

@ -10035,7 +10035,7 @@ class ComputeAPITestCase(BaseTestCase):
instance.refresh() instance.refresh()
self.assertEqual(instance['task_state'], task_states.MIGRATING) self.assertEqual(instance['task_state'], task_states.MIGRATING)
def test_evacuate(self): def _test_evacuate(self, force=None):
instance = self._create_fake_instance_obj(services=True) instance = self._create_fake_instance_obj(services=True)
self.assertIsNone(instance.task_state) self.assertIsNone(instance.task_state)
@ -10044,25 +10044,37 @@ class ComputeAPITestCase(BaseTestCase):
fake_spec = objects.RequestSpec() fake_spec = objects.RequestSpec()
def fake_rebuild_instance(*args, **kwargs): def fake_rebuild_instance(*args, **kwargs):
instance.host = kwargs['host'] # NOTE(sbauza): Host can be set to None, we need to fake a correct
# destination if this is the case.
instance.host = kwargs['host'] or 'fake_dest_host'
instance.save() instance.save()
@mock.patch.object(self.compute_api.compute_task_api, @mock.patch.object(self.compute_api.compute_task_api,
'rebuild_instance') 'rebuild_instance')
@mock.patch.object(objects.ComputeNodeList, 'get_all_by_host')
@mock.patch.object(objects.RequestSpec, @mock.patch.object(objects.RequestSpec,
'get_by_instance_uuid') 'get_by_instance_uuid')
@mock.patch.object(self.compute_api.servicegroup_api, 'service_is_up') @mock.patch.object(self.compute_api.servicegroup_api, 'service_is_up')
def do_test(service_is_up, get_by_instance_uuid, rebuild_instance): def do_test(service_is_up, get_by_instance_uuid, get_all_by_host,
rebuild_instance):
service_is_up.return_value = False service_is_up.return_value = False
get_by_instance_uuid.return_value = fake_spec get_by_instance_uuid.return_value = fake_spec
rebuild_instance.side_effect = fake_rebuild_instance rebuild_instance.side_effect = fake_rebuild_instance
get_all_by_host.return_value = objects.ComputeNodeList(
objects=[objects.ComputeNode(
host='fake_dest_host',
hypervisor_hostname='fake_dest_node')])
self.compute_api.evacuate(ctxt, self.compute_api.evacuate(ctxt,
instance, instance,
host='fake_dest_host', host='fake_dest_host',
on_shared_storage=True, on_shared_storage=True,
admin_password=None) admin_password=None,
force=force)
if force is False:
host = None
else:
host = 'fake_dest_host'
rebuild_instance.assert_called_once_with( rebuild_instance.assert_called_once_with(
ctxt, ctxt,
instance=instance, instance=instance,
@ -10075,7 +10087,7 @@ class ComputeAPITestCase(BaseTestCase):
recreate=True, recreate=True,
on_shared_storage=True, on_shared_storage=True,
request_spec=fake_spec, request_spec=fake_spec,
host='fake_dest_host') host=host)
do_test() do_test()
instance.refresh() instance.refresh()
@ -10088,6 +10100,32 @@ class ComputeAPITestCase(BaseTestCase):
self.assertEqual('accepted', migs[0].status) self.assertEqual('accepted', migs[0].status)
self.assertEqual('compute.instance.evacuate', self.assertEqual('compute.instance.evacuate',
fake_notifier.NOTIFICATIONS[0].event_type) fake_notifier.NOTIFICATIONS[0].event_type)
if force is False:
req_dest = fake_spec.requested_destination
self.assertIsNotNone(req_dest)
self.assertIsInstance(req_dest, objects.Destination)
self.assertEqual('fake_dest_host', req_dest.host)
self.assertEqual('fake_dest_node', req_dest.node)
def test_evacuate(self):
self._test_evacuate()
def test_evacuate_with_not_forced_host(self):
self._test_evacuate(force=False)
def test_evacuate_with_forced_host(self):
self._test_evacuate(force=True)
@mock.patch('nova.servicegroup.api.API.service_is_up',
return_value=False)
def test_fail_evacuate_with_non_existing_destination(self, _service_is_up):
instance = self._create_fake_instance_obj(services=True)
self.assertIsNone(instance.task_state)
self.assertRaises(exception.ComputeHostNotFound,
self.compute_api.evacuate, self.context.elevated(), instance,
host='fake_dest_host', on_shared_storage=True,
admin_password=None, force=False)
def test_fail_evacuate_from_non_existing_host(self): def test_fail_evacuate_from_non_existing_host(self):
inst = {} inst = {}

View File

@ -127,15 +127,16 @@ class CellsComputeAPITestCase(test_compute.ComputeAPITestCase):
def test_instance_metadata(self): def test_instance_metadata(self):
self.skipTest("Test is incompatible with cells.") self.skipTest("Test is incompatible with cells.")
def test_evacuate(self): def _test_evacuate(self, force=None):
@mock.patch.object(compute_api.API, 'evacuate') @mock.patch.object(compute_api.API, 'evacuate')
def _test(mock_evacuate): def _test(mock_evacuate):
instance = objects.Instance(uuid=uuids.evacuate_instance, instance = objects.Instance(uuid=uuids.evacuate_instance,
cell_name='fake_cell_name') cell_name='fake_cell_name')
dest_host = 'fake_cell_name@fakenode2' dest_host = 'fake_cell_name@fakenode2'
self.compute_api.evacuate(self.context, instance, host=dest_host) self.compute_api.evacuate(self.context, instance, host=dest_host,
force=force)
mock_evacuate.assert_called_once_with( mock_evacuate.assert_called_once_with(
self.context, instance, 'fakenode2') self.context, instance, 'fakenode2', force=force)
_test() _test()

View File

@ -0,0 +1,11 @@
---
features:
- On evacuate actions, the default behaviour when providing a host in
the request body changed. Now, instead of bypassing the scheduler when
asking for a destination, it will instead call it with the requested
destination to make sure the proposed host is accepted by all the filters
and the original request.
In case the administrator doesn't want to call the scheduler when providing
a destination, a new request body field called ``force`` (defaulted to
False) will modify that new behaviour by forcing the evacuate operation
to the destination without verifying the scheduler.