From 4404790656b7a6f7c15ed2eb3faf226c28965c23 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Wed, 27 Mar 2024 20:41:51 +0000 Subject: [PATCH] api: Add response body schemas for flavors APIs This is one of the trickier APIs, mostly due to the number of APIs and the number of fields in the response (just wait until we get to the servers APIs, says you /o\). It's done now though. Change-Id: Iaec9cd04b1cd19b707258ba9a5d3dab18543e661 Signed-off-by: Stephen Finucane --- nova/api/openstack/compute/flavors.py | 37 +++- nova/api/openstack/compute/schemas/flavors.py | 203 +++++++++++++++++- .../tests/unit/policies/test_flavor_manage.py | 13 ++ 3 files changed, 239 insertions(+), 14 deletions(-) diff --git a/nova/api/openstack/compute/flavors.py b/nova/api/openstack/compute/flavors.py index 53f79ff46e..be950240d9 100644 --- a/nova/api/openstack/compute/flavors.py +++ b/nova/api/openstack/compute/flavors.py @@ -41,6 +41,7 @@ class FlavorsController(wsgi.Controller): # return no response body. @wsgi.response(202) @wsgi.expected_errors(404) + @validation.response_body_schema(schema.delete_response) def delete(self, req, id): context = req.environ['nova.context'] context.can(fm_policies.POLICY_ROOT % 'delete', target={}) @@ -56,8 +57,11 @@ class FlavorsController(wsgi.Controller): @wsgi.expected_errors((400, 409)) @validation.schema(schema.create_v20, '2.0', '2.0') @validation.schema(schema.create, '2.1', '2.54') - @validation.schema(schema.create_v2_55, - flavors_view.FLAVOR_DESCRIPTION_MICROVERSION) + @validation.schema(schema.create_v255, '2.55') + @validation.response_body_schema(schema.create_response, '2.0', '2.54') + @validation.response_body_schema(schema.create_response_v255, '2.55', '2.60') # noqa: E501 + @validation.response_body_schema(schema.create_response_v261, '2.61', '2.74') # noqa: E501 + @validation.response_body_schema(schema.create_response_v275, '2.75') def create(self, req, body): context = req.environ['nova.context'] context.can(fm_policies.POLICY_ROOT % 'create', target={}) @@ -106,10 +110,12 @@ class FlavorsController(wsgi.Controller): return self._view_builder.show(req, flavor, include_description, include_extra_specs=include_extra_specs) - @wsgi.Controller.api_version(flavors_view.FLAVOR_DESCRIPTION_MICROVERSION) + @wsgi.Controller.api_version('2.55') @wsgi.expected_errors((400, 404)) - @validation.schema(schema.update_v2_55, - flavors_view.FLAVOR_DESCRIPTION_MICROVERSION) + @validation.schema(schema.update, '2.55') + @validation.response_body_schema(schema.update_response, '2.55', '2.60') + @validation.response_body_schema(schema.update_response_v261, '2.61', '2.74') # noqa: E501 + @validation.response_body_schema(schema.update_response_v275, '2.75') def update(self, req, id, body): # Validate the policy. context = req.environ['nova.context'] @@ -131,17 +137,22 @@ class FlavorsController(wsgi.Controller): return self._view_builder.show(req, flavor, include_description=True, include_extra_specs=include_extra_specs) - @validation.query_schema(schema.index_query_275, '2.75') - @validation.query_schema(schema.index_query, '2.0', '2.74') @wsgi.expected_errors(400) + @validation.query_schema(schema.index_query, '2.0', '2.74') + @validation.query_schema(schema.index_query_275, '2.75') + @validation.response_body_schema(schema.index_response, '2.0', '2.54') + @validation.response_body_schema(schema.index_response_v255, '2.55') def index(self, req): """Return all flavors in brief.""" limited_flavors = self._get_flavors(req) return self._view_builder.index(req, limited_flavors) - @validation.query_schema(schema.index_query_275, '2.75') - @validation.query_schema(schema.index_query, '2.0', '2.74') @wsgi.expected_errors(400) + @validation.query_schema(schema.index_query, '2.0', '2.74') + @validation.query_schema(schema.index_query_275, '2.75') + @validation.response_body_schema(schema.detail_response, '2.0', '2.54') + @validation.response_body_schema(schema.detail_response_v255, '2.55', '2.60') # noqa: E501 + @validation.response_body_schema(schema.detail_response_v261, '2.61') def detail(self, req): """Return all flavors in detail.""" context = req.environ['nova.context'] @@ -156,6 +167,10 @@ class FlavorsController(wsgi.Controller): @wsgi.expected_errors(404) @validation.query_schema(schema.show_query) + @validation.response_body_schema(schema.show_response, '2.0', '2.54') + @validation.response_body_schema(schema.show_response_v255, '2.55', '2.60') + @validation.response_body_schema(schema.show_response_v261, '2.61', '2.74') + @validation.response_body_schema(schema.show_response_v275, '2.75') def show(self, req, id): """Return data about the given flavor id.""" context = req.environ['nova.context'] @@ -222,8 +237,8 @@ class FlavorsController(wsgi.Controller): raise webob.exc.HTTPBadRequest(explanation=msg) try: - limited_flavors = objects.FlavorList.get_all(context, - filters=filters, sort_key=sort_key, sort_dir=sort_dir, + limited_flavors = objects.FlavorList.get_all( + context, filters=filters, sort_key=sort_key, sort_dir=sort_dir, limit=limit, marker=marker) except exception.MarkerNotFound: msg = _('marker [%s] not found') % marker diff --git a/nova/api/openstack/compute/schemas/flavors.py b/nova/api/openstack/compute/schemas/flavors.py index c00a4a883e..0246a099d7 100644 --- a/nova/api/openstack/compute/schemas/flavors.py +++ b/nova/api/openstack/compute/schemas/flavors.py @@ -90,12 +90,12 @@ _flavor_description = { } -create_v2_55 = copy.deepcopy(create) -create_v2_55['properties']['flavor']['properties']['description'] = ( +create_v255 = copy.deepcopy(create) +create_v255['properties']['flavor']['properties']['description'] = ( _flavor_description) -update_v2_55 = { +update = { 'type': 'object', 'properties': { 'flavor': { @@ -145,3 +145,200 @@ show_query = { 'properties': {}, 'additionalProperties': True, } + +_flavor_basic = { + '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, + }, + }, + 'name': {'type': 'string'}, + }, + 'required': ['id', 'links', 'name'], + 'additionalProperties': False, +} + +_flavor_basic_v255 = copy.deepcopy(_flavor_basic) +_flavor_basic_v255['properties']['description'] = {'type': ['string', 'null']} +_flavor_basic_v255['required'].append('description') + +_flavor = { + 'type': 'object', + 'properties': { + 'disk': {'type': 'integer'}, + 'id': {'type': 'string'}, + 'links': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'href': {'type': 'string', 'format': 'uri'}, + 'rel': {'type': 'string'}, + }, + 'required': ['href', 'rel'], + }, + }, + 'name': {'type': 'string'}, + 'os-flavor-access:is_public': {}, + 'ram': {'type': 'integer'}, + 'rxtx_factor': {}, + 'swap': { + 'anyOf': [ + {'type': 'integer'}, + {'const': ''}, + ], + }, + 'vcpus': {'type': 'integer'}, + 'OS-FLV-EXT-DATA:ephemeral': {'type': 'integer'}, + 'OS-FLV-DISABLED:disabled': {'type': 'boolean'}, + }, + 'required': [ + 'disk', + 'id', + 'links', + 'name', + 'os-flavor-access:is_public', + 'ram', + 'rxtx_factor', + 'swap', + 'vcpus', + 'OS-FLV-DISABLED:disabled', + 'OS-FLV-EXT-DATA:ephemeral', + ], + 'additionalProperties': False, +} + +_flavor_v255 = copy.deepcopy(_flavor) +_flavor_v255['properties']['description'] = {'type': ['string', 'null']} +_flavor_v255['required'].append('description') + +_flavor_v261 = copy.deepcopy(_flavor_v255) +_flavor_v261['properties']['extra_specs'] = { + 'type': 'object', + 'patternProperties': { + '^[a-zA-Z0-9-_:. ]{1,255}$': {'type': 'string', 'maxLength': 255}, + }, + 'additionalProperties': False, +} + +_flavor_v275 = copy.deepcopy(_flavor_v261) +# we completely overwrite this since the new variant is much simpler +_flavor_v275['properties']['swap'] = {'type': 'integer'} + +_flavors_links = { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'href': {'type': 'string', 'format': 'uri'}, + 'rel': {'type': 'string'}, + }, + 'required': ['href', 'rel'], + 'additionalProperties': False, + }, +} + +delete_response = { + 'type': 'null', +} + +create_response = { + 'type': 'object', + 'properties': { + 'flavor': copy.deepcopy(_flavor), + }, + 'required': ['flavor'], + 'additionalProperties': False, +} + +create_response_v255 = copy.deepcopy(create_response) +create_response_v255['properties']['flavor'] = copy.deepcopy(_flavor_v255) + +create_response_v261 = copy.deepcopy(create_response_v255) +create_response_v261['properties']['flavor'] = copy.deepcopy(_flavor_v261) + +create_response_v275 = copy.deepcopy(create_response_v261) +create_response_v275['properties']['flavor'] = copy.deepcopy(_flavor_v275) + +# NOTE(stephenfin): update is only available from 2.55 and the response is +# identical to the create and show response from that point forward +update_response = { + 'type': 'object', + 'properties': { + 'flavor': copy.deepcopy(_flavor_v255), + }, + 'required': ['flavor'], + 'additionalProperties': False, +} + +update_response_v261 = copy.deepcopy(update_response) +update_response_v261['properties']['flavor'] = copy.deepcopy(_flavor_v261) + +update_response_v275 = copy.deepcopy(update_response_v261) +update_response_v275['properties']['flavor'] = copy.deepcopy(_flavor_v275) + +index_response = { + 'type': 'object', + 'properties': { + 'flavors': { + 'type': 'array', + 'items': _flavor_basic, + }, + 'flavors_links': _flavors_links, + }, + 'required': ['flavors'], + 'additionalProperties': False, +} + +index_response_v255 = copy.deepcopy(index_response) +index_response_v255['properties']['flavors']['items'] = _flavor_basic_v255 + +detail_response = { + 'type': 'object', + 'properties': { + 'flavors': { + 'type': 'array', + 'items': _flavor, + }, + 'flavors_links': _flavors_links, + }, + 'required': ['flavors'], + 'additionalProperties': False, +} + +detail_response_v255 = copy.deepcopy(detail_response) +detail_response_v255['properties']['flavors']['items'] = _flavor_v255 + +detail_response_v261 = copy.deepcopy(detail_response_v255) +detail_response_v261['properties']['flavors']['items'] = _flavor_v261 + +detail_response_v275 = copy.deepcopy(detail_response_v261) +detail_response_v275['properties']['flavors']['items'] = _flavor_v275 + +show_response = { + 'type': 'object', + 'properties': { + 'flavor': copy.deepcopy(_flavor), + }, + 'required': ['flavor'], + 'additionalProperties': False, +} + +show_response_v255 = copy.deepcopy(show_response) +show_response_v255['properties']['flavor'] = copy.deepcopy(_flavor_v255) + +show_response_v261 = copy.deepcopy(show_response_v255) +show_response_v261['properties']['flavor'] = copy.deepcopy(_flavor_v261) + +show_response_v275 = copy.deepcopy(show_response_v261) +show_response_v275['properties']['flavor'] = copy.deepcopy(_flavor_v275) diff --git a/nova/tests/unit/policies/test_flavor_manage.py b/nova/tests/unit/policies/test_flavor_manage.py index 08f79e5d68..e64d16f9ea 100644 --- a/nova/tests/unit/policies/test_flavor_manage.py +++ b/nova/tests/unit/policies/test_flavor_manage.py @@ -15,6 +15,7 @@ from unittest import mock from oslo_utils.fixture import uuidsentinel as uuids from nova.api.openstack.compute import flavors +from nova import objects from nova.policies import flavor_manage as fm_policies from nova.tests.unit.api.openstack import fakes from nova.tests.unit.policies import base @@ -69,6 +70,18 @@ class FlavorManagePolicyTest(base.BasePolicyTest): @mock.patch('nova.objects.Flavor.save') def test_update_flavor_policy(self, mock_save, mock_get): rule_name = fm_policies.POLICY_ROOT % 'update' + mock_get.return_value = objects.Flavor( + flavorid=uuids.fake_id, + name='test', + memory_mb=512, + vcpus=2, + root_gb=1, + ephemeral_gb=1, + swap=512, + rxtx_factor=1.0, + is_public=True, + disabled=False, + ) req = fakes.HTTPRequest.blank('', version='2.55') self.common_policy_auth(self.admin_authorized_contexts, rule_name, self.controller.update,