VYPR
Moderate severityNVD Advisory· Published Jan 23, 2015· Updated May 6, 2026

CVE-2014-9623

CVE-2014-9623

Description

OpenStack Glance 2014.2.x through 2014.2.1, 2014.1.3, and earlier allows remote authenticated users to bypass the storage quota and cause a denial of service (disk consumption) by deleting an image in the saving state.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
glancePyPI
< 11.0.0a011.0.0a0

Affected products

1

Patches

3
0dc8fbb3479a

Cleanup chunks for deleted image that was 'saving'

https://github.com/openstack/glanceZhi Yan LiuDec 30, 2014via ghsa
15 files changed · +101 84
  • glance/api/authorization.py+2 2 modified
    @@ -161,10 +161,10 @@ def add(self, image_member):
                 raise exception.Forbidden(message
                                           % self.image.image_id)
     
    -    def save(self, image_member):
    +    def save(self, image_member, from_state=None):
             if (self.context.is_admin or
                     self.context.owner == image_member.member_id):
    -            self.member_repo.save(image_member)
    +            self.member_repo.save(image_member, from_state=from_state)
             else:
                 message = _("You cannot update image member %s")
                 raise exception.Forbidden(message % image_member.member_id)
    
  • glance/api/policy.py+4 4 modified
    @@ -118,9 +118,9 @@ def list(self, *args, **kwargs):
             self.policy.enforce(self.context, 'get_images', {})
             return super(ImageRepoProxy, self).list(*args, **kwargs)
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             self.policy.enforce(self.context, 'modify_image', {})
    -        return super(ImageRepoProxy, self).save(image)
    +        return super(ImageRepoProxy, self).save(image, from_state=from_state)
     
         def add(self, image):
             self.policy.enforce(self.context, 'add_image', {})
    @@ -221,9 +221,9 @@ def get(self, member_id):
             self.policy.enforce(self.context, 'get_member', {})
             return self.member_repo.get(member_id)
     
    -    def save(self, member):
    +    def save(self, member, from_state=None):
             self.policy.enforce(self.context, 'modify_member', {})
    -        self.member_repo.save(member)
    +        self.member_repo.save(member, from_state=from_state)
     
         def list(self, *args, **kwargs):
             self.policy.enforce(self.context, 'get_members', {})
    
  • glance/api/v1/upload_utils.py+15 8 modified
    @@ -153,14 +153,21 @@ def _kill_mismatched(image_meta, attr, actual):
             update_data = {'checksum': checksum,
                            'size': size}
             try:
    -            image_meta = registry.update_image_metadata(req.context,
    -                                                        image_id,
    -                                                        update_data,
    -                                                        from_state='saving')
    -
    -        except exception.NotFound as e:
    -            msg = _("Image %s could not be found after upload. The image may"
    -                    " have been deleted during the upload.") % image_id
    +            try:
    +                state = 'saving'
    +                image_meta = registry.update_image_metadata(req.context,
    +                                                            image_id,
    +                                                            update_data,
    +                                                            from_state=state)
    +            except exception.Duplicate:
    +                image = registry.get_image_metadata(req.context, image_id)
    +                if image['status'] == 'deleted':
    +                    raise exception.NotFound()
    +                else:
    +                    raise
    +        except exception.NotFound:
    +            msg = _LI("Image %s could not be found after upload. The image may"
    +                      " have been deleted during the upload.") % image_id
                 LOG.info(msg)
     
                 # NOTE(jculp): we need to clean up the datastore if an image
    
  • glance/api/v2/image_data.py+9 7 modified
    @@ -72,14 +72,12 @@ def upload(self, req, image_id, data, size):
                 try:
                     image_repo.save(image)
                     image.set_data(data, size)
    -                image_repo.save(image)
    -            except exception.NotFound as e:
    -                msg = (_("Image %(id)s could not be found after upload."
    +                image_repo.save(image, from_state='saving')
    +            except (exception.NotFound, exception.Conflict):
    +                msg = (_("Image %s could not be found after upload. "
                              "The image may have been deleted during the "
    -                         "upload: %(error)s Cleaning up the chunks "
    -                         "uploaded") %
    -                       {'id': image_id,
    -                        'error': utils.exception_to_str(e)})
    +                         "upload, cleaning up the chunks uploaded.") %
    +                       image_id)
                     LOG.warn(msg)
                     # NOTE(sridevi): Cleaning up the uploaded chunks.
                     try:
    @@ -152,6 +150,10 @@ def upload(self, req, image_id, data, size):
                 raise webob.exc.HTTPServiceUnavailable(explanation=msg,
                                                        request=req)
     
    +        except webob.exc.HTTPGone as e:
    +            with excutils.save_and_reraise_exception():
    +                LOG.error(_LE("Failed to upload image data due to HTTP error"))
    +
             except webob.exc.HTTPError as e:
                 with excutils.save_and_reraise_exception():
                     LOG.error(_LE("Failed to upload image data due to HTTP error"))
    
  • glance/db/__init__.py+4 3 modified
    @@ -166,15 +166,16 @@ def add(self, image):
             image.created_at = new_values['created_at']
             image.updated_at = new_values['updated_at']
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             image_values = self._format_image_to_db(image)
             if image_values['size'] > CONF.image_size_cap:
                 raise exception.ImageSizeLimitExceeded
             try:
                 new_values = self.db_api.image_update(self.context,
                                                       image.image_id,
                                                       image_values,
    -                                                  purge_props=True)
    +                                                  purge_props=True,
    +                                                  from_state=from_state)
             except (exception.NotFound, exception.Forbidden):
                 msg = _("No image found with ID %s") % image.image_id
                 raise exception.NotFound(msg)
    @@ -267,7 +268,7 @@ def remove(self, image_member):
                 msg = _("The specified member %s could not be found")
                 raise exception.NotFound(msg % image_member.id)
     
    -    def save(self, image_member):
    +    def save(self, image_member, from_state=None):
             image_member_values = self._format_image_member_to_db(image_member)
             try:
                 new_values = self.db_api.image_member_update(self.context,
    
  • glance/domain/proxy.py+2 2 modified
    @@ -94,9 +94,9 @@ def add(self, item):
             result = self.base.add(base_item)
             return self.helper.proxy(result)
     
    -    def save(self, item):
    +    def save(self, item, from_state=None):
             base_item = self.helper.unproxy(item)
    -        result = self.base.save(base_item)
    +        result = self.base.save(base_item, from_state=from_state)
             return self.helper.proxy(result)
     
         def remove(self, item):
    
  • glance/location.py+2 2 modified
    @@ -61,8 +61,8 @@ def add(self, image):
             self._set_acls(image)
             return result
     
    -    def save(self, image):
    -        result = super(ImageRepoProxy, self).save(image)
    +    def save(self, image, from_state=None):
    +        result = super(ImageRepoProxy, self).save(image, from_state=from_state)
             self._set_acls(image)
             return result
     
    
  • glance/notifier.py+2 2 modified
    @@ -125,8 +125,8 @@ def __init__(self, image_repo, context, notifier):
                                                  item_proxy_class=ImageProxy,
                                                  item_proxy_kwargs=proxy_kwargs)
     
    -    def save(self, image):
    -        super(ImageRepoProxy, self).save(image)
    +    def save(self, image, from_state=None):
    +        super(ImageRepoProxy, self).save(image, from_state=from_state)
             self.notifier.info('image.update',
                                format_image_notification(image))
     
    
  • glance/quota/__init__.py+2 2 modified
    @@ -104,10 +104,10 @@ def _enforce_image_property_quota(self, attempted):
                 LOG.debug(six.text_type(exc))
                 raise exc
     
    -    def save(self, image):
    +    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)
    +        return super(ImageRepoProxy, self).save(image, from_state=from_state)
     
         def add(self, image):
             self._enforce_image_property_quota(len(image.extra_properties))
    
  • glance/tests/unit/test_domain_proxy.py+7 5 modified
    @@ -74,7 +74,7 @@ def test_add(self):
             self._test_method('add', 'snuff', 'enough')
     
         def test_save(self):
    -        self._test_method('save', 'snuff', 'enough')
    +        self._test_method('save', 'snuff', 'enough', from_state=None)
     
         def test_remove(self):
             self._test_method('add', None, 'flying')
    @@ -121,14 +121,14 @@ def test_list(self):
                 self.assertEqual(tuple(), results[i].args)
                 self.assertEqual({'a': 1}, results[i].kwargs)
     
    -    def _test_method_with_proxied_argument(self, name, result):
    +    def _test_method_with_proxied_argument(self, name, result, **kwargs):
             self.fake_repo.result = result
             item = FakeProxy('snoop')
             method = getattr(self.proxy_repo, name)
             proxy_result = method(item)
     
             self.assertEqual(('snoop',), self.fake_repo.args)
    -        self.assertEqual({}, self.fake_repo.kwargs)
    +        self.assertEqual(kwargs, self.fake_repo.kwargs)
     
             if result is None:
                 self.assertIsNone(proxy_result)
    @@ -145,10 +145,12 @@ def test_add_with_no_result(self):
             self._test_method_with_proxied_argument('add', None)
     
         def test_save(self):
    -        self._test_method_with_proxied_argument('save', 'dog')
    +        self._test_method_with_proxied_argument('save', 'dog',
    +                                                from_state=None)
     
         def test_save_with_no_result(self):
    -        self._test_method_with_proxied_argument('save', None)
    +        self._test_method_with_proxied_argument('save', None,
    +                                                from_state=None)
     
         def test_remove(self):
             self._test_method_with_proxied_argument('remove', 'dog')
    
  • glance/tests/unit/test_policy.py+1 1 modified
    @@ -78,7 +78,7 @@ def add(self, image_member):
         def get(self, *args, **kwargs):
             return 'member_repo_get'
     
    -    def save(self, image_member):
    +    def save(self, image_member, from_state=None):
             image_member.output = 'member_repo_save'
     
         def list(self, *args, **kwargs):
    
  • glance/tests/unit/test_quota.py+11 6 modified
    @@ -366,7 +366,8 @@ def test_save_image_with_image_property(self):
             self.image.extra_properties = {'foo': 'bar'}
             self.image_repo_proxy.save(self.image)
     
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
     
         def test_save_image_too_many_image_properties(self):
             self.config(image_property_quota=1)
    @@ -382,7 +383,8 @@ def test_save_image_unlimited_image_properties(self):
             self.image.extra_properties = {'foo': 'bar'}
             self.image_repo_proxy.save(self.image)
     
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
     
         def test_add_image_with_image_property(self):
             self.config(image_property_quota=1)
    @@ -421,7 +423,8 @@ def test_modify_image_properties_when_quota_exceeded(self):
             self.config(image_property_quota=1)
             self.image.extra_properties = {'foo': 'frob', 'spam': 'eggs'}
             self.image_repo_proxy.save(self.image)
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
             self.assertEqual('frob', self.base_image.extra_properties['foo'])
             self.assertEqual('eggs', self.base_image.extra_properties['spam'])
     
    @@ -430,7 +433,8 @@ def test_delete_image_properties_when_quota_exceeded(self):
             self.config(image_property_quota=1)
             del self.image.extra_properties['foo']
             self.image_repo_proxy.save(self.image)
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
             self.assertNotIn('foo', self.base_image.extra_properties)
             self.assertEqual('ham', self.base_image.extra_properties['spam'])
     
    @@ -452,7 +456,7 @@ def test_exceed_quota_during_patch_operation(self):
             del self.image.extra_properties['frob']
             del self.image.extra_properties['lorem']
             self.image_repo_proxy.save(self.image)
    -        call_args = mock.call(self.base_image)
    +        call_args = mock.call(self.base_image, from_state=None)
             self.assertEqual(call_args, self.image_repo_mock.save.call_args)
             self.assertEqual('bar', self.base_image.extra_properties['foo'])
             self.assertEqual('ham', self.base_image.extra_properties['spam'])
    @@ -471,7 +475,8 @@ def test_quota_exceeded_after_delete_image_properties(self):
             self.config(image_property_quota=1)
             del self.image.extra_properties['foo']
             self.image_repo_proxy.save(self.image)
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
             self.assertNotIn('foo', self.base_image.extra_properties)
             self.assertEqual('ham', self.base_image.extra_properties['spam'])
             self.assertEqual('baz', self.base_image.extra_properties['frob'])
    
  • glance/tests/unit/test_store_image.py+1 1 modified
    @@ -35,7 +35,7 @@ class ImageRepoStub(object):
         def add(self, image):
             return image
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             return image
     
     
    
  • glance/tests/unit/v1/test_api.py+25 29 modified
    @@ -1741,17 +1741,16 @@ def mock_store_add_to_backend_w_exception(*args, **kwargs):
     
             self.assertEqual(1, mock_store_add_to_backend.call_count)
     
    -    def test_delete_during_image_upload(self):
    -        req = unit_test_utils.get_fake_request()
    +    def _check_delete_during_image_upload(self, is_admin=False):
     
             fixture_headers = {'x-image-meta-store': 'file',
                                'x-image-meta-disk-format': 'vhd',
                                'x-image-meta-container-format': 'ovf',
                                'x-image-meta-name': 'fake image #3',
                                'x-image-meta-property-key1': 'value1'}
     
    -        req = webob.Request.blank("/images")
    -        req.method = 'POST'
    +        req = unit_test_utils.get_fake_request(path="/images",
    +                                               is_admin=is_admin)
             for k, v in six.iteritems(fixture_headers):
                 req.headers[k] = v
     
    @@ -1776,31 +1775,18 @@ def mock_initiate_deletion(*args, **kwargs):
                            mock_initiate_deletion)
     
             orig_update_image_metadata = registry.update_image_metadata
    -        ctlr = glance.api.v1.controller.BaseController
    -        orig_get_image_meta_or_404 = ctlr.get_image_meta_or_404
    -
    -        def mock_update_image_metadata(*args, **kwargs):
     
    -            if args[2].get('status', None) == 'deleted':
    +        data = "somedata"
     
    -                # One shot.
    -                def mock_get_image_meta_or_404(*args, **kwargs):
    -                    ret = orig_get_image_meta_or_404(*args, **kwargs)
    -                    ret['status'] = 'queued'
    -                    self.stubs.Set(ctlr, 'get_image_meta_or_404',
    -                                   orig_get_image_meta_or_404)
    -                    return ret
    -
    -                self.stubs.Set(ctlr, 'get_image_meta_or_404',
    -                               mock_get_image_meta_or_404)
    +        def mock_update_image_metadata(*args, **kwargs):
     
    -                req = webob.Request.blank("/images/%s" % image_id)
    -                req.method = 'PUT'
    -                req.headers['Content-Type'] = 'application/octet-stream'
    -                req.body = "somedata"
    +            if args[2].get('size', None) == len(data):
    +                path = "/images/%s" % image_id
    +                req = unit_test_utils.get_fake_request(path=path,
    +                                                       method='DELETE',
    +                                                       is_admin=is_admin)
                     res = req.get_response(self.api)
                     self.assertEqual(200, res.status_int)
    -                self.assertFalse(res.location)
     
                     self.stubs.Set(registry, 'update_image_metadata',
                                    orig_update_image_metadata)
    @@ -1810,20 +1796,30 @@ def mock_get_image_meta_or_404(*args, **kwargs):
             self.stubs.Set(registry, 'update_image_metadata',
                            mock_update_image_metadata)
     
    -        req = webob.Request.blank("/images/%s" % image_id)
    -        req.method = 'DELETE'
    +        req = unit_test_utils.get_fake_request(path="/images/%s" % image_id,
    +                                               method='PUT')
    +        req.headers['Content-Type'] = 'application/octet-stream'
    +        req.body = data
             res = req.get_response(self.api)
    -        self.assertEqual(200, res.status_int)
    +        self.assertEqual(412, res.status_int)
    +        self.assertFalse(res.location)
     
             self.assertTrue(called['initiate_deletion'])
     
    -        req = webob.Request.blank("/images/%s" % image_id)
    -        req.method = 'HEAD'
    +        req = unit_test_utils.get_fake_request(path="/images/%s" % image_id,
    +                                               method='HEAD',
    +                                               is_admin=True)
             res = req.get_response(self.api)
             self.assertEqual(200, res.status_int)
             self.assertEqual('True', res.headers['x-image-meta-deleted'])
             self.assertEqual('deleted', res.headers['x-image-meta-status'])
     
    +    def test_delete_during_image_upload_by_normal_user(self):
    +        self._check_delete_during_image_upload(is_admin=False)
    +
    +    def test_delete_during_image_upload_by_admin(self):
    +        self._check_delete_during_image_upload(is_admin=True)
    +
         def test_disable_purge_props(self):
             """
             Test the special x-glance-registry-purge-props header controls
    
  • glance/tests/unit/v2/test_image_data_resource.py+14 10 modified
    @@ -79,7 +79,7 @@ def get(self, image_id):
             else:
                 return self.result
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             self.saved_image = image
     
     
    @@ -182,17 +182,21 @@ def test_upload_invalid(self):
                               request, unit_test_utils.UUID1, 'YYYY', 4)
     
         def test_upload_non_existent_image_during_save_initiates_deletion(self):
    -        def fake_save(self):
    +        def fake_save_not_found(self):
                 raise exception.NotFound()
     
    -        request = unit_test_utils.get_fake_request()
    -        image = FakeImage('abcd', locations=['http://example.com/image'])
    -        self.image_repo.result = image
    -        self.image_repo.save = fake_save
    -        image.delete = mock.Mock()
    -        self.assertRaises(webob.exc.HTTPGone, self.controller.upload,
    -                          request, str(uuid.uuid4()), 'ABC', 3)
    -        self.assertTrue(image.delete.called)
    +        def fake_save_conflict(self):
    +            raise exception.Conflict()
    +
    +        for fun in [fake_save_not_found, fake_save_conflict]:
    +            request = unit_test_utils.get_fake_request()
    +            image = FakeImage('abcd', locations=['http://example.com/image'])
    +            self.image_repo.result = image
    +            self.image_repo.save = fun
    +            image.delete = mock.Mock()
    +            self.assertRaises(webob.exc.HTTPGone, self.controller.upload,
    +                              request, str(uuid.uuid4()), 'ABC', 3)
    +            self.assertTrue(image.delete.called)
     
         def test_upload_non_existent_image_raises_not_found_exception(self):
             def fake_save(self):
    
7d5d8657fd70

Cleanup chunks for deleted image that was 'saving'

https://github.com/openstack/glanceZhi Yan LiuDec 30, 2014via ghsa
15 files changed · +98 80
  • glance/api/authorization.py+2 2 modified
    @@ -158,10 +158,10 @@ def add(self, image_member):
                 raise exception.Forbidden(message
                                           % self.image.image_id)
     
    -    def save(self, image_member):
    +    def save(self, image_member, from_state=None):
             if (self.context.is_admin or
                     self.context.owner == image_member.member_id):
    -            self.member_repo.save(image_member)
    +            self.member_repo.save(image_member, from_state=from_state)
             else:
                 message = _("You cannot update image member %s")
                 raise exception.Forbidden(message % image_member.member_id)
    
  • glance/api/policy.py+4 4 modified
    @@ -182,9 +182,9 @@ def list(self, *args, **kwargs):
             self.policy.enforce(self.context, 'get_images', {})
             return super(ImageRepoProxy, self).list(*args, **kwargs)
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             self.policy.enforce(self.context, 'modify_image', {})
    -        return super(ImageRepoProxy, self).save(image)
    +        return super(ImageRepoProxy, self).save(image, from_state=from_state)
     
         def add(self, image):
             self.policy.enforce(self.context, 'add_image', {})
    @@ -285,9 +285,9 @@ def get(self, member_id):
             self.policy.enforce(self.context, 'get_member', {})
             return self.member_repo.get(member_id)
     
    -    def save(self, member):
    +    def save(self, member, from_state=None):
             self.policy.enforce(self.context, 'modify_member', {})
    -        self.member_repo.save(member)
    +        self.member_repo.save(member, from_state=from_state)
     
         def list(self, *args, **kwargs):
             self.policy.enforce(self.context, 'get_members', {})
    
  • glance/api/v1/upload_utils.py+13 6 modified
    @@ -153,12 +153,19 @@ def _kill_mismatched(image_meta, attr, actual):
             update_data = {'checksum': checksum,
                            'size': size}
             try:
    -            image_meta = registry.update_image_metadata(req.context,
    -                                                        image_id,
    -                                                        update_data,
    -                                                        from_state='saving')
    -
    -        except exception.NotFound as e:
    +            try:
    +                state = 'saving'
    +                image_meta = registry.update_image_metadata(req.context,
    +                                                            image_id,
    +                                                            update_data,
    +                                                            from_state=state)
    +            except exception.Duplicate:
    +                image = registry.get_image_metadata(req.context, image_id)
    +                if image['status'] == 'deleted':
    +                    raise exception.NotFound()
    +                else:
    +                    raise
    +        except exception.NotFound:
                 msg = _LI("Image %s could not be found after upload. The image may"
                           " have been deleted during the upload.") % image_id
                 LOG.info(msg)
    
  • glance/api/v2/image_data.py+6 2 modified
    @@ -73,8 +73,8 @@ def upload(self, req, image_id, data, size):
                 try:
                     image_repo.save(image)
                     image.set_data(data, size)
    -                image_repo.save(image)
    -            except exception.NotFound as e:
    +                image_repo.save(image, from_state='saving')
    +            except (exception.NotFound, exception.Conflict) as e:
                     msg = (_("Image %(id)s could not be found after upload."
                              "The image may have been deleted during the upload: "
                              "%(error)s Cleaning up the chunks uploaded") %
    @@ -152,6 +152,10 @@ def upload(self, req, image_id, data, size):
                 raise webob.exc.HTTPServiceUnavailable(explanation=msg,
                                                        request=req)
     
    +        except webob.exc.HTTPGone as e:
    +            with excutils.save_and_reraise_exception():
    +                LOG.error(_LE("Failed to upload image data due to HTTP error"))
    +
             except webob.exc.HTTPError as e:
                 with excutils.save_and_reraise_exception():
                     LOG.error(_LE("Failed to upload image data due to HTTP error"))
    
  • glance/db/__init__.py+4 3 modified
    @@ -164,15 +164,16 @@ def add(self, image):
             image.created_at = new_values['created_at']
             image.updated_at = new_values['updated_at']
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             image_values = self._format_image_to_db(image)
             if image_values['size'] > CONF.image_size_cap:
                 raise exception.ImageSizeLimitExceeded
             try:
                 new_values = self.db_api.image_update(self.context,
                                                       image.image_id,
                                                       image_values,
    -                                                  purge_props=True)
    +                                                  purge_props=True,
    +                                                  from_state=from_state)
             except (exception.NotFound, exception.Forbidden):
                 msg = _("No image found with ID %s") % image.image_id
                 raise exception.NotFound(msg)
    @@ -265,7 +266,7 @@ def remove(self, image_member):
                 msg = _("The specified member %s could not be found")
                 raise exception.NotFound(msg % image_member.id)
     
    -    def save(self, image_member):
    +    def save(self, image_member, from_state=None):
             image_member_values = self._format_image_member_to_db(image_member)
             try:
                 new_values = self.db_api.image_member_update(self.context,
    
  • glance/domain/proxy.py+2 2 modified
    @@ -94,9 +94,9 @@ def add(self, item):
             result = self.base.add(base_item)
             return self.helper.proxy(result)
     
    -    def save(self, item):
    +    def save(self, item, from_state=None):
             base_item = self.helper.unproxy(item)
    -        result = self.base.save(base_item)
    +        result = self.base.save(base_item, from_state=from_state)
             return self.helper.proxy(result)
     
         def remove(self, item):
    
  • glance/location.py+2 2 modified
    @@ -60,8 +60,8 @@ def add(self, image):
             self._set_acls(image)
             return result
     
    -    def save(self, image):
    -        result = super(ImageRepoProxy, self).save(image)
    +    def save(self, image, from_state=None):
    +        result = super(ImageRepoProxy, self).save(image, from_state=from_state)
             self._set_acls(image)
             return result
     
    
  • glance/notifier.py+2 2 modified
    @@ -122,8 +122,8 @@ def __init__(self, image_repo, context, notifier):
                                                  item_proxy_class=ImageProxy,
                                                  item_proxy_kwargs=proxy_kwargs)
     
    -    def save(self, image):
    -        super(ImageRepoProxy, self).save(image)
    +    def save(self, image, from_state=None):
    +        super(ImageRepoProxy, self).save(image, from_state=from_state)
             self.notifier.info('image.update',
                                format_image_notification(image))
     
    
  • glance/quota/__init__.py+2 2 modified
    @@ -104,10 +104,10 @@ def _enforce_image_property_quota(self, attempted):
                 LOG.debug(six.text_type(exc))
                 raise exc
     
    -    def save(self, image):
    +    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)
    +        return super(ImageRepoProxy, self).save(image, from_state=from_state)
     
         def add(self, image):
             self._enforce_image_property_quota(len(image.extra_properties))
    
  • glance/tests/unit/test_domain_proxy.py+8 6 modified
    @@ -74,7 +74,7 @@ def test_add(self):
             self._test_method('add', 'snuff', 'enough')
     
         def test_save(self):
    -        self._test_method('save', 'snuff', 'enough')
    +        self._test_method('save', 'snuff', 'enough', from_state=None)
     
         def test_remove(self):
             self._test_method('add', None, 'flying')
    @@ -121,14 +121,14 @@ def test_list(self):
                 self.assertEqual(results[i].args, tuple())
                 self.assertEqual(results[i].kwargs, {'a': 1})
     
    -    def _test_method_with_proxied_argument(self, name, result):
    +    def _test_method_with_proxied_argument(self, name, result, **kwargs):
             self.fake_repo.result = result
             item = FakeProxy('snoop')
             method = getattr(self.proxy_repo, name)
             proxy_result = method(item)
     
    -        self.assertEqual(self.fake_repo.args, ('snoop',))
    -        self.assertEqual(self.fake_repo.kwargs, {})
    +        self.assertEqual(('snoop',), self.fake_repo.args)
    +        self.assertEqual(kwargs, self.fake_repo.kwargs)
     
             if result is None:
                 self.assertIsNone(proxy_result)
    @@ -145,10 +145,12 @@ def test_add_with_no_result(self):
             self._test_method_with_proxied_argument('add', None)
     
         def test_save(self):
    -        self._test_method_with_proxied_argument('save', 'dog')
    +        self._test_method_with_proxied_argument('save', 'dog',
    +                                                from_state=None)
     
         def test_save_with_no_result(self):
    -        self._test_method_with_proxied_argument('save', None)
    +        self._test_method_with_proxied_argument('save', None,
    +                                                from_state=None)
     
         def test_remove(self):
             self._test_method_with_proxied_argument('remove', 'dog')
    
  • glance/tests/unit/test_policy.py+1 1 modified
    @@ -78,7 +78,7 @@ def add(self, image_member):
         def get(self, *args, **kwargs):
             return 'member_repo_get'
     
    -    def save(self, image_member):
    +    def save(self, image_member, from_state=None):
             image_member.output = 'member_repo_save'
     
         def list(self, *args, **kwargs):
    
  • glance/tests/unit/test_quota.py+11 6 modified
    @@ -367,7 +367,8 @@ def test_save_image_with_image_property(self):
             self.image.extra_properties = {'foo': 'bar'}
             self.image_repo_proxy.save(self.image)
     
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
     
         def test_save_image_too_many_image_properties(self):
             self.config(image_property_quota=1)
    @@ -383,7 +384,8 @@ def test_save_image_unlimited_image_properties(self):
             self.image.extra_properties = {'foo': 'bar'}
             self.image_repo_proxy.save(self.image)
     
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
     
         def test_add_image_with_image_property(self):
             self.config(image_property_quota=1)
    @@ -422,7 +424,8 @@ def test_modify_image_properties_when_quota_exceeded(self):
             self.config(image_property_quota=1)
             self.image.extra_properties = {'foo': 'frob', 'spam': 'eggs'}
             self.image_repo_proxy.save(self.image)
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
             self.assertEqual('frob', self.base_image.extra_properties['foo'])
             self.assertEqual('eggs', self.base_image.extra_properties['spam'])
     
    @@ -431,7 +434,8 @@ def test_delete_image_properties_when_quota_exceeded(self):
             self.config(image_property_quota=1)
             del self.image.extra_properties['foo']
             self.image_repo_proxy.save(self.image)
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
             self.assertNotIn('foo', self.base_image.extra_properties)
             self.assertEqual('ham', self.base_image.extra_properties['spam'])
     
    @@ -447,7 +451,7 @@ def test_exceed_quota_during_patch_operation(self):
             del self.image.extra_properties['frob']
             del self.image.extra_properties['lorem']
             self.image_repo_proxy.save(self.image)
    -        call_args = mock.call(self.base_image)
    +        call_args = mock.call(self.base_image, from_state=None)
             self.assertEqual(call_args, self.image_repo_mock.save.call_args)
             self.assertEqual('bar', self.base_image.extra_properties['foo'])
             self.assertEqual('ham', self.base_image.extra_properties['spam'])
    @@ -466,7 +470,8 @@ def test_quota_exceeded_after_delete_image_properties(self):
             self.config(image_property_quota=1)
             del self.image.extra_properties['foo']
             self.image_repo_proxy.save(self.image)
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
             self.assertNotIn('foo', self.base_image.extra_properties)
             self.assertEqual('ham', self.base_image.extra_properties['spam'])
             self.assertEqual('baz', self.base_image.extra_properties['frob'])
    
  • glance/tests/unit/test_store_image.py+1 1 modified
    @@ -36,7 +36,7 @@ class ImageRepoStub(object):
         def add(self, image):
             return image
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             return image
     
     
    
  • glance/tests/unit/v1/test_api.py+26 31 modified
    @@ -39,7 +39,6 @@
     from glance.db.sqlalchemy import models as db_models
     from glance.openstack.common import jsonutils
     from glance.openstack.common import timeutils
    -
     import glance.registry.client.v1.api as registry
     from glance.tests.unit import base
     import glance.tests.unit.utils as unit_test_utils
    @@ -1735,17 +1734,16 @@ def mock_store_add_to_backend_w_exception(*args, **kwargs):
     
             self.assertEqual(1, mock_store_add_to_backend.call_count)
     
    -    def test_delete_during_image_upload(self):
    -        req = unit_test_utils.get_fake_request()
    +    def _check_delete_during_image_upload(self, is_admin=False):
     
             fixture_headers = {'x-image-meta-store': 'file',
                                'x-image-meta-disk-format': 'vhd',
                                'x-image-meta-container-format': 'ovf',
                                'x-image-meta-name': 'fake image #3',
                                'x-image-meta-property-key1': 'value1'}
     
    -        req = webob.Request.blank("/images")
    -        req.method = 'POST'
    +        req = unit_test_utils.get_fake_request(path="/images",
    +                                               is_admin=is_admin)
             for k, v in six.iteritems(fixture_headers):
                 req.headers[k] = v
     
    @@ -1770,31 +1768,18 @@ def mock_initiate_deletion(*args, **kwargs):
                            mock_initiate_deletion)
     
             orig_update_image_metadata = registry.update_image_metadata
    -        ctlr = glance.api.v1.controller.BaseController
    -        orig_get_image_meta_or_404 = ctlr.get_image_meta_or_404
    -
    -        def mock_update_image_metadata(*args, **kwargs):
     
    -            if args[2].get('status', None) == 'deleted':
    +        data = "somedata"
     
    -                # One shot.
    -                def mock_get_image_meta_or_404(*args, **kwargs):
    -                    ret = orig_get_image_meta_or_404(*args, **kwargs)
    -                    ret['status'] = 'queued'
    -                    self.stubs.Set(ctlr, 'get_image_meta_or_404',
    -                                   orig_get_image_meta_or_404)
    -                    return ret
    -
    -                self.stubs.Set(ctlr, 'get_image_meta_or_404',
    -                               mock_get_image_meta_or_404)
    +        def mock_update_image_metadata(*args, **kwargs):
     
    -                req = webob.Request.blank("/images/%s" % image_id)
    -                req.method = 'PUT'
    -                req.headers['Content-Type'] = 'application/octet-stream'
    -                req.body = "somedata"
    +            if args[2].get('size', None) == len(data):
    +                path = "/images/%s" % image_id
    +                req = unit_test_utils.get_fake_request(path=path,
    +                                                       method='DELETE',
    +                                                       is_admin=is_admin)
                     res = req.get_response(self.api)
    -                self.assertEqual(res.status_int, 200)
    -                self.assertFalse(res.location)
    +                self.assertEqual(200, res.status_int)
     
                     self.stubs.Set(registry, 'update_image_metadata',
                                    orig_update_image_metadata)
    @@ -1804,20 +1789,30 @@ def mock_get_image_meta_or_404(*args, **kwargs):
             self.stubs.Set(registry, 'update_image_metadata',
                            mock_update_image_metadata)
     
    -        req = webob.Request.blank("/images/%s" % image_id)
    -        req.method = 'DELETE'
    +        req = unit_test_utils.get_fake_request(path="/images/%s" % image_id,
    +                                               method='PUT')
    +        req.headers['Content-Type'] = 'application/octet-stream'
    +        req.body = data
             res = req.get_response(self.api)
    -        self.assertEqual(res.status_int, 200)
    +        self.assertEqual(412, res.status_int)
    +        self.assertFalse(res.location)
     
             self.assertTrue(called['initiate_deletion'])
     
    -        req = webob.Request.blank("/images/%s" % image_id)
    -        req.method = 'HEAD'
    +        req = unit_test_utils.get_fake_request(path="/images/%s" % image_id,
    +                                               method='HEAD',
    +                                               is_admin=True)
             res = req.get_response(self.api)
             self.assertEqual(res.status_int, 200)
             self.assertEqual(res.headers['x-image-meta-deleted'], 'True')
             self.assertEqual(res.headers['x-image-meta-status'], 'deleted')
     
    +    def test_delete_during_image_upload_by_normal_user(self):
    +        self._check_delete_during_image_upload(is_admin=False)
    +
    +    def test_delete_during_image_upload_by_admin(self):
    +        self._check_delete_during_image_upload(is_admin=True)
    +
         def test_disable_purge_props(self):
             """
             Test the special x-glance-registry-purge-props header controls
    
  • glance/tests/unit/v2/test_image_data_resource.py+14 10 modified
    @@ -81,7 +81,7 @@ def get(self, image_id):
             else:
                 return self.result
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             self.saved_image = image
     
     
    @@ -184,17 +184,21 @@ def test_upload_invalid(self):
                               request, unit_test_utils.UUID1, 'YYYY', 4)
     
         def test_upload_non_existent_image_during_save_initiates_deletion(self):
    -        def fake_save(self):
    +        def fake_save_not_found(self):
                 raise exception.NotFound()
     
    -        request = unit_test_utils.get_fake_request()
    -        image = FakeImage('abcd', locations=['http://example.com/image'])
    -        self.image_repo.result = image
    -        self.image_repo.save = fake_save
    -        image.delete = mock.Mock()
    -        self.assertRaises(webob.exc.HTTPGone, self.controller.upload,
    -                          request, str(uuid.uuid4()), 'ABC', 3)
    -        self.assertTrue(image.delete.called)
    +        def fake_save_conflict(self):
    +            raise exception.Conflict()
    +
    +        for fun in [fake_save_not_found, fake_save_conflict]:
    +            request = unit_test_utils.get_fake_request()
    +            image = FakeImage('abcd', locations=['http://example.com/image'])
    +            self.image_repo.result = image
    +            self.image_repo.save = fun
    +            image.delete = mock.Mock()
    +            self.assertRaises(webob.exc.HTTPGone, self.controller.upload,
    +                              request, str(uuid.uuid4()), 'ABC', 3)
    +            self.assertTrue(image.delete.called)
     
         def test_upload_non_existent_image_raises_not_found_exception(self):
             def fake_save(self):
    
f1260cc771ee

Cleanup chunks for deleted image that was 'saving'

https://github.com/openstack/glanceZhi Yan LiuDec 30, 2014via ghsa
15 files changed · +95 78
  • glance/api/authorization.py+2 2 modified
    @@ -147,10 +147,10 @@ def add(self, image_member):
                 raise exception.Forbidden(message
                                           % self.image.image_id)
     
    -    def save(self, image_member):
    +    def save(self, image_member, from_state=None):
             if (self.context.is_admin or
                     self.context.owner == image_member.member_id):
    -            self.member_repo.save(image_member)
    +            self.member_repo.save(image_member, from_state=from_state)
             else:
                 message = _("You cannot update image member %s")
                 raise exception.Forbidden(message % image_member.member_id)
    
  • glance/api/policy.py+4 4 modified
    @@ -182,9 +182,9 @@ def list(self, *args, **kwargs):
             self.policy.enforce(self.context, 'get_images', {})
             return super(ImageRepoProxy, self).list(*args, **kwargs)
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             self.policy.enforce(self.context, 'modify_image', {})
    -        return super(ImageRepoProxy, self).save(image)
    +        return super(ImageRepoProxy, self).save(image, from_state=from_state)
     
         def add(self, image):
             self.policy.enforce(self.context, 'add_image', {})
    @@ -283,9 +283,9 @@ def get(self, member_id):
             self.policy.enforce(self.context, 'get_member', {})
             return self.member_repo.get(member_id)
     
    -    def save(self, member):
    +    def save(self, member, from_state=None):
             self.policy.enforce(self.context, 'modify_member', {})
    -        self.member_repo.save(member)
    +        self.member_repo.save(member, from_state=from_state)
     
         def list(self, *args, **kwargs):
             self.policy.enforce(self.context, 'get_members', {})
    
  • glance/api/v1/upload_utils.py+15 8 modified
    @@ -146,14 +146,21 @@ def _kill_mismatched(image_meta, attr, actual):
             update_data = {'checksum': checksum,
                            'size': size}
             try:
    -            image_meta = registry.update_image_metadata(req.context,
    -                                                        image_id,
    -                                                        update_data,
    -                                                        from_state='saving')
    -
    -        except exception.NotFound as e:
    -            msg = _("Image %s could not be found after upload. The image may "
    -                    "have been deleted during the upload.") % image_id
    +            try:
    +                state = 'saving'
    +                image_meta = registry.update_image_metadata(req.context,
    +                                                            image_id,
    +                                                            update_data,
    +                                                            from_state=state)
    +            except exception.Duplicate:
    +                image = registry.get_image_metadata(req.context, image_id)
    +                if image['status'] == 'deleted':
    +                    raise exception.NotFound()
    +                else:
    +                    raise
    +        except exception.NotFound:
    +            msg = _("Image %s could not be found after upload. The image may"
    +                    " have been deleted during the upload.") % image_id
                 LOG.info(msg)
     
                 # NOTE(jculp): we need to clean up the datastore if an image
    
  • glance/api/v2/image_data.py+11 7 modified
    @@ -22,6 +22,7 @@
     import glance.db
     import glance.gateway
     import glance.notifier
    +from glance.openstack.common import excutils
     import glance.openstack.common.log as logging
     import glance.store
     
    @@ -66,13 +67,12 @@ def upload(self, req, image_id, data, size):
                 try:
                     image_repo.save(image)
                     image.set_data(data, size)
    -                image_repo.save(image)
    -            except exception.NotFound as e:
    -                msg = (_("Image %(id)s could not be found after upload."
    -                         "The image may have been deleted during the upload: "
    -                         "%(error)s Cleaning up the chunks uploaded") %
    -                       {'id': image_id,
    -                        'error': e})
    +                image_repo.save(image, from_state='saving')
    +            except (exception.NotFound, exception.Conflict):
    +                msg = (_("Image %s could not be found after upload. "
    +                         "The image may have been deleted during the "
    +                         "upload, cleaning up the chunks uploaded.") %
    +                       image_id)
                     LOG.warn(msg)
                     # NOTE(sridevi): Cleaning up the uploaded chunks.
                     try:
    @@ -131,6 +131,10 @@ def upload(self, req, image_id, data, size):
                 raise webob.exc.HTTPServiceUnavailable(explanation=msg,
                                                        request=req)
     
    +        except webob.exc.HTTPGone as e:
    +            with excutils.save_and_reraise_exception():
    +                LOG.error(_("Failed to upload image data due to HTTP error"))
    +
             except webob.exc.HTTPError as e:
                 LOG.error(_("Failed to upload image data due to HTTP error"))
                 self._restore(image_repo, image)
    
  • glance/db/__init__.py+4 3 modified
    @@ -162,15 +162,16 @@ def add(self, image):
             image.created_at = new_values['created_at']
             image.updated_at = new_values['updated_at']
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             image_values = self._format_image_to_db(image)
             if image_values['size'] > CONF.image_size_cap:
                 raise exception.ImageSizeLimitExceeded
             try:
                 new_values = self.db_api.image_update(self.context,
                                                       image.image_id,
                                                       image_values,
    -                                                  purge_props=True)
    +                                                  purge_props=True,
    +                                                  from_state=from_state)
             except (exception.NotFound, exception.Forbidden):
                 msg = _("No image found with ID %s") % image.image_id
                 raise exception.NotFound(msg)
    @@ -263,7 +264,7 @@ def remove(self, image_member):
                 msg = _("The specified member %s could not be found")
                 raise exception.NotFound(msg % image_member.id)
     
    -    def save(self, image_member):
    +    def save(self, image_member, from_state=None):
             image_member_values = self._format_image_member_to_db(image_member)
             try:
                 new_values = self.db_api.image_member_update(self.context,
    
  • glance/domain/proxy.py+2 2 modified
    @@ -94,9 +94,9 @@ def add(self, item):
             result = self.base.add(base_item)
             return self.helper.proxy(result)
     
    -    def save(self, item):
    +    def save(self, item, from_state=None):
             base_item = self.helper.unproxy(item)
    -        result = self.base.save(base_item)
    +        result = self.base.save(base_item, from_state=from_state)
             return self.helper.proxy(result)
     
         def remove(self, item):
    
  • glance/notifier.py+2 2 modified
    @@ -178,8 +178,8 @@ def __init__(self, image_repo, context, notifier):
                                                  item_proxy_class=ImageProxy,
                                                  item_proxy_kwargs=proxy_kwargs)
     
    -    def save(self, image):
    -        super(ImageRepoProxy, self).save(image)
    +    def save(self, image, from_state=None):
    +        super(ImageRepoProxy, self).save(image, from_state=from_state)
             self.notifier.info('image.update',
                                format_image_notification(image))
     
    
  • glance/quota/__init__.py+2 2 modified
    @@ -96,9 +96,9 @@ def _enforce_image_property_quota(self, image):
                 raise exception.ImagePropertyLimitExceeded(attempted=attempted,
                                                            maximum=maximum)
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             self._enforce_image_property_quota(image)
    -        super(ImageRepoProxy, self).save(image)
    +        return super(ImageRepoProxy, self).save(image, from_state=from_state)
     
         def add(self, image):
             self._enforce_image_property_quota(image)
    
  • glance/store/__init__.py+1 1 modified
    @@ -446,7 +446,7 @@ def add(self, image):
             self._set_acls(image)
             return result
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             result = super(ImageRepoProxy, self).save(image)
             self._set_acls(image)
             return result
    
  • glance/tests/unit/test_domain_proxy.py+8 6 modified
    @@ -74,7 +74,7 @@ def test_add(self):
             self._test_method('add', 'snuff', 'enough')
     
         def test_save(self):
    -        self._test_method('save', 'snuff', 'enough')
    +        self._test_method('save', 'snuff', 'enough', from_state=None)
     
         def test_remove(self):
             self._test_method('add', None, 'flying')
    @@ -121,14 +121,14 @@ def test_list(self):
                 self.assertEqual(results[i].args, tuple())
                 self.assertEqual(results[i].kwargs, {'a': 1})
     
    -    def _test_method_with_proxied_argument(self, name, result):
    +    def _test_method_with_proxied_argument(self, name, result, **kwargs):
             self.fake_repo.result = result
             item = FakeProxy('snoop')
             method = getattr(self.proxy_repo, name)
             proxy_result = method(item)
     
    -        self.assertEqual(self.fake_repo.args, ('snoop',))
    -        self.assertEqual(self.fake_repo.kwargs, {})
    +        self.assertEqual(('snoop',), self.fake_repo.args)
    +        self.assertEqual(kwargs, self.fake_repo.kwargs)
     
             if result is None:
                 self.assertTrue(proxy_result is None)
    @@ -145,10 +145,12 @@ def test_add_with_no_result(self):
             self._test_method_with_proxied_argument('add', None)
     
         def test_save(self):
    -        self._test_method_with_proxied_argument('save', 'dog')
    +        self._test_method_with_proxied_argument('save', 'dog',
    +                                                from_state=None)
     
         def test_save_with_no_result(self):
    -        self._test_method_with_proxied_argument('save', None)
    +        self._test_method_with_proxied_argument('save', None,
    +                                                from_state=None)
     
         def test_remove(self):
             self._test_method_with_proxied_argument('remove', 'dog')
    
  • glance/tests/unit/test_policy.py+1 1 modified
    @@ -69,7 +69,7 @@ def add(self, image_member):
         def get(self, *args, **kwargs):
             return 'member_repo_get'
     
    -    def save(self, image_member):
    +    def save(self, image_member, from_state=None):
             image_member.output = 'member_repo_save'
     
         def list(self, *args, **kwargs):
    
  • glance/tests/unit/test_quota.py+4 2 modified
    @@ -290,7 +290,8 @@ def test_save_image_with_image_property(self):
             self.image.extra_properties = {'foo': 'bar'}
             self.image_repo_proxy.save(self.image)
     
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
     
         def test_save_image_too_many_image_properties(self):
             self.config(image_property_quota=1)
    @@ -306,7 +307,8 @@ def test_save_image_unlimited_image_properties(self):
             self.image.extra_properties = {'foo': 'bar'}
             self.image_repo_proxy.save(self.image)
     
    -        self.image_repo_mock.save.assert_called_once_with(self.base_image)
    +        self.image_repo_mock.save.assert_called_once_with(self.base_image,
    +                                                          from_state=None)
     
         def test_add_image_with_image_property(self):
             self.config(image_property_quota=1)
    
  • glance/tests/unit/test_store_image.py+1 1 modified
    @@ -33,7 +33,7 @@ class ImageRepoStub(object):
         def add(self, image):
             return image
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             return image
     
     
    
  • glance/tests/unit/v1/test_api.py+24 27 modified
    @@ -1583,8 +1583,7 @@ def mock_store_add_to_backend_w_exception(*args, **kwargs):
     
             self.assertEqual(1, mock_store_add_to_backend.call_count)
     
    -    def test_delete_during_image_upload(self):
    -        req = unit_test_utils.get_fake_request()
    +    def _check_delete_during_image_upload(self, is_admin=False):
     
             fixture_headers = {'x-image-meta-store': 'file',
                                'x-image-meta-disk-format': 'vhd',
    @@ -1618,30 +1617,18 @@ def mock_initiate_deletion(*args, **kwargs):
                            mock_initiate_deletion)
     
             orig_update_image_metadata = registry.update_image_metadata
    -        ctlr = glance.api.v1.controller.BaseController
    -        orig_get_image_meta_or_404 = ctlr.get_image_meta_or_404
     
    -        def mock_update_image_metadata(*args, **kwargs):
    -
    -            if args[2].get('status', None) == 'deleted':
    +        data = "somedata"
     
    -                # One shot.
    -                def mock_get_image_meta_or_404(*args, **kwargs):
    -                    ret = orig_get_image_meta_or_404(*args, **kwargs)
    -                    ret['status'] = 'queued'
    -                    self.stubs.Set(ctlr, 'get_image_meta_or_404',
    -                                   orig_get_image_meta_or_404)
    -                    return ret
    -
    -                self.stubs.Set(ctlr, 'get_image_meta_or_404',
    -                               mock_get_image_meta_or_404)
    +        def mock_update_image_metadata(*args, **kwargs):
     
    -                req = webob.Request.blank("/images/%s" % image_id)
    -                req.method = 'PUT'
    -                req.headers['Content-Type'] = 'application/octet-stream'
    -                req.body = "somedata"
    +            if args[2].get('size', None) == len(data):
    +                path = "/images/%s" % image_id
    +                req = unit_test_utils.get_fake_request(path=path,
    +                                                       method='DELETE',
    +                                                       is_admin=is_admin)
                     res = req.get_response(self.api)
    -                self.assertEqual(res.status_int, 200)
    +                self.assertEqual(200, res.status_int)
     
                     self.stubs.Set(registry, 'update_image_metadata',
                                    orig_update_image_metadata)
    @@ -1651,20 +1638,30 @@ def mock_get_image_meta_or_404(*args, **kwargs):
             self.stubs.Set(registry, 'update_image_metadata',
                            mock_update_image_metadata)
     
    -        req = webob.Request.blank("/images/%s" % image_id)
    -        req.method = 'DELETE'
    +        req = unit_test_utils.get_fake_request(path="/images/%s" % image_id,
    +                                               method='PUT')
    +        req.headers['Content-Type'] = 'application/octet-stream'
    +        req.body = data
             res = req.get_response(self.api)
    -        self.assertEqual(res.status_int, 200)
    +        self.assertEqual(412, res.status_int)
    +        self.assertFalse(res.location)
     
             self.assertTrue(called['initiate_deletion'])
     
    -        req = webob.Request.blank("/images/%s" % image_id)
    -        req.method = 'HEAD'
    +        req = unit_test_utils.get_fake_request(path="/images/%s" % image_id,
    +                                               method='HEAD',
    +                                               is_admin=True)
             res = req.get_response(self.api)
             self.assertEqual(res.status_int, 200)
             self.assertEqual(res.headers['x-image-meta-deleted'], 'True')
             self.assertEqual(res.headers['x-image-meta-status'], 'deleted')
     
    +    def test_delete_during_image_upload_by_normal_user(self):
    +        self._check_delete_during_image_upload(is_admin=False)
    +
    +    def test_delete_during_image_upload_by_admin(self):
    +        self._check_delete_during_image_upload(is_admin=True)
    +
         def test_disable_purge_props(self):
             """
             Test the special x-glance-registry-purge-props header controls
    
  • glance/tests/unit/v2/test_image_data_resource.py+14 10 modified
    @@ -79,7 +79,7 @@ def get(self, image_id):
             else:
                 return self.result
     
    -    def save(self, image):
    +    def save(self, image, from_state=None):
             self.saved_image = image
     
     
    @@ -180,17 +180,21 @@ def test_upload_invalid(self):
                               request, unit_test_utils.UUID1, 'YYYY', 4)
     
         def test_upload_non_existent_image_during_save_initiates_deletion(self):
    -        def fake_save(self):
    +        def fake_save_not_found(self):
                 raise exception.NotFound()
     
    -        request = unit_test_utils.get_fake_request()
    -        image = FakeImage('abcd', locations=['http://example.com/image'])
    -        self.image_repo.result = image
    -        self.image_repo.save = fake_save
    -        image.delete = mock.Mock()
    -        self.assertRaises(webob.exc.HTTPGone, self.controller.upload,
    -                          request, str(uuid.uuid4()), 'ABC', 3)
    -        self.assertTrue(image.delete.called)
    +        def fake_save_conflict(self):
    +            raise exception.Conflict()
    +
    +        for fun in [fake_save_not_found, fake_save_conflict]:
    +            request = unit_test_utils.get_fake_request()
    +            image = FakeImage('abcd', locations=['http://example.com/image'])
    +            self.image_repo.result = image
    +            self.image_repo.save = fun
    +            image.delete = mock.Mock()
    +            self.assertRaises(webob.exc.HTTPGone, self.controller.upload,
    +                              request, str(uuid.uuid4()), 'ABC', 3)
    +            self.assertTrue(image.delete.called)
     
         def test_upload_non_existent_image_before_save(self):
             request = unit_test_utils.get_fake_request()
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

14

News mentions

0

No linked articles in our index yet.