From 0c480d795f850b1eb940508823d68f67708bde90 Mon Sep 17 00:00:00 2001 From: Kevin_Zheng Date: Tue, 7 Jun 2016 17:24:27 +0800 Subject: [PATCH] Add pagination and changes-since for instance-actions This patch adds pagination support and changes-since filter for os-instance-actions API. Users can now use 'limit' and 'marker' to perform paginate query of instance action list. Users can also filter the results according to the actions' updated time. Co-Authored-By: Yikun Jiang Implement: blueprint pagination-add-changes-since-for-instance-action-list Change-Id: I1a1b39803e8d0449f21d2ab5ef96d4060e638aa8 --- api-ref/source/os-instance-actions.inc | 12 ++- api-ref/source/parameters.yaml | 65 ++++++++++++++++ .../instance-action-get-non-admin-resp.json | 20 +++++ .../v2.58/instance-action-get-resp.json | 21 +++++ .../v2.58/instance-actions-list-resp.json | 24 ++++++ ...instance-actions-list-with-limit-resp.json | 20 +++++ ...nstance-actions-list-with-marker-resp.json | 14 ++++ ...ce-actions-list-with-timestamp-filter.json | 14 ++++ .../versions/v21-version-get-resp.json | 2 +- .../versions/versions-get-resp.json | 2 +- nova/api/openstack/api_version_request.py | 4 +- .../api/openstack/compute/instance_actions.py | 56 ++++++++++++- .../compute/rest_api_version_history.rst | 9 +++ .../compute/schemas/instance_actions.py | 29 +++++++ .../compute/views/instance_actions.py | 23 ++++++ nova/compute/api.py | 5 +- nova/compute/cells_api.py | 4 +- ...nstance-action-get-non-admin-resp.json.tpl | 20 +++++ .../v2.58/instance-action-get-resp.json.tpl | 21 +++++ .../v2.58/instance-actions-list-resp.json.tpl | 24 ++++++ ...ance-actions-list-with-limit-resp.json.tpl | 20 +++++ ...nce-actions-list-with-marker-resp.json.tpl | 14 ++++ ...ctions-list-with-timestamp-filter.json.tpl | 14 ++++ .../api_sample_tests/test_instance_actions.py | 73 +++++++++++++++++ .../tests/functional/api_samples_test_base.py | 2 + .../compute/test_instance_actions.py | 78 +++++++++++++++++-- ...for-instance-actions-1c14cb3fc9887d2a.yaml | 8 ++ 27 files changed, 580 insertions(+), 18 deletions(-) create mode 100644 doc/api_samples/os-instance-actions/v2.58/instance-action-get-non-admin-resp.json create mode 100644 doc/api_samples/os-instance-actions/v2.58/instance-action-get-resp.json create mode 100644 doc/api_samples/os-instance-actions/v2.58/instance-actions-list-resp.json create mode 100644 doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-limit-resp.json create mode 100644 doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-marker-resp.json create mode 100644 doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-timestamp-filter.json create mode 100644 nova/api/openstack/compute/schemas/instance_actions.py create mode 100644 nova/api/openstack/compute/views/instance_actions.py create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-action-get-non-admin-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-action-get-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-limit-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-marker-resp.json.tpl create mode 100644 nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-timestamp-filter.json.tpl create mode 100644 releasenotes/notes/bp-add-pagination-for-instance-actions-1c14cb3fc9887d2a.yaml diff --git a/api-ref/source/os-instance-actions.inc b/api-ref/source/os-instance-actions.inc index c2ac18b5f780..2bb5b043e815 100644 --- a/api-ref/source/os-instance-actions.inc +++ b/api-ref/source/os-instance-actions.inc @@ -22,7 +22,7 @@ through the ``policy.json`` file. Normal response codes: 200 -Error response codes: unauthorized(401), forbidden(403), itemNotFound(404) +Error response codes: badRequest(400), unauthorized(401), forbidden(403), itemNotFound(404) Request ------- @@ -31,6 +31,9 @@ Request - server_id: server_id_path + - limit: instance_action_limit + - marker: instance_action_marker + - changes-since: changes_since_instance_action Response -------- @@ -46,12 +49,18 @@ Response - request_id: request_id_body - start_time: start_time - user_id: user_id + - updated_at: updated_instance_action + - instance_actions_links: instance_actions_next_links **Example List Actions For Server: JSON response** .. literalinclude:: ../../doc/api_samples/os-instance-actions/instance-actions-list-resp.json :language: javascript +**Example List Actions For Server With Links (v2.58):** + +.. literalinclude:: ../../doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-limit-resp.json + :language: javascript Show Server Action Details ========================== @@ -102,6 +111,7 @@ Response - events.finish_time: event_finish_time - events.result: event_result - events.traceback: event_traceback + - updated_at: updated_instance_action **Example Show Server Action Details For Admin (v2.1)** diff --git a/api-ref/source/parameters.yaml b/api-ref/source/parameters.yaml index ac76c23536bb..a672381ae18c 100644 --- a/api-ref/source/parameters.yaml +++ b/api-ref/source/parameters.yaml @@ -429,6 +429,23 @@ changes-since: in: query required: false type: string +changes_since_instance_action: + description: | + Filters the response by a date and time stamp when the instance action last + changed. + + The date and time stamp format is `ISO 8601 `_: + :: + + CCYY-MM-DDThh:mm:ss±hh:mm + + The ``±hh:mm`` value, if included, returns the time zone as an offset from UTC. + For example, ``2015-08-27T09:49:58-05:00``. + If you omit the time zone, the UTC time zone is assumed. + in: query + required: false + type: string + min_version: 2.58 changes_since_server: description: | Filters the response by a date and time stamp when the server last @@ -679,6 +696,26 @@ include: in: query required: false type: string +instance_action_limit: + description: | + Requests a page size of items. Returns a number of items up to a limit value. + Use the ``limit`` parameter to make an initial limited request and use the + last-seen item from the response as the ``marker`` parameter value in a + subsequent limited request. + in: query + required: false + type: integer + min_version: 2.58 +instance_action_marker: + description: | + The ``request_id`` of the last-seen instance action. Use the ``limit`` + parameter to make an initial limited request and use the last-seen + item from the response as the ``marker`` parameter value in a subsequent + limited request. + in: query + required: false + type: string + min_version: 2.58 ip6_query: description: | An IPv6 address to filter results by. @@ -3398,6 +3435,17 @@ instance_action_events_2_51: required: true type: array min_version: 2.51 +instance_actions_next_links: + description: | + Links pertaining to the instance action. + This parameter is returned when paging and more data is available. + See `API Guide / Links and References + `_ + for more info. + in: body + required: false + type: array + min_version: 2.58 instance_id_body: description: | The UUID of the server. @@ -5794,6 +5842,23 @@ updated_consider_null: in: body required: true type: string +updated_instance_action: + description: | + The date and time when the instance action or the action event of + instance action was updated. The date and time stamp format is + `ISO 8601 `_ + + :: + + CCYY-MM-DDThh:mm:ss±hh:mm + + For example, ``2015-08-27T09:49:58-05:00``. The ``±hh:mm`` + value, if included, is the time zone as an offset from UTC. In + the previous example, the offset value is ``-05:00``. + in: body + required: true + type: string + min_version: 2.58 updated_version: description: | This is a fixed string. It is ``2011-01-21T11:33:21Z`` in version 2.0, diff --git a/doc/api_samples/os-instance-actions/v2.58/instance-action-get-non-admin-resp.json b/doc/api_samples/os-instance-actions/v2.58/instance-action-get-non-admin-resp.json new file mode 100644 index 000000000000..70bd2d3840dd --- /dev/null +++ b/doc/api_samples/os-instance-actions/v2.58/instance-action-get-non-admin-resp.json @@ -0,0 +1,20 @@ +{ + "instanceAction": { + "action": "stop", + "events": [ + { + "event": "compute_stop_instance", + "finish_time": "2017-12-07T11:07:06.431902", + "result": "Success", + "start_time": "2017-12-07T11:07:06.251280" + } + ], + "instance_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13", + "message": null, + "project_id": "6f70656e737461636b20342065766572", + "request_id": "req-3293a3f1-b44c-4609-b8d2-d81b105636b8", + "start_time": "2017-12-07T11:07:06.088644", + "updated_at": "2017-12-07T11:07:06.431902", + "user_id": "fake" + } +} diff --git a/doc/api_samples/os-instance-actions/v2.58/instance-action-get-resp.json b/doc/api_samples/os-instance-actions/v2.58/instance-action-get-resp.json new file mode 100644 index 000000000000..249a08e7774a --- /dev/null +++ b/doc/api_samples/os-instance-actions/v2.58/instance-action-get-resp.json @@ -0,0 +1,21 @@ +{ + "instanceAction": { + "action": "stop", + "events": [ + { + "event": "compute_stop_instance", + "finish_time": "2017-12-07T11:07:06.431902", + "result": "Success", + "start_time": "2017-12-07T11:07:06.251280", + "traceback": null + } + ], + "instance_uuid": "b48316c5-71e8-45e4-9884-6c78055b9b13", + "message": "", + "project_id": "6f70656e737461636b20342065766572", + "request_id": "req-3293a3f1-b44c-4609-b8d2-d81b105636b8", + "start_time": "2017-12-07T11:07:06.088644", + "updated_at": "2017-12-07T11:07:06.431902", + "user_id": "fake" + } +} diff --git a/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-resp.json b/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-resp.json new file mode 100644 index 000000000000..2956ad5f2532 --- /dev/null +++ b/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-resp.json @@ -0,0 +1,24 @@ +{ + "instanceActions": [ + { + "instance_uuid": "e357e6d8-952e-4d1d-b74f-c8519e937706", + "user_id": "fake", + "start_time": "2017-12-07T11:07:06.088644", + "updated_at": "2017-12-07T11:07:06.431902", + "request_id": "req-e80018f1-c5bd-45ee-aaa9-290f2f5ef7bc", + "action": "stop", + "message": null, + "project_id": "6f70656e737461636b20342065766572" + }, + { + "instance_uuid": "e357e6d8-952e-4d1d-b74f-c8519e937706", + "user_id": "fake", + "start_time": "2017-12-07T11:07:04.313653", + "updated_at": "2017-12-07T11:07:06.058351", + "request_id": "req-c8fd339d-d2bf-43c2-a98a-84328281f83e", + "action": "create", + "message": null, + "project_id": "6f70656e737461636b20342065766572" + } + ] +} diff --git a/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-limit-resp.json b/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-limit-resp.json new file mode 100644 index 000000000000..ba5b47127637 --- /dev/null +++ b/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-limit-resp.json @@ -0,0 +1,20 @@ +{ + "instanceActions": [ + { + "instance_uuid": "e357e6d8-952e-4d1d-b74f-c8519e937706", + "user_id": "fake", + "start_time": "2017-12-07T11:07:06.088644", + "updated_at": "2017-12-07T11:07:06.431902", + "request_id": "req-e80018f1-c5bd-45ee-aaa9-290f2f5ef7bc", + "action": "stop", + "message": null, + "project_id": "6f70656e737461636b20342065766572" + } + ], + "links": [ + { + "href": "http://openstack.example.com/v2.1/6f70656e737461636b20342065766572/servers/e357e6d8-952e-4d1d-b74f-c8519e937706/os-instance-actions?limit=1&marker=req-e80018f1-c5bd-45ee-aaa9-290f2f5ef7bc", + "rel": "next" + } + ] +} diff --git a/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-marker-resp.json b/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-marker-resp.json new file mode 100644 index 000000000000..2f1eabe55e9e --- /dev/null +++ b/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-marker-resp.json @@ -0,0 +1,14 @@ +{ + "instanceActions": [ + { + "instance_uuid": "e357e6d8-952e-4d1d-b74f-c8519e937706", + "user_id": "fake", + "start_time": "2017-12-07T11:07:04.313653", + "updated_at": "2017-12-07T11:07:06.058351", + "request_id": "req-c8fd339d-d2bf-43c2-a98a-84328281f83e", + "action": "create", + "message": null, + "project_id": "6f70656e737461636b20342065766572" + } + ] +} diff --git a/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-timestamp-filter.json b/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-timestamp-filter.json new file mode 100644 index 000000000000..03aaf075603a --- /dev/null +++ b/doc/api_samples/os-instance-actions/v2.58/instance-actions-list-with-timestamp-filter.json @@ -0,0 +1,14 @@ +{ + "instanceActions": [ + { + "instance_uuid": "e357e6d8-952e-4d1d-b74f-c8519e937706", + "user_id": "fake", + "start_time": "2017-12-07T11:07:06.088644", + "updated_at": "2017-12-07T11:07:06.431902", + "request_id": "req-e80018f1-c5bd-45ee-aaa9-290f2f5ef7bc", + "action": "stop", + "message": null, + "project_id": "6f70656e737461636b20342065766572" + } + ] +} diff --git a/doc/api_samples/versions/v21-version-get-resp.json b/doc/api_samples/versions/v21-version-get-resp.json index 076594566224..141bce3d7eb2 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.57", + "version": "2.58", "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 4d4c7af4aafc..dafc1d0c009c 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.57", + "version": "2.58", "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 2dabb24c6281..85d9de756f0b 100644 --- a/nova/api/openstack/api_version_request.py +++ b/nova/api/openstack/api_version_request.py @@ -137,6 +137,8 @@ REST_API_VERSION_HISTORY = """REST API Version History: server action APIs. Added the ability to pass new user_data to the rebuild server action API. Personality / file injection related limits and quota resources are also removed. + * 2.58 - Add pagination support and changes-since filter for + os-instance-actions API. """ # The minimum and maximum versions of the API supported @@ -145,7 +147,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.57" +_MAX_API_VERSION = "2.58" 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/instance_actions.py b/nova/api/openstack/compute/instance_actions.py index 7f2ff0df9a13..4a9e7dbbf97f 100644 --- a/nova/api/openstack/compute/instance_actions.py +++ b/nova/api/openstack/compute/instance_actions.py @@ -15,30 +15,42 @@ from webob import exc +from oslo_utils import timeutils + from nova.api.openstack import api_version_request from nova.api.openstack import common +from nova.api.openstack.compute.schemas \ + import instance_actions as schema_instance_actions +from nova.api.openstack.compute.views \ + import instance_actions as instance_actions_view from nova.api.openstack import extensions from nova.api.openstack import wsgi +from nova.api import validation from nova import compute +from nova import exception from nova.i18n import _ from nova.policies import instance_actions as ia_policies from nova import utils + ACTION_KEYS = ['action', 'instance_uuid', 'request_id', 'user_id', 'project_id', 'start_time', 'message'] +ACTION_KEYS_V258 = ['action', 'instance_uuid', 'request_id', 'user_id', + 'project_id', 'start_time', 'message', 'updated_at'] EVENT_KEYS = ['event', 'start_time', 'finish_time', 'result', 'traceback'] class InstanceActionsController(wsgi.Controller): + _view_builder_class = instance_actions_view.ViewBuilder def __init__(self): super(InstanceActionsController, self).__init__() self.compute_api = compute.API() self.action_api = compute.InstanceActionAPI() - def _format_action(self, action_raw): + def _format_action(self, action_raw, action_keys): action = {} - for key in ACTION_KEYS: + for key in action_keys: action[key] = action_raw.get(key) return action @@ -60,6 +72,7 @@ class InstanceActionsController(wsgi.Controller): with utils.temporary_mutation(context, read_deleted='yes'): return common.get_instance(self.compute_api, context, server_id) + @wsgi.Controller.api_version("2.1", "2.57") @extensions.expected_errors(404) def index(self, req, server_id): """Returns the list of actions recorded for a given instance.""" @@ -67,9 +80,41 @@ class InstanceActionsController(wsgi.Controller): instance = self._get_instance(req, context, server_id) context.can(ia_policies.BASE_POLICY_NAME, instance) actions_raw = self.action_api.actions_get(context, instance) - actions = [self._format_action(action) for action in actions_raw] + actions = [self._format_action(action, ACTION_KEYS) + for action in actions_raw] return {'instanceActions': actions} + @wsgi.Controller.api_version("2.58") # noqa + @extensions.expected_errors((400, 404)) + @validation.query_schema(schema_instance_actions.list_query_params_v258, + "2.58") + def index(self, req, server_id): + """Returns the list of actions recorded for a given instance.""" + context = req.environ["nova.context"] + instance = self._get_instance(req, context, server_id) + context.can(ia_policies.BASE_POLICY_NAME, instance) + search_opts = {} + search_opts.update(req.GET) + if 'changes-since' in search_opts: + search_opts['changes-since'] = timeutils.parse_isotime( + search_opts['changes-since']) + + limit, marker = common.get_limit_and_marker(req) + try: + actions_raw = self.action_api.actions_get(context, instance, + limit=limit, + marker=marker, + filters=search_opts) + except exception.MarkerNotFound as e: + raise exc.HTTPBadRequest(explanation=e.format_message()) + actions = [self._format_action(action, ACTION_KEYS_V258) + for action in actions_raw] + actions_dict = {'instanceActions': actions} + actions_links = self._view_builder.get_links(req, server_id, actions) + if actions_links: + actions_dict['links'] = actions_links + return actions_dict + @extensions.expected_errors(404) def show(self, req, server_id, id): """Return data about the given instance action.""" @@ -83,7 +128,10 @@ class InstanceActionsController(wsgi.Controller): raise exc.HTTPNotFound(explanation=msg) action_id = action['id'] - action = self._format_action(action) + if api_version_request.is_supported(req, min_version="2.58"): + action = self._format_action(action, ACTION_KEYS_V258) + else: + action = self._format_action(action, ACTION_KEYS) # Prior to microversion 2.51, events would only be returned in the # response for admins by default policy rules. Starting in # microversion 2.51, events are returned for admin_or_owner (of the diff --git a/nova/api/openstack/compute/rest_api_version_history.rst b/nova/api/openstack/compute/rest_api_version_history.rst index 6ed3f62b5136..11ac45c526c9 100644 --- a/nova/api/openstack/compute/rest_api_version_history.rst +++ b/nova/api/openstack/compute/rest_api_version_history.rst @@ -729,3 +729,12 @@ The 2.57 microversion makes the following changes: * The ``injected_files``, ``injected_file_content_bytes`` and ``injected_file_path_bytes`` quotas are removed from the ``os-quota-sets`` and ``os-quota-class-sets`` APIs. + +2.58 +---- + + Add pagination support and ``changes-since`` filter for os-instance-actions + API. Users can now use ``limit`` and ``marker`` to perform paginated query + when listing instance actions. Users can also use ``changes-since`` filter + to filter the results based on the last time the instance action was + updated. diff --git a/nova/api/openstack/compute/schemas/instance_actions.py b/nova/api/openstack/compute/schemas/instance_actions.py new file mode 100644 index 000000000000..25e61ff61ac6 --- /dev/null +++ b/nova/api/openstack/compute/schemas/instance_actions.py @@ -0,0 +1,29 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.validation import parameter_types + +list_query_params_v258 = { + 'type': 'object', + 'properties': { + # The 2.58 microversion added support for paging by limit and marker + # and filtering by changes-since. + 'limit': parameter_types.single_param( + parameter_types.non_negative_integer), + 'marker': parameter_types.single_param({'type': 'string'}), + 'changes-since': parameter_types.single_param( + {'type': 'string', 'format': 'date-time'}), + }, + 'additionalProperties': False +} diff --git a/nova/api/openstack/compute/views/instance_actions.py b/nova/api/openstack/compute/views/instance_actions.py new file mode 100644 index 000000000000..fc164f5dc54b --- /dev/null +++ b/nova/api/openstack/compute/views/instance_actions.py @@ -0,0 +1,23 @@ +# Copyright 2017 Huawei Technologies Co.,LTD. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.openstack import common + + +class ViewBuilder(common.ViewBuilder): + + def get_links(self, request, server_id, instance_actions): + collection_name = 'servers/%s/os-instance-actions' % server_id + return self._get_collection_links(request, instance_actions, + collection_name, 'request_id') diff --git a/nova/compute/api.py b/nova/compute/api.py index a23c66a59c9b..ae44d458c5c2 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -4757,9 +4757,10 @@ class HostAPI(base.Base): class InstanceActionAPI(base.Base): """Sub-set of the Compute Manager API for managing instance actions.""" - def actions_get(self, context, instance): + def actions_get(self, context, instance, limit=None, marker=None, + filters=None): return objects.InstanceActionList.get_by_instance_uuid( - context, instance.uuid) + context, instance.uuid, limit, marker, filters) def action_get_by_request_id(self, context, instance, request_id): return objects.InstanceAction.get_by_request_id( diff --git a/nova/compute/cells_api.py b/nova/compute/cells_api.py index 345c881828f3..df006e7c15c9 100644 --- a/nova/compute/cells_api.py +++ b/nova/compute/cells_api.py @@ -676,7 +676,9 @@ class InstanceActionAPI(compute_api.InstanceActionAPI): super(InstanceActionAPI, self).__init__() self.cells_rpcapi = cells_rpcapi.CellsAPI() - def actions_get(self, context, instance): + def actions_get(self, context, instance, limit=None, marker=None, + filters=None): + # Paging and filtering isn't supported in cells v1. return self.cells_rpcapi.actions_get(context, instance) def action_get_by_request_id(self, context, instance, request_id): diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-action-get-non-admin-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-action-get-non-admin-resp.json.tpl new file mode 100644 index 000000000000..ea0f8524f499 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-action-get-non-admin-resp.json.tpl @@ -0,0 +1,20 @@ +{ + "instanceAction": { + "action": "stop", + "instance_uuid": "%(uuid)s", + "request_id": "%(request_id)s", + "user_id": "%(user_id)s", + "project_id": "%(project_id)s", + "start_time": "%(strtime)s", + "updated_at": "%(strtime)s", + "message": "", + "events": [ + { + "event": "compute_stop_instance", + "start_time": "%(strtime)s", + "finish_time": "%(strtime)s", + "result": "Success" + } + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-action-get-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-action-get-resp.json.tpl new file mode 100644 index 000000000000..8646fb8bcab9 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-action-get-resp.json.tpl @@ -0,0 +1,21 @@ +{ + "instanceAction": { + "action": "stop", + "instance_uuid": "%(uuid)s", + "request_id": "%(request_id)s", + "user_id": "%(user_id)s", + "project_id": "%(project_id)s", + "start_time": "%(strtime)s", + "updated_at": "%(strtime)s", + "message": "", + "events": [ + { + "event": "compute_stop_instance", + "start_time": "%(strtime)s", + "finish_time": "%(strtime)s", + "result": "Success", + "traceback": "" + } + ] + } +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-resp.json.tpl new file mode 100644 index 000000000000..99407aa08ada --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-resp.json.tpl @@ -0,0 +1,24 @@ +{ + "instanceActions": [ + { + "action": "stop", + "instance_uuid": "%(uuid)s", + "request_id": "%(request_id)s", + "user_id": "%(user_id)s", + "project_id": "%(project_id)s", + "start_time": "%(strtime)s", + "updated_at": "%(strtime)s", + "message": "" + }, + { + "action": "create", + "instance_uuid": "%(uuid)s", + "request_id": "%(request_id)s", + "user_id": "%(user_id)s", + "project_id": "%(project_id)s", + "start_time": "%(strtime)s", + "updated_at": "%(strtime)s", + "message": "" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-limit-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-limit-resp.json.tpl new file mode 100644 index 000000000000..3a66a24a1cb1 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-limit-resp.json.tpl @@ -0,0 +1,20 @@ +{ + "instanceActions": [ + { + "action": "stop", + "instance_uuid": "%(uuid)s", + "request_id": "%(request_id)s", + "user_id": "%(user_id)s", + "project_id": "%(project_id)s", + "start_time": "%(strtime)s", + "updated_at": "%(strtime)s", + "message": "" + } + ], + "links": [ + { + "href": "%(versioned_compute_endpoint)s/servers/%(uuid)s/os-instance-actions?limit=1&marker=%(request_id)s", + "rel": "next" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-marker-resp.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-marker-resp.json.tpl new file mode 100644 index 000000000000..2561555ead92 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-marker-resp.json.tpl @@ -0,0 +1,14 @@ +{ + "instanceActions": [ + { + "action": "create", + "instance_uuid": "%(uuid)s", + "request_id": "%(request_id)s", + "user_id": "%(user_id)s", + "project_id": "%(project_id)s", + "start_time": "%(strtime)s", + "updated_at": "%(strtime)s", + "message": "" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-timestamp-filter.json.tpl b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-timestamp-filter.json.tpl new file mode 100644 index 000000000000..441b3aa13064 --- /dev/null +++ b/nova/tests/functional/api_sample_tests/api_samples/os-instance-actions/v2.58/instance-actions-list-with-timestamp-filter.json.tpl @@ -0,0 +1,14 @@ +{ + "instanceActions": [ + { + "action": "stop", + "instance_uuid": "%(uuid)s", + "request_id": "%(request_id)s", + "user_id": "%(user_id)s", + "project_id": "%(project_id)s", + "start_time": "%(strtime)s", + "updated_at": "%(strtime)s", + "message": "" + } + ] +} diff --git a/nova/tests/functional/api_sample_tests/test_instance_actions.py b/nova/tests/functional/api_sample_tests/test_instance_actions.py index c5f8cb09929d..2d511ead271a 100644 --- a/nova/tests/functional/api_sample_tests/test_instance_actions.py +++ b/nova/tests/functional/api_sample_tests/test_instance_actions.py @@ -18,6 +18,9 @@ import copy import six from nova.tests.functional.api_sample_tests import api_sample_base +from nova.tests.functional.api_sample_tests import test_servers +from nova.tests.functional import api_samples_test_base +from nova.tests.functional import integrated_helpers from nova.tests.unit import fake_instance from nova.tests.unit import fake_server_actions from nova.tests.unit import utils as test_utils @@ -127,3 +130,73 @@ class ServerActionsV251NonAdminSampleJsonTest(ServerActionsSampleJsonTest): ADMIN_API = False microversion = '2.51' scenarios = [('v2_51', {'api_major_version': 'v2.1'})] + + +class ServerActionsV258SampleJsonTest(test_servers.ServersSampleBase, + integrated_helpers.InstanceHelperMixin): + microversion = '2.58' + scenarios = [('v2_58', {'api_major_version': 'v2.1'})] + sample_dir = 'os-instance-actions' + ADMIN_API = True + + def setUp(self): + super(ServerActionsV258SampleJsonTest, self).setUp() + # Create and stop a server + self.uuid = self._post_server() + self._get_response('servers/%s/action' % self.uuid, 'POST', + '{"os-stop": null}') + response = self._do_get('servers/%s/os-instance-actions' % self.uuid) + response_data = api_samples_test_base.pretty_data(response.content) + actions = api_samples_test_base.objectify(response_data) + self.action_stop = actions['instanceActions'][0] + self._wait_for_state_change(self.api, {'id': self.uuid}, 'SHUTOFF') + + def _get_subs(self): + return { + 'uuid': self.uuid, + 'project_id': self.action_stop['project_id'] + } + + def test_instance_action_get(self): + req_id = self.action_stop['request_id'] + response = self._do_get('servers/%s/os-instance-actions/%s' % + (self.uuid, req_id)) + # Non-admins can see event details except for the "traceback" field + # starting in the 2.51 microversion. + if self.ADMIN_API: + name = 'instance-action-get-resp' + else: + name = 'instance-action-get-non-admin-resp' + self._verify_response(name, self._get_subs(), response, 200) + + def test_instance_actions_list(self): + response = self._do_get('servers/%s/os-instance-actions' % self.uuid) + self._verify_response('instance-actions-list-resp', self._get_subs(), + response, 200) + + def test_instance_actions_list_with_limit(self): + response = self._do_get('servers/%s/os-instance-actions' + '?limit=1' % self.uuid) + self._verify_response('instance-actions-list-with-limit-resp', + self._get_subs(), response, 200) + + def test_instance_actions_list_with_marker(self): + + marker = self.action_stop['request_id'] + response = self._do_get('servers/%s/os-instance-actions' + '?marker=%s' % (self.uuid, marker)) + self._verify_response('instance-actions-list-with-marker-resp', + self._get_subs(), response, 200) + + def test_instance_actions_with_timestamp_filter(self): + stop_action_time = self.action_stop['start_time'] + response = self._do_get( + 'servers/%s/os-instance-actions' + '?changes-since=%s' % (self.uuid, stop_action_time)) + self._verify_response( + 'instance-actions-list-with-timestamp-filter', + self._get_subs(), response, 200) + + +class ServerActionsV258NonAdminSampleJsonTest(ServerActionsV258SampleJsonTest): + ADMIN_API = False diff --git a/nova/tests/functional/api_samples_test_base.py b/nova/tests/functional/api_samples_test_base.py index 4bf1f92cc99d..848f8ea9e522 100644 --- a/nova/tests/functional/api_samples_test_base.py +++ b/nova/tests/functional/api_samples_test_base.py @@ -434,6 +434,8 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase): '-[0-9a-f]{4}-[0-9a-f]{12})', 'uuid': '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}' '-[0-9a-f]{4}-[0-9a-f]{12}', + 'request_id': 'req-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}' + '-[0-9a-f]{4}-[0-9a-f]{12}', 'reservation_id': 'r-[0-9a-zA-Z]{8}', 'private_key': '(-----BEGIN RSA PRIVATE KEY-----|)' '[a-zA-Z0-9\n/+=]*' diff --git a/nova/tests/unit/api/openstack/compute/test_instance_actions.py b/nova/tests/unit/api/openstack/compute/test_instance_actions.py index 631c62cf155b..5f183ab2fd35 100644 --- a/nova/tests/unit/api/openstack/compute/test_instance_actions.py +++ b/nova/tests/unit/api/openstack/compute/test_instance_actions.py @@ -34,9 +34,11 @@ from nova.tests import uuidsentinel as uuids FAKE_UUID = fake_server_actions.FAKE_UUID FAKE_REQUEST_ID = fake_server_actions.FAKE_REQUEST_ID1 +FAKE_EVENT_ID = fake_server_actions.FAKE_ACTION_ID1 +FAKE_REQUEST_NOTFOUND_ID = 'req-' + uuids.req_not_found -def format_action(action): +def format_action(action, expect_traceback=True): '''Remove keys that aren't serialized.''' to_delete = ('id', 'finish_time', 'created_at', 'updated_at', 'deleted_at', 'deleted') @@ -47,14 +49,16 @@ def format_action(action): # NOTE(danms): Without WSGI above us, these will be just stringified action['start_time'] = str(action['start_time'].replace(tzinfo=None)) for event in action.get('events', []): - format_event(event) + format_event(event, expect_traceback) return action -def format_event(event): +def format_event(event, expect_traceback=True): '''Remove keys that aren't serialized.''' - to_delete = ('id', 'created_at', 'updated_at', 'deleted_at', 'deleted', - 'action_id') + to_delete = ['id', 'created_at', 'updated_at', 'deleted_at', 'deleted', + 'action_id'] + if not expect_traceback: + to_delete.append('traceback') for key in to_delete: if key in event: del(event[key]) @@ -109,6 +113,7 @@ class InstanceActionsPolicyTestV21(test.NoDBTestCase): class InstanceActionsTestV21(test.NoDBTestCase): instance_actions = instance_actions_v21 wsgi_api_version = os_wsgi.DEFAULT_API_VERSION + expect_events_non_admin = False def fake_get(self, context, instance_uuid, expected_attrs=None): return objects.Instance(uuid=instance_uuid) @@ -188,8 +193,13 @@ class InstanceActionsTestV21(test.NoDBTestCase): req = self._get_http_req('os-instance-actions/1') res_dict = self.controller.show(req, FAKE_UUID, FAKE_REQUEST_ID) fake_action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID] - self.assertEqual(format_action(fake_action), - format_action(res_dict['instanceAction'])) + if self.expect_events_non_admin: + fake_event = fake_server_actions.FAKE_EVENTS[FAKE_EVENT_ID] + fake_action['events'] = copy.deepcopy(fake_event) + # By default, non-admins are not allowed to see traceback details. + self.assertEqual(format_action(fake_action, expect_traceback=False), + format_action(res_dict['instanceAction'], + expect_traceback=False)) def test_action_not_found(self): def fake_no_action(context, uuid, action_id): @@ -223,3 +233,57 @@ class InstanceActionsTestV221(InstanceActionsTestV21): def fake_get(self, context, instance_uuid, expected_attrs=None): self.assertEqual('yes', context.read_deleted) return objects.Instance(uuid=instance_uuid) + + +class InstanceActionsTestV251(InstanceActionsTestV221): + wsgi_api_version = "2.51" + expect_events_non_admin = True + + +class InstanceActionsTestV258(InstanceActionsTestV251): + wsgi_api_version = "2.58" + + @mock.patch('nova.objects.InstanceActionList.get_by_instance_uuid') + def test_get_action_with_invalid_marker(self, mock_actions_get): + """Tests detail paging with an invalid marker (not found).""" + mock_actions_get.side_effect = exception.MarkerNotFound( + marker=FAKE_REQUEST_NOTFOUND_ID) + req = self._get_http_req('os-instance-actions?' + 'marker=%s' % FAKE_REQUEST_NOTFOUND_ID) + self.assertRaises(exc.HTTPBadRequest, + self.controller.index, req, FAKE_UUID) + + def test_get_action_with_invalid_limit(self): + """Tests get paging with an invalid limit.""" + req = self._get_http_req('os-instance-actions?limit=x') + self.assertRaises(exception.ValidationError, + self.controller.index, req) + req = self._get_http_req('os-instance-actions?limit=-1') + self.assertRaises(exception.ValidationError, + self.controller.index, req) + + def test_get_action_with_invalid_change_since(self): + """Tests get paging with a invalid change_since.""" + req = self._get_http_req('os-instance-actions?' + 'changes-since=wrong_time') + ex = self.assertRaises(exception.ValidationError, + self.controller.index, req) + self.assertIn('Invalid input for query parameters changes-since', + six.text_type(ex)) + + def test_get_action_with_invalid_params(self): + """Tests get paging with a invalid change_since.""" + req = self._get_http_req('os-instance-actions?' + 'wrong_params=xxx') + ex = self.assertRaises(exception.ValidationError, + self.controller.index, req) + self.assertIn('Additional properties are not allowed', + six.text_type(ex)) + + def test_get_action_with_multi_params(self): + """Tests get paging with multi markers.""" + req = self._get_http_req('os-instance-actions?marker=A&marker=B') + ex = self.assertRaises(exception.ValidationError, + self.controller.index, req) + self.assertIn('Invalid input for query parameters marker', + six.text_type(ex)) diff --git a/releasenotes/notes/bp-add-pagination-for-instance-actions-1c14cb3fc9887d2a.yaml b/releasenotes/notes/bp-add-pagination-for-instance-actions-1c14cb3fc9887d2a.yaml new file mode 100644 index 000000000000..90c68181beb0 --- /dev/null +++ b/releasenotes/notes/bp-add-pagination-for-instance-actions-1c14cb3fc9887d2a.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Add pagination support and ``changes-since`` filter for os-instance-actions + API. Users can now use ``limit`` and ``marker`` to perform paginated query + when listing instance actions. Users can also use ``changes-since`` filter + to filter the results based on the last time the instance action was + updated.