diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py index 52006c3f13..f3f5ed0414 100644 --- a/glance/api/v1/images.py +++ b/glance/api/v1/images.py @@ -65,9 +65,12 @@ CONF.import_opt('container_formats', 'glance.common.config', CONF.import_opt('image_property_quota', 'glance.common.config') -def validate_image_meta(req, values): +def _validate_format(req, values): + """Validates disk_format and container_format fields - name = values.get('name') + Introduced to split too complex validate_image_meta method. + """ + amazon_formats = ('aki', 'ari', 'ami') disk_format = values.get('disk_format') container_format = values.get('container_format') @@ -82,13 +85,7 @@ def validate_image_meta(req, values): "for image.") % container_format raise HTTPBadRequest(explanation=msg, request=req) - if name and len(name) > 255: - msg = _('Image name too long: %d') % len(name) - raise HTTPBadRequest(explanation=msg, request=req) - - amazon_formats = ('aki', 'ari', 'ami') - - if disk_format in amazon_formats or container_format in amazon_formats: + if any(f in amazon_formats for f in [disk_format, container_format]): if disk_format is None: values['disk_format'] = container_format elif container_format is None: @@ -100,6 +97,25 @@ def validate_image_meta(req, values): "and disk formats must match.")) raise HTTPBadRequest(explanation=msg, request=req) + +def validate_image_meta(req, values): + _validate_format(req, values) + + name = values.get('name') + checksum = values.get('checksum') + + if name and len(name) > 255: + msg = _('Image name too long: %d') % len(name) + raise HTTPBadRequest(explanation=msg, request=req) + + # check that checksum retrieved is exactly 32 characters + # as long as we expect md5 checksum + # https://bugs.launchpad.net/glance/+bug/1454730 + if checksum and len(checksum) > 32: + msg = (_("Invalid checksum '%s': can't exceed 32 characters") % + checksum) + raise HTTPBadRequest(explanation=msg, request=req) + return values diff --git a/glance/common/utils.py b/glance/common/utils.py index 1923be4117..679a20d349 100644 --- a/glance/common/utils.py +++ b/glance/common/utils.py @@ -715,6 +715,31 @@ def no_4byte_params(f): return wrapper +def validate_mysql_int(*args, **kwargs): + """ + Make sure that all arguments are less than 2 ** 31 - 1. + + This limitation is introduced because mysql stores INT in 4 bytes. + If the validation fails for some argument, exception.Invalid is raised with + appropriate information. + """ + max_int = (2 ** 31) - 1 + for param in args: + if param > max_int: + msg = _("Value %(value)d out of range, " + "must not exceed %(max)d") % {"value": param, + "max": max_int} + raise exception.Invalid(msg) + + for param_str in kwargs: + param = kwargs.get(param_str) + if param and param > max_int: + msg = _("'%(param)s' value out of range, " + "must not exceed %(max)d") % {"param": param_str, + "max": max_int} + raise exception.Invalid(msg) + + def stash_conf_values(): """ Make a copy of some of the current global CONF's settings. diff --git a/glance/db/sqlalchemy/api.py b/glance/db/sqlalchemy/api.py index cf20ca01d5..6045e54463 100644 --- a/glance/db/sqlalchemy/api.py +++ b/glance/db/sqlalchemy/api.py @@ -675,6 +675,10 @@ def _validate_image(values): msg = "Invalid image status '%s' for image." % status raise exception.Invalid(msg) + # validate integer values to eliminate DBError on save + utils.validate_mysql_int(min_disk=values.get('min_disk'), + min_ram=values.get('min_ram')) + return values diff --git a/glance/tests/functional/db/base.py b/glance/tests/functional/db/base.py index 36d454d004..f508d0f0fe 100644 --- a/glance/tests/functional/db/base.py +++ b/glance/tests/functional/db/base.py @@ -222,6 +222,25 @@ class DriverTests(object): self.assertRaises(exception.Invalid, self.db_api.image_create, self.context, fixture) + def test_image_create_bad_checksum(self): + # checksum should be no longer than 32 characters + bad_checksum = "42" * 42 + fixture = {'checksum': bad_checksum} + self.assertRaises(exception.Invalid, self.db_api.image_create, + self.context, fixture) + # if checksum is not longer than 32 characters but non-ascii -> + # still raise 400 + fixture = {'checksum': u'\u042f' * 32} + self.assertRaises(exception.Invalid, self.db_api.image_create, + self.context, fixture) + + def test_image_create_bad_int_params(self): + int_too_long = 2 ** 31 + 42 + for param in ['min_disk', 'min_ram']: + fixture = {param: int_too_long} + self.assertRaises(exception.Invalid, self.db_api.image_create, + self.context, fixture) + def test_image_create_bad_property(self): # bad value fixture = {'status': 'queued', diff --git a/glance/tests/functional/v1/test_api.py b/glance/tests/functional/v1/test_api.py index d641992c28..7e076719ac 100644 --- a/glance/tests/functional/v1/test_api.py +++ b/glance/tests/functional/v1/test_api.py @@ -18,6 +18,8 @@ import hashlib import httplib2 +import sys + from oslo_serialization import jsonutils from oslo_utils import units # NOTE(jokke): simplified transition to py3, behaves like py2 xrange @@ -35,6 +37,50 @@ class TestApi(functional.FunctionalTest): """Functional tests using httplib2 against the API server""" + def _check_image_create(self, headers, status=201, + image_data="*" * FIVE_KB): + # performs image_create request, checks the response and returns + # content + http = httplib2.Http() + path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) + response, content = http.request( + path, 'POST', headers=headers, body=image_data) + self.assertEqual(status, response.status) + return content + + def test_checksum_32_chars_at_image_create(self): + self.cleanup() + self.start_servers(**self.__dict__.copy()) + headers = minimal_headers('Image1') + image_data = "*" * FIVE_KB + + # checksum can be no longer that 32 characters (String(32)) + headers['X-Image-Meta-Checksum'] = 'x' * 42 + content = self._check_image_create(headers, 400) + self.assertIn("Invalid checksum", content) + # test positive case as well + headers['X-Image-Meta-Checksum'] = hashlib.md5(image_data).hexdigest() + self._check_image_create(headers) + + def test_param_int_too_large_at_create(self): + # currently 2 params min_disk/min_ram can cause DBError on save + self.cleanup() + self.start_servers(**self.__dict__.copy()) + # Integer field can't be greater than max 8-byte signed integer + for param in ['min_disk', 'min_ram']: + headers = minimal_headers('Image1') + # check that long numbers result in 400 + headers['X-Image-Meta-%s' % param] = str(sys.maxint + 1) + content = self._check_image_create(headers, 400) + self.assertIn("'%s' value out of range" % param, content) + # check that integers over 4 byte result in 400 + headers['X-Image-Meta-%s' % param] = str(2 ** 31) + content = self._check_image_create(headers, 400) + self.assertIn("'%s' value out of range" % param, content) + # verify positive case as well + headers['X-Image-Meta-%s' % param] = str((2 ** 31) - 1) + self._check_image_create(headers) + @skip_if_disabled def test_get_head_simple_post(self): """