e0c03c63cf
In Python 3 __ne__ by default delegates to __eq__ and inverts the result, but in Python 2 they urge you to define __ne__ when you define __eq__ for it to work properly [1].There are no implied relationships among the comparison operators. The truth of x==y does not imply that x!=y is false. Accordingly, when defining __eq__(), one should also define __ne__() so that the operators will behave as expected. [1]https://docs.python.org/2/reference/datamodel.html#object.__ne__ Change-Id: I7d41128d88c79ebb5de1c38f8aaf98cc9561367c
387 lines
14 KiB
Python
387 lines
14 KiB
Python
# Copyright 2013, Red Hat, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import copy
|
|
|
|
import glance_store as store
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
from oslo_utils import encodeutils
|
|
from oslo_utils import excutils
|
|
|
|
import glance.api.common
|
|
import glance.common.exception as exception
|
|
from glance.common import utils
|
|
import glance.domain
|
|
import glance.domain.proxy
|
|
from glance.i18n import _, _LI
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
CONF.import_opt('image_member_quota', 'glance.common.config')
|
|
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)
|
|
|
|
|
|
def _calc_required_size(context, image, locations):
|
|
required_size = None
|
|
if image.size:
|
|
required_size = image.size * len(locations)
|
|
else:
|
|
for location in locations:
|
|
size_from_backend = None
|
|
|
|
try:
|
|
size_from_backend = store.get_size_from_backend(
|
|
location['url'], context=context)
|
|
except (store.UnknownScheme, store.NotFound):
|
|
pass
|
|
except store.BadStoreUri:
|
|
raise exception.BadStoreUri
|
|
|
|
if size_from_backend:
|
|
required_size = size_from_backend * len(locations)
|
|
break
|
|
return required_size
|
|
|
|
|
|
def _enforce_image_location_quota(image, locations, is_setter=False):
|
|
if CONF.image_location_quota < 0:
|
|
# If value is negative, allow unlimited number of locations
|
|
return
|
|
|
|
attempted = len(image.locations) + len(locations)
|
|
attempted = attempted if not is_setter else len(locations)
|
|
maximum = CONF.image_location_quota
|
|
if attempted > maximum:
|
|
raise exception.ImageLocationLimitExceeded(attempted=attempted,
|
|
maximum=maximum)
|
|
|
|
|
|
class ImageRepoProxy(glance.domain.proxy.Repo):
|
|
|
|
def __init__(self, image_repo, context, db_api, store_utils):
|
|
self.image_repo = image_repo
|
|
self.db_api = db_api
|
|
proxy_kwargs = {'context': context, 'db_api': db_api,
|
|
'store_utils': store_utils}
|
|
super(ImageRepoProxy, self).__init__(image_repo,
|
|
item_proxy_class=ImageProxy,
|
|
item_proxy_kwargs=proxy_kwargs)
|
|
|
|
def _enforce_image_property_quota(self, attempted):
|
|
if CONF.image_property_quota < 0:
|
|
# If value is negative, allow unlimited number of properties
|
|
return
|
|
|
|
maximum = CONF.image_property_quota
|
|
if attempted > maximum:
|
|
kwargs = {'attempted': attempted, 'maximum': maximum}
|
|
exc = exception.ImagePropertyLimitExceeded(**kwargs)
|
|
LOG.debug(encodeutils.exception_to_unicode(exc))
|
|
raise exc
|
|
|
|
def save(self, image, from_state=None):
|
|
if image.added_new_properties():
|
|
self._enforce_image_property_quota(len(image.extra_properties))
|
|
return super(ImageRepoProxy, self).save(image, from_state=from_state)
|
|
|
|
def add(self, image):
|
|
self._enforce_image_property_quota(len(image.extra_properties))
|
|
return super(ImageRepoProxy, self).add(image)
|
|
|
|
|
|
class ImageFactoryProxy(glance.domain.proxy.ImageFactory):
|
|
def __init__(self, factory, context, db_api, store_utils):
|
|
proxy_kwargs = {'context': context, 'db_api': db_api,
|
|
'store_utils': store_utils}
|
|
super(ImageFactoryProxy, self).__init__(factory,
|
|
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 __ne__(self, other):
|
|
return not self.__eq__(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 ImageMemberFactoryProxy(glance.domain.proxy.ImageMembershipFactory):
|
|
|
|
def __init__(self, member_factory, context, db_api, store_utils):
|
|
self.db_api = db_api
|
|
self.context = context
|
|
proxy_kwargs = {'context': context, 'db_api': db_api,
|
|
'store_utils': store_utils}
|
|
super(ImageMemberFactoryProxy, self).__init__(
|
|
member_factory,
|
|
proxy_class=ImageMemberProxy,
|
|
proxy_kwargs=proxy_kwargs)
|
|
|
|
def _enforce_image_member_quota(self, image):
|
|
if CONF.image_member_quota < 0:
|
|
# If value is negative, allow unlimited number of members
|
|
return
|
|
|
|
current_member_count = self.db_api.image_member_count(self.context,
|
|
image.image_id)
|
|
attempted = current_member_count + 1
|
|
maximum = CONF.image_member_quota
|
|
if attempted > maximum:
|
|
raise exception.ImageMemberLimitExceeded(attempted=attempted,
|
|
maximum=maximum)
|
|
|
|
def new_image_member(self, image, member_id):
|
|
self._enforce_image_member_quota(image)
|
|
return super(ImageMemberFactoryProxy, self).new_image_member(image,
|
|
member_id)
|
|
|
|
|
|
class QuotaImageLocationsProxy(object):
|
|
|
|
def __init__(self, image, context, db_api):
|
|
self.image = image
|
|
self.context = context
|
|
self.db_api = db_api
|
|
self.locations = image.locations
|
|
|
|
def __cast__(self, *args, **kwargs):
|
|
return self.locations.__cast__(*args, **kwargs)
|
|
|
|
def __contains__(self, *args, **kwargs):
|
|
return self.locations.__contains__(*args, **kwargs)
|
|
|
|
def __delitem__(self, *args, **kwargs):
|
|
return self.locations.__delitem__(*args, **kwargs)
|
|
|
|
def __delslice__(self, *args, **kwargs):
|
|
return self.locations.__delslice__(*args, **kwargs)
|
|
|
|
def __eq__(self, other):
|
|
return self.locations == other
|
|
|
|
def __ne__(self, other):
|
|
return not self.__eq__(other)
|
|
|
|
def __getitem__(self, *args, **kwargs):
|
|
return self.locations.__getitem__(*args, **kwargs)
|
|
|
|
def __iadd__(self, other):
|
|
if not hasattr(other, '__iter__'):
|
|
raise TypeError()
|
|
self._check_user_storage_quota(other)
|
|
return self.locations.__iadd__(other)
|
|
|
|
def __iter__(self, *args, **kwargs):
|
|
return self.locations.__iter__(*args, **kwargs)
|
|
|
|
def __len__(self, *args, **kwargs):
|
|
return self.locations.__len__(*args, **kwargs)
|
|
|
|
def __setitem__(self, key, value):
|
|
return self.locations.__setitem__(key, value)
|
|
|
|
def count(self, *args, **kwargs):
|
|
return self.locations.count(*args, **kwargs)
|
|
|
|
def index(self, *args, **kwargs):
|
|
return self.locations.index(*args, **kwargs)
|
|
|
|
def pop(self, *args, **kwargs):
|
|
return self.locations.pop(*args, **kwargs)
|
|
|
|
def remove(self, *args, **kwargs):
|
|
return self.locations.remove(*args, **kwargs)
|
|
|
|
def reverse(self, *args, **kwargs):
|
|
return self.locations.reverse(*args, **kwargs)
|
|
|
|
def _check_user_storage_quota(self, locations):
|
|
required_size = _calc_required_size(self.context,
|
|
self.image,
|
|
locations)
|
|
glance.api.common.check_quota(self.context,
|
|
required_size,
|
|
self.db_api)
|
|
_enforce_image_location_quota(self.image, locations)
|
|
|
|
def __copy__(self):
|
|
return type(self)(self.image, self.context, self.db_api)
|
|
|
|
def __deepcopy__(self, memo):
|
|
# NOTE(zhiyan): Only copy location entries, others can be reused.
|
|
self.image.locations = copy.deepcopy(self.locations, memo)
|
|
return type(self)(self.image, self.context, self.db_api)
|
|
|
|
def append(self, object):
|
|
self._check_user_storage_quota([object])
|
|
return self.locations.append(object)
|
|
|
|
def insert(self, index, object):
|
|
self._check_user_storage_quota([object])
|
|
return self.locations.insert(index, object)
|
|
|
|
def extend(self, iter):
|
|
self._check_user_storage_quota(iter)
|
|
return self.locations.extend(iter)
|
|
|
|
|
|
class ImageProxy(glance.domain.proxy.Image):
|
|
|
|
def __init__(self, image, context, db_api, store_utils):
|
|
self.image = image
|
|
self.context = context
|
|
self.db_api = db_api
|
|
self.store_utils = store_utils
|
|
super(ImageProxy, self).__init__(image)
|
|
self.orig_props = set(image.extra_properties.keys())
|
|
|
|
def set_data(self, data, size=None):
|
|
remaining = glance.api.common.check_quota(
|
|
self.context, size, self.db_api, image_id=self.image.image_id)
|
|
if remaining is not None:
|
|
# NOTE(jbresnah) we are trying to enforce a quota, put a limit
|
|
# reader on the data
|
|
data = utils.LimitingReader(data, remaining)
|
|
try:
|
|
self.image.set_data(data, size=size)
|
|
except exception.ImageSizeLimitExceeded:
|
|
raise exception.StorageQuotaFull(image_size=size,
|
|
remaining=remaining)
|
|
|
|
# NOTE(jbresnah) If two uploads happen at the same time and neither
|
|
# properly sets the size attribute[1] then there is a race condition
|
|
# that will allow for the quota to be broken[2]. Thus we must recheck
|
|
# the quota after the upload and thus after we know the size.
|
|
#
|
|
# Also, when an upload doesn't set the size properly then the call to
|
|
# check_quota above returns None and so utils.LimitingReader is not
|
|
# used above. Hence the store (e.g. filesystem store) may have to
|
|
# download the entire file before knowing the actual file size. Here
|
|
# also we need to check for the quota again after the image has been
|
|
# downloaded to the store.
|
|
#
|
|
# [1] For e.g. when using chunked transfers the 'Content-Length'
|
|
# header is not set.
|
|
# [2] For e.g.:
|
|
# - Upload 1 does not exceed quota but upload 2 exceeds quota.
|
|
# Both uploads are to different locations
|
|
# - Upload 2 completes before upload 1 and writes image.size.
|
|
# - Immediately, upload 1 completes and (over)writes image.size
|
|
# with the smaller size.
|
|
# - Now, to glance, image has not exceeded quota but, in
|
|
# reality, the quota has been exceeded.
|
|
|
|
try:
|
|
glance.api.common.check_quota(
|
|
self.context, self.image.size, self.db_api,
|
|
image_id=self.image.image_id)
|
|
except exception.StorageQuotaFull:
|
|
with excutils.save_and_reraise_exception():
|
|
LOG.info(_LI('Cleaning up %s after exceeding the quota.'),
|
|
self.image.image_id)
|
|
self.store_utils.safe_delete_from_backend(
|
|
self.context, self.image.image_id, self.image.locations[0])
|
|
|
|
@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,
|
|
self.context,
|
|
self.db_api)
|
|
|
|
@locations.setter
|
|
def locations(self, value):
|
|
_enforce_image_location_quota(self.image, value, is_setter=True)
|
|
|
|
if not isinstance(value, (list, QuotaImageLocationsProxy)):
|
|
raise exception.Invalid(_('Invalid locations: %s') % value)
|
|
|
|
required_size = _calc_required_size(self.context,
|
|
self.image,
|
|
value)
|
|
|
|
glance.api.common.check_quota(
|
|
self.context, required_size, self.db_api,
|
|
image_id=self.image.image_id)
|
|
self.image.locations = value
|
|
|
|
def added_new_properties(self):
|
|
current_props = set(self.image.extra_properties.keys())
|
|
return bool(current_props.difference(self.orig_props))
|
|
|
|
|
|
class ImageMemberProxy(glance.domain.proxy.ImageMember):
|
|
|
|
def __init__(self, image_member, context, db_api, store_utils):
|
|
self.image_member = image_member
|
|
self.context = context
|
|
self.db_api = db_api
|
|
self.store_utils = store_utils
|
|
super(ImageMemberProxy, self).__init__(image_member)
|