# glance.quota

# Source code for glance.quota

    # 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:
                    if CONF.enabled_backends:
                        size_from_backend = store.get_size_from_uri_and_backend(
                            location['url'], location['metadata'].get('store'),
                            context=context)
                    else:
                        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, properties):
            if CONF.image_property_quota < 0:
                # If value is negative, allow unlimited number of properties
                return

            attempted = len(
                [x for x in properties.keys()
                 if not x.startswith(glance.api.common.GLANCE_RESERVED_NS)])

            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(image.extra_properties)
            return super(ImageRepoProxy, self).save(image, from_state=from_state)

        def add(self, image):
            self._enforce_image_property_quota(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):
            # Protect against deepcopy, which calls getattr. __getattr__
            # is only called when an attribute is not "normal", so when
            # self.tags is called, this is not.
            if name == 'tags':
                try:
                    return self.__getattribute__('tags')
                except AttributeError:
                    return None
            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, backend=None, set_active=True):
            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, exception_class=exception.StorageQuotaFull)

            self.image.set_data(data, size=size, backend=backend,
                                set_active=set_active)

            # 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)

this page last updated: 2026-03-19 02:17:34

Except where otherwise noted, this document is licensed under Creative Commons Attribution 3.0 License. See all OpenStack Legal Documents.

found an error? report a bug