diff --git a/.gitignore b/.gitignore index 5b5ea9b..058c05d 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ /glance-2014.2.rc3.tar.gz /glance-2014.2.tar.gz /glance-2014.2.1.tar.gz +/glance-2014.2.2.tar.gz diff --git a/0001-Don-t-access-the-net-while-building-docs.patch b/0001-Don-t-access-the-net-while-building-docs.patch index cd5bd09..a0cdfe8 100644 --- a/0001-Don-t-access-the-net-while-building-docs.patch +++ b/0001-Don-t-access-the-net-while-building-docs.patch @@ -1,4 +1,4 @@ -From cd28bae50255652fbe2b7e17dbc71917c38dba97 Mon Sep 17 00:00:00 2001 +From c5376ddf304b247d395f8b540629cdff6c0a9bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draig=20Brady?= Date: Fri, 6 Jan 2012 17:12:54 +0000 Subject: [PATCH] Don't access the net while building docs diff --git a/0002-Remove-runtime-dep-on-python-pbr.patch b/0002-Remove-runtime-dep-on-python-pbr.patch index ddc1642..d16274d 100644 --- a/0002-Remove-runtime-dep-on-python-pbr.patch +++ b/0002-Remove-runtime-dep-on-python-pbr.patch @@ -1,4 +1,4 @@ -From cb99e3389282ccd3ab11b0faed363305f4822b83 Mon Sep 17 00:00:00 2001 +From b78bbe8ade2bf833f479112c5738d2cc646c35c4 Mon Sep 17 00:00:00 2001 From: John Bresnahan Date: Mon, 9 Sep 2013 17:00:28 -1000 Subject: [PATCH] Remove runtime dep on python pbr diff --git a/0003-avoid-unsupported-storage-drivers.patch b/0003-avoid-unsupported-storage-drivers.patch index f7bf354..632fed8 100644 --- a/0003-avoid-unsupported-storage-drivers.patch +++ b/0003-avoid-unsupported-storage-drivers.patch @@ -1,4 +1,4 @@ -From 0ae098262a879f3d15dd36900bc2afb03459e874 Mon Sep 17 00:00:00 2001 +From da5a6193bf3985335bccb9d37d2f388634eccefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A1draig=20Brady?= Date: Wed, 9 Apr 2014 10:31:27 +0100 Subject: [PATCH] avoid unsupported storage drivers diff --git a/0004-notify-calling-process-we-are-ready-to-serve.patch b/0004-notify-calling-process-we-are-ready-to-serve.patch index 97df109..9433be7 100644 --- a/0004-notify-calling-process-we-are-ready-to-serve.patch +++ b/0004-notify-calling-process-we-are-ready-to-serve.patch @@ -1,4 +1,4 @@ -From 495611818e6de5d5802d8cb3f3630f579c8c18ca Mon Sep 17 00:00:00 2001 +From cb15117aa220d4d103c3fad2cd78cb372cc2a767 Mon Sep 17 00:00:00 2001 From: Alan Pevec Date: Tue, 11 Feb 2014 22:36:00 +0100 Subject: [PATCH] notify calling process we are ready to serve diff --git a/0005-To-prevent-client-use-v2-patch-api-to-handle-file-an.patch b/0005-To-prevent-client-use-v2-patch-api-to-handle-file-an.patch deleted file mode 100644 index 7672148..0000000 --- a/0005-To-prevent-client-use-v2-patch-api-to-handle-file-an.patch +++ /dev/null @@ -1,708 +0,0 @@ -From 292c86ef6ffa1f2e6eec1c23dc2b21016487f8c4 Mon Sep 17 00:00:00 2001 -From: Zhi Yan Liu -Date: Mon, 15 Dec 2014 12:29:55 +0800 -Subject: [PATCH] To prevent client use v2 patch api to handle file and swift - location - -The change will be used to restrict client to download and delete any -file in glance-api server. The same resone and logic as what we did in -v1: -https://github.com/openstack/glance/blob/master/glance/api/v1/images.py#L429 - -Closes-Bug: bug/1400966 -DocImpact - -Note: Even this change could fully resolve the problem for Glance, but -we still need to fix this issue from glance_store perspective -separatelly due to other projects can use the lib directly. - -Conflicts: - glance/api/v1/images.py - glance/location.py - glance/tests/functional/v2/test_images.py - glance/tests/unit/test_store_location.py - glance/tests/unit/v1/test_api.py - -(cherry-picked from 4afdb017aa1ccef01482f117cb8d0736a6da38ed) -Signed-off-by: Zhi Yan Liu - -Change-Id: I66764956aa98ec8b66b5e890297d7515c2f2f00c -Resolves: rhbz #1174485 -Upstream-Closes-Bug: #1400966 -Upstream-Kilo: https://review.openstack.org/#/c/141706/ -Upstream-Juno: https://review.openstack.org/#/c/142373/ -Upstream-change-Id: I72dbead3cb2dcb87f52658ddb880e26880cc229b -Reviewed-on: https://code.engineering.redhat.com/gerrit/38835 -Reviewed-by: Eoghan Glynn -Tested-by: Eoghan Glynn ---- - glance/api/v1/images.py | 31 ++--- - glance/common/store_utils.py | 22 ++++ - glance/location.py | 30 +++-- - glance/tests/functional/v1/test_copy_to_file.py | 4 +- - glance/tests/functional/v2/test_images.py | 158 ++++++++++-------------- - glance/tests/unit/test_store_image.py | 3 +- - glance/tests/unit/test_store_location.py | 33 ++++- - glance/tests/unit/utils.py | 9 +- - glance/tests/unit/v1/test_api.py | 62 +++++++++- - 9 files changed, 221 insertions(+), 131 deletions(-) - -diff --git a/glance/api/v1/images.py b/glance/api/v1/images.py -index c85b301..746f8cd 100644 ---- a/glance/api/v1/images.py -+++ b/glance/api/v1/images.py -@@ -23,7 +23,6 @@ import eventlet - import glance_store as store - import glance_store.location - from oslo.config import cfg --import six.moves.urllib.parse as urlparse - from webob.exc import HTTPBadRequest - from webob.exc import HTTPConflict - from webob.exc import HTTPForbidden -@@ -40,6 +39,7 @@ from glance.api.v1 import filters - from glance.api.v1 import upload_utils - from glance.common import exception - from glance.common import property_utils -+from glance.common import store_utils - from glance.common import utils - from glance.common import wsgi - from glance.i18n import _LE -@@ -415,26 +415,19 @@ class Controller(controller.BaseController): - @staticmethod - def _validate_source(source, req): - """ -- External sources (as specified via the location or copy-from headers) -- are supported only over non-local store types, i.e. S3, Swift, HTTP. -- Note the absence of 'file://' for security reasons, see LP bug #942118. -- 'swift+config://' is also absent for security reasons, see LP bug -- #1334196. -- If the above constraint is violated, we reject with 400 "Bad Request". -+ To validate if external sources (as specified via the location -+ or copy-from headers) are supported. Otherwise we reject -+ with 400 "Bad Request". - """ - if source: -- pieces = urlparse.urlparse(source) -- schemes = [scheme for scheme in store.get_known_schemes() -- if scheme != 'file' and scheme != 'swift+config'] -- for scheme in schemes: -- if pieces.scheme == scheme: -- return source -- msg = ("External sourcing not supported for " -- "store '%s'" % pieces.scheme) -- LOG.debug(msg) -- raise HTTPBadRequest(explanation=msg, -- request=req, -- content_type="text/plain") -+ if store_utils.validate_external_location(source): -+ return source -+ else: -+ msg = _("External source are not supported: '%s'") % source -+ LOG.debug(msg) -+ raise HTTPBadRequest(explanation=msg, -+ request=req, -+ content_type="text/plain") - - @staticmethod - def _copy_from(req): -diff --git a/glance/common/store_utils.py b/glance/common/store_utils.py -index 8f04d39..b7537ce 100644 ---- a/glance/common/store_utils.py -+++ b/glance/common/store_utils.py -@@ -16,6 +16,7 @@ import sys - - import glance_store as store_api - from oslo.config import cfg -+import six.moves.urllib.parse as urlparse - - from glance.common import utils - import glance.db as db_api -@@ -119,3 +120,24 @@ def delete_image_location_from_backend(context, image_id, location): - # such as uploading process failure then we can't use - # location status mechanism to support image pending delete. - safe_delete_from_backend(context, image_id, location) -+ -+ -+def validate_external_location(uri): -+ """ -+ Validate if URI of external location are supported. -+ -+ Only over non-local store types are OK, i.e. S3, Swift, -+ HTTP. Note the absence of 'file://' for security reasons, -+ see LP bug #942118, 1400966, 'swift+config://' is also -+ absent for security reasons, see LP bug #1334196. -+ -+ :param uri: The URI of external image location. -+ :return: Whether given URI of external image location are OK. -+ """ -+ -+ # TODO(zhiyan): This function could be moved to glance_store. -+ -+ pieces = urlparse.urlparse(uri) -+ valid_schemes = [scheme for scheme in store_api.get_known_schemes() -+ if scheme != 'file' and scheme != 'swift+config'] -+ return pieces.scheme in valid_schemes -diff --git a/glance/location.py b/glance/location.py -index fcdba0a..f83fa7a 100644 ---- a/glance/location.py -+++ b/glance/location.py -@@ -66,18 +66,20 @@ class ImageRepoProxy(glance.domain.proxy.Repo): - return result - - --def _check_location_uri(context, store_api, uri): -+def _check_location_uri(context, store_api, store_utils, uri): - """Check if an image location is valid. - - :param context: Glance request context - :param store_api: store API module -+ :param store_utils: store utils module - :param uri: location's uri string - """ -+ - is_ok = True - try: -- size = store_api.get_size_from_backend(uri, context=context) - # NOTE(zhiyan): Some stores return zero when it catch exception -- is_ok = size > 0 -+ is_ok = (store_utils.validate_external_location(uri) and -+ store_api.get_size_from_backend(uri, context=context) > 0) - except (store.UnknownScheme, store.NotFound): - is_ok = False - if not is_ok: -@@ -85,8 +87,8 @@ def _check_location_uri(context, store_api, uri): - raise exception.BadStoreUri(message=reason) - - --def _check_image_location(context, store_api, location): -- _check_location_uri(context, store_api, location['url']) -+def _check_image_location(context, store_api, store_utils, location): -+ _check_location_uri(context, store_api, store_utils, location['url']) - store_api.check_location_metadata(location['metadata']) - - -@@ -122,6 +124,7 @@ class ImageFactoryProxy(glance.domain.proxy.ImageFactory): - def __init__(self, factory, context, store_api, store_utils): - self.context = context - self.store_api = store_api -+ self.store_utils = store_utils - proxy_kwargs = {'context': context, 'store_api': store_api, - 'store_utils': store_utils} - super(ImageFactoryProxy, self).__init__(factory, -@@ -131,7 +134,10 @@ class ImageFactoryProxy(glance.domain.proxy.ImageFactory): - def new_image(self, **kwargs): - locations = kwargs.get('locations', []) - for loc in locations: -- _check_image_location(self.context, self.store_api, loc) -+ _check_image_location(self.context, -+ self.store_api, -+ self.store_utils, -+ loc) - loc['status'] = 'active' - if _count_duplicated_locations(locations, loc) > 1: - raise exception.DuplicateLocation(location=loc['url']) -@@ -169,7 +175,9 @@ class StoreLocations(collections.MutableSequence): - - def insert(self, i, location): - _check_image_location(self.image_proxy.context, -- self.image_proxy.store_api, location) -+ self.image_proxy.store_api, -+ self.image_proxy.store_utils, -+ location) - location['status'] = 'active' - if _count_duplicated_locations(self.value, location) > 0: - raise exception.DuplicateLocation(location=location['url']) -@@ -214,7 +222,9 @@ class StoreLocations(collections.MutableSequence): - - def __setitem__(self, i, location): - _check_image_location(self.image_proxy.context, -- self.image_proxy.store_api, location) -+ self.image_proxy.store_api, -+ self.image_proxy.store_utils, -+ location) - location['status'] = 'active' - self.value.__setitem__(i, location) - _set_image_size(self.image_proxy.context, -@@ -303,7 +313,9 @@ def _locations_proxy(target, attr): - '%s') % ori_value) - # NOTE(zhiyan): Check locations are all valid. - for location in value: -- _check_image_location(self.context, self.store_api, -+ _check_image_location(self.context, -+ self.store_api, -+ self.store_utils, - location) - location['status'] = 'active' - if _count_duplicated_locations(value, location) > 1: -diff --git a/glance/tests/functional/v1/test_copy_to_file.py b/glance/tests/functional/v1/test_copy_to_file.py -index 15bb708..b64eac6 100644 ---- a/glance/tests/functional/v1/test_copy_to_file.py -+++ b/glance/tests/functional/v1/test_copy_to_file.py -@@ -250,7 +250,7 @@ class TestCopyToFile(functional.FunctionalTest): - response, content = http.request(path, 'POST', headers=headers) - self.assertEqual(response.status, 400, content) - -- expected = 'External sourcing not supported for store \'file\'' -+ expected = 'External source are not supported: \'%s\'' % copy_from - msg = 'expected "%s" in "%s"' % (expected, content) - self.assertTrue(expected in content, msg) - -@@ -276,7 +276,7 @@ class TestCopyToFile(functional.FunctionalTest): - response, content = http.request(path, 'POST', headers=headers) - self.assertEqual(response.status, 400, content) - -- expected = 'External sourcing not supported for store \'swift+config\'' -+ expected = 'External source are not supported: \'swift+config://xxx\'' - msg = 'expected "%s" in "%s"' % (expected, content) - self.assertTrue(expected in content, msg) - -diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py -index 14fe3c7..4c32375 100644 ---- a/glance/tests/functional/v2/test_images.py -+++ b/glance/tests/functional/v2/test_images.py -@@ -16,7 +16,6 @@ - import BaseHTTPServer - import os - import signal --import tempfile - import uuid - - import requests -@@ -47,7 +46,7 @@ def get_handler_class(fixture): - self.end_headers() - return - -- def log_message(*args, **kwargs): -+ def log_message(self, *args, **kwargs): - # Override this method to prevent debug output from going - # to stderr during testing - return -@@ -75,6 +74,18 @@ class TestImages(functional.FunctionalTest): - self.cleanup() - self.api_server.deployment_flavor = 'noauth' - self.api_server.data_api = 'glance.db.sqlalchemy.api' -+ for i in range(3): -+ ret = http_server("foo_image_id%d" % i, "foo_image%d" % i) -+ setattr(self, 'http_server%d_pid' % i, ret[0]) -+ setattr(self, 'http_port%d' % i, ret[1]) -+ -+ def tearDown(self): -+ for i in range(3): -+ pid = getattr(self, 'http_server%d_pid' % i, None) -+ if pid: -+ os.kill(pid, signal.SIGKILL) -+ -+ super(TestImages, self).tearDown() - - def _url(self, path): - return 'http://127.0.0.1:%d%s' % (self.api_port, path) -@@ -329,21 +340,15 @@ class TestImages(functional.FunctionalTest): - self.assertEqual(413, response.status_code, response.text) - - # Adding 3 image locations should fail since configured limit is 2 -- for i in range(3): -- file_path = os.path.join(self.test_dir, 'fake_image_%i' % i) -- with open(file_path, 'w') as fap: -- fap.write('glance') -- - path = self._url('/v2/images/%s' % image_id) - media_type = 'application/openstack-images-v2.1-json-patch' - headers = self._headers({'content-type': media_type}) - changes = [] - for i in range(3): -+ url = ('http://127.0.0.1:%s/foo_image' % -+ getattr(self, 'http_port%d' % i)) - changes.append({'op': 'add', 'path': '/locations/-', -- 'value': {'url': 'file://{0}'.format( -- os.path.join(self.test_dir, -- 'fake_image_%i' % i)), -- 'metadata': {}}, -+ 'value': {'url': url, 'metadata': {}}, - }) - - data = jsonutils.dumps(changes) -@@ -2176,17 +2181,14 @@ class TestImages(functional.FunctionalTest): - self.assertNotIn('size', image) - self.assertNotIn('virtual_size', image) - -- file_path = os.path.join(self.test_dir, 'fake_image') -- with open(file_path, 'w') as fap: -- fap.write('glance') -- - # Update locations for the queued image - path = self._url('/v2/images/%s' % image_id) - media_type = 'application/openstack-images-v2.1-json-patch' - headers = self._headers({'content-type': media_type}) -+ url = 'http://127.0.0.1:%s/foo_image' % self.http_port0 - data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', -- 'value': [{'url': 'file://' + file_path, -- 'metadata': {}}]}]) -+ 'value': [{'url': url, 'metadata': {}}] -+ }]) - response = requests.patch(path, headers=headers, data=data) - self.assertEqual(200, response.status_code, response.text) - -@@ -2195,7 +2197,42 @@ class TestImages(functional.FunctionalTest): - response = requests.get(path, headers=headers) - self.assertEqual(200, response.status_code) - image = jsonutils.loads(response.text) -- self.assertEqual(image['size'], 6) -+ self.assertEqual(image['size'], 10) -+ -+ def test_update_locations_with_restricted_sources(self): -+ self.start_servers(**self.__dict__.copy()) -+ # Create an image -+ path = self._url('/v2/images') -+ headers = self._headers({'content-type': 'application/json'}) -+ data = jsonutils.dumps({'name': 'image-1', 'disk_format': 'aki', -+ 'container_format': 'aki'}) -+ response = requests.post(path, headers=headers, data=data) -+ self.assertEqual(201, response.status_code) -+ -+ # Returned image entity should have a generated id and status -+ image = jsonutils.loads(response.text) -+ image_id = image['id'] -+ self.assertEqual('queued', image['status']) -+ self.assertNotIn('size', image) -+ self.assertNotIn('virtual_size', image) -+ -+ # Update locations for the queued image -+ path = self._url('/v2/images/%s' % image_id) -+ media_type = 'application/openstack-images-v2.1-json-patch' -+ headers = self._headers({'content-type': media_type}) -+ data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', -+ 'value': [{'url': 'file:///foo_image', -+ 'metadata': {}}] -+ }]) -+ response = requests.patch(path, headers=headers, data=data) -+ self.assertEqual(400, response.status_code, response.text) -+ -+ data = jsonutils.dumps([{'op': 'replace', 'path': '/locations', -+ 'value': [{'url': 'swift+config:///foo_image', -+ 'metadata': {}}] -+ }]) -+ response = requests.patch(path, headers=headers, data=data) -+ self.assertEqual(400, response.status_code, response.text) - - - class TestImagesWithRegistry(TestImages): -@@ -2421,16 +2458,16 @@ class TestImageLocationSelectionStrategy(functional.FunctionalTest): - super(TestImageLocationSelectionStrategy, self).setUp() - self.cleanup() - self.api_server.deployment_flavor = 'noauth' -- self.foo_image_file = tempfile.NamedTemporaryFile() -- self.foo_image_file.write("foo image file") -- self.foo_image_file.flush() -- self.addCleanup(self.foo_image_file.close) -- ret = http_server("foo_image_id", "foo_image") -- self.http_server_pid, self.http_port = ret -+ for i in range(3): -+ ret = http_server("foo_image_id%d" % i, "foo_image%d" % i) -+ setattr(self, 'http_server%d_pid' % i, ret[0]) -+ setattr(self, 'http_port%d' % i, ret[1]) - - def tearDown(self): -- if self.http_server_pid is not None: -- os.kill(self.http_server_pid, signal.SIGKILL) -+ for i in range(3): -+ pid = getattr(self, 'http_server%d_pid' % i, None) -+ if pid: -+ os.kill(pid, signal.SIGKILL) - - super(TestImageLocationSelectionStrategy, self).tearDown() - -@@ -2483,69 +2520,10 @@ class TestImageLocationSelectionStrategy(functional.FunctionalTest): - path = self._url('/v2/images/%s' % image_id) - media_type = 'application/openstack-images-v2.1-json-patch' - headers = self._headers({'content-type': media_type}) -- values = [{'url': 'file://%s' % self.foo_image_file.name, -- 'metadata': {'idx': '1'}}, -- {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port, -- 'metadata': {'idx': '0'}}] -- doc = [{'op': 'replace', -- 'path': '/locations', -- 'value': values}] -- data = jsonutils.dumps(doc) -- response = requests.patch(path, headers=headers, data=data) -- self.assertEqual(200, response.status_code) -- -- # Image locations should be visible -- path = self._url('/v2/images/%s' % image_id) -- headers = self._headers({'Content-Type': 'application/json'}) -- response = requests.get(path, headers=headers) -- self.assertEqual(200, response.status_code) -- image = jsonutils.loads(response.text) -- self.assertTrue('locations' in image) -- self.assertEqual(image['locations'], values) -- self.assertTrue('direct_url' in image) -- self.assertEqual(image['direct_url'], values[0]['url']) -- -- self.stop_servers() -- -- def test_image_locatons_with_store_type_strategy(self): -- self.api_server.show_image_direct_url = True -- self.api_server.show_multiple_locations = True -- self.image_location_quota = 10 -- self.api_server.location_strategy = 'store_type' -- preference = "http, swift, filesystem" -- self.api_server.store_type_location_strategy_preference = preference -- self.start_servers(**self.__dict__.copy()) -- -- # Create an image -- path = self._url('/v2/images') -- headers = self._headers({'content-type': 'application/json'}) -- data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', -- 'foo': 'bar', 'disk_format': 'aki', -- 'container_format': 'aki'}) -- response = requests.post(path, headers=headers, data=data) -- self.assertEqual(201, response.status_code) -- -- # Get the image id -- image = jsonutils.loads(response.text) -- image_id = image['id'] -- -- # Image locations should not be visible before location is set -- path = self._url('/v2/images/%s' % image_id) -- headers = self._headers({'Content-Type': 'application/json'}) -- response = requests.get(path, headers=headers) -- self.assertEqual(200, response.status_code) -- image = jsonutils.loads(response.text) -- self.assertTrue('locations' in image) -- self.assertTrue(image["locations"] == []) -- -- # Update image locations via PATCH -- path = self._url('/v2/images/%s' % image_id) -- media_type = 'application/openstack-images-v2.1-json-patch' -- headers = self._headers({'content-type': media_type}) -- values = [{'url': 'file://%s' % self.foo_image_file.name, -- 'metadata': {'idx': '1'}}, -- {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port, -- 'metadata': {'idx': '0'}}] -+ values = [{'url': 'http://127.0.0.1:%s/foo_image' % self.http_port0, -+ 'metadata': {}}, -+ {'url': 'http://127.0.0.1:%s/foo_image' % self.http_port1, -+ 'metadata': {}}] - doc = [{'op': 'replace', - 'path': '/locations', - 'value': values}] -@@ -2553,8 +2531,6 @@ class TestImageLocationSelectionStrategy(functional.FunctionalTest): - response = requests.patch(path, headers=headers, data=data) - self.assertEqual(200, response.status_code) - -- values.sort(key=lambda loc: int(loc['metadata']['idx'])) -- - # Image locations should be visible - path = self._url('/v2/images/%s' % image_id) - headers = self._headers({'Content-Type': 'application/json'}) -diff --git a/glance/tests/unit/test_store_image.py b/glance/tests/unit/test_store_image.py -index 665f126..8b334ab 100644 ---- a/glance/tests/unit/test_store_image.py -+++ b/glance/tests/unit/test_store_image.py -@@ -18,6 +18,7 @@ import glance_store - - from glance.common import exception - import glance.location -+from glance.tests.unit import base as unit_test_base - from glance.tests.unit import utils as unit_test_utils - from glance.tests import utils - -@@ -759,7 +760,7 @@ class TestStoreImageRepo(utils.BaseTestCase): - self.assertEqual(acls['read'], [TENANT2]) - - --class TestImageFactory(utils.BaseTestCase): -+class TestImageFactory(unit_test_base.StoreClearingUnitTest): - - def setUp(self): - super(TestImageFactory, self).setUp() -diff --git a/glance/tests/unit/test_store_location.py b/glance/tests/unit/test_store_location.py -index 884221b..c9ee44c 100644 ---- a/glance/tests/unit/test_store_location.py -+++ b/glance/tests/unit/test_store_location.py -@@ -17,6 +17,8 @@ import mock - - import glance_store - -+from glance.common import exception -+from glance.common import store_utils - import glance.location - from glance.tests.unit import base - -@@ -32,11 +34,13 @@ CONF = {'default_store': 'file', - - class TestStoreLocation(base.StoreClearingUnitTest): - -+ class FakeImageProxy(): -+ size = None -+ context = None -+ store_api = mock.Mock() -+ store_utils = store_utils -+ - def test_add_location_for_image_without_size(self): -- class FakeImageProxy(): -- size = None -- context = None -- store_api = mock.Mock() - - def fake_get_size_from_backend(uri, context=None): - return 1 -@@ -49,14 +53,31 @@ class TestStoreLocation(base.StoreClearingUnitTest): - loc2 = {'url': 'file:///fake2.img.tar.gz', 'metadata': {}} - - # Test for insert location -- image1 = FakeImageProxy() -+ image1 = TestStoreLocation.FakeImageProxy() - locations = glance.location.StoreLocations(image1, []) - locations.insert(0, loc2) - self.assertEqual(image1.size, 1) - - # Test for set_attr of _locations_proxy -- image2 = FakeImageProxy() -+ image2 = TestStoreLocation.FakeImageProxy() - locations = glance.location.StoreLocations(image2, [loc1]) - locations[0] = loc2 - self.assertIn(loc2, locations) - self.assertEqual(image2.size, 1) -+ -+ def test_add_location_with_restricted_sources(self): -+ -+ loc1 = {'url': 'file:///fake1.img.tar.gz', 'metadata': {}} -+ loc2 = {'url': 'swift+config:///xxx', 'metadata': {}} -+ -+ # Test for insert location -+ image1 = TestStoreLocation.FakeImageProxy() -+ locations = glance.location.StoreLocations(image1, []) -+ self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc1) -+ self.assertNotIn(loc1, locations) -+ -+ # Test for set_attr of _locations_proxy -+ image2 = TestStoreLocation.FakeImageProxy() -+ locations = glance.location.StoreLocations(image2, [loc1]) -+ self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc2) -+ self.assertNotIn(loc2, locations) -diff --git a/glance/tests/unit/utils.py b/glance/tests/unit/utils.py -index df59160..f7c8d56 100644 ---- a/glance/tests/unit/utils.py -+++ b/glance/tests/unit/utils.py -@@ -14,12 +14,13 @@ - # under the License. - - import urllib --import urlparse - - import glance_store as store - from oslo.config import cfg -+import six.moves.urllib.parse as urlparse - - from glance.common import exception -+from glance.common import store_utils - from glance.common import wsgi - import glance.context - import glance.db.simple.api as simple_db -@@ -135,6 +136,12 @@ class FakeStoreUtils(object): - else: - self.safe_delete_from_backend(context, image_id, location) - -+ def validate_external_location(self, uri): -+ if uri and urlparse.urlparse(uri).scheme: -+ return store_utils.validate_external_location(uri) -+ else: -+ return True -+ - - class FakeStoreAPI(object): - def __init__(self, store_metadata=None): -diff --git a/glance/tests/unit/v1/test_api.py b/glance/tests/unit/v1/test_api.py -index bd2182e..4ec136d 100644 ---- a/glance/tests/unit/v1/test_api.py -+++ b/glance/tests/unit/v1/test_api.py -@@ -419,7 +419,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): - - res = req.get_response(self.api) - self.assertEqual(res.status_int, 400) -- self.assertIn('External sourcing not supported', res.body) -+ self.assertIn('External source are not supported', res.body) - - def test_create_with_location_bad_store_uri(self): - fixture_headers = { -@@ -1006,7 +1006,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): - res = req.get_response(self.api) - self.assertEqual(res.status_int, 409) - -- def test_add_location_with_invalid_location(self): -+ def test_add_location_with_invalid_location_on_conflict_image_size(self): - """Tests creates an image from location and conflict image size""" - fixture_headers = {'x-image-meta-store': 'file', - 'x-image-meta-disk-format': 'vhd', -@@ -1023,6 +1023,36 @@ class TestGlanceAPI(base.IsolatedUnitTest): - res = req.get_response(self.api) - self.assertEqual(res.status_int, 400) - -+ def test_add_location_with_invalid_location_on_restricted_sources(self): -+ """Tests creates an image from location and restricted sources""" -+ fixture_headers = {'x-image-meta-store': 'file', -+ 'x-image-meta-disk-format': 'vhd', -+ 'x-image-meta-location': 'file:///etc/passwd', -+ 'x-image-meta-container-format': 'ovf', -+ 'x-image-meta-name': 'fake image #F'} -+ -+ req = webob.Request.blank("/images") -+ req.headers['Content-Type'] = 'application/octet-stream' -+ req.method = 'POST' -+ for k, v in fixture_headers.iteritems(): -+ req.headers[k] = v -+ res = req.get_response(self.api) -+ self.assertEqual(400, res.status_int) -+ -+ fixture_headers = {'x-image-meta-store': 'file', -+ 'x-image-meta-disk-format': 'vhd', -+ 'x-image-meta-location': 'swift+config://xxx', -+ 'x-image-meta-container-format': 'ovf', -+ 'x-image-meta-name': 'fake image #F'} -+ -+ req = webob.Request.blank("/images") -+ req.headers['Content-Type'] = 'application/octet-stream' -+ req.method = 'POST' -+ for k, v in fixture_headers.iteritems(): -+ req.headers[k] = v -+ res = req.get_response(self.api) -+ self.assertEqual(400, res.status_int) -+ - def test_add_copy_from_with_location(self): - """Tests creates an image from copy-from and location""" - fixture_headers = {'x-image-meta-store': 'file', -@@ -1039,6 +1069,34 @@ class TestGlanceAPI(base.IsolatedUnitTest): - res = req.get_response(self.api) - self.assertEqual(res.status_int, 400) - -+ def test_add_copy_from_with_restricted_sources(self): -+ """Tests creates an image from copy-from with restricted sources""" -+ fixture_headers = {'x-image-meta-store': 'file', -+ 'x-image-meta-disk-format': 'vhd', -+ 'x-glance-api-copy-from': 'file:///etc/passwd', -+ 'x-image-meta-container-format': 'ovf', -+ 'x-image-meta-name': 'fake image #F'} -+ -+ req = webob.Request.blank("/images") -+ req.method = 'POST' -+ for k, v in six.iteritems(fixture_headers): -+ req.headers[k] = v -+ res = req.get_response(self.api) -+ self.assertEqual(400, res.status_int) -+ -+ fixture_headers = {'x-image-meta-store': 'file', -+ 'x-image-meta-disk-format': 'vhd', -+ 'x-glance-api-copy-from': 'swift+config://xxx', -+ 'x-image-meta-container-format': 'ovf', -+ 'x-image-meta-name': 'fake image #F'} -+ -+ req = webob.Request.blank("/images") -+ req.method = 'POST' -+ for k, v in six.iteritems(fixture_headers): -+ req.headers[k] = v -+ res = req.get_response(self.api) -+ self.assertEqual(400, res.status_int) -+ - def test_add_copy_from_upload_image_unauthorized_with_body(self): - rules = {"upload_image": '!', "modify_image": '@', - "add_image": '@'} diff --git a/0006-Prevent-file-swift-config-and-filesystem-schemes.patch b/0006-Prevent-file-swift-config-and-filesystem-schemes.patch deleted file mode 100644 index 6677c30..0000000 --- a/0006-Prevent-file-swift-config-and-filesystem-schemes.patch +++ /dev/null @@ -1,125 +0,0 @@ -From e4b8708c8c9d0c000c6aab39b2914671e938fc8d Mon Sep 17 00:00:00 2001 -From: Grant Murphy -Date: Wed, 7 Jan 2015 16:09:38 -0800 -Subject: [PATCH] Prevent file, swift+config and filesystem schemes - -This change ensures that 'file', 'filesystem', and 'swift+config' URI -schemes are not allowed when setting the location field. A previous -fix to CVE-2014-9493 attempted to address this issue but did not -include 'filesystem', a URI scheme allowed by the glance_store. - -Without this fix in place it is possible for a client to access any file -the glance-api server has read permissions for. - -(cherry picked from commit a2d986b976e9325a272e2d422465165315d19fe6) - -Change-Id: If250093498dfb76a82205ea2aee98109e55855b0 -Resolves: rhbz #1174485 -Upstream-Closes-Bug: #1408663 -Upstream-Kilo: https://review.openstack.org/#/c/145640/ -Upstream-Juno: https://review.openstack.org/#/c/145916/ -Upstream-change-Id: I02cd099a8634b9c7e3cf8f172bcbd33f8edcbc83 -Reviewed-on: https://code.engineering.redhat.com/gerrit/39634 -Reviewed-by: Jon Bernard -Reviewed-by: Flavio Percoco -Tested-by: Flavio Percoco ---- - glance/common/store_utils.py | 11 ++++++----- - glance/tests/unit/test_store_location.py | 3 +++ - glance/tests/unit/v1/test_api.py | 32 ++++++++++++-------------------- - 3 files changed, 21 insertions(+), 25 deletions(-) - -diff --git a/glance/common/store_utils.py b/glance/common/store_utils.py -index b7537ce..64cfa87 100644 ---- a/glance/common/store_utils.py -+++ b/glance/common/store_utils.py -@@ -38,6 +38,8 @@ store_utils_opts = [ - CONF = cfg.CONF - CONF.register_opts(store_utils_opts) - -+RESTRICTED_URI_SCHEMAS = frozenset(['file', 'filesystem', 'swift+config']) -+ - - def safe_delete_from_backend(context, image_id, location): - """ -@@ -136,8 +138,7 @@ def validate_external_location(uri): - """ - - # TODO(zhiyan): This function could be moved to glance_store. -- -- pieces = urlparse.urlparse(uri) -- valid_schemes = [scheme for scheme in store_api.get_known_schemes() -- if scheme != 'file' and scheme != 'swift+config'] -- return pieces.scheme in valid_schemes -+ # TODO(gm): Use a whitelist of allowed schemes -+ scheme = urlparse.urlparse(uri).scheme -+ return (scheme in store_api.get_known_schemes() and -+ scheme not in RESTRICTED_URI_SCHEMAS) -diff --git a/glance/tests/unit/test_store_location.py b/glance/tests/unit/test_store_location.py -index c9ee44c..efaecd8 100644 ---- a/glance/tests/unit/test_store_location.py -+++ b/glance/tests/unit/test_store_location.py -@@ -69,12 +69,15 @@ class TestStoreLocation(base.StoreClearingUnitTest): - - loc1 = {'url': 'file:///fake1.img.tar.gz', 'metadata': {}} - loc2 = {'url': 'swift+config:///xxx', 'metadata': {}} -+ loc3 = {'url': 'filesystem:///foo.img.tar.gz', 'metadata': {}} - - # Test for insert location - image1 = TestStoreLocation.FakeImageProxy() - locations = glance.location.StoreLocations(image1, []) - self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc1) -+ self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc3) - self.assertNotIn(loc1, locations) -+ self.assertNotIn(loc3, locations) - - # Test for set_attr of _locations_proxy - image2 = TestStoreLocation.FakeImageProxy() -diff --git a/glance/tests/unit/v1/test_api.py b/glance/tests/unit/v1/test_api.py -index 4ec136d..39e9a44 100644 ---- a/glance/tests/unit/v1/test_api.py -+++ b/glance/tests/unit/v1/test_api.py -@@ -1071,31 +1071,23 @@ class TestGlanceAPI(base.IsolatedUnitTest): - - def test_add_copy_from_with_restricted_sources(self): - """Tests creates an image from copy-from with restricted sources""" -- fixture_headers = {'x-image-meta-store': 'file', -+ header_template = {'x-image-meta-store': 'file', - 'x-image-meta-disk-format': 'vhd', -- 'x-glance-api-copy-from': 'file:///etc/passwd', - 'x-image-meta-container-format': 'ovf', - 'x-image-meta-name': 'fake image #F'} - -- req = webob.Request.blank("/images") -- req.method = 'POST' -- for k, v in six.iteritems(fixture_headers): -- req.headers[k] = v -- res = req.get_response(self.api) -- self.assertEqual(400, res.status_int) -+ schemas = ["file:///etc/passwd", -+ "swift+config:///xxx", -+ "filesystem:///etc/passwd"] - -- fixture_headers = {'x-image-meta-store': 'file', -- 'x-image-meta-disk-format': 'vhd', -- 'x-glance-api-copy-from': 'swift+config://xxx', -- 'x-image-meta-container-format': 'ovf', -- 'x-image-meta-name': 'fake image #F'} -- -- req = webob.Request.blank("/images") -- req.method = 'POST' -- for k, v in six.iteritems(fixture_headers): -- req.headers[k] = v -- res = req.get_response(self.api) -- self.assertEqual(400, res.status_int) -+ for schema in schemas: -+ req = webob.Request.blank("/images") -+ req.method = 'POST' -+ for k, v in six.iteritems(header_template): -+ req.headers[k] = v -+ req.headers['x-glance-api-copy-from'] = schema -+ res = req.get_response(self.api) -+ self.assertEqual(400, res.status_int) - - def test_add_copy_from_upload_image_unauthorized_with_body(self): - rules = {"upload_image": '!', "modify_image": '@', diff --git a/0007-Cleanup-chunks-for-deleted-image-that-was-saving.patch b/0007-Cleanup-chunks-for-deleted-image-that-was-saving.patch deleted file mode 100644 index b38a3fd..0000000 --- a/0007-Cleanup-chunks-for-deleted-image-that-was-saving.patch +++ /dev/null @@ -1,540 +0,0 @@ -From 4dd5813429427eb2d0207d36113d77d108a9143a Mon Sep 17 00:00:00 2001 -From: Zhi Yan Liu -Date: Tue, 30 Dec 2014 22:25:50 +0800 -Subject: [PATCH] Cleanup chunks for deleted image that was 'saving' - -Currently image data cannot be removed synchronously for an image that -is in saving state. And when, the upload operation for such an image is -completed the operator configured quota can be exceeded. - -This patch fixes the issue of left over chunks for an image which was -deleted from saving status. However, by the limitation of the design we -cannot enforce a global quota check for the image in saving status. - -This change introduces a inconsonance between http response codes of -v1 and v2 APIs. The status codes which we will now see after the upload -process completes on an image which was deleted mid way are: - -v1: 412 Precondition Failed -v2: 410 Gone - -SecurityImpact -UpgradeImpact -APIImpact - -Closes-Bug: 1383973 -Closes-Bug: 1398830 -Closes-Bug: 1188532 - -Conflicts: - glance/api/v1/upload_utils.py - glance/api/v2/image_data.py - glance/tests/unit/test_domain_proxy.py - glance/tests/unit/v1/test_api.py - -Change-Id: I47229b366c25367ec1bd48aec684e0880f3dfe60 -Signed-off-by: Zhi Yan Liu -(cherry picked from commit 0dc8fbb3479a53c5bba8475d14f4c7206904c5ea) -(cherry picked from commit 7d5d8657fd70b20518610b3c6f8e41e16c72fa31) ---- - glance/api/authorization.py | 4 +- - glance/api/policy.py | 8 ++-- - glance/api/v1/upload_utils.py | 19 +++++--- - glance/api/v2/image_data.py | 8 +++- - glance/db/__init__.py | 7 +-- - glance/domain/proxy.py | 4 +- - glance/location.py | 4 +- - glance/notifier.py | 4 +- - glance/quota/__init__.py | 4 +- - glance/tests/unit/test_domain_proxy.py | 14 +++--- - glance/tests/unit/test_policy.py | 2 +- - glance/tests/unit/test_quota.py | 17 ++++--- - glance/tests/unit/test_store_image.py | 2 +- - glance/tests/unit/v1/test_api.py | 57 +++++++++++------------- - glance/tests/unit/v2/test_image_data_resource.py | 24 +++++----- - 15 files changed, 98 insertions(+), 80 deletions(-) - -diff --git a/glance/api/authorization.py b/glance/api/authorization.py -index 149ff55..77f7ed3 100644 ---- a/glance/api/authorization.py -+++ b/glance/api/authorization.py -@@ -158,10 +158,10 @@ class ImageMemberRepoProxy(glance.domain.proxy.Repo): - 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) -diff --git a/glance/api/policy.py b/glance/api/policy.py -index 0bc8d56..e395876 100644 ---- a/glance/api/policy.py -+++ b/glance/api/policy.py -@@ -182,9 +182,9 @@ class ImageRepoProxy(glance.domain.proxy.Repo): - 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 @@ class ImageMemberRepoProxy(glance.domain.proxy.Repo): - 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', {}) -diff --git a/glance/api/v1/upload_utils.py b/glance/api/v1/upload_utils.py -index 8a190fc..60c3d3d 100644 ---- a/glance/api/v1/upload_utils.py -+++ b/glance/api/v1/upload_utils.py -@@ -153,12 +153,19 @@ def upload_data_to_store(req, image_meta, image_data, store, notifier): - 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) -diff --git a/glance/api/v2/image_data.py b/glance/api/v2/image_data.py -index 430ffc5..cdfa34b 100644 ---- a/glance/api/v2/image_data.py -+++ b/glance/api/v2/image_data.py -@@ -73,8 +73,8 @@ class ImageDataController(object): - 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 @@ class ImageDataController(object): - 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")) -diff --git a/glance/db/__init__.py b/glance/db/__init__.py -index 05db7f8..483ba21 100644 ---- a/glance/db/__init__.py -+++ b/glance/db/__init__.py -@@ -164,7 +164,7 @@ class ImageRepo(object): - 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 -@@ -172,7 +172,8 @@ class ImageRepo(object): - 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 @@ class ImageMemberRepo(object): - 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, -diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py -index 5a91d34..09c0fb2 100644 ---- a/glance/domain/proxy.py -+++ b/glance/domain/proxy.py -@@ -94,9 +94,9 @@ class Repo(object): - 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): -diff --git a/glance/location.py b/glance/location.py -index f83fa7a..b49546d 100644 ---- a/glance/location.py -+++ b/glance/location.py -@@ -60,8 +60,8 @@ class ImageRepoProxy(glance.domain.proxy.Repo): - 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 - -diff --git a/glance/notifier.py b/glance/notifier.py -index 5ec0854..21223da 100644 ---- a/glance/notifier.py -+++ b/glance/notifier.py -@@ -122,8 +122,8 @@ class ImageRepoProxy(glance.domain.proxy.Repo): - 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)) - -diff --git a/glance/quota/__init__.py b/glance/quota/__init__.py -index 4051992..d628a8c 100644 ---- a/glance/quota/__init__.py -+++ b/glance/quota/__init__.py -@@ -104,10 +104,10 @@ class ImageRepoProxy(glance.domain.proxy.Repo): - 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)) -diff --git a/glance/tests/unit/test_domain_proxy.py b/glance/tests/unit/test_domain_proxy.py -index 1bb6863..2b8f792 100644 ---- a/glance/tests/unit/test_domain_proxy.py -+++ b/glance/tests/unit/test_domain_proxy.py -@@ -74,7 +74,7 @@ class TestProxyRepoPlain(test_utils.BaseTestCase): - 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 @@ class TestProxyRepoWrapping(test_utils.BaseTestCase): - 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 @@ class TestProxyRepoWrapping(test_utils.BaseTestCase): - 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') -diff --git a/glance/tests/unit/test_policy.py b/glance/tests/unit/test_policy.py -index 5b5e870..44546a0 100644 ---- a/glance/tests/unit/test_policy.py -+++ b/glance/tests/unit/test_policy.py -@@ -78,7 +78,7 @@ class MemberRepoStub(object): - 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): -diff --git a/glance/tests/unit/test_quota.py b/glance/tests/unit/test_quota.py -index c12eda2..1c40fb4 100644 ---- a/glance/tests/unit/test_quota.py -+++ b/glance/tests/unit/test_quota.py -@@ -367,7 +367,8 @@ class TestImagePropertyQuotas(test_utils.BaseTestCase): - 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 @@ class TestImagePropertyQuotas(test_utils.BaseTestCase): - 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 @@ class TestImagePropertyQuotas(test_utils.BaseTestCase): - 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 @@ class TestImagePropertyQuotas(test_utils.BaseTestCase): - 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 @@ class TestImagePropertyQuotas(test_utils.BaseTestCase): - 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 @@ class TestImagePropertyQuotas(test_utils.BaseTestCase): - 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']) -diff --git a/glance/tests/unit/test_store_image.py b/glance/tests/unit/test_store_image.py -index 8b334ab..b656454 100644 ---- a/glance/tests/unit/test_store_image.py -+++ b/glance/tests/unit/test_store_image.py -@@ -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 - - -diff --git a/glance/tests/unit/v1/test_api.py b/glance/tests/unit/v1/test_api.py -index 39e9a44..7dc0737 100644 ---- a/glance/tests/unit/v1/test_api.py -+++ b/glance/tests/unit/v1/test_api.py -@@ -39,7 +39,6 @@ from glance.db.sqlalchemy import api as db_api - 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,8 +1734,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): - - 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', -@@ -1744,8 +1742,8 @@ class TestGlanceAPI(base.IsolatedUnitTest): - '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 @@ class TestGlanceAPI(base.IsolatedUnitTest): - 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 @@ class TestGlanceAPI(base.IsolatedUnitTest): - 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 -diff --git a/glance/tests/unit/v2/test_image_data_resource.py b/glance/tests/unit/v2/test_image_data_resource.py -index cc8148a..a121e82 100644 ---- a/glance/tests/unit/v2/test_image_data_resource.py -+++ b/glance/tests/unit/v2/test_image_data_resource.py -@@ -81,7 +81,7 @@ class FakeImageRepo(object): - else: - return self.result - -- def save(self, image): -+ def save(self, image, from_state=None): - self.saved_image = image - - -@@ -184,17 +184,21 @@ class TestImagesController(base.StoreClearingUnitTest): - 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): diff --git a/openstack-glance.spec b/openstack-glance.spec index 117acb6..ece89b1 100644 --- a/openstack-glance.spec +++ b/openstack-glance.spec @@ -1,8 +1,8 @@ %global release_name juno Name: openstack-glance -Version: 2014.2.1 -Release: 3%{?dist} +Version: 2014.2.2 +Release: 1%{?dist} Summary: OpenStack Image Service Group: Applications/System @@ -24,9 +24,6 @@ Patch0001: 0001-Don-t-access-the-net-while-building-docs.patch Patch0002: 0002-Remove-runtime-dep-on-python-pbr.patch Patch0003: 0003-avoid-unsupported-storage-drivers.patch Patch0004: 0004-notify-calling-process-we-are-ready-to-serve.patch -Patch0005: 0005-To-prevent-client-use-v2-patch-api-to-handle-file-an.patch -Patch0006: 0006-Prevent-file-swift-config-and-filesystem-schemes.patch -Patch0007: 0007-Cleanup-chunks-for-deleted-image-that-was-saving.patch BuildArch: noarch BuildRequires: python2-devel @@ -135,9 +132,6 @@ This package contains documentation files for glance. %patch0002 -p1 %patch0003 -p1 %patch0004 -p1 -%patch0005 -p1 -%patch0006 -p1 -%patch0007 -p1 # Remove bundled egg-info rm -rf glance.egg-info @@ -328,6 +322,9 @@ exit 0 %doc doc/build/html %changelog +* Thu Feb 05 2015 Haïkel Guémar - 2014.2.2-1 +- Update to upstream 2014.2.2 + * Thu Jan 29 2015 Haïkel Guémar - 2014.2.1-3 - Usage storage quota bypass - CVE-2014-9623 (RHBZ #1187003) diff --git a/sources b/sources index a1cd9e1..350eeaa 100644 --- a/sources +++ b/sources @@ -1 +1 @@ -7f762bdf8b82bbc1d9a776ecec471ab8 glance-2014.2.1.tar.gz +ff3daf8172212dd14af946442c29ad9b glance-2014.2.2.tar.gz