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 <yikunkero@gmail.com>

Implement: blueprint pagination-add-changes-since-for-instance-action-list

Change-Id: I1a1b39803e8d0449f21d2ab5ef96d4060e638aa8
This commit is contained in:
Kevin_Zheng 2016-06-07 17:24:27 +08:00 committed by Yikun Jiang (Kero)
parent d110ec5961
commit 0c480d795f
27 changed files with 580 additions and 18 deletions

View File

@ -22,7 +22,7 @@ through the ``policy.json`` file.
Normal response codes: 200 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 Request
------- -------
@ -31,6 +31,9 @@ Request
- server_id: server_id_path - server_id: server_id_path
- limit: instance_action_limit
- marker: instance_action_marker
- changes-since: changes_since_instance_action
Response Response
-------- --------
@ -46,12 +49,18 @@ Response
- request_id: request_id_body - request_id: request_id_body
- start_time: start_time - start_time: start_time
- user_id: user_id - user_id: user_id
- updated_at: updated_instance_action
- instance_actions_links: instance_actions_next_links
**Example List Actions For Server: JSON response** **Example List Actions For Server: JSON response**
.. literalinclude:: ../../doc/api_samples/os-instance-actions/instance-actions-list-resp.json .. literalinclude:: ../../doc/api_samples/os-instance-actions/instance-actions-list-resp.json
:language: javascript :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 Show Server Action Details
========================== ==========================
@ -102,6 +111,7 @@ Response
- events.finish_time: event_finish_time - events.finish_time: event_finish_time
- events.result: event_result - events.result: event_result
- events.traceback: event_traceback - events.traceback: event_traceback
- updated_at: updated_instance_action
**Example Show Server Action Details For Admin (v2.1)** **Example Show Server Action Details For Admin (v2.1)**

View File

@ -429,6 +429,23 @@ changes-since:
in: query in: query
required: false required: false
type: string 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 <https://en.wikipedia.org/wiki/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: changes_since_server:
description: | description: |
Filters the response by a date and time stamp when the server last Filters the response by a date and time stamp when the server last
@ -679,6 +696,26 @@ include:
in: query in: query
required: false required: false
type: string 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: ip6_query:
description: | description: |
An IPv6 address to filter results by. An IPv6 address to filter results by.
@ -3398,6 +3435,17 @@ instance_action_events_2_51:
required: true required: true
type: array type: array
min_version: 2.51 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
<http://developer.openstack.org/api-guide/compute/links_and_references.html>`_
for more info.
in: body
required: false
type: array
min_version: 2.58
instance_id_body: instance_id_body:
description: | description: |
The UUID of the server. The UUID of the server.
@ -5794,6 +5842,23 @@ updated_consider_null:
in: body in: body
required: true required: true
type: string 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 <https://en.wikipedia.org/wiki/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: updated_version:
description: | description: |
This is a fixed string. It is ``2011-01-21T11:33:21Z`` in version 2.0, This is a fixed string. It is ``2011-01-21T11:33:21Z`` in version 2.0,

View File

@ -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"
}
}

View File

@ -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"
}
}

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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"
}
]
}

View File

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

View File

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

View File

@ -137,6 +137,8 @@ REST_API_VERSION_HISTORY = """REST API Version History:
server action APIs. Added the ability to pass new user_data to server action APIs. Added the ability to pass new user_data to
the rebuild server action API. Personality / file injection the rebuild server action API. Personality / file injection
related limits and quota resources are also removed. 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 # 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 # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1" _MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.57" _MAX_API_VERSION = "2.58"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION
# Almost all proxy APIs which are related to network, images and baremetal # Almost all proxy APIs which are related to network, images and baremetal

View File

@ -15,30 +15,42 @@
from webob import exc from webob import exc
from oslo_utils import timeutils
from nova.api.openstack import api_version_request from nova.api.openstack import api_version_request
from nova.api.openstack import common 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 extensions
from nova.api.openstack import wsgi from nova.api.openstack import wsgi
from nova.api import validation
from nova import compute from nova import compute
from nova import exception
from nova.i18n import _ from nova.i18n import _
from nova.policies import instance_actions as ia_policies from nova.policies import instance_actions as ia_policies
from nova import utils from nova import utils
ACTION_KEYS = ['action', 'instance_uuid', 'request_id', 'user_id', ACTION_KEYS = ['action', 'instance_uuid', 'request_id', 'user_id',
'project_id', 'start_time', 'message'] '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'] EVENT_KEYS = ['event', 'start_time', 'finish_time', 'result', 'traceback']
class InstanceActionsController(wsgi.Controller): class InstanceActionsController(wsgi.Controller):
_view_builder_class = instance_actions_view.ViewBuilder
def __init__(self): def __init__(self):
super(InstanceActionsController, self).__init__() super(InstanceActionsController, self).__init__()
self.compute_api = compute.API() self.compute_api = compute.API()
self.action_api = compute.InstanceActionAPI() self.action_api = compute.InstanceActionAPI()
def _format_action(self, action_raw): def _format_action(self, action_raw, action_keys):
action = {} action = {}
for key in ACTION_KEYS: for key in action_keys:
action[key] = action_raw.get(key) action[key] = action_raw.get(key)
return action return action
@ -60,6 +72,7 @@ class InstanceActionsController(wsgi.Controller):
with utils.temporary_mutation(context, read_deleted='yes'): with utils.temporary_mutation(context, read_deleted='yes'):
return common.get_instance(self.compute_api, context, server_id) return common.get_instance(self.compute_api, context, server_id)
@wsgi.Controller.api_version("2.1", "2.57")
@extensions.expected_errors(404) @extensions.expected_errors(404)
def index(self, req, server_id): def index(self, req, server_id):
"""Returns the list of actions recorded for a given instance.""" """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) instance = self._get_instance(req, context, server_id)
context.can(ia_policies.BASE_POLICY_NAME, instance) context.can(ia_policies.BASE_POLICY_NAME, instance)
actions_raw = self.action_api.actions_get(context, 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} 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) @extensions.expected_errors(404)
def show(self, req, server_id, id): def show(self, req, server_id, id):
"""Return data about the given instance action.""" """Return data about the given instance action."""
@ -83,7 +128,10 @@ class InstanceActionsController(wsgi.Controller):
raise exc.HTTPNotFound(explanation=msg) raise exc.HTTPNotFound(explanation=msg)
action_id = action['id'] 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 # Prior to microversion 2.51, events would only be returned in the
# response for admins by default policy rules. Starting in # response for admins by default policy rules. Starting in
# microversion 2.51, events are returned for admin_or_owner (of the # microversion 2.51, events are returned for admin_or_owner (of the

View File

@ -729,3 +729,12 @@ The 2.57 microversion makes the following changes:
* The ``injected_files``, ``injected_file_content_bytes`` and * The ``injected_files``, ``injected_file_content_bytes`` and
``injected_file_path_bytes`` quotas are removed from the ``os-quota-sets`` ``injected_file_path_bytes`` quotas are removed from the ``os-quota-sets``
and ``os-quota-class-sets`` APIs. 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.

View File

@ -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
}

View File

@ -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')

View File

@ -4757,9 +4757,10 @@ class HostAPI(base.Base):
class InstanceActionAPI(base.Base): class InstanceActionAPI(base.Base):
"""Sub-set of the Compute Manager API for managing instance actions.""" """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( 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): def action_get_by_request_id(self, context, instance, request_id):
return objects.InstanceAction.get_by_request_id( return objects.InstanceAction.get_by_request_id(

View File

@ -676,7 +676,9 @@ class InstanceActionAPI(compute_api.InstanceActionAPI):
super(InstanceActionAPI, self).__init__() super(InstanceActionAPI, self).__init__()
self.cells_rpcapi = cells_rpcapi.CellsAPI() 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) return self.cells_rpcapi.actions_get(context, instance)
def action_get_by_request_id(self, context, instance, request_id): def action_get_by_request_id(self, context, instance, request_id):

View File

@ -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"
}
]
}
}

View File

@ -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": ""
}
]
}
}

View File

@ -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": ""
}
]
}

View File

@ -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"
}
]
}

View File

@ -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": ""
}
]
}

View File

@ -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": ""
}
]
}

View File

@ -18,6 +18,9 @@ import copy
import six import six
from nova.tests.functional.api_sample_tests import api_sample_base 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_instance
from nova.tests.unit import fake_server_actions from nova.tests.unit import fake_server_actions
from nova.tests.unit import utils as test_utils from nova.tests.unit import utils as test_utils
@ -127,3 +130,73 @@ class ServerActionsV251NonAdminSampleJsonTest(ServerActionsSampleJsonTest):
ADMIN_API = False ADMIN_API = False
microversion = '2.51' microversion = '2.51'
scenarios = [('v2_51', {'api_major_version': 'v2.1'})] 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

View File

@ -434,6 +434,8 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase):
'-[0-9a-f]{4}-[0-9a-f]{12})', '-[0-9a-f]{4}-[0-9a-f]{12})',
'uuid': '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}' 'uuid': '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}'
'-[0-9a-f]{4}-[0-9a-f]{12}', '-[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}', 'reservation_id': 'r-[0-9a-zA-Z]{8}',
'private_key': '(-----BEGIN RSA PRIVATE KEY-----|)' 'private_key': '(-----BEGIN RSA PRIVATE KEY-----|)'
'[a-zA-Z0-9\n/+=]*' '[a-zA-Z0-9\n/+=]*'

View File

@ -34,9 +34,11 @@ from nova.tests import uuidsentinel as uuids
FAKE_UUID = fake_server_actions.FAKE_UUID FAKE_UUID = fake_server_actions.FAKE_UUID
FAKE_REQUEST_ID = fake_server_actions.FAKE_REQUEST_ID1 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.''' '''Remove keys that aren't serialized.'''
to_delete = ('id', 'finish_time', 'created_at', 'updated_at', 'deleted_at', to_delete = ('id', 'finish_time', 'created_at', 'updated_at', 'deleted_at',
'deleted') 'deleted')
@ -47,14 +49,16 @@ def format_action(action):
# NOTE(danms): Without WSGI above us, these will be just stringified # NOTE(danms): Without WSGI above us, these will be just stringified
action['start_time'] = str(action['start_time'].replace(tzinfo=None)) action['start_time'] = str(action['start_time'].replace(tzinfo=None))
for event in action.get('events', []): for event in action.get('events', []):
format_event(event) format_event(event, expect_traceback)
return action return action
def format_event(event): def format_event(event, expect_traceback=True):
'''Remove keys that aren't serialized.''' '''Remove keys that aren't serialized.'''
to_delete = ('id', 'created_at', 'updated_at', 'deleted_at', 'deleted', to_delete = ['id', 'created_at', 'updated_at', 'deleted_at', 'deleted',
'action_id') 'action_id']
if not expect_traceback:
to_delete.append('traceback')
for key in to_delete: for key in to_delete:
if key in event: if key in event:
del(event[key]) del(event[key])
@ -109,6 +113,7 @@ class InstanceActionsPolicyTestV21(test.NoDBTestCase):
class InstanceActionsTestV21(test.NoDBTestCase): class InstanceActionsTestV21(test.NoDBTestCase):
instance_actions = instance_actions_v21 instance_actions = instance_actions_v21
wsgi_api_version = os_wsgi.DEFAULT_API_VERSION wsgi_api_version = os_wsgi.DEFAULT_API_VERSION
expect_events_non_admin = False
def fake_get(self, context, instance_uuid, expected_attrs=None): def fake_get(self, context, instance_uuid, expected_attrs=None):
return objects.Instance(uuid=instance_uuid) return objects.Instance(uuid=instance_uuid)
@ -188,8 +193,13 @@ class InstanceActionsTestV21(test.NoDBTestCase):
req = self._get_http_req('os-instance-actions/1') req = self._get_http_req('os-instance-actions/1')
res_dict = self.controller.show(req, FAKE_UUID, FAKE_REQUEST_ID) res_dict = self.controller.show(req, FAKE_UUID, FAKE_REQUEST_ID)
fake_action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID] fake_action = self.fake_actions[FAKE_UUID][FAKE_REQUEST_ID]
self.assertEqual(format_action(fake_action), if self.expect_events_non_admin:
format_action(res_dict['instanceAction'])) 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 test_action_not_found(self):
def fake_no_action(context, uuid, action_id): 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): def fake_get(self, context, instance_uuid, expected_attrs=None):
self.assertEqual('yes', context.read_deleted) self.assertEqual('yes', context.read_deleted)
return objects.Instance(uuid=instance_uuid) 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))

View File

@ -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.