api: Add response body schemas for servers APIs (1/6)

In this part we focus on the show view.

Change-Id: Ia178c1314f99c719827e3eb78735d1019852a273
Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
This commit is contained in:
Stephen Finucane
2025-08-28 10:43:56 +01:00
parent d00485f13b
commit fc089c4824
4 changed files with 511 additions and 64 deletions
+481 -53
View File
@@ -15,6 +15,7 @@
import copy
from nova.api.validation import parameter_types
from nova.api.validation import response_types
from nova.objects import instance
_legacy_block_device_mapping = {
@@ -87,8 +88,19 @@ _hints = {
'type': 'object',
'properties': {
'group': {
'type': 'string',
'format': 'uuid'
'oneOf': [
{
'type': 'array',
'items': {
'type': 'string',
'format': 'uuid'
},
},
{
'type': 'string',
'format': 'uuid'
},
],
},
'different_host': {
# NOTE: The value of 'different_host' is the set of server
@@ -607,7 +619,6 @@ VALID_SORT_KEYS_V275['enum'] = list(
set(VALID_SORT_KEYS_V273["enum"]) - set(SERVER_LIST_IGNORE_SORT_KEY_V273)
)
query_params_v21 = {
'type': 'object',
'properties': {
@@ -720,6 +731,459 @@ show_query = {
'additionalProperties': True,
}
_server_cell_down_response = {
'type': 'object',
'properties': {
'created': {'type': 'string', 'format': 'date-time'},
'flavor': {
'type': 'object',
'properties': {
'disk': {'type': 'integer'},
'ephemeral': {'type': 'integer'},
'extra_specs': {
'type': 'object',
'patternProperties': {
'^.+$': {'type': 'string'},
},
'additionalProperties': False,
},
'original_name': {'type': 'string'},
'ram': {'type': 'integer'},
'swap': {'type': 'integer'},
'vcpus': {'type': 'integer'},
},
# extra_specs is only show if policy allows
'required': [
'disk', 'ephemeral', 'original_name', 'ram', 'swap', 'vcpus'
],
'additionalProperties': False,
},
'id': {'type': 'string', 'format': 'uuid'},
'image': {
'oneOf': [
{'type': 'string', 'const': ''},
{
'type': 'object',
'properties': {
'id': {'type': 'string', 'format': 'uuid'},
'links': response_types.links,
},
'additionalProperties': False,
},
],
},
'links': response_types.links,
'status': {'type': 'string', 'const': 'UNKNOWN'},
'tenant_id': parameter_types.project_id,
'user_id': parameter_types.user_id,
'OS-EXT-AZ:availability_zone': {'type': 'string'},
'OS-EXT-STS:power_state': {
'type': ['integer', 'null'], 'enum': [0, 1, 3, 4, 6, 7, None],
},
},
'required': [
'created',
'flavor',
'id',
'image',
'links',
'status',
'tenant_id',
'user_id',
'OS-EXT-AZ:availability_zone',
'OS-EXT-STS:power_state',
],
'additionalProperties': False,
}
_server_cell_down_response_v271 = copy.deepcopy(_server_cell_down_response)
_server_cell_down_response_v271['properties'].update({
'server_groups': {
'type': 'array',
'items': {'type': 'string', 'format': 'uuid'},
'maxLength': 1,
},
})
_server_cell_down_response_v271['required'].append('server_groups')
_server_response = {
'type': 'object',
'properties': {
'accessIPv4': {
'type': 'string',
'oneOf': [{'format': 'ipv4'}, {'const': ''}],
},
'accessIPv6': {
'type': 'string',
'oneOf': [{'format': 'ipv6'}, {'const': ''}],
},
'addresses': {
'type': 'object',
'patternProperties': {
'^.+$': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'addr': {
'type': 'string',
'oneOf': [
{'format': 'ipv4'},
{'format': 'ipv6'},
],
},
'version': {
'type': 'number', 'enum': [4, 6],
},
'OS-EXT-IPS:type': {
'type': 'string',
'enum': ['fixed', 'floating'],
},
'OS-EXT-IPS-MAC:mac_addr': {
'type': 'string', 'format': 'mac-address',
},
},
'required': [
'addr',
'version',
'OS-EXT-IPS:type',
'OS-EXT-IPS-MAC:mac_addr',
],
'additionalProperties': False,
},
},
},
'additionalProperties': False,
},
'adminPass': {'type': ['null', 'string']},
'config_drive': {
'type': ['string', 'null'], 'enum': ['', 'True', 'False', None],
},
'created': {'type': 'string', 'format': 'date-time'},
'fault': {
'type': 'object',
'properties': {
'code': {'type': 'integer'},
'created': {'type': 'string', 'format': 'date-time'},
'details': {'type': 'string'},
'message': {'type': 'string'},
},
'required': ['code', 'created', 'message'],
'additionalProperties': False,
},
'flavor': {
'type': 'object',
'properties': {
'id': {'type': 'string'},
'links': response_types.links,
},
'additionalProperties': False,
},
'hostId': {'type': 'string'},
'id': {'type': 'string', 'format': 'uuid'},
'image': {
'oneOf': [
{'type': 'string', 'const': ''},
{
'type': 'object',
'properties': {
'id': {'type': 'string', 'format': 'uuid'},
'links': response_types.links,
},
'additionalProperties': False,
},
],
},
'key_name': {'type': ['null', 'string']},
'links': response_types.links,
'metadata': {
'type': 'object',
'patternProperties': {
'^.+$': {
'type': 'string'
},
},
'additionalProperties': False,
},
'name': {'type': ['string', 'null']},
'os-extended-volumes:volumes_attached': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'id': {'type': 'string'},
},
'required': ['id'],
'additionalProperties': False,
},
},
'progress': {'type': ['null', 'number']},
'security_groups': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'name': {'type': 'string'},
},
'required': ['name'],
'additionalProperties': False,
},
},
'status': {'type': 'string'},
'tenant_id': parameter_types.project_id,
'updated': {'type': 'string', 'format': 'date-time'},
'user_id': parameter_types.user_id,
'OS-DCF:diskConfig': {'type': 'string'},
'OS-EXT-AZ:availability_zone': {'type': 'string'},
'OS-EXT-SRV-ATTR:host': {'type': ['string', 'null']},
'OS-EXT-SRV-ATTR:hypervisor_hostname': {
'type': ['string', 'null']
},
'OS-EXT-SRV-ATTR:instance_name': {'type': 'string'},
'OS-EXT-STS:power_state': {
'type': ['integer', 'null'], 'enum': [0, 1, 3, 4, 6, 7, None],
},
'OS-EXT-STS:task_state': {'type': ['string', 'null']},
'OS-EXT-STS:vm_state': {'type': ['string', 'null']},
'OS-SRV-USG:launched_at': {
'type': ['string', 'null'], 'format': 'date-time',
},
'OS-SRV-USG:terminated_at': {
'type': ['string', 'null'], 'format': 'date-time',
},
},
'required': [
# fault, progress depend on server state
'accessIPv4',
'accessIPv6',
'addresses',
'config_drive',
'created',
'flavor',
'hostId',
'id',
'image',
'key_name',
'links',
'metadata',
'name',
'os-extended-volumes:volumes_attached',
'status',
'tenant_id',
'updated',
'user_id',
'OS-DCF:diskConfig',
'OS-EXT-AZ:availability_zone',
'OS-EXT-STS:power_state',
'OS-EXT-STS:task_state',
'OS-EXT-STS:vm_state',
'OS-SRV-USG:launched_at',
'OS-SRV-USG:terminated_at',
],
'additionalProperties': False,
}
_server_response_v23 = copy.deepcopy(_server_response)
# all of these are behind policy, so we don't need to update 'required'
_server_response_v23['properties'].update({
'OS-EXT-SRV-ATTR:hostname': {'type': 'string'},
'OS-EXT-SRV-ATTR:kernel_id': {'type': ['string', 'null']},
'OS-EXT-SRV-ATTR:launch_index': {'type': 'integer'},
'OS-EXT-SRV-ATTR:ramdisk_id': {'type': ['string', 'null']},
'OS-EXT-SRV-ATTR:reservation_id': {'type': ['string', 'null']},
'OS-EXT-SRV-ATTR:root_device_name': {'type': ['string', 'null']},
'OS-EXT-SRV-ATTR:user_data': {
'type': ['string', 'null'], 'format': 'base64', 'maxLength': 65535,
},
})
_server_response_v23['properties']['os-extended-volumes:volumes_attached'] = {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'id': {'type': 'string'},
'delete_on_termination': {
'type': 'boolean',
'default': False,
},
},
'required': ['id', 'delete_on_termination'],
'additionalProperties': False,
},
}
_server_response_v29 = copy.deepcopy(_server_response_v23)
_server_response_v29['properties'].update({
'locked': {'type': 'boolean'},
})
_server_response_v29['required'].append('locked')
_server_response_v216 = copy.deepcopy(_server_response_v29)
_server_response_v216['properties'].update({
'host_status': {
'type': 'string', 'enum': ['UP', 'DOWN', 'MAINTENANCE', 'UNKNOWN', '']
},
})
_server_response_v219 = copy.deepcopy(_server_response_v216)
_server_response_v219['properties'].update({
'description': {'type': ['string', 'null']},
})
_server_response_v219['required'].append('description')
_server_response_v226 = copy.deepcopy(_server_response_v219)
_server_response_v226['properties'].update({
'tags': {'type': 'array', 'items': {'type': 'string'}, 'maxItems': 50},
})
_server_response_v226['required'].append('tags')
_server_response_v247 = copy.deepcopy(_server_response_v226)
_server_response_v247['properties']['flavor'] = {
'type': 'object',
'properties': {
'disk': {'type': 'integer'},
'ephemeral': {'type': 'integer'},
'extra_specs': {
'type': 'object',
'patternProperties': {
'^.+$': {'type': 'string'},
},
'additionalProperties': False,
},
'original_name': {'type': 'string'},
'ram': {'type': 'integer'},
'swap': {'type': 'integer'},
'vcpus': {'type': 'integer'},
},
# extra_specs is only show if policy allows
'required': [
'disk', 'ephemeral', 'original_name', 'ram', 'swap', 'vcpus'
],
'additionalProperties': False,
}
_server_response_v263 = copy.deepcopy(_server_response_v247)
_server_response_v263['properties'].update({
'trusted_image_certificates': {
'type': ['array', 'null'],
'items': {'type': 'string'},
},
})
_server_response_v263['required'].append('trusted_image_certificates')
_server_response_v271 = copy.deepcopy(_server_response_v263)
_server_response_v271['properties'].update({
'server_groups': {
'type': 'array',
'items': {'type': 'string', 'format': 'uuid'},
'maxLength': 1,
},
})
_server_response_v271['required'].append('server_groups')
_server_response_v273 = copy.deepcopy(_server_response_v271)
_server_response_v273['properties'].update({
'locked_reason': {'type': ['null', 'string']},
})
_server_response_v273['required'].append('locked_reason')
_server_response_v290 = copy.deepcopy(_server_response_v273)
_server_response_v290['required'].append('OS-EXT-SRV-ATTR:hostname')
_server_response_v296 = copy.deepcopy(_server_response_v290)
_server_response_v296['properties'].update({
'pinned_availability_zone': {'type': ['string', 'null']},
})
_server_response_v296['required'].append('pinned_availability_zone')
_server_response_v298 = copy.deepcopy(_server_response_v296)
_server_response_v298['properties']['image']['oneOf'][1]['properties'].update({
'properties': {
'type': 'object',
'patternProperties': {
'^[a-zA-Z0-9_:. ]{1,255}$': {
'type': ['string', 'null'],
'maxLength': 255,
},
},
'additionalProperties': False,
},
})
_server_response_v2100 = copy.deepcopy(_server_response_v298)
_server_response_v2100['properties'].update({
'scheduler_hints': _hints,
})
_server_response_v2100['required'].append('scheduler_hints')
show_response = {
'type': 'object',
'properties': {
'server': _server_response,
},
'required': ['server'],
'additionalProperties': False,
}
show_response_v23 = copy.deepcopy(show_response)
show_response_v23['properties']['server'] = _server_response_v23
show_response_v29 = copy.deepcopy(show_response_v23)
show_response_v29['properties']['server'] = _server_response_v29
show_response_v216 = copy.deepcopy(show_response_v29)
show_response_v216['properties']['server'] = _server_response_v216
show_response_v219 = copy.deepcopy(show_response_v216)
show_response_v219['properties']['server'] = _server_response_v219
show_response_v226 = copy.deepcopy(show_response_v219)
show_response_v226['properties']['server'] = _server_response_v226
show_response_v247 = copy.deepcopy(show_response_v226)
show_response_v247['properties']['server'] = _server_response_v247
show_response_v263 = copy.deepcopy(show_response_v247)
show_response_v263['properties']['server'] = _server_response_v263
# this is the first version to introduce down cell support. We model this as an
# entirely different schema rather than making most of the fields optional
show_response_v269 = copy.deepcopy(show_response_v263)
show_response_v269['properties']['server'] = {
'oneOf': [_server_response_v263, _server_cell_down_response],
}
show_response_v271 = copy.deepcopy(show_response_v269)
show_response_v271['properties']['server'] = {
'oneOf': [_server_response_v271, _server_cell_down_response_v271],
}
show_response_v273 = copy.deepcopy(show_response_v271)
show_response_v273['properties']['server'] = {
'oneOf': [_server_response_v273, _server_cell_down_response_v271],
}
show_response_v290 = copy.deepcopy(show_response_v273)
show_response_v290['properties']['server'] = {
'oneOf': [_server_response_v290, _server_cell_down_response_v271],
}
show_response_v296 = copy.deepcopy(show_response_v290)
show_response_v296['properties']['server'] = {
'oneOf': [_server_response_v296, _server_cell_down_response_v271],
}
show_response_v298 = copy.deepcopy(show_response_v290)
show_response_v298['properties']['server'] = {
'oneOf': [_server_response_v298, _server_cell_down_response_v271],
}
show_response_v2100 = copy.deepcopy(show_response_v298)
show_response_v2100['properties']['server'] = {
'oneOf': [_server_response_v2100, _server_cell_down_response_v271],
}
resize_response = {'type': 'null'}
confirm_resize_response = {'type': 'null'}
@@ -739,7 +1203,7 @@ create_image_response = {'type': 'null'}
create_image_response_v245 = {
'type': 'object',
'properties': {
'image_id': {'type': 'string', 'format': 'uuid'},
'image_id': {'type': ['string', 'null'], 'format': 'uuid'},
},
'required': ['image_id'],
'additionalProperties': False,
@@ -803,20 +1267,7 @@ rebuild_response = {
'type': 'object',
'properties': {
'id': {'type': 'string'},
'links': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'href': {
'type': 'string', 'format': 'uri',
},
'rel': {'type': 'string'},
},
'required': ['href', 'rel'],
"additionalProperties": False,
},
},
'links': response_types.links,
},
'additionalProperties': False,
},
@@ -828,39 +1279,14 @@ rebuild_response = {
{
'type': 'object',
'properties': {
'id': {'type': 'string'},
'links': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'href': {
'type': 'string',
'format': 'uri',
},
'rel': {'type': 'string'},
},
'required': ['href', 'rel'],
"additionalProperties": False,
},
},
'id': {'type': 'string', 'format': 'uuid'},
'links': response_types.links,
},
'additionalProperties': False,
},
],
},
'links': {
'type': 'array',
'items': {
'type': 'object',
'properties': {
'href': {'type': 'string', 'format': 'uri'},
'rel': {'type': 'string'},
},
'required': ['href', 'rel'],
'additionalProperties': False,
},
},
'links': response_types.links,
'metadata': {
'type': 'object',
'patternProperties': {
@@ -926,8 +1352,8 @@ rebuild_response_v226['properties']['server']['required'].append('tags')
# NOTE(stephenfin): We overwrite rather than extend 'flavor', since we now
# embed the flavor in this version
rebuild_response_v246 = copy.deepcopy(rebuild_response_v226)
rebuild_response_v246['properties']['server']['properties']['flavor'] = {
rebuild_response_v247 = copy.deepcopy(rebuild_response_v226)
rebuild_response_v247['properties']['server']['properties']['flavor'] = {
'type': 'object',
'properties': {
'disk': {'type': 'integer'},
@@ -946,7 +1372,7 @@ rebuild_response_v246['properties']['server']['properties']['flavor'] = {
'additionalProperties': False,
}
rebuild_response_v254 = copy.deepcopy(rebuild_response_v246)
rebuild_response_v254 = copy.deepcopy(rebuild_response_v247)
rebuild_response_v254['properties']['server']['properties']['key_name'] = {
'type': ['null', 'string'],
}
@@ -1014,10 +1440,10 @@ rebuild_response_v275['properties']['server']['properties'].update(
'OS-EXT-SRV-ATTR:reservation_id': {'type': ['string', 'null']},
'OS-EXT-SRV-ATTR:root_device_name': {'type': ['string', 'null']},
'OS-EXT-STS:power_state': {
'type': 'integer', 'enum': [0, 1, 3, 4, 6, 7],
'type': ['integer', 'null'], 'enum': [0, 1, 3, 4, 6, 7, None],
},
'OS-EXT-STS:task_state': {'type': ['null', 'string']},
'OS-EXT-STS:vm_state': {'type': 'string'},
'OS-EXT-STS:task_state': {'type': ['string', 'null']},
'OS-EXT-STS:vm_state': {'type': ['string', 'null']},
'os-extended-volumes:volumes_attached': {
'type': 'array',
'items': {
@@ -1091,7 +1517,9 @@ rebuild_response_v298['properties']['server']['properties']['image']['oneOf'][
'properties': {
'type': 'object',
'patternProperties': {
'^[a-zA-Z0-9_:. ]{1,255}$': {'type': 'string', 'maxLength': 255},
'^[a-zA-Z0-9_:. ]{1,255}$': {
'type': ['string', 'null'], 'maxLength': 255
},
},
'additionalProperties': False,
},
+17 -2
View File
@@ -458,6 +458,21 @@ class ServersController(wsgi.Controller):
@wsgi.expected_errors(404)
@validation.query_schema(schema.show_query)
@validation.response_body_schema(schema.show_response, '2.0', '2.2')
@validation.response_body_schema(schema.show_response_v23, '2.3', '2.8')
@validation.response_body_schema(schema.show_response_v29, '2.9', '2.15')
@validation.response_body_schema(schema.show_response_v216, '2.16', '2.18')
@validation.response_body_schema(schema.show_response_v219, '2.19', '2.25')
@validation.response_body_schema(schema.show_response_v226, '2.26', '2.46')
@validation.response_body_schema(schema.show_response_v247, '2.47', '2.62')
@validation.response_body_schema(schema.show_response_v263, '2.63', '2.68')
@validation.response_body_schema(schema.show_response_v269, '2.69', '2.70')
@validation.response_body_schema(schema.show_response_v271, '2.71', '2.72')
@validation.response_body_schema(schema.show_response_v273, '2.73', '2.89')
@validation.response_body_schema(schema.show_response_v290, '2.90', '2.95')
@validation.response_body_schema(schema.show_response_v296, '2.96', '2.97')
@validation.response_body_schema(schema.show_response_v298, '2.98', '2.99')
@validation.response_body_schema(schema.show_response_v2100, '2.100')
def show(self, req, id):
"""Returns server details by server id."""
context = req.environ['nova.context']
@@ -1163,8 +1178,8 @@ class ServersController(wsgi.Controller):
@validation.response_body_schema(schema.rebuild_response, '2.0', '2.8')
@validation.response_body_schema(schema.rebuild_response_v29, '2.9', '2.18') # noqa: E501
@validation.response_body_schema(schema.rebuild_response_v219, '2.19', '2.25') # noqa: E501
@validation.response_body_schema(schema.rebuild_response_v226, '2.26', '2.45') # noqa: E501
@validation.response_body_schema(schema.rebuild_response_v246, '2.46', '2.53') # noqa: E501
@validation.response_body_schema(schema.rebuild_response_v226, '2.26', '2.46') # noqa: E501
@validation.response_body_schema(schema.rebuild_response_v247, '2.47', '2.53') # noqa: E501
@validation.response_body_schema(schema.rebuild_response_v254, '2.54', '2.56') # noqa: E501
@validation.response_body_schema(schema.rebuild_response_v257, '2.57', '2.62') # noqa: E501
@validation.response_body_schema(schema.rebuild_response_v263, '2.63', '2.70') # noqa: E501
+3 -3
View File
@@ -365,9 +365,9 @@ class ViewBuilder(common.ViewBuilder):
# compatible with v2.0 for the ec2 API split out from Nova.
# After this, however, new microversions should not be using
# the OS-EXT-SRV-ATTR prefix.
properties += ['reservation_id', 'launch_index',
'hostname', 'kernel_id', 'ramdisk_id',
'root_device_name']
properties += [
'hostname', 'kernel_id', 'launch_index', 'ramdisk_id',
'reservation_id', 'root_device_name']
# NOTE(gmann): Since microversion 2.75, PUT and Rebuild
# response include all the server attributes including these
# extended attributes also. But microversion 2.57 already
+10 -6
View File
@@ -344,7 +344,8 @@ class ServersPolicyTest(base.BasePolicyTest):
self.req, self.instance.uuid)
@mock.patch('nova.objects.BlockDeviceMappingList.bdms_by_instance_uuid')
@mock.patch('nova.compute.api.API.get_instance_host_status')
@mock.patch('nova.compute.api.API.get_instance_host_status',
return_value=fields.HostStatus.UP)
def test_server_show_with_extra_specs_policy(self, mock_get, mock_block):
rule = policies.SERVERS % 'show'
# server 'show' policy is checked before flavor extra specs
@@ -1012,9 +1013,11 @@ class ServersPolicyTest(base.BasePolicyTest):
self.assertNotIn('host_status', resp['servers'][0])
@mock.patch('nova.objects.BlockDeviceMappingList.bdms_by_instance_uuid')
@mock.patch('nova.compute.api.API.get_instance_host_status')
def test_server_show_with_host_status_policy(self,
mock_status, mock_block):
@mock.patch('nova.compute.api.API.get_instance_host_status',
return_value=fields.HostStatus.UP)
def test_server_show_with_host_status_policy(
self, mock_status, mock_block,
):
rule = policies.SERVERS % 'show'
# server 'show' policy is checked before host_status
# policy so we have to allow it for everyone otherwise it will fail
@@ -1035,8 +1038,9 @@ class ServersPolicyTest(base.BasePolicyTest):
@mock.patch('nova.compute.api.API.get_instance_host_status',
return_value=fields.HostStatus.UP)
@mock.patch('nova.compute.api.API.rebuild')
def test_server_rebuild_with_host_status_policy(self, mock_rebuild,
mock_status, mock_bdm):
def test_server_rebuild_with_host_status_policy(
self, mock_rebuild, mock_status, mock_bdm,
):
rule = policies.SERVERS % 'rebuild'
# server 'rebuild' policy is checked before host_status
# policy so we have to allow it for everyone otherwise it will fail