From 2f7bf29d472d349759ffd8aece23a75f4e27a4f9 Mon Sep 17 00:00:00 2001 From: Dan Peschman Date: Tue, 18 Jul 2017 13:54:47 -0400 Subject: [PATCH] Use uuid for id in os-services API This patch introduces a new microversion to identify services by uuid instead of id, to ensure uniqueness across cells. GET /os-services returns uuid in the id field, and uuid must be provided to delete a service with DELETE /os-services/{service_uuid}. The old PUT /os-services/* APIs are now capped and replaced with a new PUT /os-services/{service_uuid} which takes a uuid path parameter to uniquely identify the service to update. It also restricts updates to nova-compute services only, since disabling or forcing-down a non-compute service like nova-scheduler doesn't make sense as it doesn't do anything. The new update() method in this microversion also avoids trying to re-use the existing private action methods like _enable and _disable since those are predicated on looking up the service by host/binary, are confusing to follow for code flow, and just don't really make sense with a pure PUT resource update method. Part of blueprint service-hyper-uuid-in-api Co-Authored-By: Matt Riedemann Change-Id: I45494a4df7ee4454edb3ef8e7c5817d8c4e9e5ad --- api-ref/source/os-services.inc | 95 ++++++- api-ref/source/parameters.yaml | 84 +++++- .../v2.53/service-disable-log-put-req.json | 4 + .../v2.53/service-disable-log-put-resp.json | 13 + .../v2.53/service-disable-put-req.json | 3 + .../v2.53/service-disable-put-resp.json | 13 + .../v2.53/service-enable-put-req.json | 3 + .../v2.53/service-enable-put-resp.json | 13 + .../v2.53/service-force-down-put-req.json | 3 + .../v2.53/service-force-down-put-resp.json | 13 + .../v2.53/services-list-get-resp.json | 48 ++++ .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- nova/api/openstack/api_version_request.py | 5 +- .../compute/rest_api_version_history.rst | 30 +++ .../api/openstack/compute/schemas/services.py | 21 ++ nova/api/openstack/compute/services.py | 135 +++++++++- nova/cells/utils.py | 14 + nova/policies/services.py | 5 + .../service-disable-log-put-req.json.tpl | 4 + .../service-disable-log-put-resp.json.tpl | 13 + .../v2.53/service-disable-put-req.json.tpl | 3 + .../v2.53/service-disable-put-resp.json.tpl | 13 + .../v2.53/service-enable-put-req.json.tpl | 3 + .../v2.53/service-enable-put-resp.json.tpl | 13 + .../v2.53/service-force-down-put-req.json.tpl | 3 + .../service-force-down-put-resp.json.tpl | 13 + .../v2.53/services-list-get-resp.json.tpl | 48 ++++ .../api_sample_tests/test_services.py | 55 ++++ .../notification_sample_base.py | 13 +- .../test_service_update.py | 57 ++++- .../api/openstack/compute/test_services.py | 240 ++++++++++++++++++ ...ce-hyper-uuid-in-api-cc7b9f21cc458e1b.yaml | 10 + 33 files changed, 968 insertions(+), 26 deletions(-) create mode 100644 doc/api_samples/os-services/v2.53/service-disable-log-put-req.json create mode 100644 doc/api_samples/os-services/v2.53/service-disable-log-put-resp.json create mode 100644 doc/api_samples/os-services/v2.53/service-disable-put-req.json create mode 100644 doc/api_samples/os-services/v2.53/service-disable-put-resp.json create mode 100644 doc/api_samples/os-services/v2.53/service-enable-put-req.json create mode 100644 doc/api_samples/os-services/v2.53/service-enable-put-resp.json create mode 100644 doc/api_samples/os-services/v2.53/service-force-down-put-req.json create mode 100644 doc/api_samples/os-services/v2.53/service-force-down-put-resp.json create mode 100644 doc/api_samples/os-services/v2.53/services-list-get-resp.json create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-log-put-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-log-put-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-put-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-put-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-enable-put-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-enable-put-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-force-down-put-req.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-force-down-put-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/services-list-get-resp.json.tpl create mode 100644 releasenotes/notes/bp-service-hyper-uuid-in-api-cc7b9f21cc458e1b.yaml diff --git a/api-ref/source/os-services.inc b/api-ref/source/os-services.inc index f38541e45b..963b134d5f 100644 --- a/api-ref/source/os-services.inc +++ b/api-ref/source/os-services.inc @@ -40,7 +40,8 @@ Response .. rest_parameters:: parameters.yaml - services: services - - id: service_id_body + - id: service_id_body_2_52 + - id: service_id_body_2_53 - binary: binary - disabled_reason: disabled_reason_body - host: host_name_body @@ -48,7 +49,7 @@ Response - status: service_status - updated_at: updated - zone: OS-EXT-AZ:availability_zone - - forced_down: forced_down + - forced_down: forced_down_2_11 **Example List Compute Services** @@ -64,6 +65,9 @@ Disables scheduling for a Compute service. Specify the service by its host name and binary name. +.. note:: Starting with microversion 2.53 this API is superseded by + ``PUT /os-services/{service_id}``. + Normal response codes: 200 Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404) @@ -105,6 +109,9 @@ Logs information to the Compute service table about why a Compute service was di Specify the service by its host name and binary name. +.. note:: Starting with microversion 2.53 this API is superseded by + ``PUT /os-services/{service_id}``. + Normal response codes: 200 Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404) @@ -148,6 +155,9 @@ Enables scheduling for a Compute service. Specify the service by its host name and binary name. +.. note:: Starting with microversion 2.53 this API is superseded by + ``PUT /os-services/{service_id}``. + Normal response codes: 200 Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404) @@ -191,6 +201,9 @@ Action ``force-down`` available as of microversion 2.11. Specify the service by its host name and binary name. +.. note:: Starting with microversion 2.53 this API is superseded by + ``PUT /os-services/{service_id}``. + Normal response codes: 200 Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404) @@ -202,7 +215,7 @@ Request - host: host_name_body - binary: binary - - forced_down: forced_down + - forced_down: forced_down_2_11 **Example Update Forced Down** @@ -217,7 +230,7 @@ Response - service: service - binary: binary - host: host_name_body - - forced_down: forced_down + - forced_down: forced_down_2_11 | @@ -226,6 +239,77 @@ Response .. literalinclude:: ../../doc/api_samples/os-services/v2.11/service-force-down-put-resp.json :language: javascript +Update Compute Service +====================== + +.. rest_method:: PUT /os-services/{service_id} + +Update a compute service to enable or disable scheduling, including recording a +reason why a compute service was disabled from scheduling. Set or unset the +``forced_down`` flag for the service. + +This API is available starting with microversion 2.53. + +Normal response codes: 200 + +Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404) + +Request +------- + +.. rest_parameters:: parameters.yaml + + - service_id: service_id_path_2_53_no_version + - status: service_status_2_53_in + - disabled_reason: disabled_reason_2_53_in + - forced_down: forced_down_2_53_in + +**Example Disable Scheduling For A Compute Service (v2.53)** + +.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-disable-log-put-req.json + :language: javascript + +**Example Enable Scheduling For A Compute Service (v2.53)** + +.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-enable-put-req.json + :language: javascript + +**Example Update Forced Down (v2.53)** + +.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-force-down-put-req.json + :language: javascript + +Response +-------- + +.. rest_parameters:: parameters.yaml + + - service: service + - id: service_id_body_2_53_no_version + - binary: binary + - disabled_reason: disabled_reason_body + - host: host_name_body + - state: service_state + - status: service_status + - updated_at: updated + - zone: OS-EXT-AZ:availability_zone + - forced_down: forced_down_2_53_out + +**Example Disable Scheduling For A Compute Service (v2.53)** + +.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-disable-log-put-resp.json + :language: javascript + +**Example Enable Scheduling For A Compute Service (v2.53)** + +.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-enable-put-resp.json + :language: javascript + +**Example Update Forced Down (v2.53)** + +.. literalinclude:: ../../doc/api_samples/os-services/v2.53/service-force-down-put-resp.json + :language: javascript + Delete Compute Service ====================== @@ -243,7 +327,8 @@ Request .. rest_parameters:: parameters.yaml - - service_id: service_id_path + - service_id: service_id_path_2_52 + - service_id: service_id_path_2_53 Response -------- diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index da3ef90ce4..8a3e66bf66 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -125,6 +125,8 @@ console_token: in: path required: true type: string +# Used in the request path for PUT /os-services/disable-log-reason before +# microversion 2.53. disabled_reason: description: | The reason for disabling a service. @@ -283,12 +285,31 @@ server_id_path: in: path required: true type: string -service_id_path: +service_id_path_2_52: description: | The id of the service. + + .. note:: This may not uniquely identify a service in a multi-cell + deployment. in: path required: true type: integer + max_version: 2.52 +service_id_path_2_53: + description: | + The id of the service as a uuid. This uniquely identifies the service in a + multi-cell deployment. + in: path + required: true + type: string + min_version: 2.53 +service_id_path_2_53_no_version: + description: | + The id of the service as a uuid. This uniquely identifies the service in a + multi-cell deployment. + in: path + required: true + type: string snapshot_id_path: description: | The UUID of the snapshot. @@ -1898,6 +1919,15 @@ device_tag_nic_attachment: required: false type: string min_version: 2.49 +# Optional input parameter in the body for PUT /os-services/{service_id} added +# in microversion 2.53. +disabled_reason_2_53_in: + description: | + The reason for disabling a service. The minimum length is 1 and the + maximum length is 255. This may only be requested with ``status=disabled``. + in: body + required: false + type: string disabled_reason_body: description: | The reason for disabling a service. @@ -2633,7 +2663,9 @@ force_snapshot: in: body required: false type: boolean -forced_down: +# This is both the request and response parameter for +# PUT /os-services/force-down which was added in 2.11. +forced_down_2_11: description: | Whether or not this service was forced down manually by an administrator. This value is useful to know that some 3rd party has @@ -2642,6 +2674,26 @@ forced_down: required: true type: boolean min_version: 2.11 +# This is the optional request input parameter for +# PUT /os-services/{service_id} added in 2.53. +forced_down_2_53_in: + description: | + Whether or not this service was forced down manually by an + administrator. This value is useful to know that some 3rd party has + verified the service should be marked down. + in: body + required: false + type: boolean +# This is the response output parameter for +# PUT /os-services/{service_id} added in 2.53. +forced_down_2_53_out: + description: | + Whether or not this service was forced down manually by an + administrator. This value is useful to know that some 3rd party has + verified the service should be marked down. + in: body + required: true + type: boolean forceDelete: description: | The action. @@ -5064,6 +5116,26 @@ service_id_body: in: body required: true type: integer +service_id_body_2_52: + description: | + The id of the service. + in: body + required: true + type: integer + max_version: 2.52 +service_id_body_2_53: + description: | + The id of the service as a uuid. + in: body + required: true + type: string + min_version: 2.53 +service_id_body_2_53_no_version: + description: | + The id of the service as a uuid. + in: body + required: true + type: string service_state: description: | The state of the service. One of ``up`` or ``down``. @@ -5076,6 +5148,14 @@ service_status: in: body required: true type: string +# This is an optional input parameter to PUT /os-services/{service_id} added +# in microversion 2.53. +service_status_2_53_in: + description: | + The status of the service. One of ``enabled`` or ``disabled``. + in: body + required: false + type: string services: description: | A list of service objects. diff --git a/doc/api_samples/os-services/v2.53/service-disable-log-put-req.json b/doc/api_samples/os-services/v2.53/service-disable-log-put-req.json new file mode 100644 index 0000000000..6491f6316c --- /dev/null +++ b/doc/api_samples/os-services/v2.53/service-disable-log-put-req.json @@ -0,0 +1,4 @@ +{ + "status": "disabled", + "disabled_reason": "maintenance" +} \ No newline at end of file diff --git a/doc/api_samples/os-services/v2.53/service-disable-log-put-resp.json b/doc/api_samples/os-services/v2.53/service-disable-log-put-resp.json new file mode 100644 index 0000000000..bbf18edaf1 --- /dev/null +++ b/doc/api_samples/os-services/v2.53/service-disable-log-put-resp.json @@ -0,0 +1,13 @@ +{ + "service": { + "id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339", + "binary": "nova-compute", + "disabled_reason": "maintenance", + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "2012-10-29T13:42:05.000000", + "forced_down": false, + "zone": "nova" + } +} \ No newline at end of file diff --git a/doc/api_samples/os-services/v2.53/service-disable-put-req.json b/doc/api_samples/os-services/v2.53/service-disable-put-req.json new file mode 100644 index 0000000000..c39c93446a --- /dev/null +++ b/doc/api_samples/os-services/v2.53/service-disable-put-req.json @@ -0,0 +1,3 @@ +{ + "status": "disabled" +} \ No newline at end of file diff --git a/doc/api_samples/os-services/v2.53/service-disable-put-resp.json b/doc/api_samples/os-services/v2.53/service-disable-put-resp.json new file mode 100644 index 0000000000..5f05d3002e --- /dev/null +++ b/doc/api_samples/os-services/v2.53/service-disable-put-resp.json @@ -0,0 +1,13 @@ +{ + "service": { + "id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339", + "binary": "nova-compute", + "disabled_reason": null, + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "2012-10-29T13:42:05.000000", + "forced_down": false, + "zone": "nova" + } +} \ No newline at end of file diff --git a/doc/api_samples/os-services/v2.53/service-enable-put-req.json b/doc/api_samples/os-services/v2.53/service-enable-put-req.json new file mode 100644 index 0000000000..274ae2d6b3 --- /dev/null +++ b/doc/api_samples/os-services/v2.53/service-enable-put-req.json @@ -0,0 +1,3 @@ +{ + "status": "enabled" +} \ No newline at end of file diff --git a/doc/api_samples/os-services/v2.53/service-enable-put-resp.json b/doc/api_samples/os-services/v2.53/service-enable-put-resp.json new file mode 100644 index 0000000000..b301843949 --- /dev/null +++ b/doc/api_samples/os-services/v2.53/service-enable-put-resp.json @@ -0,0 +1,13 @@ +{ + "service": { + "id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339", + "binary": "nova-compute", + "disabled_reason": null, + "host": "host1", + "state": "up", + "status": "enabled", + "updated_at": "2012-10-29T13:42:05.000000", + "forced_down": false, + "zone": "nova" + } +} \ No newline at end of file diff --git a/doc/api_samples/os-services/v2.53/service-force-down-put-req.json b/doc/api_samples/os-services/v2.53/service-force-down-put-req.json new file mode 100644 index 0000000000..1545475c8f --- /dev/null +++ b/doc/api_samples/os-services/v2.53/service-force-down-put-req.json @@ -0,0 +1,3 @@ +{ + "forced_down": true +} \ No newline at end of file diff --git a/doc/api_samples/os-services/v2.53/service-force-down-put-resp.json b/doc/api_samples/os-services/v2.53/service-force-down-put-resp.json new file mode 100644 index 0000000000..cb37faeaaf --- /dev/null +++ b/doc/api_samples/os-services/v2.53/service-force-down-put-resp.json @@ -0,0 +1,13 @@ +{ + "service": { + "id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339", + "binary": "nova-compute", + "disabled_reason": "test2", + "host": "host1", + "state": "down", + "status": "disabled", + "updated_at": "2012-10-29T13:42:05.000000", + "forced_down": true, + "zone": "nova" + } +} \ No newline at end of file diff --git a/doc/api_samples/os-services/v2.53/services-list-get-resp.json b/doc/api_samples/os-services/v2.53/services-list-get-resp.json new file mode 100644 index 0000000000..351908830a --- /dev/null +++ b/doc/api_samples/os-services/v2.53/services-list-get-resp.json @@ -0,0 +1,48 @@ +{ + "services": [ + { + "id": "c4726392-27de-4ff9-b2e0-5aa1d08a520f", + "binary": "nova-scheduler", + "disabled_reason": "test1", + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "2012-10-29T13:42:02.000000", + "forced_down": false, + "zone": "internal" + }, + { + "id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339", + "binary": "nova-compute", + "disabled_reason": "test2", + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "2012-10-29T13:42:05.000000", + "forced_down": false, + "zone": "nova" + }, + { + "id": "bbd684ff-d3f6-492e-a30a-a12a2d2db0e0", + "binary": "nova-scheduler", + "disabled_reason": null, + "host": "host2", + "state": "down", + "status": "enabled", + "updated_at": "2012-09-19T06:55:34.000000", + "forced_down": false, + "zone": "internal" + }, + { + "id": "13aa304e-5340-45a7-a7fb-b6d6e914d272", + "binary": "nova-compute", + "disabled_reason": "test4", + "host": "host2", + "state": "down", + "status": "disabled", + "updated_at": "2012-09-18T08:03:38.000000", + "forced_down": false, + "zone": "nova" + } + ] +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 4e8809b178..1fc439ffae 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.52", + "version": "2.53", "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 e3cc545b5d..f9965b4167 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.52", + "version": "2.53", "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 9a1702ee84..27f7f3f4d4 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -124,6 +124,9 @@ REST_API_VERSION_HISTORY = """REST API Version History: non-admins can see instance action event details except for the traceback field. * 2.52 - Adds support for applying tags when creating a server. + * 2.53 - Service database ids are hidden. The os-services API now returns + a uuid in the id field, and takes a uuid in + DELETE /services/{service_uuid}. """ # The minimum and maximum versions of the API supported @@ -132,7 +135,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.52" +_MAX_API_VERSION = "2.53" DEFAULT_API_VERSION = _MIN_API_VERSION # Almost all proxy APIs which 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 aa8b7be0a4..a3db6c2a89 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -627,3 +627,33 @@ user documentation. Adds support for applying tags when creating a server. The tag schema is the same as in the `2.26`_ microversion. + +2.53 +---- + + **os-services** + + Services are now identified by uuid instead of database id to ensure + uniqueness across cells. This microversion brings the following changes: + + * ``GET /os-services`` returns a uuid in the ``id`` field of the response + * ``DELETE /os-services/{service_uuid}`` requires a service uuid in the path + * The following APIs have been superseded by + ``PUT /os-services/{service_uuid}/``: + + * ``PUT /os-services/disable`` + * ``PUT /os-services/disable-log-reason`` + * ``PUT /os-services/enable`` + * ``PUT /os-services/force-down`` + + ``PUT /os-services/{service_uuid}`` takes the following fields in the body: + + * ``status`` - can be either "enabled" or "disabled" to enable or disable + the given service + * ``disabled_reason`` - specify with status="disabled" to log a reason for + why the service is disabled + * ``forced_down`` - boolean indicating if the service was forced down by + an external service + + * ``PUT /os-services/{service_uuid}`` will now return a full service resource + representation like in a ``GET`` response diff --git a/nova/api/openstack/compute/schemas/services.py b/nova/api/openstack/compute/schemas/services.py index f62dd0ccf5..c90b74c9f6 100644 --- a/nova/api/openstack/compute/schemas/services.py +++ b/nova/api/openstack/compute/schemas/services.py @@ -44,3 +44,24 @@ service_update_v211 = { 'required': ['host', 'binary'], 'additionalProperties': False } + +# The 2.53 body is for updating a service's status and/or forced_down fields. +# There are no required attributes since the service is identified using a +# unique service_id on the request path, and status and/or forced_down can +# be specified in the body. If status=='disabled', then 'disabled_reason' is +# also checked in the body but is not required. Requesting status='enabled' and +# including a 'disabled_reason' results in a 400, but this is checked in code. +service_update_v2_53 = { + 'type': 'object', + 'properties': { + 'status': { + 'type': 'string', + 'enum': ['enabled', 'disabled'], + }, + 'disabled_reason': { + 'type': 'string', 'minLength': 1, 'maxLength': 255, + }, + 'forced_down': parameter_types.boolean + }, + 'additionalProperties': False +} diff --git a/nova/api/openstack/compute/services.py b/nova/api/openstack/compute/services.py index c84976b0fb..c911ced1f3 100644 --- a/nova/api/openstack/compute/services.py +++ b/nova/api/openstack/compute/services.py @@ -13,6 +13,7 @@ # under the License. from oslo_utils import strutils +from oslo_utils import uuidutils import webob.exc from nova.api.openstack import api_version_request @@ -20,6 +21,7 @@ from nova.api.openstack.compute.schemas import services from nova.api.openstack import extensions from nova.api.openstack import wsgi from nova.api import validation +from nova import availability_zones from nova import compute from nova import exception from nova.i18n import _ @@ -27,6 +29,8 @@ from nova.policies import services as services_policies from nova import servicegroup from nova import utils +UUID_FOR_ID_MIN_VERSION = '2.53' + class ServiceController(wsgi.Controller): @@ -67,7 +71,7 @@ class ServiceController(wsgi.Controller): return _services - def _get_service_detail(self, svc, additional_fields): + def _get_service_detail(self, svc, additional_fields, req): alive = self.servicegroup_api.service_is_up(svc) state = (alive and "up") or "down" active = 'enabled' @@ -75,9 +79,22 @@ class ServiceController(wsgi.Controller): active = 'disabled' updated_time = self.servicegroup_api.get_updated_time(svc) + uuid_for_id = api_version_request.is_supported( + req, min_version=UUID_FOR_ID_MIN_VERSION) + + if 'availability_zone' not in svc: + # The service wasn't loaded with the AZ so we need to do it here. + # Yes this looks weird, but set_availability_zones makes a copy of + # the list passed in and mutates the objects within it, so we have + # to pull it back out from the resulting copied list. + svc.availability_zone = ( + availability_zones.set_availability_zones( + req.environ['nova.context'], + [svc])[0]['availability_zone']) + service_detail = {'binary': svc['binary'], 'host': svc['host'], - 'id': svc['id'], + 'id': svc['uuid' if uuid_for_id else 'id'], 'zone': svc['availability_zone'], 'status': active, 'state': state, @@ -91,7 +108,7 @@ class ServiceController(wsgi.Controller): def _get_services_list(self, req, additional_fields=()): _services = self._get_services(req) - return [self._get_service_detail(svc, additional_fields) + return [self._get_service_detail(svc, additional_fields, req) for svc in _services] def _enable(self, body, context): @@ -179,10 +196,17 @@ class ServiceController(wsgi.Controller): context = req.environ['nova.context'] context.can(services_policies.BASE_POLICY_NAME) - try: - utils.validate_integer(id, 'id') - except exception.InvalidInput as exc: - raise webob.exc.HTTPBadRequest(explanation=exc.format_message()) + if api_version_request.is_supported( + req, min_version=UUID_FOR_ID_MIN_VERSION): + if not uuidutils.is_uuid_like(id): + msg = _('Invalid uuid %s') % id + raise webob.exc.HTTPBadRequest(explanation=msg) + else: + try: + utils.validate_integer(id, 'id') + except exception.InvalidInput as exc: + raise webob.exc.HTTPBadRequest( + explanation=exc.format_message()) try: service = self.host_api.service_get_by_id(context, id) @@ -215,11 +239,18 @@ class ServiceController(wsgi.Controller): return {'services': _services} + @wsgi.Controller.api_version('2.1', '2.52') @extensions.expected_errors((400, 404)) @validation.schema(services.service_update, '2.0', '2.10') - @validation.schema(services.service_update_v211, '2.11') + @validation.schema(services.service_update_v211, '2.11', '2.52') def update(self, req, id, body): - """Perform service update""" + """Perform service update + + Before microversion 2.53, the body contains a host and binary value + to identify the service on which to perform the action. There is no + service ID passed on the path, just the action, for example + PUT /os-services/disable. + """ if api_version_request.is_supported(req, min_version='2.11'): actions = self.actions.copy() actions["force-down"] = self._forced_down @@ -227,3 +258,89 @@ class ServiceController(wsgi.Controller): actions = self.actions return self._perform_action(req, id, body, actions) + + @wsgi.Controller.api_version(UUID_FOR_ID_MIN_VERSION) # noqa F811 + @extensions.expected_errors((400, 404)) + @validation.schema(services.service_update_v2_53, UUID_FOR_ID_MIN_VERSION) + def update(self, req, id, body): + """Perform service update + + Starting with microversion 2.53, the service uuid is passed in on the + path of the request to uniquely identify the service record on which to + perform a given update, which is defined in the body of the request. + """ + service_id = id + # Validate that the service ID is a UUID. + if not uuidutils.is_uuid_like(service_id): + msg = _('Invalid uuid %s') % service_id + raise webob.exc.HTTPBadRequest(explanation=msg) + + # Validate the request context against the policy. + context = req.environ['nova.context'] + context.can(services_policies.BASE_POLICY_NAME) + + # Get the service by uuid. + try: + service = self.host_api.service_get_by_id(context, service_id) + # At this point the context is targeted to the cell that the + # service was found in so we don't need to do any explicit cell + # targeting below. + except exception.ServiceNotFound as e: + raise webob.exc.HTTPNotFound(explanation=e.format_message()) + + # Return 400 if service.binary is not nova-compute. + # Before the earlier PUT handlers were made cells-aware, you could + # technically disable a nova-scheduler service, although that doesn't + # really do anything within Nova and is just confusing. Now trying to + # do that will fail as a nova-scheduler service won't have a host + # mapping so you'll get a 404. In this new microversion, we close that + # old gap and make sure you can only enable/disable and set forced_down + # on nova-compute services since those are the only ones that make + # sense to update for those operations. + if service.binary != 'nova-compute': + msg = (_('Updating a %(binary)s service is not supported. Only ' + 'nova-compute services can be updated.') % + {'binary': service.binary}) + raise webob.exc.HTTPBadRequest(explanation=msg) + + # Now determine the update to perform based on the body. We are + # intentionally not using _perform_action or the other old-style + # action functions. + if 'status' in body: + # This is a status update for either enabled or disabled. + if body['status'] == 'enabled': + + # Fail if 'disabled_reason' was requested when enabling the + # service since those two combined don't make sense. + if body.get('disabled_reason'): + msg = _("Specifying 'disabled_reason' with status " + "'enabled' is invalid.") + raise webob.exc.HTTPBadRequest(explanation=msg) + + service.disabled = False + service.disabled_reason = None + elif body['status'] == 'disabled': + service.disabled = True + # The disabled reason is optional. + service.disabled_reason = body.get('disabled_reason') + + # This is intentionally not an elif, i.e. it's in addition to the + # status update. + if 'forced_down' in body: + service.forced_down = strutils.bool_from_string( + body['forced_down'], strict=True) + + # Check to see if anything was actually updated since the schema does + # not define any required fields. + if not service.obj_what_changed(): + msg = _("No updates were requested. Fields 'status' or " + "'forced_down' should be specified.") + raise webob.exc.HTTPBadRequest(explanation=msg) + + # Now save our updates to the service record in the database. + service.save() + + # Return the full service record details. + additional_fields = ['forced_down'] + return {'service': self._get_service_detail( + service, additional_fields, req)} diff --git a/nova/cells/utils.py b/nova/cells/utils.py index 7c90784585..b3c29f3821 100644 --- a/nova/cells/utils.py +++ b/nova/cells/utils.py @@ -69,6 +69,20 @@ class _CellProxy(object): return getattr(self._obj, key) + def __contains__(self, key): + """Pass-through "in" check to the wrapped object. + + This is needed to proxy any types of checks in the calling code + like:: + + if 'availability_zone' in service: + ... + + :param key: They key to look for in the wrapped object. + :returns: True if key is in the wrapped object, False otherwise. + """ + return key in self._obj + def obj_to_primitive(self): obj_p = self._obj.obj_to_primitive() obj_p['cell_proxy.class_name'] = self.__class__.__name__ diff --git a/nova/policies/services.py b/nova/policies/services.py index a6c5cd815d..fcec2483f4 100644 --- a/nova/policies/services.py +++ b/nova/policies/services.py @@ -50,6 +50,11 @@ services_policies = [ 'method': 'PUT', 'path': '/os-services/force-down' }, + { + # Added in microversion 2.53. + 'method': 'PUT', + 'path': '/os-services/{service_id}' + }, { 'method': 'DELETE', 'path': '/os-services/{service_id}' diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-log-put-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-log-put-req.json.tpl new file mode 100644 index 0000000000..e9ec7d6227 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-log-put-req.json.tpl @@ -0,0 +1,4 @@ +{ + "status": "disabled", + "disabled_reason": "%(disabled_reason)s" +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-log-put-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-log-put-resp.json.tpl new file mode 100644 index 0000000000..64e57f6c56 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-log-put-resp.json.tpl @@ -0,0 +1,13 @@ +{ + "service": { + "id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339", + "binary": "nova-compute", + "disabled_reason": "maintenance", + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "%(strtime)s", + "forced_down": false, + "zone": "nova" + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-put-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-put-req.json.tpl new file mode 100644 index 0000000000..920ce18c58 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-put-req.json.tpl @@ -0,0 +1,3 @@ +{ + "status": "disabled" +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-put-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-put-resp.json.tpl new file mode 100644 index 0000000000..cbe862ca05 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-disable-put-resp.json.tpl @@ -0,0 +1,13 @@ +{ + "service": { + "id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339", + "binary": "nova-compute", + "disabled_reason": null, + "host": "host1", + "state": "up", + "status": "disabled", + "updated_at": "%(strtime)s", + "forced_down": false, + "zone": "nova" + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-enable-put-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-enable-put-req.json.tpl new file mode 100644 index 0000000000..fb16b19494 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-enable-put-req.json.tpl @@ -0,0 +1,3 @@ +{ + "status": "enabled" +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-enable-put-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-enable-put-resp.json.tpl new file mode 100644 index 0000000000..b301843949 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-enable-put-resp.json.tpl @@ -0,0 +1,13 @@ +{ + "service": { + "id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339", + "binary": "nova-compute", + "disabled_reason": null, + "host": "host1", + "state": "up", + "status": "enabled", + "updated_at": "2012-10-29T13:42:05.000000", + "forced_down": false, + "zone": "nova" + } +} \ No newline at end of file diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-force-down-put-req.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-force-down-put-req.json.tpl new file mode 100644 index 0000000000..5ccbb866f5 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-force-down-put-req.json.tpl @@ -0,0 +1,3 @@ +{ + "forced_down": %(forced_down)s +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-force-down-put-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-force-down-put-resp.json.tpl new file mode 100644 index 0000000000..e278f37002 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/service-force-down-put-resp.json.tpl @@ -0,0 +1,13 @@ +{ + "service": { + "id": "e81d66a4-ddd3-4aba-8a84-171d1cb4d339", + "binary": "nova-compute", + "disabled_reason": "test2", + "host": "host1", + "state": "down", + "status": "disabled", + "updated_at": "%(strtime)s", + "forced_down": true, + "zone": "nova" + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/services-list-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/services-list-get-resp.json.tpl new file mode 100644 index 0000000000..2d2261b3a5 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-services/v2.53/services-list-get-resp.json.tpl @@ -0,0 +1,48 @@ +{ + "services": [ + { + "binary": "nova-scheduler", + "disabled_reason": "test1", + "forced_down": false, + "host": "host1", + "id": "%(id)s", + "state": "up", + "status": "disabled", + "updated_at": "%(strtime)s", + "zone": "internal" + }, + { + "binary": "nova-compute", + "disabled_reason": "test2", + "forced_down": false, + "host": "host1", + "id": "%(id)s", + "state": "up", + "status": "disabled", + "updated_at": "%(strtime)s", + "zone": "nova" + }, + { + "binary": "nova-scheduler", + "disabled_reason": null, + "forced_down": false, + "host": "host2", + "id": "%(id)s", + "state": "down", + "status": "enabled", + "updated_at": "%(strtime)s", + "zone": "internal" + }, + { + "binary": "nova-compute", + "disabled_reason": "test4", + "forced_down": false, + "host": "host2", + "id": "%(id)s", + "state": "down", + "status": "disabled", + "updated_at": "%(strtime)s", + "zone": "nova" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/test_services.py b/nova/tests/functional/api_sample_tests/test_services.py index 9890c630d8..0b92b52ea0 100644 --- a/nova/tests/functional/api_sample_tests/test_services.py +++ b/nova/tests/functional/api_sample_tests/test_services.py @@ -15,6 +15,7 @@ from oslo_utils import fixture as utils_fixture +from nova import exception from nova.tests.functional.api_sample_tests import api_sample_base from nova.tests.unit.api.openstack.compute import test_services @@ -104,3 +105,57 @@ class ServicesV211JsonTest(ServicesJsonTest): 'service-force-down-put-req', subs) self._verify_response('service-force-down-put-resp', subs, response, 200) + + +class ServicesV253JsonTest(ServicesV211JsonTest): + microversion = '2.53' + scenarios = [('v2_53', {'api_major_version': 'v2.1'})] + + def setUp(self): + super(ServicesV253JsonTest, self).setUp() + + def db_service_get_by_uuid(ctxt, service_uuid): + for svc in test_services.fake_services_list: + if svc['uuid'] == service_uuid: + return svc + raise exception.ServiceNotFound(service_id=service_uuid) + self.stub_out('nova.db.service_get_by_uuid', db_service_get_by_uuid) + + def test_service_enable(self): + """Enable an existing service.""" + response = self._do_put( + 'os-services/%s' % test_services.FAKE_UUID_COMPUTE_HOST1, + 'service-enable-put-req', subs={}) + self._verify_response('service-enable-put-resp', {}, response, 200) + + def test_service_disable(self): + """Disable an existing service.""" + response = self._do_put( + 'os-services/%s' % test_services.FAKE_UUID_COMPUTE_HOST1, + 'service-disable-put-req', subs={}) + self._verify_response('service-disable-put-resp', {}, response, 200) + + def test_service_disable_log_reason(self): + """Disable an existing service and log the reason.""" + subs = {'disabled_reason': 'maintenance'} + response = self._do_put( + 'os-services/%s' % test_services.FAKE_UUID_COMPUTE_HOST1, + 'service-disable-log-put-req', subs) + self._verify_response('service-disable-log-put-resp', + subs, response, 200) + + def test_service_delete(self): + """Delete an existing service.""" + response = self._do_delete( + 'os-services/%s' % test_services.FAKE_UUID_COMPUTE_HOST1) + self.assertEqual(204, response.status_code) + self.assertEqual("", response.text) + + def test_force_down(self): + """Set forced_down flag""" + subs = {'forced_down': 'true'} + response = self._do_put( + 'os-services/%s' % test_services.FAKE_UUID_COMPUTE_HOST1, + 'service-force-down-put-req', subs) + self._verify_response('service-force-down-put-resp', subs, + response, 200) diff --git a/nova/tests/functional/notification_sample_tests/notification_sample_base.py b/nova/tests/functional/notification_sample_tests/notification_sample_base.py index ecfe3c0d2e..70488e41e7 100644 --- a/nova/tests/functional/notification_sample_tests/notification_sample_base.py +++ b/nova/tests/functional/notification_sample_tests/notification_sample_base.py @@ -54,6 +54,13 @@ class NotificationSampleTestBase(test.TestCase, REQUIRES_LOCKING = True + # NOTE(gibi): Notification payloads always reflect the data needed + # for every supported API microversion so we can safe to use the latest + # API version in the tests. This helps the test to use the new API + # features too. This can be overridden by subclasses that need to cap + # at a specific microversion for older APIs. + MAX_MICROVERSION = 'latest' + def setUp(self): super(NotificationSampleTestBase, self).setUp() @@ -63,11 +70,7 @@ class NotificationSampleTestBase(test.TestCase, self.api = api_fixture.api self.admin_api = api_fixture.admin_api - # NOTE(gibi): Notification payloads always reflect the data needed - # for every supported API microversion so we can safe to use the latest - # API version in the tests. This helps the test to use the new API - # features too. - max_version = 'latest' + max_version = self.MAX_MICROVERSION self.api.microversion = max_version self.admin_api.microversion = max_version diff --git a/nova/tests/functional/notification_sample_tests/test_service_update.py b/nova/tests/functional/notification_sample_tests/test_service_update.py index fa17109dea..2a01254432 100644 --- a/nova/tests/functional/notification_sample_tests/test_service_update.py +++ b/nova/tests/functional/notification_sample_tests/test_service_update.py @@ -14,17 +14,22 @@ from oslo_utils import fixture as utils_fixture +from nova import exception from nova.tests import fixtures from nova.tests.functional.notification_sample_tests \ import notification_sample_base from nova.tests.unit.api.openstack.compute import test_services -class TestServiceUpdateNotificationSample( +class TestServiceUpdateNotificationSamplev2_52( notification_sample_base.NotificationSampleTestBase): + # These tests have to be capped at 2.52 since the PUT format changes in + # the 2.53 microversion. + MAX_MICROVERSION = '2.52' + def setUp(self): - super(TestServiceUpdateNotificationSample, self).setUp() + super(TestServiceUpdateNotificationSamplev2_52, self).setUp() self.stub_out("nova.db.service_get_by_host_and_binary", test_services.fake_service_get_by_host_binary) self.stub_out("nova.db.service_update", @@ -69,3 +74,51 @@ class TestServiceUpdateNotificationSample( 'disabled': True, 'disabled_reason': 'test2', 'uuid': self.service_uuid}) + + +class TestServiceUpdateNotificationSampleLatest( + TestServiceUpdateNotificationSamplev2_52): + """Tests the PUT /os-services/{service_id} API notifications.""" + + MAX_MICROVERSION = 'latest' + + def setUp(self): + super(TestServiceUpdateNotificationSampleLatest, self).setUp() + + def db_service_get_by_uuid(ctxt, service_uuid): + for svc in test_services.fake_services_list: + if svc['uuid'] == service_uuid: + return svc + raise exception.ServiceNotFound(service_id=service_uuid) + self.stub_out('nova.db.service_get_by_uuid', db_service_get_by_uuid) + + def test_service_enable(self): + body = {'status': 'enabled'} + self.admin_api.api_put('os-services/%s' % self.service_uuid, body) + self._verify_notification('service-update', + replacements={'uuid': self.service_uuid}) + + def test_service_disabled(self): + body = {'status': 'disabled'} + self.admin_api.api_put('os-services/%s' % self.service_uuid, body) + self._verify_notification('service-update', + replacements={'disabled': True, + 'uuid': self.service_uuid}) + + def test_service_disabled_log_reason(self): + body = {'status': 'disabled', + 'disabled_reason': 'test2'} + self.admin_api.api_put('os-services/%s' % self.service_uuid, body) + self._verify_notification('service-update', + replacements={'disabled': True, + 'disabled_reason': 'test2', + 'uuid': self.service_uuid}) + + def test_service_force_down(self): + body = {'forced_down': True} + self.admin_api.api_put('os-services/%s' % self.service_uuid, body) + self._verify_notification('service-update', + replacements={'forced_down': True, + 'disabled': True, + 'disabled_reason': 'test2', + 'uuid': self.service_uuid}) diff --git a/nova/tests/unit/api/openstack/compute/test_services.py b/nova/tests/unit/api/openstack/compute/test_services.py index 38f4ba7f24..60f789600a 100644 --- a/nova/tests/unit/api/openstack/compute/test_services.py +++ b/nova/tests/unit/api/openstack/compute/test_services.py @@ -19,6 +19,7 @@ import datetime import iso8601 import mock from oslo_utils import fixture as utils_fixture +import six import webob.exc from nova.api.openstack import api_version_request as api_version @@ -36,6 +37,10 @@ from nova import test from nova.tests import fixtures from nova.tests.unit.api.openstack import fakes from nova.tests.unit.objects import test_service +from nova.tests import uuidsentinel + +# This is tied into the os-services API samples functional tests. +FAKE_UUID_COMPUTE_HOST1 = 'e81d66a4-ddd3-4aba-8a84-171d1cb4d339' fake_services_list = [ @@ -43,6 +48,7 @@ fake_services_list = [ binary='nova-scheduler', host='host1', id=1, + uuid=uuidsentinel.svc1, disabled=True, topic='scheduler', updated_at=datetime.datetime(2012, 10, 29, 13, 42, 2), @@ -54,6 +60,7 @@ fake_services_list = [ binary='nova-compute', host='host1', id=2, + uuid=FAKE_UUID_COMPUTE_HOST1, disabled=True, topic='compute', updated_at=datetime.datetime(2012, 10, 29, 13, 42, 5), @@ -65,6 +72,7 @@ fake_services_list = [ binary='nova-scheduler', host='host2', id=3, + uuid=uuidsentinel.svc3, disabled=False, topic='scheduler', updated_at=datetime.datetime(2012, 9, 19, 6, 55, 34), @@ -76,6 +84,7 @@ fake_services_list = [ binary='nova-compute', host='host2', id=4, + uuid=uuidsentinel.svc4, disabled=True, topic='compute', updated_at=datetime.datetime(2012, 9, 18, 8, 3, 38), @@ -88,6 +97,7 @@ fake_services_list = [ binary='nova-osapi_compute', host='host2', id=5, + uuid=uuidsentinel.svc5, disabled=False, topic=None, updated_at=None, @@ -99,6 +109,7 @@ fake_services_list = [ binary='nova-metadata', host='host2', id=6, + uuid=uuidsentinel.svc6, disabled=False, topic=None, updated_at=None, @@ -927,6 +938,235 @@ class ServicesTestV211(ServicesTestV21): self.controller.update, req, 'force-down', body=req_body) +class ServicesTestV252(ServicesTestV211): + """This is a boundary test to ensure that 2.52 behaves the same as 2.11.""" + wsgi_api_version = '2.52' + + +class FakeServiceGroupAPI(object): + def service_is_up(self, *args, **kwargs): + return True + + def get_updated_time(self, *args, **kwargs): + return mock.sentinel.updated_time + + +class ServicesTestV253(test.TestCase): + """Tests for the 2.53 microversion in the os-services API.""" + + def setUp(self): + super(ServicesTestV253, self).setUp() + self.controller = services_v21.ServiceController() + self.controller.servicegroup_api = FakeServiceGroupAPI() + self.req = fakes.HTTPRequest.blank( + '', version=services_v21.UUID_FOR_ID_MIN_VERSION) + + def assert_services_equal(self, s1, s2): + for k in ('binary', 'host'): + self.assertEqual(s1[k], s2[k]) + + def test_list_has_uuid_in_id_field(self): + """Tests that a GET response includes an id field but the value is + the service uuid rather than the id integer primary key. + """ + service_uuids = [s['uuid'] for s in fake_services_list] + with mock.patch.object( + self.controller.host_api, 'service_get_all', + side_effect=fake_service_get_all(fake_services_list)): + resp = self.controller.index(self.req) + + for service in resp['services']: + # Make sure a uuid field wasn't returned. + self.assertNotIn('uuid', service) + # Make sure the id field is one of our known uuids. + self.assertIn(service['id'], service_uuids) + # Make sure this service was in our known list of fake services. + expected = next(iter(filter( + lambda s: s['uuid'] == service['id'], + fake_services_list))) + self.assert_services_equal(expected, service) + + def test_delete_takes_uuid_for_id(self): + """Tests that a DELETE request correctly deletes a service when a valid + service uuid is provided for an existing service. + """ + service = self.start_service( + 'compute', 'fake-compute-host').service_ref + with mock.patch.object(self.controller.host_api, + 'service_delete') as service_delete: + self.controller.delete(self.req, service.uuid) + service_delete.assert_called_once_with( + self.req.environ['nova.context'], service.uuid) + self.assertEqual(204, self.controller.delete.wsgi_code) + + def test_delete_uuid_not_found(self): + """Tests that we get a 404 response when attempting to delete a service + that is not found by the given uuid. + """ + self.assertRaises(webob.exc.HTTPNotFound, + self.controller.delete, self.req, uuidsentinel.svc2) + + def test_delete_invalid_uuid(self): + """Tests that the service uuid is validated in a DELETE request.""" + ex = self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.delete, self.req, 1234) + self.assertIn('Invalid uuid', six.text_type(ex)) + + def test_update_invalid_service_uuid(self): + """Tests that the service uuid is validated in a PUT request.""" + ex = self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, self.req, 1234, body={}) + self.assertIn('Invalid uuid', six.text_type(ex)) + + def test_update_policy_failed(self): + """Tests that policy is checked with microversion 2.53.""" + rule_name = "os_compute_api:os-services" + self.policy.set_rules({rule_name: "project_id:non_fake"}) + exc = self.assertRaises( + exception.PolicyNotAuthorized, + self.controller.update, self.req, uuidsentinel.service_uuid, + body={}) + self.assertEqual( + "Policy doesn't allow %s to be performed." % rule_name, + exc.format_message()) + + def test_update_service_not_found(self): + """Tests that we get a 404 response if the service is not found by + the given uuid when handling a PUT request. + """ + self.assertRaises(webob.exc.HTTPNotFound, self.controller.update, + self.req, uuidsentinel.service_uuid, body={}) + + def test_update_invalid_status(self): + """Tests that jsonschema validates the status field in the request body + and fails if it's not "enabled" or "disabled". + """ + service = self.start_service( + 'compute', 'fake-compute-host').service_ref + self.assertRaises( + exception.ValidationError, self.controller.update, self.req, + service.uuid, body={'status': 'invalid'}) + + def test_update_disabled_no_reason_then_enable(self): + """Tests disabling a service with no reason given. Then enables it + to see the change in the response body. + """ + service = self.start_service( + 'compute', 'fake-compute-host').service_ref + resp = self.controller.update(self.req, service.uuid, + body={'status': 'disabled'}) + expected_resp = { + 'service': { + 'status': 'disabled', + 'state': 'up', + 'binary': 'nova-compute', + 'host': 'fake-compute-host', + 'zone': 'nova', # Comes from CONF.default_availability_zone + 'updated_at': mock.sentinel.updated_time, + 'disabled_reason': None, + 'id': service.uuid, + 'forced_down': False + } + } + self.assertDictEqual(expected_resp, resp) + + # Now enable the service to see the response change. + req = fakes.HTTPRequest.blank( + '', version=services_v21.UUID_FOR_ID_MIN_VERSION) + resp = self.controller.update(req, service.uuid, + body={'status': 'enabled'}) + expected_resp['service']['status'] = 'enabled' + self.assertDictEqual(expected_resp, resp) + + def test_update_enable_with_disabled_reason_fails(self): + """Validates that requesting to both enable a service and set the + disabled_reason results in a 400 BadRequest error. + """ + service = self.start_service( + 'compute', 'fake-compute-host').service_ref + ex = self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, self.req, service.uuid, + body={'status': 'enabled', + 'disabled_reason': 'invalid'}) + self.assertIn("Specifying 'disabled_reason' with status 'enabled' " + "is invalid.", six.text_type(ex)) + + def test_update_disabled_reason_and_forced_down(self): + """Tests disabling a service with a reason and forcing it down is + reflected back in the response. + """ + service = self.start_service( + 'compute', 'fake-compute-host').service_ref + resp = self.controller.update(self.req, service.uuid, + body={'status': 'disabled', + 'disabled_reason': 'maintenance', + # Also tests bool_from_string usage + 'forced_down': 'yes'}) + expected_resp = { + 'service': { + 'status': 'disabled', + 'state': 'up', + 'binary': 'nova-compute', + 'host': 'fake-compute-host', + 'zone': 'nova', # Comes from CONF.default_availability_zone + 'updated_at': mock.sentinel.updated_time, + 'disabled_reason': 'maintenance', + 'id': service.uuid, + 'forced_down': True + } + } + self.assertDictEqual(expected_resp, resp) + + def test_update_forced_down_invalid_value(self): + """Tests that passing an invalid value for forced_down results in + a validation error. + """ + service = self.start_service( + 'compute', 'fake-compute-host').service_ref + self.assertRaises(exception.ValidationError, + self.controller.update, + self.req, service.uuid, + body={'status': 'disabled', + 'disabled_reason': 'maintenance', + 'forced_down': 'invalid'}) + + def test_update_forced_down_invalid_service(self): + """Tests that you can't update a non-nova-compute service.""" + service = self.start_service( + 'scheduler', 'fake-scheduler-host').service_ref + ex = self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, + self.req, service.uuid, + body={'forced_down': True}) + self.assertEqual('Updating a nova-scheduler service is not supported. ' + 'Only nova-compute services can be updated.', + six.text_type(ex)) + + def test_update_empty_body(self): + """Tests that the caller gets a 400 error if they don't request any + updates. + """ + service = self.start_service('compute').service_ref + ex = self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, + self.req, service.uuid, body={}) + self.assertEqual("No updates were requested. Fields 'status' or " + "'forced_down' should be specified.", + six.text_type(ex)) + + def test_update_only_disabled_reason(self): + """Tests that the caller gets a 400 error if they only specify + disabled_reason but don't also specify status='disabled'. + """ + service = self.start_service('compute').service_ref + ex = self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.update, self.req, service.uuid, + body={'disabled_reason': 'missing status'}) + self.assertEqual("No updates were requested. Fields 'status' or " + "'forced_down' should be specified.", + six.text_type(ex)) + + class ServicesCellsTestV21(test.TestCase): def setUp(self): diff --git a/releasenotes/notes/bp-service-hyper-uuid-in-api-cc7b9f21cc458e1b.yaml b/releasenotes/notes/bp-service-hyper-uuid-in-api-cc7b9f21cc458e1b.yaml new file mode 100644 index 0000000000..3efd1aa3ad --- /dev/null +++ b/releasenotes/notes/bp-service-hyper-uuid-in-api-cc7b9f21cc458e1b.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Microversion 2.53 changes service IDs to UUIDs to ensure uniqueness across + cells. Prior to this, ID collisions were possible in multi-cell + deployments. See the `REST API Version History`_ and + `Compute API reference`_ for details. + + .. _REST API Version History: https://docs.openstack.org/developer/nova/api_microversion_history.html + .. _Compute API reference: https://developer.openstack.org/api-ref/compute/ \ No newline at end of file