diff --git a/nova/api/openstack/compute/contrib/flavorextraspecs.py b/nova/api/openstack/compute/contrib/flavorextraspecs.py index 427bb9d27d..ca35f0fc90 100644 --- a/nova/api/openstack/compute/contrib/flavorextraspecs.py +++ b/nova/api/openstack/compute/contrib/flavorextraspecs.py @@ -20,11 +20,11 @@ from webob import exc from nova.api.openstack import extensions from nova.api.openstack import wsgi from nova.api.openstack import xmlutil +from nova.compute import flavors from nova import db from nova import exception from nova.openstack.common.gettextutils import _ - authorize = extensions.extension_authorizer('compute', 'flavorextraspecs') @@ -55,6 +55,12 @@ class FlavorExtraSpecsController(object): expl = _('No Request Body') raise exc.HTTPBadRequest(explanation=expl) + def _check_key_names(self, keys): + try: + flavors.validate_extra_spec_keys(keys) + except exception.InvalidInput as error: + raise exc.HTTPBadRequest(explanation=error.format_message()) + @wsgi.serializers(xml=ExtraSpecsTemplate) def index(self, req, flavor_id): """Returns the list of extra specs for a given flavor.""" @@ -68,6 +74,7 @@ class FlavorExtraSpecsController(object): authorize(context, action='create') self._check_body(body) specs = body.get('extra_specs') + self._check_key_names(specs.keys()) try: db.flavor_extra_specs_update_or_create(context, flavor_id, diff --git a/nova/api/openstack/compute/plugins/v3/flavors_extraspecs.py b/nova/api/openstack/compute/plugins/v3/flavors_extraspecs.py index ee0e06221a..bb774379e6 100644 --- a/nova/api/openstack/compute/plugins/v3/flavors_extraspecs.py +++ b/nova/api/openstack/compute/plugins/v3/flavors_extraspecs.py @@ -17,6 +17,7 @@ import webob from nova.api.openstack import extensions from nova.api.openstack import wsgi +from nova.compute import flavors from nova import db from nova import exception from nova.openstack.common.db import exception as db_exc @@ -41,6 +42,12 @@ class FlavorExtraSpecsController(object): expl = _('No Request Body') raise webob.exc.HTTPBadRequest(explanation=expl) + def _check_key_names(self, keys): + try: + flavors.validate_extra_spec_keys(keys) + except exception.InvalidInput as error: + raise webob.exc.HTTPBadRequest(explanation=error.format_message()) + @extensions.expected_errors(()) def index(self, req, flavor_id): """Returns the list of extra specs for a given flavor.""" @@ -57,6 +64,7 @@ class FlavorExtraSpecsController(object): specs = body.get('extra_specs', {}) if not specs or type(specs) is not dict: raise webob.exc.HTTPBadRequest(_('No or bad extra_specs provided')) + self._check_key_names(specs.keys()) try: db.flavor_extra_specs_update_or_create(context, flavor_id, specs) diff --git a/nova/compute/flavors.py b/nova/compute/flavors.py index 2a0ecaf764..94be63b210 100644 --- a/nova/compute/flavors.py +++ b/nova/compute/flavors.py @@ -53,6 +53,9 @@ LOG = logging.getLogger(__name__) VALID_ID_REGEX = re.compile("^[\w\.\- ]*$") VALID_NAME_REGEX = re.compile("^[\w\.\- ]*$", re.UNICODE) +# Validate extra specs key names. +VALID_EXTRASPEC_NAME_REGEX = re.compile(r"[\w\.\- :]+$", re.UNICODE) + def _int_or_none(val): if val is not None: @@ -307,3 +310,11 @@ def delete_flavor_info(metadata, *prefixes): del metadata[to_key] pci_request.delete_flavor_pci_info(metadata, *prefixes) return metadata + + +def validate_extra_spec_keys(key_names_list): + for key_name in key_names_list: + if not VALID_EXTRASPEC_NAME_REGEX.match(key_name): + expl = _('Key Names can only contain alphanumeric characters, ' + 'periods, dashes, underscores, colons and spaces.') + raise exception.InvalidInput(message=expl) diff --git a/nova/tests/api/openstack/compute/contrib/test_flavors_extra_specs.py b/nova/tests/api/openstack/compute/contrib/test_flavors_extra_specs.py index d6f594fe8e..6336a3ce6b 100644 --- a/nova/tests/api/openstack/compute/contrib/test_flavors_extra_specs.py +++ b/nova/tests/api/openstack/compute/contrib/test_flavors_extra_specs.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import webob from nova.api.openstack.compute.contrib import flavorextraspecs @@ -151,6 +152,30 @@ class FlavorsExtraSpecsTest(test.TestCase): self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, req, 1, '') + @mock.patch('nova.db.flavor_extra_specs_update_or_create') + def test_create_invalid_specs_key(self, mock_flavor_extra_specs): + invalid_keys = ("key1/", "", "$$akey$", "!akey", "") + mock_flavor_extra_specs.side_effects = return_create_flavor_extra_specs + + for key in invalid_keys: + body = {"extra_specs": {key: "value1"}} + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, 1, body) + + @mock.patch('nova.db.flavor_extra_specs_update_or_create') + def test_create_valid_specs_key(self, mock_flavor_extra_specs): + valid_keys = ("key1", "month.price", "I_am-a Key", "finance:g2") + mock_flavor_extra_specs.side_effects = return_create_flavor_extra_specs + + for key in valid_keys: + body = {"extra_specs": {key: "value1"}} + req = fakes.HTTPRequest.blank('/v2/fake/flavors/1/os-extra_specs', + use_admin_context=True) + res_dict = self.controller.create(req, 1, body) + self.assertEqual('value1', res_dict['extra_specs'][key]) + def test_update_item(self): self.stubs.Set(nova.db, 'flavor_extra_specs_update_or_create', diff --git a/nova/tests/api/openstack/compute/plugins/v3/test_flavors_extra_specs.py b/nova/tests/api/openstack/compute/plugins/v3/test_flavors_extra_specs.py index f61f0f810e..a848c06d47 100644 --- a/nova/tests/api/openstack/compute/plugins/v3/test_flavors_extra_specs.py +++ b/nova/tests/api/openstack/compute/plugins/v3/test_flavors_extra_specs.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import webob from nova.api.openstack.compute.plugins.v3 import flavors_extraspecs @@ -176,6 +177,33 @@ class FlavorsExtraSpecsTest(test.TestCase): self.assertRaises(webob.exc.HTTPConflict, self.controller.create, req, 1, body) + @mock.patch('nova.db.flavor_extra_specs_update_or_create') + def test_create_invalid_specs_key(self, mock_flavor_extra_specs): + invalid_keys = ("key1/", "", "$$akey$", "!akey", "") + mock_flavor_extra_specs.side_effects = return_create_flavor_extra_specs + + for key in invalid_keys: + body = {"extra_specs": {key: "value1"}} + + req = fakes.HTTPRequest.blank('/v3/flavors/1/extra-specs', + use_admin_context=True) + self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, + req, 1, body) + + @mock.patch('nova.db.flavor_extra_specs_update_or_create') + def test_create_valid_specs_key(self, mock_flavor_extra_specs): + valid_keys = ("key1", "month.price", "I_am-a Key", "finance:g2") + mock_flavor_extra_specs.side_effects = return_create_flavor_extra_specs + + for key in valid_keys: + body = {"extra_specs": {key: "value1"}} + + req = fakes.HTTPRequest.blank('/v3/flavors/1/extra-specs', + use_admin_context=True) + res_dict = self.controller.create(req, 1, body) + self.assertEqual('value1', res_dict['extra_specs'][key]) + self.assertEqual(self.controller.create.wsgi_code, 201) + def test_update_item(self): self.stubs.Set(nova.db, 'flavor_extra_specs_update_or_create',