From 09239fc2eadcf266b42c640e386c7cebad715eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: Mon, 13 Jun 2022 15:22:43 +0200 Subject: [PATCH] Allow unshelve to a specific host (REST API part) This adds support to the REST API, in a new microversion, for specifying a destination host to unshelve server action when the server is shelved offloaded. This patch also supports the ability to unpin the availability_zone of an instance that is bound to it. Note that the functional test changes are due to those tests using the "latest" microversion 2.91. Implements: blueprint unshelve-to-host Change-Id: I9e95428c208582741e6cd99bd3260d6742fcc6b7 --- api-ref/source/parameters.yaml | 16 +- api-ref/source/servers-action-shelve.inc | 94 +++- .../{os-unshelve.json => os-unshelve-az.json} | 0 .../os-shelve/v2.91/os-unshelve-az-host.json | 6 + .../v2.91/os-unshelve-host-and-unpin-az.json | 6 + .../os-shelve/v2.91/os-unshelve-host.json | 5 + .../os-shelve/v2.91/os-unshelve-unpin-az.json | 5 + .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- nova/api/openstack/api_version_request.py | 4 +- .../compute/rest_api_version_history.rst | 9 + nova/api/openstack/compute/schemas/shelve.py | 54 ++- nova/api/openstack/compute/shelve.py | 55 ++- nova/compute/api.py | 9 + nova/policies/shelve.py | 12 + .../os-shelve/v2.77/os-unshelve-az.json.tpl | 5 + .../os-shelve/v2.77/os-unshelve.json.tpl | 4 +- .../os-shelve.json.tpl} | 0 .../v2.91/os-unshelve-az-host.json.tpl | 6 + .../os-shelve/v2.91/os-unshelve-az.json.tpl | 5 + .../os-unshelve-host-and-unpin-az.json.tpl | 6 + .../os-shelve/v2.91/os-unshelve-host.json.tpl | 5 + .../v2.91/os-unshelve-unpin-az.json.tpl | 5 + .../os-shelve/v2.91/os-unshelve.json.tpl | 2 +- .../api_sample_tests/test_shelve.py | 297 +++++++++++- .../functional/test_availability_zones.py | 448 +++++++++++++++++- nova/tests/functional/test_servers.py | 51 ++ .../unit/api/openstack/compute/test_shelve.py | 251 +++++++++- nova/tests/unit/test_policy.py | 1 + .../bp-unshelve_to_host-c9047d518eb67747.yaml | 10 + 30 files changed, 1318 insertions(+), 57 deletions(-) rename doc/api_samples/os-shelve/v2.77/{os-unshelve.json => os-unshelve-az.json} (100%) create mode 100644 doc/api_samples/os-shelve/v2.91/os-unshelve-az-host.json create mode 100644 doc/api_samples/os-shelve/v2.91/os-unshelve-host-and-unpin-az.json create mode 100644 doc/api_samples/os-shelve/v2.91/os-unshelve-host.json create mode 100644 doc/api_samples/os-shelve/v2.91/os-unshelve-unpin-az.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve-az.json.tpl rename nova/tests/functional/api_sample_tests/api_samples/os-shelve/{v2.77/os-unshelve-null.json.tpl => v2.91/os-shelve.json.tpl} (100%) create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-az-host.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-az.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-host-and-unpin-az.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-host.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-unpin-az.json.tpl rename doc/api_samples/os-shelve/v2.77/os-unshelve-null.json => nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve.json.tpl (92%) create mode 100644 releasenotes/notes/bp-unshelve_to_host-c9047d518eb67747.yaml diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index 5ea19faab931..9853ad23f120 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -1858,8 +1858,11 @@ availability_zone_state: 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. + allowed when the server status is ``SHELVED_OFFLOADED`` otherwise + HTTP 409 conflict response is returned. + + Since microversion 2.91 ``"availability_zone":null`` allows unpinning the + instance from any availability_zone it is pinned to. in: body required: false type: string @@ -3690,6 +3693,15 @@ host_status_update_rebuild: required: false type: string min_version: 2.75 +host_unshelve: + description: | + The destination host name. Specifying a destination host is by default only + allowed to project_admin, if it not the case HTTP 403 forbidden response + is returned. + in: body + required: false + type: string + min_version: 2.91 host_zone: description: | The available zone of the host. diff --git a/api-ref/source/servers-action-shelve.inc b/api-ref/source/servers-action-shelve.inc index 08ca65daddb1..af8bc1969bca 100644 --- a/api-ref/source/servers-action-shelve.inc +++ b/api-ref/source/servers-action-shelve.inc @@ -121,9 +121,65 @@ Policy defaults enable only users with the administrative role or the owner of t **Preconditions** -The server status must be ``SHELVED`` or ``SHELVED_OFFLOADED``. +Unshelving a server without parameters requires its status to be ``SHELVED`` or ``SHELVED_OFFLOADED``. + +Unshelving a server with availability_zone and/or host parameters requires its status to be only ``SHELVED_OFFLOADED`` otherwise HTTP 409 conflict response is returned. + +If a server is locked, you must have administrator privileges to unshelve the server. + +As of ``microversion 2.91``, you can unshelve to a specific compute node if you have PROJECT_ADMIN privileges. +This microversion also gives the ability to pin a server to an availability_zone and to unpin a server +from any availability_zone. + +When a server is pinned to an availability_zone, the server move operations will keep the server in that +availability_zone. However, when the server is not pinned to any availability_zone, the move operations can +move the server to nodes in different availability_zones. + +The behavior according to unshelve parameters will follow the below table. + ++----------+---------------------------+----------+--------------------------------+ +| Boot | AZ (1) | Host (1) | Result | ++==========+===========================+==========+================================+ +| No AZ | No AZ or AZ=null | No | Free scheduling (2) | ++----------+---------------------------+----------+--------------------------------+ +| No AZ | No AZ or AZ=null | Host1 | Schedule to Host1. | +| | | | Server remains unpinned. | ++----------+---------------------------+----------+--------------------------------+ +| No AZ | AZ="AZ1" | No | Schedule to any host in "AZ1". | +| | | | Server is pined to "AZ1". | ++----------+---------------------------+----------+--------------------------------+ +| No AZ | AZ="AZ1" | Host1 | Verify Host1 is in "AZ1", | +| | | | then schedule to Host1, | +| | | | otherwise reject the request. | +| | | | Server is pined to "AZ1". | ++----------+---------------------------+----------+--------------------------------+ +| AZ1 | No AZ | No | Schedule to any host in "AZ1". | +| | | | Server remains pined to "AZ1". | ++----------+---------------------------+----------+--------------------------------+ +| AZ1 | AZ=null | No | Free scheduling (2). | +| | | | Server is unpinned. | ++----------+---------------------------+----------+--------------------------------+ +| AZ1 | No AZ | Host1 | Verify Host1 is in "AZ1", | +| | | | then schedule to Host1, | +| | | | otherwise reject the request. | +| | | | Server remains pined to "AZ1". | ++----------+---------------------------+----------+--------------------------------+ +| AZ1 | AZ=null | Host1 | Schedule to Host1. | +| | | | Server is unpinned. | ++----------+---------------------------+----------+--------------------------------+ +| AZ1 | AZ="AZ2" | No | Schedule to any host in "AZ2". | +| | | | Server is pined to "AZ2". | ++----------+---------------------------+----------+--------------------------------+ +| AZ1 | AZ="AZ2" | Host1 | Verify Host1 is in "AZ2" then | +| | | | schedule to Host1, | +| | | | otherwise reject the request. | +| | | | Server is pined to "AZ2". | ++----------+---------------------------+----------+--------------------------------+ + +(1) Unshelve body parameters +(2) Schedule to any host available. + -If the server is locked, you must have administrator privileges to unshelve the server. **Asynchronous Postconditions** @@ -147,11 +203,30 @@ Request {"unshelve": null} or {"unshelve": {"availability_zone": }}. A request body of {"unshelve": {}} is not allowed. +.. note:: Since microversion 2.91, allowed request body schema are + + - {"unshelve": null} (Keep compatibility with previous microversions) + + or + + - {"unshelve": {"availability_zone": }} (Unshelve and pin server to availability_zone) + - {"unshelve": {"availability_zone": null}} (Unshelve and unpin server from any availability zone) + - {"unshelve": {"host": }} + - {"unshelve": {"availability_zone": , "host": }} + - {"unshelve": {"availability_zone": null, "host": }} + + Everything else is not allowed, examples: + + - {"unshelve": {}} + - {"unshelve": {"host": , "host": }} + - {"unshelve": {"foo": }} + .. rest_parameters:: parameters.yaml - server_id: server_id_path - unshelve: unshelve - availability_zone: availability_zone_unshelve + - host: host_unshelve | @@ -162,9 +237,22 @@ Request **Example Unshelve server (unshelve Action) (v2.77)** -.. literalinclude:: ../../doc/api_samples/os-shelve/v2.77/os-unshelve.json +.. literalinclude:: ../../doc/api_samples/os-shelve/v2.77/os-unshelve-az.json :language: javascript +**Examples Unshelve server (unshelve Action) (v2.91)** + +.. literalinclude:: ../../doc/api_samples/os-shelve/v2.91/os-unshelve-host.json + :language: javascript + +.. literalinclude:: ../../doc/api_samples/os-shelve/v2.91/os-unshelve-az-host.json + :language: javascript + +.. literalinclude:: ../../doc/api_samples/os-shelve/v2.91/os-unshelve-host-and-unpin-az.json + :language: javascript + +.. literalinclude:: ../../doc/api_samples/os-shelve/v2.91/os-unshelve-unpin-az.json + :language: javascript Response -------- diff --git a/doc/api_samples/os-shelve/v2.77/os-unshelve.json b/doc/api_samples/os-shelve/v2.77/os-unshelve-az.json similarity index 100% rename from doc/api_samples/os-shelve/v2.77/os-unshelve.json rename to doc/api_samples/os-shelve/v2.77/os-unshelve-az.json diff --git a/doc/api_samples/os-shelve/v2.91/os-unshelve-az-host.json b/doc/api_samples/os-shelve/v2.91/os-unshelve-az-host.json new file mode 100644 index 000000000000..6d5e7b1a2e3c --- /dev/null +++ b/doc/api_samples/os-shelve/v2.91/os-unshelve-az-host.json @@ -0,0 +1,6 @@ +{ + "unshelve": { + "availability_zone": "nova", + "host": "host01" + } +} diff --git a/doc/api_samples/os-shelve/v2.91/os-unshelve-host-and-unpin-az.json b/doc/api_samples/os-shelve/v2.91/os-unshelve-host-and-unpin-az.json new file mode 100644 index 000000000000..e04cc4e7f4da --- /dev/null +++ b/doc/api_samples/os-shelve/v2.91/os-unshelve-host-and-unpin-az.json @@ -0,0 +1,6 @@ +{ + "unshelve": { + "availability_zone": null, + "host": "host01" + } +} diff --git a/doc/api_samples/os-shelve/v2.91/os-unshelve-host.json b/doc/api_samples/os-shelve/v2.91/os-unshelve-host.json new file mode 100644 index 000000000000..bd68363d6e5c --- /dev/null +++ b/doc/api_samples/os-shelve/v2.91/os-unshelve-host.json @@ -0,0 +1,5 @@ +{ + "unshelve": { + "host": "host01" + } +} diff --git a/doc/api_samples/os-shelve/v2.91/os-unshelve-unpin-az.json b/doc/api_samples/os-shelve/v2.91/os-unshelve-unpin-az.json new file mode 100644 index 000000000000..598710aed95f --- /dev/null +++ b/doc/api_samples/os-shelve/v2.91/os-unshelve-unpin-az.json @@ -0,0 +1,5 @@ +{ + "unshelve": { + "availability_zone": null + } +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index f976225f9c4d..1c17bd42a4f3 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.90", + "version": "2.91", "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 327dbd82d66e..bd609a25e433 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.90", + "version": "2.91", "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 6c205fbae996..4345e2c914f6 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -247,6 +247,8 @@ REST_API_VERSION_HISTORY = """REST API Version History: updating or rebuilding an instance. The ``OS-EXT-SRV-ATTR:hostname`` attribute is now returned in various server responses regardless of policy configuration. + * 2.91 - Add support to unshelve instance to a specific host and + to pin/unpin AZ. """ # The minimum and maximum versions of the API supported @@ -255,7 +257,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.90' +_MAX_API_VERSION = '2.91' 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 703e18bcbec9..b1dfadf3fb7a 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -1202,3 +1202,12 @@ hostname based on the display name. In addition, the ``OS-EXT-SRV-ATTR:hostname`` field for all server responses is now visible to all users. Previously this was an admin-only field. + +.. _microversion 2.91: + +2.91 +---- + +Add support to unshelve instance to a specific host. + +Add support to pin a server to an availability zone or unpin a server from any availability zone. diff --git a/nova/api/openstack/compute/schemas/shelve.py b/nova/api/openstack/compute/schemas/shelve.py index e8d2f1c24061..4653338126f6 100644 --- a/nova/api/openstack/compute/schemas/shelve.py +++ b/nova/api/openstack/compute/schemas/shelve.py @@ -15,7 +15,7 @@ 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. +# schema is applied only for version < 2.91 with unshelve a server API. # Anything working in old version keep working as it is. unshelve_v277 = { 'type': 'object', @@ -35,3 +35,55 @@ unshelve_v277 = { 'required': ['unshelve'], 'additionalProperties': False, } + +# NOTE(rribaud): +# schema is applied only for version >= 2.91 with unshelve a server API. +# Add host parameter to specify to unshelve to this specific host. +# +# Schema has been redefined for better clarity instead of extend 2.77. +# +# API can be called with the following body: +# +# - {"unshelve": null} (Keep compatibility with previous microversions) +# +# or +# +# - {"unshelve": {"availability_zone": }} +# - {"unshelve": {"availability_zone": null}} (Unpin availability zone) +# - {"unshelve": {"host": }} +# - {"unshelve": {"availability_zone": , "host": }} +# - {"unshelve": {"availability_zone": null, "host": }} +# +# +# Everything else is not allowed, examples: +# +# - {"unshelve": {}} +# - {"unshelve": {"host": , "host": }} +# - {"unshelve": {"foo": }} + +unshelve_v291 = { + "type": "object", + "properties": { + "unshelve": { + "oneOf": [ + { + "type": ["object"], + "properties": { + "availability_zone": { + "oneOf": [ + {"type": ["null"]}, + {"type": "string"}] + }, + "host": { + "type": "string" + } + }, + "additionalProperties": False + }, + {"type": ["null"]} + ] + } + }, + "required": ["unshelve"], + "additionalProperties": False +} diff --git a/nova/api/openstack/compute/shelve.py b/nova/api/openstack/compute/shelve.py index deef3265f521..abcb42ee8e98 100644 --- a/nova/api/openstack/compute/shelve.py +++ b/nova/api/openstack/compute/shelve.py @@ -68,7 +68,6 @@ class ShelveController(wsgi.Controller): context.can(shelve_policies.POLICY_ROOT % 'shelve_offload', target={'user_id': instance.user_id, 'project_id': instance.project_id}) - try: self.compute_api.shelve_offload(context, instance) except exception.InstanceIsLocked as e: @@ -87,33 +86,59 @@ class ShelveController(wsgi.Controller): # 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') + @validation.schema( + shelve_schemas.unshelve_v277, + min_version='2.77', + max_version='2.90' + ) + # In microversion 2.91 we support specifying 'host' to + # unshelve an instance to a specific hostself. + # 'availability_zone' = None is supported as well to unpin the + # availability zone of an instance bonded to this availability_zone + @validation.schema(shelve_schemas.unshelve_v291, min_version='2.91') def _unshelve(self, req, id, body): """Restore an instance from shelved mode.""" context = req.environ["nova.context"] instance = common.get_instance(self.compute_api, context, id) - context.can(shelve_policies.POLICY_ROOT % 'unshelve', - target={'project_id': instance.project_id}) + context.can( + shelve_policies.POLICY_ROOT % 'unshelve', + target={'project_id': instance.project_id} + ) unshelve_args = {} - unshelve_dict = body['unshelve'] - support_az = api_version_request.is_supported(req, '2.77') - if support_az and unshelve_dict: - unshelve_args['new_az'] = unshelve_dict['availability_zone'] + unshelve_dict = body.get('unshelve') + support_az = api_version_request.is_supported( + req, '2.77') + support_host = api_version_request.is_supported( + req, '2.91') + if unshelve_dict: + if support_az and 'availability_zone' in unshelve_dict: + unshelve_args['new_az'] = ( + unshelve_dict['availability_zone'] + ) + if support_host: + unshelve_args['host'] = unshelve_dict.get('host') try: - self.compute_api.unshelve(context, instance, **unshelve_args) - except (exception.InstanceIsLocked, - exception.UnshelveInstanceInvalidState, - exception.MismatchVolumeAZException) as e: + self.compute_api.unshelve( + context, + instance, + **unshelve_args, + ) + except ( + exception.InstanceIsLocked, + exception.UnshelveInstanceInvalidState, + exception.UnshelveHostNotInAZ, + 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) + common.raise_http_conflict_for_instance_invalid_state( + state_error, 'unshelve', id) except ( exception.InvalidRequest, exception.ExtendedResourceRequestOldCompute, + exception.ComputeHostNotFound, ) as e: raise exc.HTTPBadRequest(explanation=e.format_message()) diff --git a/nova/compute/api.py b/nova/compute/api.py index 3b4bf4d26123..8e2063fd4cf5 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -75,6 +75,7 @@ from nova.objects import quotas as quotas_obj from nova.objects import service as service_obj from nova.pci import request as pci_request from nova.policies import servers as servers_policies +from nova.policies import shelve as shelve_policies import nova.policy from nova import profiler from nova import rpc @@ -4504,6 +4505,14 @@ class API: # host is requested, so we have to see if it exists and does not # contradict with the AZ of the instance if host: + # Make sure only admin can unshelve to a specific host. + context.can( + shelve_policies.POLICY_ROOT % 'unshelve_to_host', + target={ + 'user_id': instance.user_id, + 'project_id': instance.project_id + } + ) # Ensure that the requested host exists otherwise raise # a ComputeHostNotFound exception objects.ComputeNode.get_first_node_by_host_for_old_compat( diff --git a/nova/policies/shelve.py b/nova/policies/shelve.py index 6137882588ec..eb06ffaa2fcc 100644 --- a/nova/policies/shelve.py +++ b/nova/policies/shelve.py @@ -44,6 +44,18 @@ shelve_policies = [ } ], scope_types=['project']), + policy.DocumentedRuleDefault( + name=POLICY_ROOT % 'unshelve_to_host', + check_str=base.PROJECT_ADMIN, + description="Unshelve (restore) shelve offloaded server to a " + "specific host", + operations=[ + { + 'method': 'POST', + 'path': '/servers/{server_id}/action (unshelve)' + } + ], + scope_types=['project']), policy.DocumentedRuleDefault( name=POLICY_ROOT % 'shelve_offload', check_str=base.PROJECT_ADMIN, diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve-az.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve-az.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-az.json.tpl @@ -0,0 +1,5 @@ +{ + "%(action)s": { + "availability_zone": "%(availability_zone)s" + } +} 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 index 9bcd25139a2c..d78efa84e131 100644 --- 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 @@ -1,5 +1,3 @@ { - "%(action)s": { - "availability_zone": "%(availability_zone)s" - } + "unshelve": 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.91/os-shelve.json.tpl similarity index 100% rename from nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.77/os-unshelve-null.json.tpl rename to nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-shelve.json.tpl diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-az-host.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-az-host.json.tpl new file mode 100644 index 000000000000..eecc4271cbdc --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-az-host.json.tpl @@ -0,0 +1,6 @@ +{ + "%(action)s": { + "availability_zone": "%(availability_zone)s", + "host": "%(host)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-az.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-az.json.tpl new file mode 100644 index 000000000000..9bcd25139a2c --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-az.json.tpl @@ -0,0 +1,5 @@ +{ + "%(action)s": { + "availability_zone": "%(availability_zone)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-host-and-unpin-az.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-host-and-unpin-az.json.tpl new file mode 100644 index 000000000000..f9d2a2b17a13 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-host-and-unpin-az.json.tpl @@ -0,0 +1,6 @@ +{ + "%(action)s": { + "availability_zone": null, + "host": "%(host)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-host.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-host.json.tpl new file mode 100644 index 000000000000..3363b524ee5d --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-host.json.tpl @@ -0,0 +1,5 @@ +{ + "%(action)s": { + "host": "%(host)s" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-unpin-az.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-unpin-az.json.tpl new file mode 100644 index 000000000000..3815586c5ca5 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve-unpin-az.json.tpl @@ -0,0 +1,5 @@ +{ + "%(action)s": { + "availability_zone": null + } +} diff --git a/doc/api_samples/os-shelve/v2.77/os-unshelve-null.json b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve.json.tpl similarity index 92% rename from doc/api_samples/os-shelve/v2.77/os-unshelve-null.json rename to nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve.json.tpl index fd05c2a2fe67..d78efa84e131 100644 --- a/doc/api_samples/os-shelve/v2.77/os-unshelve-null.json +++ b/nova/tests/functional/api_sample_tests/api_samples/os-shelve/v2.91/os-unshelve.json.tpl @@ -1,3 +1,3 @@ { "unshelve": null -} \ No newline at end of file +} diff --git a/nova/tests/functional/api_sample_tests/test_shelve.py b/nova/tests/functional/api_sample_tests/test_shelve.py index 37d24b6cea28..0dfef71055bc 100644 --- a/nova/tests/functional/api_sample_tests/test_shelve.py +++ b/nova/tests/functional/api_sample_tests/test_shelve.py @@ -15,10 +15,25 @@ import nova.conf +from nova import objects from nova.tests.functional.api_sample_tests import test_servers +from oslo_utils.fixture import uuidsentinel +from unittest import mock CONF = nova.conf.CONF +fake_aggregate = { + 'deleted': 0, + 'deleted_at': None, + 'created_at': None, + 'updated_at': None, + 'id': 123, + 'uuid': uuidsentinel.fake_aggregate, + 'name': 'us-west', + 'hosts': ['host01'], + 'metadetails': {'availability_zone': 'us-west'}, +} + class ShelveJsonTest(test_servers.ServersSampleBase): # The 'os_compute_api:os-shelve:shelve_offload' policy is admin-only @@ -30,9 +45,11 @@ class ShelveJsonTest(test_servers.ServersSampleBase): # Don't offload instance, so we can test the offload call. CONF.set_override('shelved_offload_time', -1) - def _test_server_action(self, uuid, template, action): + 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, {'action': action}) + template, subs) self.assertEqual(202, response.status_code) self.assertEqual("", response.text) @@ -51,26 +68,288 @@ class ShelveJsonTest(test_servers.ServersSampleBase): self._test_server_action(uuid, 'os-unshelve', 'unshelve') -class UnshelveJson277Test(test_servers.ServersSampleBase): +class UnshelveJson277Test(ShelveJsonTest): + ADMIN_API = False sample_dir = "os-shelve" microversion = '2.77' scenarios = [('v2_77', {'api_major_version': 'v2.1'})] + def setUp(self): + super(UnshelveJson277Test, self).setUp() + # Almost all next tests require the instance to be shelve offloaded. + # So shelve offload the instance and skip the shelve_offload_test + # below. + CONF.set_override('shelved_offload_time', 0) + + def test_shelve_offload(self): + # Skip this test as the instance is already shelve offloaded. + pass + + 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-az', + 'unshelve', + subs={"availability_zone": "us-west"} + ) + + +class UnshelveJson291Test(UnshelveJson277Test): + ADMIN_API = True + sample_dir = "os-shelve" + microversion = '2.91' + scenarios = [('v2_91', {'api_major_version': 'v2.1'})] + + def _test_server_action_invalid( + self, uuid, template, action, subs=None, msg=None): + subs = subs or {} + subs.update({'action': action}) + response = self._do_post('servers/%s/action' % uuid, + template, subs) + self.assertEqual(400, response.status_code) + self.assertIn(msg, response.text) + + def test_unshelve_with_non_valid_host(self): + """Ensure an exception rise if host is invalid and + a http 400 error + """ + uuid = self._post_server() + self._test_server_action(uuid, 'os-shelve', 'shelve') + self._test_server_action_invalid( + uuid, 'os-unshelve-host', + 'unshelve', + subs={'host': 'host01'}, + msg='Compute host host01 could not be found.') + + @mock.patch('nova.objects.aggregate._get_by_host_from_db') + @mock.patch('nova.objects.ComputeNodeList.get_all_by_host') + def test_unshelve_with_valid_host( + self, compute_node_get_all_by_host, mock_api_get_by_host): + """Ensure we can unshelve to a host + """ + # Put compute in the correct az + mock_api_get_by_host.return_value = [fake_aggregate] + + uuid = self._post_server() + self._test_server_action(uuid, 'os-shelve', 'shelve') + fake_computes = objects.ComputeNodeList( + objects=[ + objects.ComputeNode( + host='host01', + uuid=uuidsentinel.host1, + hypervisor_hostname='host01') + ] + ) + compute_node_get_all_by_host.return_value = fake_computes + + self._test_server_action( + uuid, + 'os-unshelve-host', + 'unshelve', + subs={'host': 'host01'} + ) + + @mock.patch('nova.objects.aggregate._get_by_host_from_db') + @mock.patch('nova.objects.ComputeNodeList.get_all_by_host') + def test_unshelve_with_az_and_host( + self, compute_node_get_all_by_host, mock_api_get_by_host): + """Ensure we can unshelve to a host and az + """ + # Put compute in the correct az + mock_api_get_by_host.return_value = [fake_aggregate] + + uuid = self._post_server() + self._test_server_action(uuid, 'os-shelve', 'shelve') + fake_computes = objects.ComputeNodeList( + objects=[ + objects.ComputeNode( + host='host01', + uuid=uuidsentinel.host1, + hypervisor_hostname='host01') + ] + ) + compute_node_get_all_by_host.return_value = fake_computes + + self._test_server_action( + uuid, + 'os-unshelve-host', + 'unshelve', + subs={'host': 'host01', 'availability_zone': 'us-west'}, + ) + + @mock.patch('nova.objects.aggregate._get_by_host_from_db') + @mock.patch('nova.objects.ComputeNodeList.get_all_by_host') + def test_unshelve_with_unpin_az_and_host( + self, compute_node_get_all_by_host, mock_api_get_by_host): + """Ensure we can unshelve to a host and az + """ + # Put compute in the correct az + mock_api_get_by_host.return_value = [fake_aggregate] + + uuid = self._post_server() + self._test_server_action(uuid, 'os-shelve', 'shelve') + fake_computes = objects.ComputeNodeList( + objects=[ + objects.ComputeNode( + host='host01', + uuid=uuidsentinel.host1, + hypervisor_hostname='host01') + ] + ) + compute_node_get_all_by_host.return_value = fake_computes + + self._test_server_action( + uuid, + 'os-unshelve-host-and-unpin-az', + 'unshelve', + subs={'host': 'host01'}, + ) + + @mock.patch('nova.objects.aggregate._get_by_host_from_db') + @mock.patch('nova.objects.ComputeNodeList.get_all_by_host') + def test_unshelve_with_unpin_az( + self, compute_node_get_all_by_host, mock_api_get_by_host): + """Ensure we can unpin an az + """ + # Put compute in the correct az + mock_api_get_by_host.return_value = [fake_aggregate] + + uuid = self._post_server() + self._test_server_action(uuid, 'os-shelve', 'shelve') + fake_computes = objects.ComputeNodeList( + objects=[ + objects.ComputeNode( + host='host01', + uuid=uuidsentinel.host1, + hypervisor_hostname='host01') + ] + ) + compute_node_get_all_by_host.return_value = fake_computes + + self._test_server_action( + uuid, + 'os-unshelve-unpin-az', + 'unshelve', + subs={'host': 'host01'}, + ) + + +class UnshelveJson291NonAdminTest(UnshelveJson291Test): + # Use non admin api credentials. + ADMIN_API = False + sample_dir = "os-shelve" + microversion = '2.91' + scenarios = [('v2_91', {'api_major_version': 'v2.1'})] + + def _test_server_action_invalid(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(403, response.status_code) + self.assertIn( + "Policy doesn\'t allow os_compute_api:os-shelve:unshelve_to_host" + + " to be performed.", response.text) + 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) + self.assertEqual('', response.text) - def test_unshelve_with_az(self): + def test_unshelve_with_non_valid_host(self): + """Ensure an exception rise if user is not admin. + a http 403 error + """ 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"}) + self._test_server_action_invalid( + uuid, + 'os-unshelve-host', + 'unshelve', + subs={'host': 'host01'} + ) + + @mock.patch('nova.objects.aggregate._get_by_host_from_db') + @mock.patch('nova.objects.ComputeNodeList.get_all_by_host') + def test_unshelve_with_unpin_az_and_host( + self, compute_node_get_all_by_host, mock_api_get_by_host): + # Put compute in the correct az + mock_api_get_by_host.return_value = [fake_aggregate] - 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') + fake_computes = objects.ComputeNodeList( + objects=[ + objects.ComputeNode( + host='host01', + uuid=uuidsentinel.host1, + hypervisor_hostname='host01') + ] + ) + compute_node_get_all_by_host.return_value = fake_computes + + self._test_server_action_invalid( + uuid, + 'os-unshelve-host-and-unpin-az', + 'unshelve', + subs={'host': 'host01'}, + ) + + @mock.patch('nova.objects.aggregate._get_by_host_from_db') + @mock.patch('nova.objects.ComputeNodeList.get_all_by_host') + def test_unshelve_with_valid_host( + self, compute_node_get_all_by_host, mock_api_get_by_host): + # Put compute in the correct az + mock_api_get_by_host.return_value = [fake_aggregate] + + uuid = self._post_server() + self._test_server_action(uuid, 'os-shelve', 'shelve') + fake_computes = objects.ComputeNodeList( + objects=[ + objects.ComputeNode( + host='host01', + uuid=uuidsentinel.host1, + hypervisor_hostname='host01') + ] + ) + compute_node_get_all_by_host.return_value = fake_computes + + self._test_server_action_invalid( + uuid, + 'os-unshelve-host', + 'unshelve', + subs={'host': 'host01'} + ) + + @mock.patch('nova.objects.aggregate._get_by_host_from_db') + @mock.patch('nova.objects.ComputeNodeList.get_all_by_host') + def test_unshelve_with_az_and_host( + self, compute_node_get_all_by_host, mock_api_get_by_host): + """Ensure we can unshelve to a host and az + """ + # Put compute in the correct az + mock_api_get_by_host.return_value = [fake_aggregate] + + uuid = self._post_server() + self._test_server_action(uuid, 'os-shelve', 'shelve') + fake_computes = objects.ComputeNodeList( + objects=[ + objects.ComputeNode( + host='host01', + uuid=uuidsentinel.host1, + hypervisor_hostname='host01') + ] + ) + compute_node_get_all_by_host.return_value = fake_computes + + self._test_server_action_invalid( + uuid, + 'os-unshelve-host', + 'unshelve', + subs={'host': 'host01', 'availability_zone': 'us-west'}, + ) diff --git a/nova/tests/functional/test_availability_zones.py b/nova/tests/functional/test_availability_zones.py index 991f86148d8f..c37642330328 100644 --- a/nova/tests/functional/test_availability_zones.py +++ b/nova/tests/functional/test_availability_zones.py @@ -10,12 +10,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +from nova.api.openstack.compute import hosts +from nova.compute import instance_actions from nova import context from nova import objects from nova import test from nova.tests import fixtures as nova_fixtures +from nova.tests.functional.api import client as api_client from nova.tests.functional import fixtures as func_fixtures from nova.tests.functional import integrated_helpers +from nova.tests.unit.api.openstack import fakes class TestAvailabilityZoneScheduling( @@ -36,6 +40,9 @@ class TestAvailabilityZoneScheduling( self.api = api_fixture.admin_api self.api.microversion = 'latest' + self.controller = hosts.HostController() + self.req = fakes.HTTPRequest.blank('', use_admin_context=True) + self.start_service('conductor') self.start_service('scheduler') @@ -68,18 +75,18 @@ class TestAvailabilityZoneScheduling( self.api.api_post( '/os-aggregates/%s/action' % aggregate['id'], add_host_body) - def _create_server(self, name): + def _create_server(self, name, zone=None): # Create a server, it doesn't matter which host it ends up in. server = super(TestAvailabilityZoneScheduling, self)._create_server( flavor_id=self.flavor1, - networks='none',) - original_host = server['OS-EXT-SRV-ATTR:host'] - # Assert the server has the AZ set (not None or 'nova'). - expected_zone = 'zone1' if original_host == 'host1' else 'zone2' - self.assertEqual(expected_zone, server['OS-EXT-AZ:availability_zone']) + networks='none', + az=zone, + ) return server - def _assert_instance_az(self, server, expected_zone): + def _assert_instance_az_and_host( + self, server, expected_zone, expected_host=None): + # Check AZ # Check the API. self.assertEqual(expected_zone, server['OS-EXT-AZ:availability_zone']) # Check the DB. @@ -88,6 +95,51 @@ class TestAvailabilityZoneScheduling( ctxt, self.cell_mappings[test.CELL1_NAME]) as cctxt: instance = objects.Instance.get_by_uuid(cctxt, server['id']) self.assertEqual(expected_zone, instance.availability_zone) + # Check host + if expected_host: + self.assertEqual(expected_host, server['OS-EXT-SRV-ATTR:host']) + + def _assert_request_spec_az(self, ctxt, server, az): + request_spec = objects.RequestSpec.get_by_instance_uuid( + ctxt, server['id']) + self.assertEqual(request_spec.availability_zone, az) + + def _assert_server_with_az_unshelved_to_specified_az(self, server, az): + """Ensure a server with an az constraints is unshelved in the + corresponding az. + """ + host_to_disable = 'host1' if az == 'zone1' else 'host2' + self._shelve_server(server, expected_state='SHELVED_OFFLOADED') + compute_service_id = self.api.get_services( + host=host_to_disable, binary='nova-compute')[0]['id'] + self.api.put_service(compute_service_id, {'status': 'disabled'}) + + req = { + 'unshelve': None + } + + self.api.post_server_action(server['id'], req) + + server = self._wait_for_action_fail_completion( + server, instance_actions.UNSHELVE, 'schedule_instances') + self.assertIn('Error', server['result']) + self.assertIn('No valid host', server['details']) + + def _shelve_unshelve_server(self, ctxt, server, req): + self._shelve_server(server, expected_state='SHELVED_OFFLOADED') + + self.api.post_server_action(server['id'], req) + server = self._wait_for_server_parameter( + server, + {'status': 'ACTIVE', }, + ) + return self.api.get_server(server['id']) + + def other_az_than(self, az): + return 'zone2' if az == 'zone1' else 'zone1' + + def other_host_than(self, host): + return 'host2' if host == 'host1' else 'host1' def test_live_migrate_implicit_az(self): """Tests live migration of an instance with an implicit AZ. @@ -111,7 +163,8 @@ class TestAvailabilityZoneScheduling( still not restricted to its current zone even if it says it is in one. """ server = self._create_server('test_live_migrate_implicit_az') - original_host = server['OS-EXT-SRV-ATTR:host'] + original_az = server['OS-EXT-AZ:availability_zone'] + expected_zone = self.other_az_than(original_az) # Attempt to live migrate the instance; again, we don't specify a host # because there are only two hosts so the scheduler would only be able @@ -132,8 +185,379 @@ class TestAvailabilityZoneScheduling( # the database because the API will return the AZ from the host # aggregate if instance.host is not None. server = self.api.get_server(server['id']) - expected_zone = 'zone2' if original_host == 'host1' else 'zone1' - self._assert_instance_az(server, expected_zone) + self._assert_instance_az_and_host(server, expected_zone) + + def test_create_server(self): + """Create a server without an AZ constraint and make sure asking a new + request spec will not have the request_spec.availability_zone set. + """ + ctxt = context.get_admin_context() + server = self._create_server('server01') + self._assert_request_spec_az(ctxt, server, None) + + def test_create_server_to_zone(self): + """Create a server with an AZ constraint and make sure asking a new + request spec will have the request_spec.availability_zone to the + required zone. + """ + ctxt = context.get_admin_context() + server = self._create_server('server01', 'zone2') + + server = self.api.get_server(server['id']) + self._assert_instance_az_and_host(server, 'zone2') + self._assert_request_spec_az(ctxt, server, 'zone2') + + def test_cold_migrate_cross_az(self): + """Test a cold migration cross AZ. + """ + server = self._create_server('server01') + original_host = server['OS-EXT-SRV-ATTR:host'] + original_az = server['OS-EXT-AZ:availability_zone'] + expected_host = self.other_host_than(original_host) + expected_zone = self.other_az_than(original_az) + + self._migrate_server(server) + self._confirm_resize(server) + + server = self.api.get_server(server['id']) + self._assert_instance_az_and_host(server, expected_zone, expected_host) + +# Next tests attempt to check the following behavior +# +----------+---------------------------+-------+----------------------------+ +# | Boot | Unshelve after offload AZ | Host | Result | +# +==========+===========================+=======+============================+ +# | No AZ | No AZ or AZ=null | No | Free scheduling, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | No AZ or AZ=null | Host1 | Schedule to host1, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | AZ="AZ1" | No | Schedule to AZ1, | +# | | | | reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | AZ="AZ1" | Host1 | Verify that host1 in AZ1, | +# | | | | or (1). Schedule to | +# | | | | host1, reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | No AZ | No | Schedule to AZ1, | +# | | | | reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ=null | No | Free scheduling, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | No AZ | Host1 | If host1 is in AZ1, | +# | | | | then schedule to host1, | +# | | | | reqspec.AZ="AZ1", otherwise| +# | | | | reject the request (1) | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ=null | Host1 | Schedule to host1, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ="AZ2" | No | Schedule to AZ2, | +# | | | | reqspec.AZ="AZ2" | +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ="AZ2" | Host1 | If host1 in AZ2 then | +# | | | | schedule to host1, | +# | | | | reqspec.AZ="AZ2", | +# | | | | otherwise reject (1) | +# +----------+---------------------------+-------+----------------------------+ +# +# (1) Check at the api and return an error. +# +# +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | No AZ or AZ=null | No | Free scheduling, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ + + def test_unshelve_server_without_az_contraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01') + + req = { + 'unshelve': None + } + + self._shelve_unshelve_server(ctxt, server, req) + self._assert_request_spec_az(ctxt, server, None) + + def test_unshelve_unpin_az_server_without_az_contraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01') + + req = { + 'unshelve': {'availability_zone': None} + } + + self._shelve_unshelve_server(ctxt, server, req) + self._assert_request_spec_az(ctxt, server, None) + +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | No AZ or AZ=null | Host1 | Schedule to host1, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ + def test_unshelve_to_host_server_without_az_contraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01') + original_host = server['OS-EXT-SRV-ATTR:host'] + original_az = server['OS-EXT-AZ:availability_zone'] + dest_hostname = self.other_host_than(original_host) + expected_zone = self.other_az_than(original_az) + + req = { + 'unshelve': {'host': dest_hostname} + } + + server = self._shelve_unshelve_server(ctxt, server, req) + self._assert_instance_az_and_host(server, expected_zone, dest_hostname) + self._assert_request_spec_az(ctxt, server, None) + + def test_unshelve_to_host_and_unpin_server_without_az_contraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01') + original_host = server['OS-EXT-SRV-ATTR:host'] + original_az = server['OS-EXT-AZ:availability_zone'] + dest_hostname = self.other_host_than(original_host) + expected_zone = self.other_az_than(original_az) + + req = { + 'unshelve': { + 'host': dest_hostname, + 'availability_zone': None, + } + } + + server = self._shelve_unshelve_server(ctxt, server, req) + self._assert_instance_az_and_host(server, expected_zone, dest_hostname) + self._assert_request_spec_az(ctxt, server, None) + +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | AZ="AZ1" | No | Schedule to AZ1, | +# | | | | reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ + def test_unshelve_to_az_server_without_az_constraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01') + original_host = server['OS-EXT-SRV-ATTR:host'] + original_az = server['OS-EXT-AZ:availability_zone'] + dest_hostname = 'host2' if original_host == 'host1' else 'host1' + dest_az = self.other_az_than(original_az) + + req = { + 'unshelve': {'availability_zone': dest_az} + } + + server = self._shelve_unshelve_server(ctxt, server, req) + self._assert_instance_az_and_host(server, dest_az, dest_hostname) + self._assert_request_spec_az(ctxt, server, dest_az) + self._assert_server_with_az_unshelved_to_specified_az( + server, dest_az) + +# +----------+---------------------------+-------+----------------------------+ +# | No AZ | AZ="AZ1" | Host1 | Verify that host1 in AZ1, | +# | | | | or (3). Schedule to | +# | | | | host1, reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ + def test_unshelve_to_az_and_host_server_without_az_constraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01') + original_host = server['OS-EXT-SRV-ATTR:host'] + original_az = server['OS-EXT-AZ:availability_zone'] + dest_hostname = 'host2' if original_host == 'host1' else 'host1' + dest_az = self.other_az_than(original_az) + + req = { + 'unshelve': {'host': dest_hostname, 'availability_zone': dest_az} + } + + server = self._shelve_unshelve_server(ctxt, server, req) + self._assert_instance_az_and_host(server, dest_az, dest_hostname) + self._assert_request_spec_az(ctxt, server, dest_az) + self._assert_server_with_az_unshelved_to_specified_az( + server, dest_az) + + def test_unshelve_to_wrong_az_and_host_server_without_az_constraint(self): + server = self._create_server('server01') + original_host = server['OS-EXT-SRV-ATTR:host'] + original_az = server['OS-EXT-AZ:availability_zone'] + dest_hostname = 'host2' if original_host == 'host1' else 'host1' + + req = { + 'unshelve': {'host': dest_hostname, + 'availability_zone': original_az} + } + + self._shelve_server(server, expected_state='SHELVED_OFFLOADED') + exc = self.assertRaises( + api_client.OpenStackApiException, + self.api.post_server_action, + server['id'], + req + ) + + self.assertEqual(409, exc.response.status_code) + self.assertIn( + 'Host \\\"{}\\\" is not in the availability zone \\\"{}\\\".' + .format(dest_hostname, original_az), + exc.response.text + ) + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | No AZ | No | Schedule to AZ1, | +# | | | | reqspec.AZ="AZ1" | +# +----------+---------------------------+-------+----------------------------+ + def test_unshelve_a_server_with_az_contraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01', 'zone2') + + req = { + 'unshelve': None + } + + self._shelve_unshelve_server(ctxt, server, req) + self._assert_request_spec_az(ctxt, server, 'zone2') + self._assert_server_with_az_unshelved_to_specified_az( + server, 'zone2') + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ=null | No | Free scheduling, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ + def test_unshelve_to_unpin_az_a_server_with_az_constraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01', 'zone2') + + req = { + 'unshelve': {'availability_zone': None} + } + + server = self._shelve_unshelve_server(ctxt, server, req) + self._assert_request_spec_az(ctxt, server, None) + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | No AZ | Host1 | If host1 is in AZ1, | +# | | | | then schedule to host1, | +# | | | | reqspec.AZ="AZ1", otherwise| +# | | | | reject the request (3) | +# +----------+---------------------------+-------+----------------------------+ + def test_unshelve_to_host_server_with_az_contraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01', 'zone1') + + req = { + 'unshelve': {'host': 'host1'} + } + + server = self._shelve_unshelve_server(ctxt, server, req) + self._assert_instance_az_and_host(server, 'zone1', 'host1') + self._assert_request_spec_az(ctxt, server, 'zone1') + self._assert_server_with_az_unshelved_to_specified_az( + server, 'zone1') + + def test_unshelve_to_host_wrong_az_server_with_az_contraint(self): + server = self._create_server('server01', 'zone1') + + req = { + 'unshelve': {'host': 'host2'} + } + + self._shelve_server(server, expected_state='SHELVED_OFFLOADED') + exc = self.assertRaises( + api_client.OpenStackApiException, + self.api.post_server_action, + server['id'], + req + ) + + self.assertEqual(409, exc.response.status_code) + self.assertIn( + 'Host \\\"host2\\\" is not in the availability ' + 'zone \\\"zone1\\\".', + exc.response.text + ) + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ=null | Host1 | Schedule to host1, | +# | | | | reqspec.AZ=None | +# +----------+---------------------------+-------+----------------------------+ + def test_unshelve_to_host_and_unpin_server_with_az_contraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01', 'zone1') + + req = { + 'unshelve': {'host': 'host2', + 'availability_zone': None, + } + } + + server = self._shelve_unshelve_server(ctxt, server, req) + self._assert_instance_az_and_host(server, 'zone2', 'host2') + self._assert_request_spec_az(ctxt, server, None) + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ="AZ2" | No | Schedule to AZ2, | +# | | | | reqspec.AZ="AZ2" | +# +----------+---------------------------+-------+----------------------------+ + def test_unshelve_to_az_a_server_with_az_constraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01', 'zone1') + + req = { + 'unshelve': {'availability_zone': 'zone2'} + } + + server = self._shelve_unshelve_server(ctxt, server, req) + self._assert_instance_az_and_host(server, 'zone2', 'host2') + self._assert_request_spec_az(ctxt, server, 'zone2') + self._assert_server_with_az_unshelved_to_specified_az( + server, 'zone2') + +# +----------+---------------------------+-------+----------------------------+ +# | AZ1 | AZ="AZ2" | Host1 | If host1 in AZ2 then | +# | | | | schedule to host1, | +# | | | | reqspec.AZ="AZ2", | +# | | | | otherwise reject (3) | +# +----------+---------------------------+-------+----------------------------+ + def test_unshelve_to_host_and_az_a_server_with_az_constraint(self): + ctxt = context.get_admin_context() + server = self._create_server('server01', 'zone1') + + req = { + 'unshelve': {'host': 'host2', + 'availability_zone': 'zone2', + } + } + + server = self._shelve_unshelve_server(ctxt, server, req) + self._assert_instance_az_and_host(server, 'zone2', 'host2') + self._assert_request_spec_az(ctxt, server, 'zone2') + self._assert_server_with_az_unshelved_to_specified_az( + server, 'zone2') + + def test_unshelve_to_host_and_wrong_az_a_server_with_az_constraint(self): + server = self._create_server('server01', 'zone1') + + req = { + 'unshelve': {'host': 'host2', + 'availability_zone': 'zone1', + } + } + + self._shelve_server(server, expected_state='SHELVED_OFFLOADED') + exc = self.assertRaises( + api_client.OpenStackApiException, + self.api.post_server_action, + server['id'], + req + ) + + self.assertEqual(409, exc.response.status_code) + self.assertIn( + 'Host \\\"host2\\\" is not in the availability ' + 'zone \\\"zone1\\\".', + exc.response.text + + ) def test_resize_revert_across_azs(self): """Creates two compute service hosts in separate AZs. Creates a server @@ -152,9 +576,9 @@ class TestAvailabilityZoneScheduling( # Now the server should be in the other AZ. new_zone = 'zone2' if original_host == 'host1' else 'zone1' - self._assert_instance_az(server, new_zone) + self._assert_instance_az_and_host(server, new_zone) # Revert the resize and the server should be back in the original AZ. self.api.post_server_action(server['id'], {'revertResize': None}) server = self._wait_for_state_change(server, 'ACTIVE') - self._assert_instance_az(server, original_az) + self._assert_instance_az_and_host(server, original_az) diff --git a/nova/tests/functional/test_servers.py b/nova/tests/functional/test_servers.py index 22939020f05b..f65dd5990216 100644 --- a/nova/tests/functional/test_servers.py +++ b/nova/tests/functional/test_servers.py @@ -2519,6 +2519,57 @@ class ServerMovingTests(integrated_helpers.ProviderUsageBaseTestCase): self._delete_and_check_allocations(server) + def test_shelve_unshelve_to_host(self): + source_hostname = self.compute1.host + dest_hostname = self.compute2.host + source_rp_uuid = self._get_provider_uuid_by_host(source_hostname) + dest_rp_uuid = \ + self._get_provider_uuid_by_host(dest_hostname) + + server = self._boot_then_shelve_and_check_allocations( + source_hostname, source_rp_uuid) + + self._shelve_offload_and_check_allocations(server, source_rp_uuid) + + req = { + 'unshelve': {'host': dest_hostname} + } + + self.api.post_server_action(server['id'], req) + self._wait_for_server_parameter( + server, {'OS-EXT-SRV-ATTR:host': dest_hostname, 'status': 'ACTIVE'} + ) + + self.assertFlavorMatchesUsage(dest_rp_uuid, self.flavor1) + + # the server has an allocation on only the dest node + self.assertFlavorMatchesAllocation( + self.flavor1, server['id'], dest_rp_uuid) + + self._delete_and_check_allocations(server) + + def test_shelve_unshelve_to_host_instance_not_offloaded(self): + source_hostname = self.compute1.host + dest_hostname = self.compute2.host + source_rp_uuid = self._get_provider_uuid_by_host(source_hostname) + + server = self._boot_then_shelve_and_check_allocations( + source_hostname, source_rp_uuid) + + req = { + 'unshelve': {'host': dest_hostname} + } + + ex = self.assertRaises( + client.OpenStackApiException, + self.api.post_server_action, + server['id'], req + ) + self.assertEqual(409, ex.response.status_code) + self.assertIn( + "The server status must be SHELVED_OFFLOADED", + ex.response.text) + def _shelve_offload_and_check_allocations(self, server, source_rp_uuid): req = { 'shelveOffload': {} diff --git a/nova/tests/unit/api/openstack/compute/test_shelve.py b/nova/tests/unit/api/openstack/compute/test_shelve.py index c3fb973c97a7..c361d0b9ea52 100644 --- a/nova/tests/unit/api/openstack/compute/test_shelve.py +++ b/nova/tests/unit/api/openstack/compute/test_shelve.py @@ -134,10 +134,12 @@ class UnshelveServerControllerTestV277(test.NoDBTestCase): '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.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'], @@ -197,10 +199,11 @@ class UnshelveServerControllerTestV277(test.NoDBTestCase): '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) + self.assertRaises( + exception.ValidationError, + self.controller._unshelve, + self.req, + fakes.FAKE_UUID, body=body) def test_unshelve_with_additional_param(self): body = { @@ -214,3 +217,235 @@ class UnshelveServerControllerTestV277(test.NoDBTestCase): self.controller._unshelve, self.req, fakes.FAKE_UUID, body=body) self.assertIn("Additional properties are not allowed", str(exc)) + + +class UnshelveServerControllerTestV291(test.NoDBTestCase): + """Server controller test for microversion 2.91 + + Add host parameter to unshelve a shelved-offloaded server of + 2.91 microversion. + """ + wsgi_api_version = '2.91' + + def setUp(self): + super(UnshelveServerControllerTestV291, self).setUp() + self.controller = shelve_v21.ShelveController() + self.req = fakes.HTTPRequest.blank( + '/%s/servers/a/action' % fakes.FAKE_PROJECT_ID, + use_admin_context=True, version=self.wsgi_api_version) + + 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_91(self, mock_get_instance): + """Make sure specifying an AZ before microversion 2.91 + is still working. + """ + 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.77')) + 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='us-east', + ) + + @mock.patch('nova.api.openstack.common.get_instance') + def test_unshelve_without_parameters_2_91(self, mock_get_instance): + """Make sure not specifying parameters with microversion 2.91 + is working. + """ + 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.91')) + 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, + ) + + @mock.patch('nova.api.openstack.common.get_instance') + def test_unshelve_with_az_2_91(self, mock_get_instance): + """Make sure specifying an AZ with microversion 2.91 + is working. + """ + 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.91')) + 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='us-east', + host=None, + ) + + @mock.patch('nova.api.openstack.common.get_instance') + def test_unshelve_with_az_none_2_91(self, mock_get_instance): + """Make sure specifying an AZ to none (unpin server) + is working. + """ + instance = self.fake_get_instance() + mock_get_instance.return_value = instance + + body = { + 'unshelve': { + 'availability_zone': None, + }} + self.req.body = jsonutils.dump_as_bytes(body) + self.req.api_version_request = ( + api_version_request.APIVersionRequest('2.91')) + 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, + host=None, + ) + + @mock.patch('nova.api.openstack.common.get_instance') + def test_unshelve_with_host_2_91(self, mock_get_instance): + """Make sure specifying a host with microversion 2.91 + is working. + """ + instance = self.fake_get_instance() + mock_get_instance.return_value = instance + + body = { + 'unshelve': { + 'host': 'server02', + }} + self.req.body = jsonutils.dump_as_bytes(body) + self.req.api_version_request = ( + api_version_request.APIVersionRequest('2.91')) + 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, + host='server02', + ) + + @mock.patch('nova.compute.api.API.unshelve') + @mock.patch('nova.api.openstack.common.get_instance') + def test_unshelve_with_az_and_host_with_v2_91( + self, mock_get_instance, mock_unshelve): + """Make sure specifying a host and an availability_zone with + microversion 2.91 is working. + """ + instance = self.fake_get_instance() + mock_get_instance.return_value = instance + + body = { + 'unshelve': { + 'availability_zone': 'us-east', + 'host': 'server01', + }} + self.req.body = jsonutils.dump_as_bytes(body) + self.req.api_version_request = ( + api_version_request.APIVersionRequest('2.91')) + 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='us-east', + host='server01', + ) + + def test_invalid_az_name_with_int(self): + body = { + 'unshelve': { + 'host': 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': { + 'host': 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_invalid_host_fqdn_with_int(self): + body = { + 'unshelve': { + 'host': 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_host(self): + body = { + 'unshelve': { + 'host': 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': { + 'host': 'server01', + '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("Invalid input for field/attribute unshelve.", str(exc)) diff --git a/nova/tests/unit/test_policy.py b/nova/tests/unit/test_policy.py index c78b4bfba63f..3aabe5a717a0 100644 --- a/nova/tests/unit/test_policy.py +++ b/nova/tests/unit/test_policy.py @@ -358,6 +358,7 @@ class RealRolePolicyTestCase(test.NoDBTestCase): "os_compute_api:os-services:update", "os_compute_api:os-services:delete", "os_compute_api:os-shelve:shelve_offload", +"os_compute_api:os-shelve:unshelve_to_host", "os_compute_api:os-availability-zone:detail", "os_compute_api:os-assisted-volume-snapshots:create", "os_compute_api:os-assisted-volume-snapshots:delete", diff --git a/releasenotes/notes/bp-unshelve_to_host-c9047d518eb67747.yaml b/releasenotes/notes/bp-unshelve_to_host-c9047d518eb67747.yaml new file mode 100644 index 000000000000..cde698803151 --- /dev/null +++ b/releasenotes/notes/bp-unshelve_to_host-c9047d518eb67747.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Microversion 2.91 adds the optional parameter ``host`` to + the ``unshelve`` server action API. + Specifying a destination host is only + allowed to admin users and server status must be ``SHELVED_OFFLOADED`` + otherwise a HTTP 400 (bad request) response is returned. + It also allows to set ``availability_zone`` to None to unpin a server + from an availability_zone.