text: # 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 six
  
  import glance_store as store
  from oslo.config import cfg
  
  import glance.api.common
  import glance.common.exception as exception
  from glance.common import utils
  import glance.domain
  import glance.domain.proxy
  from glance.openstack.common import excutils
  from glance.openstack.common import gettextutils
  import glance.openstack.common.log as logging
  
  
  LOG = logging.getLogger(__name__)
  _LI = gettextutils._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))
  