From 6e8395a4211c11b3d4fde305b7d372b7e1632617 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 6 Aug 2025 17:06:20 +0100 Subject: [PATCH] api: Add response body schemas for servers APIs (3/6) Next up, the detail view. The benefits from being a carbon copy of the show view. Change-Id: I5bd2a3f41c8814f338a15aff4de8edbbd185186b Signed-off-by: Stephen Finucane --- nova/api/openstack/compute/schemas/servers.py | 146 +++++++++++++++++- nova/api/openstack/compute/servers.py | 22 ++- .../api/openstack/compute/test_servers.py | 4 +- nova/tests/unit/policies/test_servers.py | 20 +-- 4 files changed, 175 insertions(+), 17 deletions(-) diff --git a/nova/api/openstack/compute/schemas/servers.py b/nova/api/openstack/compute/schemas/servers.py index a6f7216271a7..72db5d6a91df 100644 --- a/nova/api/openstack/compute/schemas/servers.py +++ b/nova/api/openstack/compute/schemas/servers.py @@ -752,6 +752,8 @@ _server_status = { 'SHUTOFF', 'SOFT_DELETED', 'SUSPENDED', + # UNKNOWN can be returned if the DB is corrupt + 'UNKNOWN', 'VERIFY_RESIZE', ], } @@ -862,6 +864,24 @@ _server_cell_down_response = { 'additionalProperties': False, } +_server_detail_cell_down_response = copy.deepcopy(_server_cell_down_response) +del _server_detail_cell_down_response['properties']['flavor'] +del _server_detail_cell_down_response['properties']['image'] +del _server_detail_cell_down_response['properties']['user_id'] +del _server_detail_cell_down_response['properties'][ + 'OS-EXT-AZ:availability_zone' +] +del _server_detail_cell_down_response['properties']['OS-EXT-STS:power_state'] +_server_detail_cell_down_response['required'].remove('flavor') +_server_detail_cell_down_response['required'].remove('image') +_server_detail_cell_down_response['required'].remove('user_id') +_server_detail_cell_down_response['required'].remove( + 'OS-EXT-AZ:availability_zone' +) +_server_detail_cell_down_response['required'].remove( + 'OS-EXT-STS:power_state' +) + _server_cell_down_response_v271 = copy.deepcopy(_server_cell_down_response) _server_cell_down_response_v271['properties'].update({ 'server_groups': { @@ -1054,7 +1074,7 @@ _server_response_v23 = copy.deepcopy(_server_response) _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:launch_index': {'type': ['integer', 'null']}, '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']}, @@ -1137,6 +1157,9 @@ _server_response_v263['properties'].update({ }) _server_response_v263['required'].append('trusted_image_certificates') +# Unfortunately from here the server show and server detail list views differ +# since server_groups are not shown for the latter. We should remedy that but +# for now, we need to take different paths. _server_response_v271 = copy.deepcopy(_server_response_v263) _server_response_v271['properties'].update({ 'server_groups': { @@ -1153,15 +1176,31 @@ _server_response_v273['properties'].update({ }) _server_response_v273['required'].append('locked_reason') +# Note that we based on v2.63 to exclude the server_groups addition +_server_detail_response_v273 = copy.deepcopy(_server_response_v263) +_server_detail_response_v273['properties'].update({ + 'locked_reason': {'type': ['null', 'string']}, +}) +_server_detail_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_detail_response_v290 = copy.deepcopy(_server_detail_response_v273) +_server_detail_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_detail_response_v296 = copy.deepcopy(_server_detail_response_v290) +_server_detail_response_v296['properties'].update({ + 'pinned_availability_zone': {'type': ['string', 'null']}, +}) +_server_detail_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': { @@ -1176,12 +1215,115 @@ _server_response_v298['properties']['image']['oneOf'][1]['properties'].update({ }, }) +_server_detail_response_v298 = copy.deepcopy(_server_detail_response_v296) +_server_detail_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') +_server_detail_response_v2100 = copy.deepcopy(_server_detail_response_v298) +_server_detail_response_v2100['properties'].update({ + 'scheduler_hints': _hints, +}) +_server_detail_response_v2100['required'].append('scheduler_hints') + +detail_response = { + 'type': 'object', + 'properties': { + 'servers': { + 'type': 'array', + 'items': _server_response, + }, + 'servers_links': response_types.collection_links, + }, + 'required': ['servers'], + 'additionalProperties': False, +} + +detail_response_v23 = copy.deepcopy(detail_response) +detail_response_v23['properties']['servers']['items'] = ( + _server_response_v23 +) + +detail_response_v29 = copy.deepcopy(detail_response_v23) +detail_response_v29['properties']['servers']['items'] = ( + _server_response_v29 +) + +detail_response_v216 = copy.deepcopy(detail_response_v29) +detail_response_v216['properties']['servers']['items'] = ( + _server_response_v216 +) + +detail_response_v219 = copy.deepcopy(detail_response_v216) +detail_response_v219['properties']['servers']['items'] = ( + _server_response_v219 +) + +detail_response_v226 = copy.deepcopy(detail_response_v219) +detail_response_v226['properties']['servers']['items'] = ( + _server_response_v226 +) + +detail_response_v247 = copy.deepcopy(detail_response_v226) +detail_response_v247['properties']['servers']['items'] = ( + _server_response_v247 +) + +detail_response_v263 = copy.deepcopy(detail_response_v247) +detail_response_v263['properties']['servers']['items'] = ( + _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 +detail_response_v269 = copy.deepcopy(detail_response_v263) +detail_response_v269['properties']['servers']['items'] = { + 'oneOf': [_server_response_v263, _server_detail_cell_down_response], +} + +detail_response_v273 = copy.deepcopy(detail_response_v263) +detail_response_v273['properties']['servers']['items'] = { + 'oneOf': [_server_detail_response_v273, _server_detail_cell_down_response], +} + +detail_response_v290 = copy.deepcopy(detail_response_v273) +detail_response_v290['properties']['servers']['items'] = { + 'oneOf': [_server_detail_response_v290, _server_detail_cell_down_response], +} + +detail_response_v296 = copy.deepcopy(detail_response_v290) +detail_response_v296['properties']['servers']['items'] = { + 'oneOf': [_server_detail_response_v296, _server_detail_cell_down_response], +} + +detail_response_v298 = copy.deepcopy(detail_response_v296) +detail_response_v298['properties']['servers']['items'] = { + 'oneOf': [_server_detail_response_v298, _server_detail_cell_down_response], +} + +detail_response_v2100 = copy.deepcopy(detail_response_v298) +detail_response_v2100['properties']['servers']['items'] = { + 'oneOf': [ + _server_detail_response_v2100, _server_detail_cell_down_response + ], +} show_response = { 'type': 'object', @@ -1501,7 +1643,7 @@ rebuild_response_v275['properties']['server']['properties'].update( 'OS-EXT-SRV-ATTR:hypervisor_hostname': {'type': ['string', 'null']}, 'OS-EXT-SRV-ATTR:instance_name': {'type': 'string'}, 'OS-EXT-SRV-ATTR:kernel_id': {'type': ['string', 'null']}, - 'OS-EXT-SRV-ATTR:launch_index': {'type': 'integer'}, + 'OS-EXT-SRV-ATTR:launch_index': {'type': ['integer', 'null']}, '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']}, diff --git a/nova/api/openstack/compute/servers.py b/nova/api/openstack/compute/servers.py index 60562165b77b..e95b88316a3c 100644 --- a/nova/api/openstack/compute/servers.py +++ b/nova/api/openstack/compute/servers.py @@ -131,11 +131,25 @@ class ServersController(wsgi.Controller): return servers @wsgi.expected_errors((400, 403)) - @validation.query_schema(schema.query_params_v275, '2.75') - @validation.query_schema(schema.query_params_v273, '2.73', '2.74') - @validation.query_schema(schema.query_params_v266, '2.66', '2.72') - @validation.query_schema(schema.query_params_v226, '2.26', '2.65') @validation.query_schema(schema.query_params_v21, '2.1', '2.25') + @validation.query_schema(schema.query_params_v226, '2.26', '2.65') + @validation.query_schema(schema.query_params_v266, '2.66', '2.72') + @validation.query_schema(schema.query_params_v273, '2.73', '2.74') + @validation.query_schema(schema.query_params_v275, '2.75') + @validation.response_body_schema(schema.detail_response, '2.1', '2.2') + @validation.response_body_schema(schema.detail_response_v23, '2.3', '2.8') + @validation.response_body_schema(schema.detail_response_v29, '2.9', '2.15') + @validation.response_body_schema(schema.detail_response_v216, '2.16', '2.18') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v219, '2.19', '2.25') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v226, '2.26', '2.46') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v247, '2.47', '2.62') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v263, '2.63', '2.68') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v269, '2.69', '2.72') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v273, '2.73', '2.89') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v290, '2.90', '2.95') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v296, '2.96', '2.97') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v298, '2.98', '2.99') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v2100, '2.100') def detail(self, req): """Returns a list of server details for a given user.""" context = req.environ['nova.context'] diff --git a/nova/tests/unit/api/openstack/compute/test_servers.py b/nova/tests/unit/api/openstack/compute/test_servers.py index bd2951c875a0..4400002b744b 100644 --- a/nova/tests/unit/api/openstack/compute/test_servers.py +++ b/nova/tests/unit/api/openstack/compute/test_servers.py @@ -1732,8 +1732,8 @@ class ServersControllerTest(_ServersControllerTest): return objects.InstanceList( objects=[fakes.stub_instance_obj(None, id=i + 1, - user_id='fake', - project_id='fake', + user_id=uuids.user_id, + project_id=uuids.project_id, host=i % 2, uuid=fakes.get_fake_uuid(i)) for i in range(5)]) diff --git a/nova/tests/unit/policies/test_servers.py b/nova/tests/unit/policies/test_servers.py index 265c832de2ba..285516bcb0f9 100644 --- a/nova/tests/unit/policies/test_servers.py +++ b/nova/tests/unit/policies/test_servers.py @@ -78,8 +78,8 @@ class ServersPolicyTest(base.BasePolicyTest): hostname='foo', launch_index=0) self.mock_flavor = self.useFixture( - fixtures.MockPatch('nova.compute.flavors.get_flavor_by_flavor_id' - )).mock + fixtures.MockPatch('nova.compute.flavors.get_flavor_by_flavor_id') + ).mock self.mock_flavor.return_value = fake_flavor.fake_flavor_obj( self.req.environ['nova.context'], flavorid='1') @@ -109,12 +109,13 @@ class ServersPolicyTest(base.BasePolicyTest): ) ) - self.servers = [fakes.stub_instance_obj( - 1, vm_state=vm_states.ACTIVE, uuid=uuids.fake, - project_id=self.project_id, user_id='user1'), - fakes.stub_instance_obj( - 2, vm_state=vm_states.ACTIVE, uuid=uuids.fake, - project_id='proj2', user_id='user2')] + self.servers = [ + fakes.stub_instance_obj( + 1, vm_state=vm_states.ACTIVE, uuid=uuids.fake, + project_id=self.project_id, user_id=uuids.user_a_id), + fakes.stub_instance_obj( + 2, vm_state=vm_states.ACTIVE, uuid=uuids.fake, + project_id=self.project_id_other, user_id=uuids.user_b_id)] fakes.stub_out_secgroup_api( self, security_groups=[{'name': 'default'}]) self.mock_get_all = self.useFixture(fixtures.MockPatchObject( @@ -126,7 +127,8 @@ class ServersPolicyTest(base.BasePolicyTest): 'flavorRef': uuids.fake_id, }, } - self.extended_attr = ['OS-EXT-SRV-ATTR:host', + self.extended_attr = [ + 'OS-EXT-SRV-ATTR:host', 'OS-EXT-SRV-ATTR:hypervisor_hostname', 'OS-EXT-SRV-ATTR:instance_name', 'OS-EXT-SRV-ATTR:hostname',