glance/glance/quota/__init__.py

369 lines
13 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.utils import excutils
import six
import glance.api.common
import glance.common.exception as exception
from glance.common import utils
import glance.domain
import glance.domain.proxy
from glance import i18n
import glance.openstack.common.log as logging
LOG = logging.getLogger(__name__)
_ = i18n._
_LI = i18n._LI
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
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(six.text_type(exc))
raise exc
def save(self, image):
if image.added_new_properties():
self._enforce_image_property_quota(len(image.extra_properties))
return super(ImageRepoProxy, self).save(image)
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 __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,
image_proxy_class=ImageProxy,
image_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 __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))