diff -Nru glance-2014.1.3/AUTHORS glance-2014.1.4/AUTHORS --- glance-2014.1.3/AUTHORS 2014-10-02 23:14:58.000000000 +0000 +++ glance-2014.1.4/AUTHORS 2015-03-13 00:27:11.000000000 +0000 @@ -70,10 +70,12 @@ Fengqian Gao Flaper Fesp Flavio Percoco +Flavio Percoco Florent Flament Gabriel Hurley Gauvain Pocentek Gerardo Porras +Grant Murphy He Yongli Hemanth Makkapati Hengqing Hu @@ -219,6 +221,7 @@ Zhongyue Luo abhishek-kekane amalaba +ankitagrawal annegentle daisy-ycguo eddie-sheffield diff -Nru glance-2014.1.3/ChangeLog glance-2014.1.4/ChangeLog --- glance-2014.1.3/ChangeLog 2014-10-02 23:14:58.000000000 +0000 +++ glance-2014.1.4/ChangeLog 2015-03-13 00:27:11.000000000 +0000 @@ -1,9 +1,23 @@ CHANGES ======= +2014.1.4 +-------- + +* Updated from global requirements +* Cleanup chunks for deleted image that was 'saving' +* Updated from global requirements +* Prevent file, swift+config and filesystem schemes +* To prevent client use v2 patch api to handle file and swift location +* Can not delete images if db deadlock occurs +* Move oslo.vmware higher in requirements.txt +* Bump stable/icehouse next version to 2014.1.4 +* Make rbd store's pool handling more universal + 2014.1.3 -------- +* Do not log password in swift URLs in g-registry * Updated from global requirements * Block sqlalchemy-migrate 0.9.2 * Check on schemes not stores diff -Nru glance-2014.1.3/debian/changelog glance-2014.1.4/debian/changelog --- glance-2014.1.3/debian/changelog 2014-10-06 12:49:36.000000000 +0000 +++ glance-2014.1.4/debian/changelog 2015-03-30 12:55:24.000000000 +0000 @@ -1,3 +1,23 @@ +glance (1:2014.1.4-0ubuntu2) trusty; urgency=medium + + * d/control: Set minimum python-six dependency to 1.5.2 (LP: #1403114). + + -- Corey Bryant Mon, 30 Mar 2015 08:54:37 -0400 + +glance (1:2014.1.4-0ubuntu1) trusty; urgency=medium + + * Resynchronize with stable/icehouse (81ea399) (LP: #1432608): + - [f1260cc] Cleanup chunks for deleted image that was 'saving' + - [7d3a1db] Prevent file, swift+config and filesystem schemes + - [8bdb7ed] To prevent client use v2 patch api to handle file and swift location + - [4b5cb74] Can not delete images if db deadlock occurs + - [ef77c79] Move oslo.vmware higher in requirements.txt + - [312e93e] Make rbd store's pool handling more universal + - [81ea399] Do not log password in swift URLs in g-registry + * d/p/fix-requirements.patch: Rebased + + -- Corey Bryant Mon, 30 Mar 2015 08:53:50 -0400 + glance (1:2014.1.3-0ubuntu1) trusty; urgency=medium [ Corey Bryant ] diff -Nru glance-2014.1.3/debian/control glance-2014.1.4/debian/control --- glance-2014.1.3/debian/control 2014-10-06 12:49:36.000000000 +0000 +++ glance-2014.1.4/debian/control 2015-03-30 12:55:24.000000000 +0000 @@ -33,7 +33,7 @@ python-requests (>= 1.1), python-routes, python-suds, - python-six, + python-six (>= 1.5.2), python-setuptools, python-sphinx, python-sqlalchemy-ext | python-sqlalchemy (>= 0.8.2), @@ -72,7 +72,7 @@ python-paste, python-pastedeploy, python-routes, - python-six, + python-six (>= 1.5.2), python-sqlalchemy-ext | python-sqlalchemy (>= 0.8.2), python-swiftclient (>= 1:1.2), python-webob (>= 1.2.3), diff -Nru glance-2014.1.3/debian/patches/fix-requirements.patch glance-2014.1.4/debian/patches/fix-requirements.patch --- glance-2014.1.3/debian/patches/fix-requirements.patch 2014-10-06 12:49:36.000000000 +0000 +++ glance-2014.1.4/debian/patches/fix-requirements.patch 2015-03-30 12:55:24.000000000 +0000 @@ -1,15 +1,13 @@ Description: Set min version of six. Author: Corey Bryant Forwarded: Not needed. -Index: glance/requirements.txt -=================================================================== --- a/requirements.txt +++ b/requirements.txt -@@ -38,6 +38,6 @@ +@@ -39,6 +39,6 @@ pyOpenSSL>=0.11 # Required by openstack.common libraries -six>=1.6.0 +six>=1.5.2 - oslo.messaging>=1.3.0 + oslo.messaging>=1.3.0,<1.5 diff -Nru glance-2014.1.3/glance/api/authorization.py glance-2014.1.4/glance/api/authorization.py --- glance-2014.1.3/glance/api/authorization.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/api/authorization.py 2015-03-13 00:25:40.000000000 +0000 @@ -147,10 +147,10 @@ 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 -Nru glance-2014.1.3/glance/api/policy.py glance-2014.1.4/glance/api/policy.py --- glance-2014.1.3/glance/api/policy.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/api/policy.py 2015-03-13 00:25:40.000000000 +0000 @@ -182,9 +182,9 @@ 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 @@ 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 -Nru glance-2014.1.3/glance/api/v1/images.py glance-2014.1.4/glance/api/v1/images.py --- glance-2014.1.3/glance/api/v1/images.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/api/v1/images.py 2015-03-13 00:25:40.000000000 +0000 @@ -21,7 +21,6 @@ import eventlet 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 @@ -48,6 +47,7 @@ from glance.store import get_size_from_backend from glance.store import get_store_from_location from glance.store import get_store_from_scheme +from glance.store import validate_external_location LOG = logging.getLogger(__name__) SUPPORTED_PARAMS = glance.api.v1.SUPPORTED_PARAMS @@ -404,23 +404,19 @@ @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. - 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 get_known_schemes() - if scheme != 'file'] - for scheme in schemes: - if pieces.scheme == scheme: - return source - msg = _("External sourcing not supported for store %s") % source - LOG.debug(msg) - raise HTTPBadRequest(explanation=msg, - request=req, - content_type="text/plain") + if 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 -Nru glance-2014.1.3/glance/api/v1/upload_utils.py glance-2014.1.4/glance/api/v1/upload_utils.py --- glance-2014.1.3/glance/api/v1/upload_utils.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/api/v1/upload_utils.py 2015-03-13 00:25:40.000000000 +0000 @@ -146,14 +146,21 @@ 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 diff -Nru glance-2014.1.3/glance/api/v2/image_data.py glance-2014.1.4/glance/api/v2/image_data.py --- glance-2014.1.3/glance/api/v2/image_data.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/api/v2/image_data.py 2015-03-13 00:25:40.000000000 +0000 @@ -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 @@ 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 @@ 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) diff -Nru glance-2014.1.3/glance/db/__init__.py glance-2014.1.4/glance/db/__init__.py --- glance-2014.1.3/glance/db/__init__.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/db/__init__.py 2015-03-13 00:25:40.000000000 +0000 @@ -162,7 +162,7 @@ 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 @@ -170,7 +170,8 @@ 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 @@ 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 -Nru glance-2014.1.3/glance/db/sqlalchemy/api.py glance-2014.1.4/glance/db/sqlalchemy/api.py --- glance-2014.1.3/glance/db/sqlalchemy/api.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/db/sqlalchemy/api.py 2015-03-13 00:25:40.000000000 +0000 @@ -20,6 +20,9 @@ """Defines interface for DB access.""" +import functools +import time + from oslo.config import cfg from six.moves import xrange import sqlalchemy @@ -51,6 +54,24 @@ _FACADE = None +def _retry_on_deadlock(f): + """Decorator to retry a DB API call if Deadlock was received.""" + @functools.wraps(f) + def wrapped(*args, **kwargs): + while True: + try: + return f(*args, **kwargs) + except db_exception.DBDeadlock: + LOG.warn(_("Deadlock detected when running " + "'%(func_name)s': Retrying..."), + dict(func_name=f.__name__)) + # Retry! + time.sleep(0.5) + continue + functools.update_wrapper(wrapped, f) + return wrapped + + def _create_facade_lazily(): global _FACADE if _FACADE is None: @@ -107,6 +128,7 @@ from_state=from_state) +@_retry_on_deadlock def image_destroy(context, image_id): """Destroy the image or raise if it does not exist.""" session = get_session() diff -Nru glance-2014.1.3/glance/domain/proxy.py glance-2014.1.4/glance/domain/proxy.py --- glance-2014.1.3/glance/domain/proxy.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/domain/proxy.py 2015-03-13 00:25:40.000000000 +0000 @@ -94,9 +94,9 @@ 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 -Nru glance-2014.1.3/glance/notifier.py glance-2014.1.4/glance/notifier.py --- glance-2014.1.3/glance/notifier.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/notifier.py 2015-03-13 00:25:40.000000000 +0000 @@ -178,8 +178,8 @@ 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 -Nru glance-2014.1.3/glance/quota/__init__.py glance-2014.1.4/glance/quota/__init__.py --- glance-2014.1.3/glance/quota/__init__.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/quota/__init__.py 2015-03-13 00:25:40.000000000 +0000 @@ -96,9 +96,9 @@ 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) diff -Nru glance-2014.1.3/glance/registry/api/v1/images.py glance-2014.1.4/glance/registry/api/v1/images.py --- glance-2014.1.3/glance/registry/api/v1/images.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/registry/api/v1/images.py 2015-03-13 00:25:40.000000000 +0000 @@ -440,9 +440,10 @@ purge_props = req.headers.get("X-Glance-Registry-Purge-Props", "false") try: - LOG.debug(_("Updating image %(id)s with metadata: " - "%(image_data)r"), {'id': id, - 'image_data': image_data}) + LOG.debug("Updating image %(id)s with metadata: %(image_data)r", + {'id': id, + 'image_data': dict((k, v) for k, v in image_data.items() + if k != 'locations')}) image_data = _normalize_image_location_for_db(image_data) if purge_props == "true": purge_props = True diff -Nru glance-2014.1.3/glance/store/__init__.py glance-2014.1.4/glance/store/__init__.py --- glance-2014.1.3/glance/store/__init__.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/store/__init__.py 2015-03-13 00:25:40.000000000 +0000 @@ -19,6 +19,7 @@ from oslo.config import cfg import six +import six.moves.urllib.parse as urlparse from glance.common import exception from glance.common import utils @@ -75,6 +76,8 @@ 'glance.store.vmware_datastore.Store' ] +RESTRICTED_URI_SCHEMAS = frozenset(['file', 'filesystem', 'swift+config']) + class BackendException(Exception): pass @@ -421,6 +424,25 @@ LOG.debug(_("Skipping store.set_acls... not implemented.")) +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(gm): Use a whitelist of allowed schemes + scheme = urlparse.urlparse(uri).scheme + return (scheme in get_known_schemes() and + scheme not in RESTRICTED_URI_SCHEMAS) + + class ImageRepoProxy(glance.domain.proxy.Repo): def __init__(self, image_repo, context, store_api): @@ -446,29 +468,30 @@ 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 def _check_location_uri(context, store_api, uri): - """ - Check if an image location uri is valid. + """Check if an image location is valid. :param context: Glance request context :param store_api: store API module :param uri: location's uri string """ + is_ok = True try: - size = store_api.get_size_from_backend(context, uri) # NOTE(zhiyan): Some stores return zero when it catch exception - is_ok = size > 0 + is_ok = (store_api.validate_external_location(uri) and + store_api.get_size_from_backend(context, uri) > 0) except (exception.UnknownScheme, exception.NotFound): is_ok = False if not is_ok: - raise exception.BadStoreUri(_('Invalid location: %s') % uri) + reason = _('Invalid location') + raise exception.BadStoreUri(message=reason) def _check_image_location(context, store_api, location): diff -Nru glance-2014.1.3/glance/store/rbd.py glance-2014.1.4/glance/store/rbd.py --- glance-2014.1.3/glance/store/rbd.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/store/rbd.py 2015-03-13 00:25:40.000000000 +0000 @@ -144,9 +144,10 @@ Reads data from an RBD image, one chunk at a time. """ - def __init__(self, name, store): + def __init__(self, pool, name, snapshot, store): + self.pool = pool or store.pool self.name = name - self.pool = store.pool + self.snapshot = snapshot self.user = store.user self.conf_file = store.conf_file self.chunk_size = store.chunk_size @@ -156,7 +157,8 @@ with rados.Rados(conffile=self.conf_file, rados_id=self.user) as conn: with conn.open_ioctx(self.pool) as ioctx: - with rbd.Image(ioctx, self.name) as image: + with rbd.Image(ioctx, self.name, + snapshot=self.snapshot) as image: img_info = image.stat() size = img_info['size'] bytes_left = size @@ -211,7 +213,8 @@ :raises `glance.exception.NotFound` if image does not exist """ loc = location.store_location - return (ImageIterator(loc.image, self), self.get_size(location)) + return (ImageIterator(loc.pool, loc.image, loc.snapshot, self), + self.get_size(location)) def get_size(self, location): """ @@ -223,9 +226,12 @@ :raises `glance.exception.NotFound` if image does not exist """ loc = location.store_location + # if there is a pool specific in the location, use it; otherwise + # we fall back to the default pool specified in the config + target_pool = loc.pool or self.pool with rados.Rados(conffile=self.conf_file, rados_id=self.user) as conn: - with conn.open_ioctx(self.pool) as ioctx: + with conn.open_ioctx(target_pool) as ioctx: try: with rbd.Image(ioctx, loc.image, snapshot=loc.snapshot) as image: @@ -260,7 +266,7 @@ librbd.create(ioctx, image_name, size, order, old_format=True) return StoreLocation({'image': image_name}) - def _delete_image(self, image_name, snapshot_name=None): + def _delete_image(self, target_pool, image_name, snapshot_name=None): """ Delete RBD image and snapshot. @@ -271,7 +277,7 @@ InUseByStore if image is in use or snapshot unprotect failed """ with rados.Rados(conffile=self.conf_file, rados_id=self.user) as conn: - with conn.open_ioctx(self.pool) as ioctx: + with conn.open_ioctx(target_pool) as ioctx: try: # First remove snapshot. if snapshot_name is not None: @@ -387,4 +393,5 @@ InUseByStore if image is in use or snapshot unprotect failed """ loc = location.store_location - self._delete_image(loc.image, loc.snapshot) + target_pool = loc.pool or self.pool + self._delete_image(target_pool, loc.image, loc.snapshot) diff -Nru glance-2014.1.3/glance/tests/functional/v1/test_copy_to_file.py glance-2014.1.4/glance/tests/functional/v1/test_copy_to_file.py --- glance-2014.1.3/glance/tests/functional/v1/test_copy_to_file.py 2014-10-02 23:13:04.000000000 +0000 +++ glance-2014.1.4/glance/tests/functional/v1/test_copy_to_file.py 2015-03-13 00:25:40.000000000 +0000 @@ -248,9 +248,35 @@ path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) http = httplib2.Http() response, content = http.request(path, 'POST', headers=headers) - self.assertEqual(response.status, 400, content) + self.assertEqual(400, response.status, content) - expected = 'External sourcing not supported for store ' + copy_from + expected = 'External source are not supported: \'%s\'' % copy_from + msg = 'expected "%s" in "%s"' % (expected, content) + self.assertTrue(expected in content, msg) + + self.stop_servers() + + @skip_if_disabled + def test_copy_from_swift_config(self): + """ + Ensure we can't copy from swift+config + """ + self.cleanup() + + self.start_servers(**self.__dict__.copy()) + + # POST /images with public image copied from file (to file) + headers = {'X-Image-Meta-Name': 'copied', + 'X-Image-Meta-disk_format': 'raw', + 'X-Image-Meta-container_format': 'ovf', + 'X-Image-Meta-Is-Public': 'True', + 'X-Glance-API-Copy-From': 'swift+config://xxx'} + path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) + http = httplib2.Http() + response, content = http.request(path, 'POST', headers=headers) + self.assertEqual(400, response.status, content) + + expected = 'External source are not supported: \'swift+config://xxx\'' msg = 'expected "%s" in "%s"' % (expected, content) self.assertTrue(expected in content, msg) diff -Nru glance-2014.1.3/glance/tests/functional/v2/test_images.py glance-2014.1.4/glance/tests/functional/v2/test_images.py --- glance-2014.1.3/glance/tests/functional/v2/test_images.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/functional/v2/test_images.py 2015-03-13 00:25:40.000000000 +0000 @@ -15,7 +15,6 @@ import os import signal -import tempfile import uuid import requests @@ -38,6 +37,19 @@ self.cleanup() self.api_server.deployment_flavor = 'noauth' self.start_servers(**self.__dict__.copy()) + for i in range(3): + ret = test_http.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) @@ -282,21 +294,15 @@ 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) @@ -1811,17 +1817,14 @@ 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) @@ -1830,7 +1833,42 @@ response = requests.get(path, headers=headers) self.assertEqual(200, response.status_code) image = jsonutils.loads(response.text) - self.assertEqual(image['size'], 6) + self.assertEqual(10, image['size']) + + 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 TestImageDirectURLVisibility(functional.FunctionalTest): @@ -2040,16 +2078,17 @@ 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 = test_http.http_server("foo_image_id", "foo_image") - self.http_server_pid, self.http_port = ret + for i in range(3): + ret = test_http.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() @@ -2098,14 +2137,14 @@ self.assertTrue('locations' in image) self.assertTrue(image["locations"] == []) - # Update image locations via PATCH + # 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}] @@ -2115,67 +2154,6 @@ # 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'}}] - 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) - - 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'}) response = requests.get(path, headers=headers) self.assertEqual(200, response.status_code) diff -Nru glance-2014.1.3/glance/tests/unit/test_db.py glance-2014.1.4/glance/tests/unit/test_db.py --- glance-2014.1.3/glance/tests/unit/test_db.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/test_db.py 2015-03-13 00:25:40.000000000 +0000 @@ -23,6 +23,7 @@ from glance.common import exception import glance.context import glance.db +from glance.openstack.common.db import exception as db_exception import glance.tests.unit.utils as unit_test_utils import glance.tests.utils as test_utils @@ -679,3 +680,31 @@ self.assertRaises(exception.NotFound, self.task_repo.get_task_and_details, task.task_id) + + +class RetryOnDeadlockTestCase(test_utils.BaseTestCase): + + def test_raise_deadlock(self): + + class TestException(Exception): + pass + + self.attempts = 3 + + def _mock_get_session(): + def _raise_exceptions(): + self.attempts -= 1 + if self.attempts <= 0: + raise TestException("Exit") + raise db_exception.DBDeadlock("Fake Exception") + return _raise_exceptions + + # Test retry on image destroy if db deadlock occurs + with mock.patch.object(glance.db.sqlalchemy.api, + 'get_session') as sess: + sess.side_effect = _mock_get_session() + + try: + glance.db.sqlalchemy.api.image_destroy(None, 'fake-id') + except TestException: + self.assertEqual(sess.call_count, 3) diff -Nru glance-2014.1.3/glance/tests/unit/test_domain_proxy.py glance-2014.1.4/glance/tests/unit/test_domain_proxy.py --- glance-2014.1.3/glance/tests/unit/test_domain_proxy.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/test_domain_proxy.py 2015-03-13 00:25:40.000000000 +0000 @@ -74,7 +74,7 @@ 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 @@ 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 @@ 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 -Nru glance-2014.1.3/glance/tests/unit/test_policy.py glance-2014.1.4/glance/tests/unit/test_policy.py --- glance-2014.1.3/glance/tests/unit/test_policy.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/test_policy.py 2015-03-13 00:25:40.000000000 +0000 @@ -69,7 +69,7 @@ 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 -Nru glance-2014.1.3/glance/tests/unit/test_quota.py glance-2014.1.4/glance/tests/unit/test_quota.py --- glance-2014.1.3/glance/tests/unit/test_quota.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/test_quota.py 2015-03-13 00:25:40.000000000 +0000 @@ -290,7 +290,8 @@ 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 @@ 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) diff -Nru glance-2014.1.3/glance/tests/unit/test_rbd_store.py glance-2014.1.4/glance/tests/unit/test_rbd_store.py --- glance-2014.1.3/glance/tests/unit/test_rbd_store.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/test_rbd_store.py 2015-03-13 00:25:40.000000000 +0000 @@ -37,7 +37,8 @@ self.store.chunk_size = 2 self.called_commands_actual = [] self.called_commands_expected = [] - self.store_specs = {'image': 'fake_image', + self.store_specs = {'pool': 'fake_pool', + 'image': 'fake_image', 'snapshot': 'fake_snapshot'} self.location = StoreLocation(self.store_specs) # Provide enough data to get more than one chunk iteration. @@ -99,7 +100,7 @@ self.called_commands_actual.append('remove') self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) - self.store._delete_image(self.location) + self.store._delete_image('fake_pool', self.location) self.called_commands_expected = ['remove'] def test__delete_image_w_snap(self): @@ -115,7 +116,8 @@ self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) self.stubs.Set(mock_rbd.Image, 'unprotect_snap', _fake_unprotect_snap) self.stubs.Set(mock_rbd.Image, 'remove_snap', _fake_remove_snap) - self.store._delete_image(self.location, snapshot_name='snap') + self.store._delete_image('fake_pool', self.location, + snapshot_name='snap') self.called_commands_expected = ['unprotect_snap', 'remove_snap', 'remove'] @@ -127,7 +129,7 @@ self.stubs.Set(mock_rbd.Image, 'unprotect_snap', _fake_unprotect_snap) self.assertRaises(exception.NotFound, self.store._delete_image, - self.location, snapshot_name='snap') + 'fake_pool', self.location, snapshot_name='snap') self.called_commands_expected = ['unprotect_snap'] @@ -138,7 +140,7 @@ self.stubs.Set(mock_rbd.RBD, 'remove', _fake_remove) self.assertRaises(exception.NotFound, self.store._delete_image, - self.location, snapshot_name='snap') + 'fake_pool', self.location, snapshot_name='snap') self.called_commands_expected = ['remove'] diff -Nru glance-2014.1.3/glance/tests/unit/test_store_image.py glance-2014.1.4/glance/tests/unit/test_store_image.py --- glance-2014.1.3/glance/tests/unit/test_store_image.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/test_store_image.py 2015-03-13 00:25:40.000000000 +0000 @@ -16,6 +16,7 @@ from glance.common import exception import glance.store +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 @@ -33,7 +34,7 @@ def add(self, image): return image - def save(self, image): + def save(self, image, from_state=None): return image @@ -731,7 +732,7 @@ self.assertEqual(acls['read'], [TENANT2]) -class TestImageFactory(utils.BaseTestCase): +class TestImageFactory(unit_test_base.StoreClearingUnitTest): def setUp(self): super(TestImageFactory, self).setUp() diff -Nru glance-2014.1.3/glance/tests/unit/test_store_location.py glance-2014.1.4/glance/tests/unit/test_store_location.py --- glance-2014.1.3/glance/tests/unit/test_store_location.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/test_store_location.py 2015-03-13 00:25:40.000000000 +0000 @@ -24,6 +24,7 @@ import glance.store.swift import glance.store.vmware_datastore from glance.tests.unit import base +from glance.tests.unit import utils class TestStoreLocation(base.StoreClearingUnitTest): @@ -488,11 +489,14 @@ ctx, store) + class FakeImageProxy(object): + size = None + context = None + + def __init__(self, store_api): + self.store_api = store_api + 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(context, uri): return 1 @@ -504,14 +508,34 @@ loc2 = {'url': 'file:///fake2.img.tar.gz', 'metadata': {}} # Test for insert location - image1 = FakeImageProxy() + image1 = TestStoreLocation.FakeImageProxy(mock.Mock()) locations = glance.store.StoreLocations(image1, []) locations.insert(0, loc2) self.assertEqual(image1.size, 1) # Test for set_attr of _locations_proxy - image2 = FakeImageProxy() + image2 = TestStoreLocation.FakeImageProxy(mock.Mock()) locations = glance.store.StoreLocations(image2, [loc1]) locations[0] = loc2 self.assertTrue(loc2 in 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': {}} + loc3 = {'url': 'filesystem:///foo.img.tar.gz', 'metadata': {}} + + # Test for insert location + image1 = TestStoreLocation.FakeImageProxy(utils.FakeStoreAPI()) + locations = glance.store.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(utils.FakeStoreAPI()) + locations = glance.store.StoreLocations(image2, [loc1]) + self.assertRaises(exception.BadStoreUri, locations.insert, 0, loc2) + self.assertNotIn(loc2, locations) diff -Nru glance-2014.1.3/glance/tests/unit/utils.py glance-2014.1.4/glance/tests/unit/utils.py --- glance-2014.1.3/glance/tests/unit/utils.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/utils.py 2015-03-13 00:25:40.000000000 +0000 @@ -14,9 +14,9 @@ # under the License. import urllib -import urlparse from oslo.config import cfg +import six.moves.urllib.parse as urlparse from glance.common import exception from glance.common import wsgi @@ -188,6 +188,12 @@ def check_location_metadata(self, val, key=''): glance.store.check_location_metadata(val) + def validate_external_location(self, uri): + if uri and urlparse.urlparse(uri).scheme: + return glance.store.validate_external_location(uri) + else: + return True + class FakePolicyEnforcer(object): def __init__(self, *_args, **kwargs): diff -Nru glance-2014.1.3/glance/tests/unit/v1/test_api.py glance-2014.1.4/glance/tests/unit/v1/test_api.py --- glance-2014.1.3/glance/tests/unit/v1/test_api.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/v1/test_api.py 2015-03-13 00:25:40.000000000 +0000 @@ -379,7 +379,7 @@ res = req.get_response(self.api) self.assertEqual(res.status_int, 400) - self.assertTrue('External sourcing not supported' in res.body) + self.assertIn('External source are not supported', res.body) def test_create_with_location_bad_store_uri(self): fixture_headers = { @@ -962,6 +962,36 @@ res = req.get_response(self.api) self.assertEqual(res.status_int, 409) + 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', @@ -978,6 +1008,26 @@ 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""" + header_template = {'x-image-meta-store': 'file', + 'x-image-meta-disk-format': 'vhd', + 'x-image-meta-container-format': 'ovf', + 'x-image-meta-name': 'fake image #F'} + + schemas = ["file:///etc/passwd", + "swift+config:///xxx", + "filesystem:///etc/passwd"] + + 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": '@', "add_image": '@'} @@ -1583,8 +1633,7 @@ 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 +1667,18 @@ 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): + data = "somedata" - if args[2].get('status', None) == 'deleted': + def mock_update_image_metadata(*args, **kwargs): - # 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) - - 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 +1688,30 @@ 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 -Nru glance-2014.1.3/glance/tests/unit/v1/test_registry_api.py glance-2014.1.4/glance/tests/unit/v1/test_registry_api.py --- glance-2014.1.3/glance/tests/unit/v1/test_registry_api.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/v1/test_registry_api.py 2015-03-13 00:25:40.000000000 +0000 @@ -18,6 +18,7 @@ import datetime import uuid +import mock from oslo.config import cfg import routes import webob @@ -1305,6 +1306,37 @@ res = self.get_api_response_ext(200, url='/images/%s' % UUID2, body=body, method='PUT', + content_type='json') + + res_dict = jsonutils.loads(res.body) + + self.assertNotEqual(res_dict['image']['created_at'], + res_dict['image']['updated_at']) + + for k, v in fixture.iteritems(): + self.assertEqual(v, res_dict['image'][k]) + + @mock.patch.object(rserver.images.LOG, 'debug') + def test_update_image_not_log_sensitive_info(self, log_debug): + """ + Tests that there is no any sensitive info of image location + was logged in glance during the image update operation. + """ + + def fake_log_debug(fmt_str, image_meta): + self.assertNotIn("'locations'", fmt_str % image_meta) + + fixture = {'name': 'fake public image #2', + 'min_disk': 5, + 'min_ram': 256, + 'disk_format': 'raw', + 'location': 'fake://image'} + body = jsonutils.dumps(dict(image=fixture)) + + log_debug.side_effect = fake_log_debug + + res = self.get_api_response_ext(200, url='/images/%s' % UUID2, + body=body, method='PUT', content_type='json') res_dict = jsonutils.loads(res.body) diff -Nru glance-2014.1.3/glance/tests/unit/v2/test_image_data_resource.py glance-2014.1.4/glance/tests/unit/v2/test_image_data_resource.py --- glance-2014.1.3/glance/tests/unit/v2/test_image_data_resource.py 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/glance/tests/unit/v2/test_image_data_resource.py 2015-03-13 00:25:40.000000000 +0000 @@ -79,7 +79,7 @@ else: return self.result - def save(self, image): + def save(self, image, from_state=None): self.saved_image = image @@ -180,17 +180,21 @@ 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() diff -Nru glance-2014.1.3/glance.egg-info/pbr.json glance-2014.1.4/glance.egg-info/pbr.json --- glance-2014.1.3/glance.egg-info/pbr.json 1970-01-01 00:00:00.000000000 +0000 +++ glance-2014.1.4/glance.egg-info/pbr.json 2015-03-13 00:27:11.000000000 +0000 @@ -0,0 +1 @@ +{"is_release": true, "git_version": "a111215"} \ No newline at end of file diff -Nru glance-2014.1.3/glance.egg-info/PKG-INFO glance-2014.1.4/glance.egg-info/PKG-INFO --- glance-2014.1.3/glance.egg-info/PKG-INFO 2014-10-02 23:14:58.000000000 +0000 +++ glance-2014.1.4/glance.egg-info/PKG-INFO 2015-03-13 00:27:11.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: glance -Version: 2014.1.3 +Version: 2014.1.4 Summary: OpenStack Image Service Home-page: http://www.openstack.org/ Author: OpenStack diff -Nru glance-2014.1.3/glance.egg-info/requires.txt glance-2014.1.4/glance.egg-info/requires.txt --- glance-2014.1.3/glance.egg-info/requires.txt 2014-10-02 23:14:58.000000000 +0000 +++ glance-2014.1.4/glance.egg-info/requires.txt 2015-03-13 00:27:11.000000000 +0000 @@ -1,27 +1,27 @@ pbr>=0.6,<1.0 greenlet>=0.3.2 +oslo.vmware<0.6.0 # Apache-2.0 SQLAlchemy>=0.7.8,!=0.9.5,<=0.9.99 anyjson>=0.3.3 -eventlet>=0.13.0 +eventlet>=0.13.0,<0.16.0 PasteDeploy>=1.5.0 Routes>=1.12.3,!=2.0 WebOb>=1.2.3 argparse -boto>=2.12.0,!=2.13.0 -sqlalchemy-migrate>=0.8.2,!=0.8.4,!=0.9.2 +boto>=2.12.0,!=2.13.0,<2.35.0 +sqlalchemy-migrate>=0.8.2,!=0.8.4,<=0.9.1 httplib2>=0.7.5 kombu>=2.4.8 pycrypto>=2.6 iso8601>=0.1.9 ordereddict -oslo.config>=1.2.0 -stevedore>=0.14 -python-swiftclient>=1.6 -oslo.vmware>=0.2 # Apache-2.0 +oslo.config>=1.2.0,<1.5 +stevedore>=0.14,<1.2 +python-swiftclient>=1.6,<=2.3.1 Paste jsonschema>=2.0.0,<3.0.0 -python-cinderclient>=1.0.6 -python-keystoneclient>=0.7.0 +python-cinderclient>=1.0.6,<=1.1.1 +python-keystoneclient>=0.7.0,<=0.11.2 pyOpenSSL>=0.11 six>=1.6.0 -oslo.messaging>=1.3.0 +oslo.messaging>=1.3.0,<1.5 diff -Nru glance-2014.1.3/glance.egg-info/SOURCES.txt glance-2014.1.4/glance.egg-info/SOURCES.txt --- glance-2014.1.3/glance.egg-info/SOURCES.txt 2014-10-02 23:14:58.000000000 +0000 +++ glance-2014.1.4/glance.egg-info/SOURCES.txt 2015-03-13 00:27:12.000000000 +0000 @@ -71,6 +71,7 @@ glance.egg-info/dependency_links.txt glance.egg-info/entry_points.txt glance.egg-info/not-zip-safe +glance.egg-info/pbr.json glance.egg-info/requires.txt glance.egg-info/top_level.txt glance/api/__init__.py diff -Nru glance-2014.1.3/PKG-INFO glance-2014.1.4/PKG-INFO --- glance-2014.1.3/PKG-INFO 2014-10-02 23:14:58.000000000 +0000 +++ glance-2014.1.4/PKG-INFO 2015-03-13 00:27:12.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: glance -Version: 2014.1.3 +Version: 2014.1.4 Summary: OpenStack Image Service Home-page: http://www.openstack.org/ Author: OpenStack diff -Nru glance-2014.1.3/requirements.txt glance-2014.1.4/requirements.txt --- glance-2014.1.3/requirements.txt 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/requirements.txt 2015-03-13 00:25:40.000000000 +0000 @@ -5,39 +5,40 @@ # package to get the right headers... greenlet>=0.3.2 +# For VMware storage backed. Listed early to ensure its transitive eventlet +# dependency on eventlet is satisifed. +oslo.vmware<0.6.0 # Apache-2.0 + # < 0.8.0/0.8 does not work, see https://bugs.launchpad.net/bugs/1153983 SQLAlchemy>=0.7.8,!=0.9.5,<=0.9.99 anyjson>=0.3.3 -eventlet>=0.13.0 +eventlet>=0.13.0,<0.16.0 PasteDeploy>=1.5.0 Routes>=1.12.3,!=2.0 WebOb>=1.2.3 argparse -boto>=2.12.0,!=2.13.0 -sqlalchemy-migrate>=0.8.2,!=0.8.4,!=0.9.2 +boto>=2.12.0,!=2.13.0,<2.35.0 +sqlalchemy-migrate>=0.8.2,!=0.8.4,<=0.9.1 httplib2>=0.7.5 kombu>=2.4.8 pycrypto>=2.6 iso8601>=0.1.9 ordereddict -oslo.config>=1.2.0 -stevedore>=0.14 +oslo.config>=1.2.0,<1.5 +stevedore>=0.14,<1.2 # For Swift storage backend. -python-swiftclient>=1.6 - -# For VMware storage backed. -oslo.vmware>=0.2 # Apache-2.0 +python-swiftclient>=1.6,<=2.3.1 # For paste.util.template used in keystone.common.template Paste jsonschema>=2.0.0,<3.0.0 -python-cinderclient>=1.0.6 -python-keystoneclient>=0.7.0 +python-cinderclient>=1.0.6,<=1.1.1 +python-keystoneclient>=0.7.0,<=0.11.2 pyOpenSSL>=0.11 # Required by openstack.common libraries six>=1.6.0 -oslo.messaging>=1.3.0 +oslo.messaging>=1.3.0,<1.5 diff -Nru glance-2014.1.3/setup.cfg glance-2014.1.4/setup.cfg --- glance-2014.1.3/setup.cfg 2014-10-02 23:14:58.000000000 +0000 +++ glance-2014.1.4/setup.cfg 2015-03-13 00:27:12.000000000 +0000 @@ -1,6 +1,6 @@ [metadata] name = glance -version = 2014.1.3 +version = 2014.1.4 summary = OpenStack Image Service description-file = README.rst diff -Nru glance-2014.1.3/test-requirements.txt glance-2014.1.4/test-requirements.txt --- glance-2014.1.3/test-requirements.txt 2014-10-02 23:13:07.000000000 +0000 +++ glance-2014.1.4/test-requirements.txt 2015-03-13 00:25:40.000000000 +0000 @@ -13,7 +13,7 @@ sphinx>=1.1.2,<1.1.999 requests>=1.1 testrepository>=0.0.18 -testtools>=0.9.34 +testtools>=0.9.34,!=1.2.0,!=1.4.0 psutil>=1.1.1 # Optional packages that should be installed when testing