diff --git a/nova/api/openstack/compute/instance_actions.py b/nova/api/openstack/compute/instance_actions.py index 19c12a372f..d2746df2a9 100644 --- a/nova/api/openstack/compute/instance_actions.py +++ b/nova/api/openstack/compute/instance_actions.py @@ -38,6 +38,7 @@ ACTION_KEYS_V258 = ['action', 'instance_uuid', 'request_id', 'user_id', EVENT_KEYS = ['event', 'start_time', 'finish_time', 'result', 'traceback'] +@validation.validated class InstanceActionsController(wsgi.Controller): _view_builder_class = instance_actions_view.ViewBuilder @@ -80,9 +81,11 @@ class InstanceActionsController(wsgi.Controller): @wsgi.expected_errors(404, "2.1", "2.57") @wsgi.expected_errors((400, 404), "2.58") - @validation.query_schema(schema.list_query, "2.1", "2.57") - @validation.query_schema(schema.list_query_v258, "2.58", "2.65") - @validation.query_schema(schema.list_query_v266, "2.66") + @validation.query_schema(schema.index_query, "2.1", "2.57") + @validation.query_schema(schema.index_query_v258, "2.58", "2.65") + @validation.query_schema(schema.index_query_v266, "2.66") + @validation.response_body_schema(schema.index_response, "2.1", "2.57") + @validation.response_body_schema(schema.index_response_v258, "2.58") def index(self, req, server_id): """Returns the list of actions recorded for a given instance.""" context = req.environ["nova.context"] @@ -137,6 +140,11 @@ class InstanceActionsController(wsgi.Controller): @wsgi.expected_errors(404) @validation.query_schema(schema.show_query) + @validation.response_body_schema(schema.show_response, "2.1", "2.50") + @validation.response_body_schema(schema.show_response_v251, "2.51", "2.57") + @validation.response_body_schema(schema.show_response_v258, "2.58", "2.61") + @validation.response_body_schema(schema.show_response_v262, "2.62", "2.83") + @validation.response_body_schema(schema.show_response_v284, "2.84") def show(self, req, server_id, id): """Return data about the given instance action.""" context = req.environ['nova.context'] @@ -154,6 +162,7 @@ class InstanceActionsController(wsgi.Controller): 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 @@ -167,7 +176,8 @@ class InstanceActionsController(wsgi.Controller): fatal=False): # For all microversions, the user can see all event details # including the traceback. - show_events = show_traceback = True + show_events = True + show_traceback = True show_host = api_version_request.is_supported(req, '2.62') elif api_version_request.is_supported(req, '2.51'): # The user is not able to see all event details, but they can at diff --git a/nova/api/openstack/compute/schemas/instance_actions.py b/nova/api/openstack/compute/schemas/instance_actions.py index 1bff320f34..67dc1b3fac 100644 --- a/nova/api/openstack/compute/schemas/instance_actions.py +++ b/nova/api/openstack/compute/schemas/instance_actions.py @@ -15,14 +15,15 @@ import copy from nova.api.validation import parameter_types +from nova.api.validation import response_types -list_query = { +index_query = { 'type': 'object', 'properties': {}, 'additionalProperties': True, } -list_query_v258 = { +index_query_v258 = { 'type': 'object', 'properties': { # The 2.58 microversion added support for paging by limit and marker @@ -36,8 +37,8 @@ list_query_v258 = { 'additionalProperties': False } -list_query_v266 = copy.deepcopy(list_query_v258) -list_query_v266['properties'].update({ +index_query_v266 = copy.deepcopy(index_query_v258) +index_query_v266['properties'].update({ 'changes-before': parameter_types.single_param( {'type': 'string', 'format': 'date-time'}), }) @@ -47,3 +48,153 @@ show_query = { 'properties': {}, 'additionalProperties': True, } + +index_response = { + 'type': 'object', + 'properties': { + 'instanceActions': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'action': {'type': 'string'}, + 'instance_uuid': {'type': 'string', 'format': 'uuid'}, + 'message': {'type': ['null', 'string']}, + 'project_id': { + 'type': ['null', 'string'], + 'pattern': '^[a-zA-Z0-9-]*$', + 'minLength': 1, + 'maxLength': 255, + }, + 'request_id': {'type': 'string'}, + 'start_time': {'type': 'string', 'format': 'date-time'}, + 'user_id': { + 'type': ['null', 'string'], + 'pattern': '^[a-zA-Z0-9-]*$', + 'minLength': 1, + 'maxLength': 255, + }, + }, + 'required': [ + 'action', + 'instance_uuid', + 'message', + 'project_id', + 'request_id', + 'start_time', + 'user_id', + ], + 'additionalProperties': False, + }, + }, + }, + 'required': ['instanceActions'], + 'additionalProperties': False, +} + +index_response_v258 = copy.deepcopy(index_response) +index_response_v258['properties']['instanceActions']['items'][ + 'properties' +].update({ + 'updated_at': {'type': ['null', 'string'], 'format': 'date-time'}, +}) +index_response_v258['properties']['instanceActions']['items'][ + 'required' +].append('updated_at') +index_response_v258['properties']['links'] = response_types.collection_links + +show_response = { + 'type': 'object', + 'properties': { + 'instanceAction': { + 'type': 'object', + 'properties': { + 'action': {'type': 'string'}, + 'instance_uuid': {'type': 'string', 'format': 'uuid'}, + 'events': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'event': {'type': 'string'}, + 'finish_time': { + 'type': ['string', 'null'], + 'format': 'date-time', + }, + 'result': {'type': ['string', 'null']}, + 'start_time': { + 'type': 'string', 'format': 'date-time', + }, + 'traceback': {'type': ['null', 'string']}, + }, + 'required': [ + 'event', + 'finish_time', + 'result', + 'start_time', + ], + 'additionalProperties': False, + }, + }, + 'message': {'type': ['null', 'string']}, + 'project_id': { + 'type': ['null', 'string'], + 'pattern': '^[a-zA-Z0-9-]*$', + 'minLength': 1, + 'maxLength': 255, + }, + 'request_id': {'type': 'string'}, + 'start_time': {'type': 'string', 'format': 'date-time'}, + 'user_id': { + 'type': ['null', 'string'], + 'pattern': '^[a-zA-Z0-9-]*$', + 'minLength': 1, + 'maxLength': 255, + }, + }, + 'required': [ + 'action', + 'instance_uuid', + 'message', + 'project_id', + 'request_id', + 'start_time', + 'user_id', + ], + 'additionalProperties': False, + }, + }, + 'required': ['instanceAction'], + 'additionalProperties': False, +} + +show_response_v251 = copy.deepcopy(show_response) +show_response_v251['properties']['instanceAction']['required'].append( + 'events' +) + +show_response_v258 = copy.deepcopy(show_response_v251) +show_response_v258['properties']['instanceAction']['properties'].update({ + 'updated_at': {'type': ['null', 'string'], 'format': 'date-time'}, +}) +show_response_v258['properties']['instanceAction']['required'].append( + 'updated_at' +) + +show_response_v262 = copy.deepcopy(show_response_v258) +show_response_v262['properties']['instanceAction']['properties']['events'][ + 'items' +]['properties'].update({ + 'hostId': {'type': 'string'}, + 'host': {'type': 'string'}, +}) +show_response_v262['properties']['instanceAction']['properties']['events'][ + 'items' +]['required'].append('hostId') + +show_response_v284 = copy.deepcopy(show_response_v262) +show_response_v284['properties']['instanceAction']['properties']['events'][ + 'items' +]['properties'].update({ + 'details': {'type': ['string', 'null']}, +}) diff --git a/nova/api/validation/response_types.py b/nova/api/validation/response_types.py new file mode 100644 index 0000000000..9fa87b6811 --- /dev/null +++ b/nova/api/validation/response_types.py @@ -0,0 +1,53 @@ +# 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. + +"""Common field types for validating API responses.""" + +links = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'rel': { + 'type': 'string', + 'enum': ['self', 'bookmark'], + }, + 'href': { + 'type': 'string', + 'format': 'uri', + }, + }, + 'required': ['rel', 'href'], + 'additionalProperties': False, + }, +} + +collection_links = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'rel': { + 'const': 'next', + }, + 'href': { + 'type': 'string', + 'format': 'uri', + }, + }, + 'required': ['rel', 'href'], + 'additionalProperties': False, + }, + # there should be one and only one link object + 'minItems': 1, + 'maxItems': 1, +} diff --git a/nova/tests/unit/fake_server_actions.py b/nova/tests/unit/fake_server_actions.py index 2eeacab8c3..29af943372 100644 --- a/nova/tests/unit/fake_server_actions.py +++ b/nova/tests/unit/fake_server_actions.py @@ -20,40 +20,44 @@ FAKE_REQUEST_ID1 = 'req-3293a3f1-b44c-4609-b8d2-d81b105636b8' FAKE_REQUEST_ID2 = 'req-25517360-b757-47d3-be45-0e8d2a01b36a' FAKE_ACTION_ID1 = 123 FAKE_ACTION_ID2 = 456 -FAKE_HOST_ID1 = '74824069503a752aaa3abf194f73200fcdd117ef70ab28b576e5bf7a' -FAKE_HOST_ID2 = '858f5ed465b4967dd1306a38078e9b83b8705bdedfa7f16f898119b4' +# the value of the hostId fields depends on the value of the projectID field, +# so we define these statically +FAKE_PROJECT_ID1 = '9ccc9bd7-8b23-4d57-8421-291fe888bdc6' +FAKE_PROJECT_ID2 = '427b71e6-49d2-4c30-a8a0-d23adfe772e2' +FAKE_HOST_ID1 = 'b7e03ca48116ea93152de5d2eff1c69f515a5198f3cfbe59103faf17' +FAKE_HOST_ID2 = '7bd9ecf0b8cec52cd1410aee9b4bee5370c698bfc11f9478b35b80e2' FAKE_ACTIONS = { FAKE_UUID: { - FAKE_REQUEST_ID1: {'id': FAKE_ACTION_ID1, - 'action': 'reboot', - 'instance_uuid': FAKE_UUID, - 'request_id': FAKE_REQUEST_ID1, - 'project_id': '147', - 'user_id': '789', - 'start_time': datetime.datetime( - 2012, 12, 5, 0, 0, 0, 0), - 'finish_time': None, - 'message': '', - 'created_at': None, - 'updated_at': None, - 'deleted_at': None, - 'deleted': False, + FAKE_REQUEST_ID1: { + 'id': FAKE_ACTION_ID1, + 'action': 'reboot', + 'instance_uuid': FAKE_UUID, + 'request_id': FAKE_REQUEST_ID1, + 'project_id': FAKE_PROJECT_ID1, + 'user_id': '091d35ff-a42d-4717-8ba4-7dabfa7b13a4', + 'start_time': datetime.datetime(2012, 12, 5, 0, 0, 0, 0), + 'finish_time': None, + 'message': '', + 'created_at': None, + 'updated_at': None, + 'deleted_at': None, + 'deleted': False, }, - FAKE_REQUEST_ID2: {'id': FAKE_ACTION_ID2, - 'action': 'resize', - 'instance_uuid': FAKE_UUID, - 'request_id': FAKE_REQUEST_ID2, - 'user_id': '789', - 'project_id': '842', - 'start_time': datetime.datetime( - 2012, 12, 5, 1, 0, 0, 0), - 'finish_time': None, - 'message': '', - 'created_at': None, - 'updated_at': None, - 'deleted_at': None, - 'deleted': False, + FAKE_REQUEST_ID2: { + 'id': FAKE_ACTION_ID2, + 'action': 'resize', + 'instance_uuid': FAKE_UUID, + 'request_id': FAKE_REQUEST_ID2, + 'project_id': FAKE_PROJECT_ID2, + 'user_id': 'c0ab3ebb-ad1b-4b60-8e63-b8a5656315d0', + 'start_time': datetime.datetime(2012, 12, 5, 1, 0, 0, 0), + 'finish_time': None, + 'message': '', + 'created_at': None, + 'updated_at': None, + 'deleted_at': None, + 'deleted': False, } } }