Merge "api: Add response body schemas for instance actions"

This commit is contained in:
Zuul
2025-06-10 22:47:33 +00:00
committed by Gerrit Code Review
4 changed files with 256 additions and 38 deletions
+14 -4
View File
@@ -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
@@ -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']},
})
+53
View File
@@ -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,
}
+34 -30
View File
@@ -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,
}
}
}