Add config option to limit image tags
This patch adds the image_tag_quota config option. This allows a deployer to limit the number of image tags allowed on an image for v2. The default value is 128. If an image is somehow over the limit, tags can still be removed but no more may be added. Fixes bug 1252337 docImpact Change-Id: I2036e2a88601d7c5aa85fad32d90fe0ed30b84c8
This commit is contained in:
parent
574936f650
commit
4e7d9cdaf9
@ -434,6 +434,9 @@ scrubber_datadir = /var/lib/glance/scrubber
|
||||
# The maximum number of image properties allowed per image
|
||||
#image_property_quota = 128
|
||||
|
||||
# The maximum number of tags allowed per image
|
||||
#image_tag_quota = 128
|
||||
|
||||
# Set a system wide quota for every user. This value is the total number
|
||||
# of bytes that a user can use across all storage systems. A value of
|
||||
# 0 means unlimited.
|
||||
|
@ -48,6 +48,8 @@ class Controller(object):
|
||||
raise webob.exc.HTTPNotFound(explanation=unicode(e))
|
||||
except exception.Forbidden as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=unicode(e))
|
||||
except exception.ImageTagLimitExceeded as e:
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(explanation=unicode(e))
|
||||
|
||||
@utils.mutating
|
||||
def delete(self, req, image_id, tag_value):
|
||||
|
@ -63,7 +63,7 @@ class ImagesController(object):
|
||||
raise webob.exc.HTTPBadRequest(explanation=unicode(dup))
|
||||
except exception.Forbidden as e:
|
||||
raise webob.exc.HTTPForbidden(explanation=unicode(e))
|
||||
except exception.ImagePropertyLimitExceeded as e:
|
||||
except exception.LimitExceeded as e:
|
||||
LOG.info(unicode(e))
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(
|
||||
explanation=unicode(e), request=req, content_type='text/plain')
|
||||
@ -134,7 +134,7 @@ class ImagesController(object):
|
||||
LOG.info(msg)
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(
|
||||
explanation=msg, request=req, content_type='text/plain')
|
||||
except exception.ImagePropertyLimitExceeded as e:
|
||||
except exception.LimitExceeded as e:
|
||||
LOG.info(unicode(e))
|
||||
raise webob.exc.HTTPRequestEntityTooLarge(
|
||||
explanation=unicode(e), request=req, content_type='text/plain')
|
||||
|
@ -47,6 +47,9 @@ common_opts = [
|
||||
cfg.IntOpt('image_property_quota', default=128,
|
||||
help=_('Maximum number of properties allowed on an image. '
|
||||
'Negative values evaluate to unlimited.')),
|
||||
cfg.IntOpt('image_tag_quota', default=128,
|
||||
help=_('Maximum number of tags allowed on an image. '
|
||||
'Negative values evaluate to unlimited.')),
|
||||
cfg.StrOpt('data_api', default='glance.db.sqlalchemy.api',
|
||||
help=_('Python module path of data access API')),
|
||||
cfg.IntOpt('limit_param_default', default=25,
|
||||
|
@ -289,11 +289,16 @@ class ImageSizeLimitExceeded(GlanceException):
|
||||
message = _("The provided image is too large.")
|
||||
|
||||
|
||||
class ImagePropertyLimitExceeded(GlanceException):
|
||||
class ImagePropertyLimitExceeded(LimitExceeded):
|
||||
message = _("The limit has been exceeded on the number of allowed image "
|
||||
"properties. Attempted: %(attempted)s, Maximum: %(maximum)s")
|
||||
|
||||
|
||||
class ImageTagLimitExceeded(LimitExceeded):
|
||||
message = _("The limit has been exceeded on the number of allowed image "
|
||||
"tags. Attempted: %(attempted)s, Maximum: %(maximum)s")
|
||||
|
||||
|
||||
class RPCError(GlanceException):
|
||||
message = _("%(cls)s exception was raised in the last rpc call: %(val)s")
|
||||
|
||||
|
@ -28,6 +28,20 @@ import glance.openstack.common.log as logging
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
CONF.import_opt('image_property_quota', 'glance.common.config')
|
||||
CONF.import_opt('image_tag_quota', 'glance.common.config')
|
||||
|
||||
|
||||
def _enforce_image_tag_quota(tags):
|
||||
if CONF.image_tag_quota < 0:
|
||||
# If value is negative, allow unlimited number of tags
|
||||
return
|
||||
|
||||
if not tags:
|
||||
return
|
||||
|
||||
if len(tags) > CONF.image_tag_quota:
|
||||
raise exception.ImageTagLimitExceeded(attempted=len(tags),
|
||||
maximum=CONF.image_tag_quota)
|
||||
|
||||
|
||||
class ImageRepoProxy(glance.domain.proxy.Repo):
|
||||
@ -67,6 +81,42 @@ class ImageFactoryProxy(glance.domain.proxy.ImageFactory):
|
||||
proxy_class=ImageProxy,
|
||||
proxy_kwargs=proxy_kwargs)
|
||||
|
||||
def new_image(self, **kwargs):
|
||||
tags = kwargs.pop('tags', set([]))
|
||||
|
||||
_enforce_image_tag_quota(tags)
|
||||
return super(ImageFactoryProxy, self).new_image(tags=tags, **kwargs)
|
||||
|
||||
|
||||
class QuotaImageTagsProxy(object):
|
||||
|
||||
def __init__(self, orig_set):
|
||||
if orig_set is None:
|
||||
orig_set = set([])
|
||||
self.tags = orig_set
|
||||
|
||||
def add(self, item):
|
||||
self.tags.add(item)
|
||||
_enforce_image_tag_quota(self.tags)
|
||||
|
||||
def __cast__(self, *args, **kwargs):
|
||||
return self.tags.__cast__(*args, **kwargs)
|
||||
|
||||
def __contains__(self, *args, **kwargs):
|
||||
return self.tags.__contains__(*args, **kwargs)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.tags == other
|
||||
|
||||
def __iter__(self, *args, **kwargs):
|
||||
return self.tags.__iter__(*args, **kwargs)
|
||||
|
||||
def __len__(self, *args, **kwargs):
|
||||
return self.tags.__len__(*args, **kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.tags, name)
|
||||
|
||||
|
||||
class QuotaImageLocationsProxy(object):
|
||||
|
||||
@ -181,6 +231,15 @@ class ImageProxy(glance.domain.proxy.Image):
|
||||
location, self.context, self.image.image_id)
|
||||
raise
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
return QuotaImageTagsProxy(self.image.tags)
|
||||
|
||||
@tags.setter
|
||||
def tags(self, value):
|
||||
_enforce_image_tag_quota(value)
|
||||
self.image.tags = value
|
||||
|
||||
@property
|
||||
def locations(self):
|
||||
return QuotaImageLocationsProxy(self.image,
|
||||
|
@ -313,6 +313,7 @@ class ApiServer(Server):
|
||||
self.policy_default_rule = 'default'
|
||||
self.property_protection_rule_format = 'roles'
|
||||
self.image_property_quota = 10
|
||||
self.image_tag_quota = 10
|
||||
|
||||
self.needs_database = True
|
||||
default_sql_connection = 'sqlite:////%s/tests.sqlite' % self.test_dir
|
||||
@ -375,6 +376,7 @@ enable_v2_api= %(enable_v2_api)s
|
||||
property_protection_file = %(property_protection_file)s
|
||||
property_protection_rule_format = %(property_protection_rule_format)s
|
||||
image_property_quota=%(image_property_quota)s
|
||||
image_tag_quota=%(image_tag_quota)s
|
||||
[paste_deploy]
|
||||
flavor = %(deployment_flavor)s
|
||||
"""
|
||||
|
@ -1228,6 +1228,70 @@ class TestImages(functional.FunctionalTest):
|
||||
tags = json.loads(response.text)['tags']
|
||||
self.assertEqual(['sniff'], tags)
|
||||
|
||||
# Delete all tags
|
||||
for tag in tags:
|
||||
path = self._url('/v2/images/%s/tags/%s' % (image_id, tag))
|
||||
response = requests.delete(path, headers=self._headers())
|
||||
self.assertEqual(204, response.status_code)
|
||||
|
||||
# Update image with too many tags via PUT
|
||||
# Configured limit is 10 tags
|
||||
for i in range(10):
|
||||
path = self._url('/v2/images/%s/tags/foo%i' % (image_id, i))
|
||||
response = requests.put(path, headers=self._headers())
|
||||
self.assertEqual(204, response.status_code)
|
||||
|
||||
# 11th tag should fail
|
||||
path = self._url('/v2/images/%s/tags/fail_me' % image_id)
|
||||
response = requests.put(path, headers=self._headers())
|
||||
self.assertEqual(413, response.status_code)
|
||||
|
||||
# Make sure the 11th tag was not added
|
||||
path = self._url('/v2/images/%s' % image_id)
|
||||
response = requests.get(path, headers=self._headers())
|
||||
self.assertEqual(200, response.status_code)
|
||||
tags = json.loads(response.text)['tags']
|
||||
self.assertEqual(10, len(tags))
|
||||
|
||||
# Update image tags via PATCH
|
||||
path = self._url('/v2/images/%s' % image_id)
|
||||
media_type = 'application/openstack-images-v2.1-json-patch'
|
||||
headers = self._headers({'content-type': media_type})
|
||||
doc = [
|
||||
{
|
||||
'op': 'replace',
|
||||
'path': '/tags',
|
||||
'value': ['foo'],
|
||||
},
|
||||
]
|
||||
data = json.dumps(doc)
|
||||
response = requests.patch(path, headers=headers, data=data)
|
||||
self.assertEqual(200, response.status_code)
|
||||
|
||||
# Update image with too many tags via PATCH
|
||||
# Configured limit is 10 tags
|
||||
path = self._url('/v2/images/%s' % image_id)
|
||||
media_type = 'application/openstack-images-v2.1-json-patch'
|
||||
headers = self._headers({'content-type': media_type})
|
||||
tags = ['foo%d' % i for i in range(11)]
|
||||
doc = [
|
||||
{
|
||||
'op': 'replace',
|
||||
'path': '/tags',
|
||||
'value': tags,
|
||||
},
|
||||
]
|
||||
data = json.dumps(doc)
|
||||
response = requests.patch(path, headers=headers, data=data)
|
||||
self.assertEqual(413, response.status_code)
|
||||
|
||||
# Tags should not have changed since request was over limit
|
||||
path = self._url('/v2/images/%s' % image_id)
|
||||
response = requests.get(path, headers=self._headers())
|
||||
self.assertEqual(200, response.status_code)
|
||||
tags = json.loads(response.text)['tags']
|
||||
self.assertEqual(['foo'], tags)
|
||||
|
||||
# Update image with duplicate tag - it should be ignored
|
||||
path = self._url('/v2/images/%s' % image_id)
|
||||
media_type = 'application/openstack-images-v2.1-json-patch'
|
||||
|
@ -67,6 +67,7 @@ class FakeImage(object):
|
||||
size = None
|
||||
image_id = 'someid'
|
||||
locations = [{'url': 'file:///not/a/path', 'metadata': {}}]
|
||||
tags = set([])
|
||||
|
||||
def set_data(self, data, size=None):
|
||||
self.size = 0
|
||||
@ -269,6 +270,7 @@ class TestImagePropertyQuotas(test_utils.BaseTestCase):
|
||||
mock.Mock())
|
||||
|
||||
self.image_repo_mock = mock.Mock()
|
||||
|
||||
self.image_repo_proxy = glance.quota.ImageRepoProxy(
|
||||
self.image_repo_mock,
|
||||
mock.Mock(),
|
||||
@ -321,3 +323,102 @@ class TestImagePropertyQuotas(test_utils.BaseTestCase):
|
||||
self.image_repo_proxy.add(self.image)
|
||||
|
||||
self.image_repo_mock.add.assert_called_once_with(self.base_image)
|
||||
|
||||
|
||||
class TestImageTagQuotas(test_utils.BaseTestCase):
|
||||
def setUp(self):
|
||||
super(TestImageTagQuotas, self).setUp()
|
||||
self.base_image = mock.Mock()
|
||||
self.base_image.tags = set([])
|
||||
self.image = glance.quota.ImageProxy(self.base_image,
|
||||
mock.Mock(),
|
||||
mock.Mock())
|
||||
|
||||
self.image_repo_mock = mock.Mock()
|
||||
self.image_repo_proxy = glance.quota.ImageRepoProxy(
|
||||
self.image_repo_mock,
|
||||
mock.Mock(),
|
||||
mock.Mock())
|
||||
|
||||
def test_replace_image_tag(self):
|
||||
self.config(image_tag_quota=1)
|
||||
self.image.tags = ['foo']
|
||||
self.assertEqual(len(self.image.tags), 1)
|
||||
|
||||
def test_replace_too_many_image_tags(self):
|
||||
self.config(image_tag_quota=0)
|
||||
|
||||
exc = self.assertRaises(exception.ImageTagLimitExceeded,
|
||||
setattr, self.image, 'tags', ['foo', 'bar'])
|
||||
self.assertTrue('Attempted: 2, Maximum: 0' in str(exc))
|
||||
self.assertEqual(len(self.image.tags), 0)
|
||||
|
||||
def test_replace_unlimited_image_tags(self):
|
||||
self.config(image_tag_quota=-1)
|
||||
self.image.tags = ['foo']
|
||||
self.assertEqual(len(self.image.tags), 1)
|
||||
|
||||
def test_add_image_tag(self):
|
||||
self.config(image_tag_quota=1)
|
||||
self.image.tags.add('foo')
|
||||
self.assertEqual(len(self.image.tags), 1)
|
||||
|
||||
def test_add_too_many_image_tags(self):
|
||||
self.config(image_tag_quota=1)
|
||||
self.image.tags.add('foo')
|
||||
exc = self.assertRaises(exception.ImageTagLimitExceeded,
|
||||
self.image.tags.add, 'bar')
|
||||
self.assertTrue('Attempted: 2, Maximum: 1' in str(exc))
|
||||
|
||||
def test_add_unlimited_image_tags(self):
|
||||
self.config(image_tag_quota=-1)
|
||||
self.image.tags.add('foo')
|
||||
self.assertEqual(len(self.image.tags), 1)
|
||||
|
||||
def test_remove_image_tag_while_over_quota(self):
|
||||
self.config(image_tag_quota=1)
|
||||
self.image.tags.add('foo')
|
||||
self.assertEqual(len(self.image.tags), 1)
|
||||
self.config(image_tag_quota=0)
|
||||
self.image.tags.remove('foo')
|
||||
self.assertEqual(len(self.image.tags), 0)
|
||||
|
||||
|
||||
class TestQuotaImageTagsProxy(test_utils.BaseTestCase):
|
||||
def setUp(self):
|
||||
super(TestQuotaImageTagsProxy, self).setUp()
|
||||
|
||||
def test_add(self):
|
||||
proxy = glance.quota.QuotaImageTagsProxy(set([]))
|
||||
proxy.add('foo')
|
||||
self.assertTrue('foo' in proxy)
|
||||
|
||||
def test_add_too_many_tags(self):
|
||||
self.config(image_tag_quota=0)
|
||||
proxy = glance.quota.QuotaImageTagsProxy(set([]))
|
||||
exc = self.assertRaises(exception.ImageTagLimitExceeded,
|
||||
proxy.add, 'bar')
|
||||
self.assertTrue('Attempted: 1, Maximum: 0' in str(exc))
|
||||
|
||||
def test_equals(self):
|
||||
proxy = glance.quota.QuotaImageTagsProxy(set([]))
|
||||
self.assertEqual(set([]), proxy)
|
||||
|
||||
def test_contains(self):
|
||||
proxy = glance.quota.QuotaImageTagsProxy(set(['foo']))
|
||||
self.assertTrue('foo' in proxy)
|
||||
|
||||
def test_len(self):
|
||||
proxy = glance.quota.QuotaImageTagsProxy(set(['foo',
|
||||
'bar',
|
||||
'baz',
|
||||
'niz']))
|
||||
self.assertEqual(len(proxy), 4)
|
||||
|
||||
def test_iter(self):
|
||||
items = set(['foo', 'bar', 'baz', 'niz'])
|
||||
proxy = glance.quota.QuotaImageTagsProxy(items.copy())
|
||||
self.assertEqual(len(items), 4)
|
||||
for item in proxy:
|
||||
items.remove(item)
|
||||
self.assertEqual(len(items), 0)
|
||||
|
@ -35,6 +35,13 @@ class TestImageTagsController(base.IsolatedUnitTest):
|
||||
tags = self.db.image_tag_get_all(context, unit_test_utils.UUID1)
|
||||
self.assertEqual(1, len([tag for tag in tags if tag == 'dink']))
|
||||
|
||||
def test_create_too_many_tags(self):
|
||||
self.config(image_tag_quota=0)
|
||||
request = unit_test_utils.get_fake_request()
|
||||
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
|
||||
self.controller.update,
|
||||
request, unit_test_utils.UUID1, 'dink')
|
||||
|
||||
def test_create_duplicate_tag_ignored(self):
|
||||
request = unit_test_utils.get_fake_request()
|
||||
self.controller.update(request, unit_test_utils.UUID1, 'dink')
|
||||
|
@ -567,12 +567,23 @@ class TestImagesController(base.IsolatedUnitTest):
|
||||
self.assertEqual(output_log['event_type'], 'image.create')
|
||||
self.assertEqual(output_log['payload']['id'], output.image_id)
|
||||
|
||||
def test_create_with_too_many_tags(self):
|
||||
self.config(image_tag_quota=1)
|
||||
request = unit_test_utils.get_fake_request()
|
||||
tags = ['ping', 'pong']
|
||||
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
|
||||
self.controller.create,
|
||||
request, image={}, extra_properties={},
|
||||
tags=tags)
|
||||
|
||||
def test_update_no_changes(self):
|
||||
request = unit_test_utils.get_fake_request()
|
||||
output = self.controller.update(request, UUID1, changes=[])
|
||||
self.assertEqual(output.image_id, UUID1)
|
||||
self.assertEqual(output.created_at, output.updated_at)
|
||||
self.assertEqual(output.tags, set(['ping', 'pong']))
|
||||
self.assertEqual(len(output.tags), 2)
|
||||
self.assertTrue('ping' in output.tags)
|
||||
self.assertTrue('pong' in output.tags)
|
||||
output_logs = self.notifier.get_logs()
|
||||
#NOTE(markwash): don't send a notification if nothing is updated
|
||||
self.assertTrue(len(output_logs) == 0)
|
||||
@ -605,7 +616,9 @@ class TestImagesController(base.IsolatedUnitTest):
|
||||
]
|
||||
output = self.controller.update(request, UUID1, changes)
|
||||
self.assertEqual(output.image_id, UUID1)
|
||||
self.assertEqual(output.tags, set(['king', 'kong']))
|
||||
self.assertEqual(len(output.tags), 2)
|
||||
self.assertTrue('king' in output.tags)
|
||||
self.assertTrue('kong' in output.tags)
|
||||
self.assertNotEqual(output.created_at, output.updated_at)
|
||||
|
||||
def test_update_replace_property(self):
|
||||
@ -1404,7 +1417,8 @@ class TestImagesController(base.IsolatedUnitTest):
|
||||
{'op': 'replace', 'path': ['tags'], 'value': ['ping', 'ping']},
|
||||
]
|
||||
output = self.controller.update(request, UUID1, changes)
|
||||
self.assertEqual(set(['ping']), output.tags)
|
||||
self.assertEqual(len(output.tags), 1)
|
||||
self.assertTrue('ping' in output.tags)
|
||||
output_logs = self.notifier.get_logs()
|
||||
self.assertEqual(len(output_logs), 1)
|
||||
output_log = output_logs[0]
|
||||
|
Loading…
Reference in New Issue
Block a user