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,