From 27b6c18c666389ee68935f28cf340b7673879d6f Mon Sep 17 00:00:00 2001 From: zhangbailin Date: Fri, 7 Jun 2019 15:11:29 +0800 Subject: [PATCH] Specify availability_zone to unshelve This adds support, in a new microversion, for specifying an availability zone to the unshelve server action when the server is shelved offloaded. Note that the functional test changes are due to those tests using the "latest" microversion where an empty dict is not allowed for unshelve with 2.77 so the value is changed from an empty dict to None. Implements: blueprint support-specifying-az-when-restore-shelved-server Closes-Bug: #1723880 Change-Id: I4b13483eef42bed91d69eabf1f30762d6866f957 --- api-ref/source/parameters.yaml | 9 ++ api-ref/source/servers-action-shelve.inc | 8 +- .../os-shelve/v2.77/os-shelve.json | 3 + .../os-shelve/v2.77/os-unshelve-null.json | 3 + .../os-shelve/v2.77/os-unshelve.json | 5 + .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- doc/source/user/aggregates.rst | 3 + nova/api/openstack/api_version_request.py | 4 +- .../compute/rest_api_version_history.rst | 5 + nova/api/openstack/compute/schemas/shelve.py | 37 +++++ nova/api/openstack/compute/shelve.py | 22 ++- nova/compute/api.py | 71 +++++++++- nova/exception.py | 13 ++ .../os-shelve/v2.77/os-shelve.json.tpl | 3 + .../os-shelve/v2.77/os-unshelve-null.json.tpl | 3 + .../os-shelve/v2.77/os-unshelve.json.tpl | 5 + .../api_sample_tests/test_shelve.py | 26 ++++ nova/tests/functional/test_servers.py | 12 +- .../unit/api/openstack/compute/test_shelve.py | 123 ++++++++++++++++- nova/tests/unit/compute/test_shelve.py | 126 +++++++++++++++++- ...z-to-unshelve-server-aa355fef1eab2c02.yaml | 14 ++ 22 files changed, 481 insertions(+), 18 deletions(-) create mode 100644 doc/api_samples/os-shelve/v2.77/os-shelve.json create mode 100644 doc/api_samples/os-shelve/v2.77/os-unshelve-null.json create mode 100644 doc/api_samples/os-shelve/v2.77/os-unshelve.json create mode 100644 nova/api/openstack/compute/schemas/shelve.py create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-shelve.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve-null.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve.json.tpl create mode 100644 releasenotes/notes/bp-specifying-az-to-unshelve-server-aa355fef1eab2c02.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 7337a3a381e7..82f9ef8801ea 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -1799,6 +1799,15 @@ availability_zone_state: in: body required: true type: object +availability_zone_unshelve: + description: | + The availability zone name. Specifying an availability zone is only + allowed when the server status is ``SHELVED_OFFLOADED`` otherwise a + 409 HTTPConflict response is returned. + in: body + required: false + type: string + min_version: 2.77 available: description: | Returns true if the availability zone is available. diff --git a/api-ref/source/servers-action-shelve.inc b/api-ref/source/servers-action-shelve.inc index b024031cdfc5..4bb63ceb7ec4 100644 --- a/api-ref/source/servers-action-shelve.inc +++ b/api-ref/source/servers-action-shelve.inc @@ -138,7 +138,7 @@ If the server status does not change to ``ACTIVE``, the unshelve operation faile Normal response codes: 202 -Error response codes: unauthorized(401), forbidden(403), itemNotFound(404), conflict(409) +Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404), conflict(409) Request ------- @@ -147,6 +147,7 @@ Request - server_id: server_id_path - unshelve: unshelve + - availability_zone: availability_zone_unshelve | @@ -155,6 +156,11 @@ Request .. literalinclude:: ../../doc/api_samples/os-shelve/os-unshelve.json :language: javascript +**Example Unshelve server (unshelve Action) (v2.77)** + +.. literalinclude:: ../../doc/api_samples/os-shelve/v2.77/os-unshelve.json + :language: javascript + Response -------- diff --git a/doc/api_samples/os-shelve/v2.77/os-shelve.json b/doc/api_samples/os-shelve/v2.77/os-shelve.json new file mode 100644 index 000000000000..e33b05865aca --- /dev/null +++ b/doc/api_samples/os-shelve/v2.77/os-shelve.json @@ -0,0 +1,3 @@ +{ + "shelve": null +} \ No newline at end of file diff --git a/doc/api_samples/os-shelve/v2.77/os-unshelve-null.json b/doc/api_samples/os-shelve/v2.77/os-unshelve-null.json new file mode 100644 index 000000000000..fd05c2a2fe67 --- /dev/null +++ b/doc/api_samples/os-shelve/v2.77/os-unshelve-null.json @@ -0,0 +1,3 @@ +{ + "unshelve": null +} \ No newline at end of file diff --git a/doc/api_samples/os-shelve/v2.77/os-unshelve.json b/doc/api_samples/os-shelve/v2.77/os-unshelve.json new file mode 100644 index 000000000000..8ca146b5933c --- /dev/null +++ b/doc/api_samples/os-shelve/v2.77/os-unshelve.json @@ -0,0 +1,5 @@ +{ + "unshelve": { + "availability_zone": "us-west" + } +} \ No newline at end of file diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index b5c1ad05e1cc..7e16157149eb 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.76", + "version": "2.77", "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 f7b96be8f219..81d4d94fe94c 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.76", + "version": "2.77", "min_version": "2.1", "updated": "2013-07-23T11:33:21Z" } diff --git a/doc/source/user/aggregates.rst b/doc/source/user/aggregates.rst index 74a1848dbc3f..6cc68fa69ca3 100644 --- a/doc/source/user/aggregates.rst +++ b/doc/source/user/aggregates.rst @@ -91,6 +91,9 @@ With respect to availability zones, a server is restricted to a zone if: parameter but the API service is configured for :oslo.config:option:`default_schedule_zone` then by default the server will be scheduled to that zone. +3. The shelved offloaded server was unshelved by specifying the + ``availability_zone`` with the ``POST /servers/{server_id}/action`` request + using microversion 2.77 or greater. If the server was not created in a specific zone then it is free to be moved to other zones, i.e. the :ref:`AvailabilityZoneFilter ` diff --git a/nova/api/openstack/api_version_request.py b/nova/api/openstack/api_version_request.py index a41c029f494e..77917a4fc2f6 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -201,6 +201,8 @@ REST_API_VERSION_HISTORY = """REST API Version History: can be viewed through ``GET /servers/{server_id}/os-instance-actions`` and ``GET /servers/{server_id}/os-instance-actions/{request_id}``. + * 2.77 - Add support for specifying ``availability_zone`` to unshelve of a + shelved offload server. """ # The minimum and maximum versions of the API supported @@ -209,7 +211,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.76" +_MAX_API_VERSION = "2.77" 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/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 6935eef4f519..4bdc30c1f76a 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -993,3 +993,8 @@ Adds ``power-update`` event name to ``os-server-external-events`` API. The changes to the power state of an instance caused by this event can be viewed through ``GET /servers/{server_id}/os-instance-actions`` and ``GET /servers/{server_id}/os-instance-actions/{request_id}``. + +2.77 +---- +API microversion 2.77 adds support for specifying availability zone when +unshelving a shelved offloaded server. diff --git a/nova/api/openstack/compute/schemas/shelve.py b/nova/api/openstack/compute/schemas/shelve.py new file mode 100644 index 000000000000..e8d2f1c24061 --- /dev/null +++ b/nova/api/openstack/compute/schemas/shelve.py @@ -0,0 +1,37 @@ +# Copyright 2019 INSPUR Corporation. All rights reserved. +# +# 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 +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.validation import parameter_types + +# NOTE(brinzhang): For older microversion there will be no change as +# schema is applied only for >2.77 with unshelve a server API. +# Anything working in old version keep working as it is. +unshelve_v277 = { + 'type': 'object', + 'properties': { + 'unshelve': { + 'type': ['object', 'null'], + 'properties': { + 'availability_zone': parameter_types.name + }, + # NOTE: The allowed request body is {'unshelve': null} or + # {'unshelve': {'availability_zone': }}, not allowed + # {'unshelve': {}} as the request body for unshelve. + 'required': ['availability_zone'], + 'additionalProperties': False, + }, + }, + 'required': ['unshelve'], + 'additionalProperties': False, +} diff --git a/nova/api/openstack/compute/shelve.py b/nova/api/openstack/compute/shelve.py index 9bc172da8cce..3293e07fa0a9 100644 --- a/nova/api/openstack/compute/shelve.py +++ b/nova/api/openstack/compute/shelve.py @@ -16,8 +16,11 @@ from webob import exc +from nova.api.openstack import api_version_request from nova.api.openstack import common +from nova.api.openstack.compute.schemas import shelve as shelve_schemas from nova.api.openstack import wsgi +from nova.api import validation from nova.compute import api as compute from nova.compute import vm_states from nova import exception @@ -72,12 +75,23 @@ class ShelveController(wsgi.Controller): @wsgi.response(202) @wsgi.expected_errors((400, 404, 409)) @wsgi.action('unshelve') + # In microversion 2.77 we support specifying 'availability_zone' to + # unshelve a server. But before 2.77 there is no request body + # schema validation (because of body=null). + @validation.schema(shelve_schemas.unshelve_v277, min_version='2.77') def _unshelve(self, req, id, body): """Restore an instance from shelved mode.""" context = req.environ["nova.context"] context.can(shelve_policies.POLICY_ROOT % 'unshelve') instance = common.get_instance(self.compute_api, context, id) + new_az = None + unshelve_dict = body['unshelve'] + if unshelve_dict and 'availability_zone' in unshelve_dict: + support_az = api_version_request.is_supported(req, '2.77') + if support_az: + new_az = unshelve_dict['availability_zone'] + # We could potentially move this check to conductor and avoid the # extra API call to neutron when we support move operations with ports # having resource requests. @@ -93,10 +107,14 @@ class ShelveController(wsgi.Controller): raise exc.HTTPBadRequest(explanation=msg) try: - self.compute_api.unshelve(context, instance) - except exception.InstanceIsLocked as e: + self.compute_api.unshelve(context, instance, new_az=new_az) + except (exception.InstanceIsLocked, + exception.UnshelveInstanceInvalidState, + exception.MismatchVolumeAZException) as e: raise exc.HTTPConflict(explanation=e.format_message()) except exception.InstanceInvalidState as state_error: common.raise_http_conflict_for_instance_invalid_state(state_error, 'unshelve', id) + except exception.InvalidRequest as e: + raise exc.HTTPBadRequest(explanation=e.format_message()) diff --git a/nova/compute/api.py b/nova/compute/api.py index af97fb7298a0..7bffd9878e44 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -3658,14 +3658,83 @@ class API(base.Base): self.compute_rpcapi.shelve_offload_instance(context, instance=instance, clean_shutdown=clean_shutdown) + def _validate_unshelve_az(self, context, instance, availability_zone): + """Verify the specified availability_zone during unshelve. + + Verifies that the server is shelved offloaded, the AZ exists and + if [cinder]/cross_az_attach=False, that any attached volumes are in + the same AZ. + + :param context: nova auth RequestContext for the unshelve action + :param instance: Instance object for the server being unshelved + :param availability_zone: The user-requested availability zone in + which to unshelve the server. + :raises: UnshelveInstanceInvalidState if the server is not shelved + offloaded + :raises: InvalidRequest if the requested AZ does not exist + :raises: MismatchVolumeAZException if [cinder]/cross_az_attach=False + and any attached volumes are not in the requested AZ + """ + if instance.vm_state != vm_states.SHELVED_OFFLOADED: + # NOTE(brinzhang): If the server status is 'SHELVED', it still + # belongs to a host, the availability_zone has not changed. + # Unshelving a shelved offloaded server will go through the + # scheduler to find a new host. + raise exception.UnshelveInstanceInvalidState( + state=instance.vm_state, instance_uuid=instance.uuid) + + available_zones = availability_zones.get_availability_zones( + context, self.host_api, get_only_available=True) + if availability_zone not in available_zones: + msg = _('The requested availability zone is not available') + raise exception.InvalidRequest(msg) + + # NOTE(brinzhang): When specifying a availability zone to unshelve + # a shelved offloaded server, and conf cross_az_attach=False, need + # to determine if attached volume AZ matches the user-specified AZ. + if not CONF.cinder.cross_az_attach: + bdms = objects.BlockDeviceMappingList.get_by_instance_uuid( + context, instance.uuid) + for bdm in bdms: + if bdm.is_volume and bdm.volume_id: + volume = self.volume_api.get(context, bdm.volume_id) + if availability_zone != volume['availability_zone']: + msg = _("The specified availability zone does not " + "match the volume %(vol_id)s attached to the " + "server. Specified availability zone is " + "%(az)s. Volume is in %(vol_zone)s.") % { + "vol_id": volume['id'], + "az": availability_zone, + "vol_zone": volume['availability_zone']} + raise exception.MismatchVolumeAZException(reason=msg) + @check_instance_lock @check_instance_state(vm_state=[vm_states.SHELVED, vm_states.SHELVED_OFFLOADED]) - def unshelve(self, context, instance): + def unshelve(self, context, instance, new_az=None): """Restore a shelved instance.""" request_spec = objects.RequestSpec.get_by_instance_uuid( context, instance.uuid) + if new_az: + self._validate_unshelve_az(context, instance, new_az) + LOG.debug("Replace the old AZ %(old_az)s in RequestSpec " + "with a new AZ %(new_az)s of the instance.", + {"old_az": request_spec.availability_zone, + "new_az": new_az}, instance=instance) + # Unshelving a shelved offloaded server will go through the + # scheduler to pick a new host, so we update the + # RequestSpec.availability_zone here. Note that if scheduling + # fails the RequestSpec will remain updated, which is not great, + # but if we want to change that we need to defer updating the + # RequestSpec until conductor which probably means RPC changes to + # pass the new_az variable to conductor. This is likely low + # priority since the RequestSpec.availability_zone on a shelved + # offloaded server does not mean much anyway and clearly the user + # is trying to put the server in the target AZ. + request_spec.availability_zone = new_az + request_spec.save() + instance.task_state = task_states.UNSHELVING instance.save(expected_task_state=[None]) diff --git a/nova/exception.py b/nova/exception.py index cd2079440c3d..4d0db83f6b1c 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1863,6 +1863,19 @@ class UnshelveException(NovaException): msg_fmt = _("Error during unshelve instance %(instance_id)s: %(reason)s") +class MismatchVolumeAZException(Invalid): + msg_fmt = _("The availability zone between the server and its attached " + "volumes do not match: %(reason)s.") + code = 409 + + +class UnshelveInstanceInvalidState(InstanceInvalidState): + msg_fmt = _('Specifying an availability zone when unshelving server ' + '%(instance_uuid)s with status "%(state)s" is not supported. ' + 'The server status must be SHELVED_OFFLOADED.') + code = 409 + + class ImageVCPULimitsRangeExceeded(Invalid): msg_fmt = _('Image vCPU topology limits (sockets=%(image_sockets)d, ' 'cores=%(image_cores)d, threads=%(image_threads)d) exceeds ' diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-shelve.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-shelve.json.tpl new file mode 100644 index 000000000000..5a19f85cffa9 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-shelve.json.tpl @@ -0,0 +1,3 @@ +{ + "%(action)s": null +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve-null.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve-null.json.tpl new file mode 100644 index 000000000000..5a19f85cffa9 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve-null.json.tpl @@ -0,0 +1,3 @@ +{ + "%(action)s": null +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve.json.tpl new file mode 100644 index 000000000000..9bcd25139a2c --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve.json.tpl @@ -0,0 +1,5 @@ +{ + "%(action)s": { + "availability_zone": "%(availability_zone)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/test_shelve.py b/nova/tests/functional/api_sample_tests/test_shelve.py index ab6b75aa7280..9c55817608af 100644 --- a/nova/tests/functional/api_sample_tests/test_shelve.py +++ b/nova/tests/functional/api_sample_tests/test_shelve.py @@ -47,3 +47,29 @@ class ShelveJsonTest(test_servers.ServersSampleBase): uuid = self._post_server() self._test_server_action(uuid, 'os-shelve', 'shelve') self._test_server_action(uuid, 'os-unshelve', 'unshelve') + + +class UnshelveJson277Test(test_servers.ServersSampleBase): + sample_dir = "os-shelve" + microversion = '2.77' + scenarios = [('v2_77', {'api_major_version': 'v2.1'})] + USE_NEUTRON = True + + def _test_server_action(self, uuid, template, action, subs=None): + subs = subs or {} + subs.update({'action': action}) + response = self._do_post('servers/%s/action' % uuid, + template, subs) + self.assertEqual(202, response.status_code) + self.assertEqual("", response.text) + + def test_unshelve_with_az(self): + uuid = self._post_server() + self._test_server_action(uuid, 'os-shelve', 'shelve') + self._test_server_action(uuid, 'os-unshelve', 'unshelve', + subs={"availability_zone": "us-west"}) + + def test_unshelve_no_az(self): + uuid = self._post_server() + self._test_server_action(uuid, 'os-shelve', 'shelve') + self._test_server_action(uuid, 'os-unshelve-null', 'unshelve') diff --git a/nova/tests/functional/test_servers.py b/nova/tests/functional/test_servers.py index 925eaf9f5c6d..fbb19c855e77 100644 --- a/nova/tests/functional/test_servers.py +++ b/nova/tests/functional/test_servers.py @@ -2577,7 +2577,7 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase): source_hostname, source_rp_uuid) req = { - 'unshelve': {} + 'unshelve': None } self.api.post_server_action(server['id'], req) self._wait_for_state_change(self.api, server, 'ACTIVE') @@ -2626,7 +2626,7 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase): self.admin_api.put_service(source_service_id, {'status': 'disabled'}) req = { - 'unshelve': {} + 'unshelve': None } self.api.post_server_action(server['id'], req) server = self._wait_for_state_change(self.api, server, 'ACTIVE') @@ -2662,7 +2662,7 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase): self.admin_api.put_service(source_service_id, {'status': 'disabled'}) req = { - 'unshelve': {} + 'unshelve': None } self.api.post_server_action(server['id'], req) server = self._wait_for_state_change(self.api, server, 'ACTIVE') @@ -3162,7 +3162,7 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase): binary='nova-compute')[0]['id'] self.admin_api.put_service(source_service_id, {'status': 'disabled'}) - req = {'unshelve': {}} + req = {'unshelve': None} self.api.post_server_action(created_server['id'], req) new_server = self._wait_for_state_change( self.api, created_server, 'ACTIVE') @@ -5906,7 +5906,7 @@ class UnsupportedPortResourceRequestBasedSchedulingTest( ex = self.assertRaises( client.OpenStackApiException, - self.api.post_server_action, server['id'], {'unshelve': {}}) + self.api.post_server_action, server['id'], {'unshelve': None}) self.assertEqual(400, ex.response.status_code) self.assertIn( @@ -5939,7 +5939,7 @@ class UnsupportedPortResourceRequestBasedSchedulingTest( # can exist with such a port. self._add_resource_request_to_a_bound_port(self.neutron.port_1['id']) - self.api.post_server_action(server['id'], {'unshelve': {}}) + self.api.post_server_action(server['id'], {'unshelve': None}) self._wait_for_state_change(self.admin_api, server, 'ACTIVE') diff --git a/nova/tests/unit/api/openstack/compute/test_shelve.py b/nova/tests/unit/api/openstack/compute/test_shelve.py index 257bc5366daa..88488b9b33d4 100644 --- a/nova/tests/unit/api/openstack/compute/test_shelve.py +++ b/nova/tests/unit/api/openstack/compute/test_shelve.py @@ -14,10 +14,14 @@ import mock from oslo_policy import policy as oslo_policy +from oslo_serialization import jsonutils from oslo_utils.fixture import uuidsentinel +import six import webob +from nova.api.openstack import api_version_request from nova.api.openstack.compute import shelve as shelve_v21 +from nova.compute import vm_states from nova import exception from nova import policy from nova import test @@ -49,7 +53,7 @@ class ShelvePolicyTestV21(test.NoDBTestCase): self.stub_out('nova.compute.api.API.unshelve', fakes.fake_actions_to_locked_server) self.assertRaises(webob.exc.HTTPConflict, self.controller._unshelve, - self.req, uuidsentinel.fake, {}) + self.req, uuidsentinel.fake, body={'unshelve': {}}) @mock.patch('nova.api.openstack.common.get_instance') def test_shelve_offload_locked_server(self, get_instance_mock): @@ -165,7 +169,7 @@ class ShelvePolicyEnforcementV21(test.NoDBTestCase): policy.set_rules(oslo_policy.Rules.from_dict(rules)) self.assertRaises(exception.Forbidden, self.controller._unshelve, - self.req, uuidsentinel.fake, {}) + self.req, uuidsentinel.fake, body={'unshelve': {}}) def test_unshelve_policy_failed(self): rule_name = "os_compute_api:os-shelve:unshelve" @@ -177,3 +181,118 @@ class ShelvePolicyEnforcementV21(test.NoDBTestCase): self.assertEqual( "Policy doesn't allow %s to be performed." % rule_name, exc.format_message()) + + +class UnshelveServerControllerTestV277(test.NoDBTestCase): + """Server controller test for microversion 2.77 + + Add availability_zone parameter to unshelve a shelved-offloaded server of + 2.77 microversion. + """ + wsgi_api_version = '2.77' + + def setUp(self): + super(UnshelveServerControllerTestV277, self).setUp() + self.controller = shelve_v21.ShelveController() + self.req = fakes.HTTPRequest.blank('/fake/servers/a/action', + use_admin_context=True, + version=self.wsgi_api_version) + # These tests don't care about ports with QoS bandwidth resources. + self.stub_out('nova.api.openstack.common.' + 'instance_has_port_with_resource_request', + lambda *a, **kw: False) + + def fake_get_instance(self): + ctxt = self.req.environ['nova.context'] + return fake_instance.fake_instance_obj( + ctxt, uuid=fakes.FAKE_UUID, vm_state=vm_states.SHELVED_OFFLOADED) + + @mock.patch('nova.api.openstack.common.get_instance') + def test_unshelve_with_az_pre_2_77_failed(self, mock_get_instance): + """Make sure specifying an AZ before microversion 2.77 is ignored.""" + instance = self.fake_get_instance() + mock_get_instance.return_value = instance + + body = { + 'unshelve': { + 'availability_zone': 'us-east' + }} + self.req.body = jsonutils.dump_as_bytes(body) + self.req.api_version_request = (api_version_request. + APIVersionRequest('2.76')) + with mock.patch.object(self.controller.compute_api, + 'unshelve') as mock_unshelve: + self.controller._unshelve(self.req, fakes.FAKE_UUID, body=body) + mock_unshelve.assert_called_once_with( + self.req.environ['nova.context'], instance, new_az=None) + + @mock.patch('nova.compute.api.API.unshelve') + @mock.patch('nova.api.openstack.common.get_instance') + def test_unshelve_with_none_pre_2_77_success( + self, mock_get_instance, mock_unshelve): + """Make sure we can unshelve server with None + before microversion 2.77. + """ + instance = self.fake_get_instance() + mock_get_instance.return_value = instance + + body = {'unshelve': None} + self.req.body = jsonutils.dump_as_bytes(body) + self.req.api_version_request = (api_version_request. + APIVersionRequest('2.76')) + self.controller._unshelve(self.req, fakes.FAKE_UUID, body=body) + mock_unshelve.assert_called_once_with( + self.req.environ['nova.context'], instance, new_az=None) + + @mock.patch('nova.compute.api.API.unshelve') + @mock.patch('nova.api.openstack.common.get_instance') + def test_unshelve_with_empty_dict_with_v2_77_failed( + self, mock_get_instance, mock_unshelve): + """Make sure we cannot unshelve server with empty dict.""" + instance = self.fake_get_instance() + mock_get_instance.return_value = instance + + body = {'unshelve': {}} + self.req.body = jsonutils.dump_as_bytes(body) + exc = self.assertRaises(exception.ValidationError, + self.controller._unshelve, + self.req, fakes.FAKE_UUID, + body=body) + self.assertIn("\'availability_zone\' is a required property", + six.text_type(exc)) + + def test_invalid_az_name_with_int(self): + body = { + 'unshelve': { + 'availability_zone': 1234 + }} + self.req.body = jsonutils.dump_as_bytes(body) + self.assertRaises(exception.ValidationError, + self.controller._unshelve, + self.req, fakes.FAKE_UUID, + body=body) + + def test_no_az_value(self): + body = { + 'unshelve': { + 'availability_zone': None + }} + self.req.body = jsonutils.dump_as_bytes(body) + self.assertRaises(exception.ValidationError, + self.controller._unshelve, + self.req, fakes.FAKE_UUID, + body=body) + + def test_unshelve_with_additional_param(self): + body = { + 'unshelve': { + 'availability_zone': 'us-east', + 'additional_param': 1 + }} + self.req.body = jsonutils.dump_as_bytes(body) + exc = self.assertRaises( + exception.ValidationError, + self.controller._unshelve, self.req, + fakes.FAKE_UUID, body=body) + self.assertIn("Additional properties are not allowed", + six.text_type(exc)) diff --git a/nova/tests/unit/compute/test_shelve.py b/nova/tests/unit/compute/test_shelve.py index e53edc32cbff..240d651ec0d6 100644 --- a/nova/tests/unit/compute/test_shelve.py +++ b/nova/tests/unit/compute/test_shelve.py @@ -15,6 +15,7 @@ from oslo_utils import fixture as utils_fixture from oslo_utils.fixture import uuidsentinel as uuids from oslo_utils import timeutils +from nova.compute import api as compute_api from nova.compute import claims from nova.compute import instance_actions from nova.compute import power_state @@ -809,8 +810,7 @@ class ShelveComputeAPITestCase(test_compute.BaseTestCase): for state in invalid_vm_states: self._test_shelve_offload_invalid_state(state) - @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') - def test_unshelve(self, get_by_instance_uuid): + def _get_specify_state_instance(self, vm_state): # Ensure instance can be unshelved. instance = self._create_fake_instance_obj() @@ -819,9 +819,16 @@ class ShelveComputeAPITestCase(test_compute.BaseTestCase): self.compute_api.shelve(self.context, instance) instance.task_state = None - instance.vm_state = vm_states.SHELVED + instance.vm_state = vm_state instance.save() + return instance + + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_unshelve(self, get_by_instance_uuid): + # Ensure instance can be unshelved. + instance = self._get_specify_state_instance(vm_states.SHELVED) + fake_spec = objects.RequestSpec() get_by_instance_uuid.return_value = fake_spec with mock.patch.object(self.compute_api.compute_task_api, @@ -834,3 +841,116 @@ class ShelveComputeAPITestCase(test_compute.BaseTestCase): self.assertEqual(instance.task_state, task_states.UNSHELVING) db.instance_destroy(self.context, instance['uuid']) + + @mock.patch('nova.availability_zones.get_availability_zones', + return_value=['az1', 'az2']) + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_specified_az_ushelve_invalid_request(self, + get_by_instance_uuid, + mock_save, + mock_availability_zones): + # Ensure instance can be unshelved. + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + new_az = "fake-new-az" + fake_spec = objects.RequestSpec() + fake_spec.availability_zone = "fake-old-az" + get_by_instance_uuid.return_value = fake_spec + + exc = self.assertRaises(exception.InvalidRequest, + self.compute_api.unshelve, + self.context, instance, new_az=new_az) + self.assertEqual("The requested availability zone is not available", + exc.format_message()) + + @mock.patch('nova.availability_zones.get_availability_zones', + return_value=['az1', 'az2']) + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_specified_az_unshelve_invalid_state(self, get_by_instance_uuid, + mock_save, + mock_availability_zones): + # Ensure instance can be unshelved. + instance = self._get_specify_state_instance(vm_states.SHELVED) + + new_az = "az1" + fake_spec = objects.RequestSpec() + fake_spec.availability_zone = "fake-old-az" + get_by_instance_uuid.return_value = fake_spec + + self.assertRaises(exception.UnshelveInstanceInvalidState, + self.compute_api.unshelve, + self.context, instance, new_az=new_az) + + @mock.patch('nova.objects.BlockDeviceMappingList.get_by_instance_uuid', + new_callable=mock.NonCallableMock) + @mock.patch('nova.availability_zones.get_availability_zones') + def test_validate_unshelve_az_cross_az_attach_true( + self, mock_get_azs, mock_get_bdms): + """Tests a case where the new AZ to unshelve does not match the volume + attached to the server but cross_az_attach=True so it's not an error. + """ + # Ensure instance can be unshelved. + instance = self._create_fake_instance_obj( + params=dict(vm_state=vm_states.SHELVED_OFFLOADED)) + + new_az = "west_az" + mock_get_azs.return_value = ["west_az", "east_az"] + self.flags(cross_az_attach=True, group='cinder') + self.compute_api._validate_unshelve_az(self.context, instance, new_az) + mock_get_azs.assert_called_once_with( + self.context, self.compute_api.host_api, get_only_available=True) + + @mock.patch('nova.volume.cinder.API.get') + @mock.patch('nova.objects.BlockDeviceMappingList.get_by_instance_uuid') + @mock.patch('nova.availability_zones.get_availability_zones') + def test_validate_unshelve_az_cross_az_attach_false( + self, mock_get_azs, mock_get_bdms, mock_get): + """Tests a case where the new AZ to unshelve does not match the volume + attached to the server and cross_az_attach=False so it's an error. + """ + # Ensure instance can be unshelved. + instance = self._create_fake_instance_obj( + params=dict(vm_state=vm_states.SHELVED_OFFLOADED)) + + new_az = "west_az" + mock_get_azs.return_value = ["west_az", "east_az"] + + bdms = [objects.BlockDeviceMapping(destination_type='volume', + volume_id=uuids.volume_id)] + mock_get_bdms.return_value = bdms + volume = {'id': uuids.volume_id, 'availability_zone': 'east_az'} + mock_get.return_value = volume + + self.flags(cross_az_attach=False, group='cinder') + self.assertRaises(exception.MismatchVolumeAZException, + self.compute_api._validate_unshelve_az, + self.context, instance, new_az) + mock_get_azs.assert_called_once_with( + self.context, self.compute_api.host_api, get_only_available=True) + mock_get_bdms.assert_called_once_with(self.context, instance.uuid) + mock_get.assert_called_once_with(self.context, uuids.volume_id) + + @mock.patch.object(compute_api.API, '_validate_unshelve_az') + @mock.patch.object(objects.RequestSpec, 'save') + @mock.patch.object(objects.RequestSpec, 'get_by_instance_uuid') + def test_specified_az_unshelve(self, get_by_instance_uuid, + mock_save, mock_validate_unshelve_az): + # Ensure instance can be unshelved. + instance = self._get_specify_state_instance( + vm_states.SHELVED_OFFLOADED) + + new_az = "west_az" + fake_spec = objects.RequestSpec() + fake_spec.availability_zone = "fake-old-az" + get_by_instance_uuid.return_value = fake_spec + + self.compute_api.unshelve(self.context, instance, new_az=new_az) + + mock_save.assert_called_once_with() + self.assertEqual(new_az, fake_spec.availability_zone) + + mock_validate_unshelve_az.assert_called_once_with( + self.context, instance, new_az) diff --git a/releasenotes/notes/bp-specifying-az-to-unshelve-server-aa355fef1eab2c02.yaml b/releasenotes/notes/bp-specifying-az-to-unshelve-server-aa355fef1eab2c02.yaml new file mode 100644 index 000000000000..41a792ad8dc2 --- /dev/null +++ b/releasenotes/notes/bp-specifying-az-to-unshelve-server-aa355fef1eab2c02.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + Microversion 2.77 adds the optional parameter ``availability_zone`` to + the ``unshelve`` server action API. + + * Specifying an availability zone is only allowed when the server status + is ``SHELVED_OFFLOADED`` otherwise a 409 HTTPConflict response is + returned. + + * If the ``[cinder]/cross_az_attach`` configuration option is False then + the specified availability zone has to be the same as the availability + zone of any volumes attached to the shelved offloaded server, otherwise + a 409 HTTPConflict error response is returned.