diff -Nru glance-2012.2.1/AUTHORS glance-2012.2.4/AUTHORS --- glance-2012.2.1/AUTHORS 2012-12-03 22:28:21.000000000 +0000 +++ glance-2012.2.4/AUTHORS 2013-04-11 19:11:56.000000000 +0000 @@ -19,6 +19,7 @@ Clark Boylan Cory Wright Dan Prince +David Ripton Dean Troyer Derek Higgins Donal Lafferty @@ -27,6 +28,7 @@ Eldar Nugaev Eoghan Glynn Ewan Mellor +Flaper Fesp Gabriel Hurley Gerardo Porras Hengqing Hu diff -Nru glance-2012.2.1/ChangeLog glance-2012.2.4/ChangeLog --- glance-2012.2.1/ChangeLog 2012-12-03 22:28:21.000000000 +0000 +++ glance-2012.2.4/ChangeLog 2013-04-11 19:11:56.000000000 +0000 @@ -1,3 +1,391 @@ +commit 3389f067d5a63ef21c5508645d789e6550515e73 +Merge: 5b4d21d cfaa2d8 +Author: Jenkins +Date: Wed Mar 27 13:46:37 2013 +0000 + + Merge "Ensure repeated member deletion fails with 404" into stable/folsom + +commit 5b4d21d8f97c2c83222893b3b585f80d2108e8bf +Author: Flaper Fesp +Date: Thu Mar 21 10:05:05 2013 +0100 + + Check if creds are present and not None + + Currently, KeystoneStrategy checks whether the required credentials are + present in the creds dict but it doesn't check whether they are valid or + not. + + This patch checks whether the required creds are present and not None + otherwise a MissingCredentials exception will be raised. + + Note: No need for checking parameters' values types since they'll be + instances of basestring once set otherwise they'll be None. + + Fixes bug 1157765 + + Change-Id: I664a604c3cbf2fca60a88c4d887cd9a4b678c8a5 + + glance/common/auth.py | 4 ++-- + glance/tests/unit/test_auth.py | 13 ++++++++++++- + 2 files changed, 14 insertions(+), 3 deletions(-) + +commit cfaa2d86dd361a2f98c8a834f232f076ec3df774 +Author: Eoghan Glynn +Date: Tue Mar 19 21:13:58 2013 +0000 + + Ensure repeated member deletion fails with 404 + + Fixes bug 1157427 + + If an image membership is deleted more than once, the second and + subsequent deletions would be expected to return a 404 Not Found + response code, as opposed to 204 No Content. + + Previously, the can_show_deleted logic got in the way, such that + for an admin caller the previously deleted membership was considered + still extant. + + We now override the show_deleted logic for deletion, as it makes no + sense to consider the existence of previously deleted database rows + when another deletion request is issued. + + Note that there is no corresponding proposal to master as this logic + has been refactored and this no longer affects master. + + Change-Id: If8200bbb74e46c814c9f8fa6b4c636e7b022ab6c + + glance/db/sqlalchemy/api.py | 8 ++++++-- + glance/registry/api/v1/members.py | 3 ++- + glance/tests/unit/v1/test_api.py | 22 ++++++++++++++++++++++ + 3 files changed, 30 insertions(+), 3 deletions(-) + +commit dd849a9be540bedd4fd904cc0b86ccd9c3e34af2 +Author: Stuart McLaren +Date: Thu Mar 14 13:43:36 2013 +0000 + + Do not return location in headers + + In some cases credentials were being leaked when downloading a cached + v1 image. + + Fixes bug 1135541, CVE-2013-1840 + + Change-Id: I3ec0a8f484fe1bdc32c3c56fce810fcef347a7f6 + + glance/api/middleware/cache.py | 3 +++ + 1 file changed, 3 insertions(+) + +commit 04f88c8d563ef1330d19679e8305ac6e107228f6 +Author: Unmesh Gurjar +Date: Tue Oct 9 03:06:00 2012 -0700 + + Fixes deletion of invalid image member + + This fixes the 500 error on deleting an invalid/non-member tenant of an image. + + Fixes LP: #1060868 + + Change-Id: I5a2dc56690d7525127be1a8843004d075a3fe5bb + + glance/registry/api/v1/members.py | 19 +++++++++++-------- + glance/tests/unit/v1/test_api.py | 13 +++++++++++++ + 2 files changed, 24 insertions(+), 8 deletions(-) + +commit 90ddf72d2d01967269f8673fcf170b511ebfb11a +Merge: 9e88df1 5597697 +Author: Jenkins +Date: Thu Mar 7 17:57:08 2013 +0000 + + Merge "Wait in TestBinGlance.test_update_copying_from until image is active" into stable/folsom + +commit 9e88df1c9209204c8417064655b3b89db50ccc27 +Merge: 1fb759d 5183360 +Author: Jenkins +Date: Thu Mar 7 17:57:04 2013 +0000 + + Merge "Clean dangling image fragments in filesystem store" into stable/folsom + +commit 1fb759d3d2b20b6c04bd3d2c76aa6c9547a1f360 +Merge: f5c0222 03dc862 +Author: Jenkins +Date: Wed Mar 6 03:14:11 2013 +0000 + + Merge "Avoid dangling partial image on size/checksum mismatch" into stable/folsom + +commit f5c0222f7c36074d4b96a1d3bdd0e7960420d89d +Merge: afe6166 12d28c3 +Author: Jenkins +Date: Wed Mar 6 02:25:13 2013 +0000 + + Merge "Swallow UserWarning from glance-cache-manage" into stable/folsom + +commit afe61664ac5f933622e349da1c0a92d134a81230 +Author: Flaper Fesp +Date: Mon Jan 21 11:00:58 2013 +0100 + + Prints list-cached dates in isoformat + + Converts dates printed by list-cached to a human readable format + (isoformat). It now checks whether the image last_access time is == 0.0 + and prints "Not Accessed Yet" if so. + + * Updates openstack.common.timeutils + * Fixes bug 1102334 + + Change-Id: I6f60525d8419d45e6962b936a2661825b606cba2 + + bin/glance-cache-manage | 14 ++++++- + glance/openstack/common/timeutils.py | 42 ++++++++++++++++++-- + .../functional/v1/test_bin_glance_cache_manage.py | 13 ++++++ + 3 files changed, 63 insertions(+), 6 deletions(-) + +commit ee1356006e610c04f31299c383941377b4809769 +Author: Brian Waldon +Date: Tue Feb 26 10:47:02 2013 -0800 + + Fix broken JSON schemas in v2 tests + + * The required attribute is not allowed in draft 4 of JSON schema + * The dependency on jsonschema can be relaxed back to all versions + + Change-Id: Id0767cfe2fe46d73999563896f851ffb01aa9489 + + glance/tests/unit/v2/test_images_resource.py | 2 -- + 1 file changed, 2 deletions(-) + +commit 55976974cc5e10ccc3ea736b869aaf2dbd390024 +Author: Sascha Peilicke +Date: Tue Jan 8 13:47:31 2013 +0100 + + Wait in TestBinGlance.test_update_copying_from until image is active + + Test randomly fails on slow machines, the updated image is still + in 'Status: saving' while 'Status: active' is expected. So loop around + the "glance show" command until the image leaves the 'saving' state + (bug 1107768). + + Change-Id: I908069b35079dcc8ccd25acb3ebc74fe43f9d524 + + glance/tests/functional/test_bin_glance.py | 18 +++++++++++++----- + 1 file changed, 13 insertions(+), 5 deletions(-) + +commit 12d28c36983ee066a1b62fc66f9fc396a1405fa7 +Author: Eoghan Glynn +Date: Tue Feb 19 11:36:38 2013 +0000 + + Swallow UserWarning from glance-cache-manage + + Fixes bug 1129445 + + Previously, we exposed internal technical debt as an apparent + user-facing warning. + + Now the warning is swallowed in the internal case, but continues + to be seen by external users of this library. + + Note that this fix is not relevant on master, as that the warning + no longer occurs in that case. + + Change-Id: I30a835e0024acac8841b4746968abe7cae9e14cb + + bin/glance-cache-manage | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +commit 5183360f4c308131adde13535af0f11ccd3b1462 +Author: Eoghan Glynn +Date: Fri Jan 25 13:18:58 2013 +0000 + + Clean dangling image fragments in filesystem store + + Fixes bug LP 1104924 + + Previously when a PUT or POST of image content was terminated + prematurely by the client, the partially saved image file was + left behind in the data directory. + + Change-Id: Id601816735e4138cd7623dad4d90be67448292c8 + + glance/store/filesystem.py | 28 ++++++++----- + glance/tests/unit/test_filesystem_store.py | 63 +++++++++++++++++++++------- + 2 files changed, 66 insertions(+), 25 deletions(-) + +commit 03dc862281feb2124368bcaa4fa766ba0ce99a14 +Author: Eoghan Glynn +Date: Mon Feb 11 18:43:18 2013 +0000 + + Avoid dangling partial image on size/checksum mismatch + + Fixes bug 1122299 + + Previously, when the supplied image size or checksum didn't match + the calculated values, partial image image data was left dangling + in the backend store. + + Now, we clean up when the mismatch is detected. + + Change-Id: I916d78ef3b2065e87df31ec4fb405915417d68aa + + glance/api/v1/images.py | 14 +++++++++----- + glance/tests/functional/v1/test_api.py | 29 +++++++++++++++++++++++++---- + 2 files changed, 34 insertions(+), 9 deletions(-) + +commit 98d9928aaf6d437182cea63e3f1c6c48d08015dc +Author: Mark McLoughlin +Date: Thu Jan 31 21:32:51 2013 +0000 + + Bump version to 2012.2.4 + + Change-Id: I9dab1dc62e41cdb4143a3c8083e65c4b13a22eb4 + + glance/version.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit a5b0f4eb81a1f5d8e89713ff7b3ccc6155762628 +Merge: 0e4e7a7 96a470b +Author: Jenkins +Date: Tue Jan 29 15:57:58 2013 +0000 + + Merge "Remove Swift location/password from messages." into stable/folsom + +commit 0e4e7a7312cf9b49f4e8e200341e65e2394990ac +Merge: fd04efb 4c96080 +Author: Jenkins +Date: Fri Jan 25 17:55:20 2013 +0000 + + Merge "Change useexisting to extend_existing to fix deprecation warnings." into stable/folsom + +commit 4c96080375553f5ffaaaa4b1470160464bcfb910 +Author: David Ripton +Date: Mon Oct 8 15:24:45 2012 -0400 + + Change useexisting to extend_existing to fix deprecation warnings. + + This squelches a deprecation warning during installation. + + We're already using extend_existing in other places, so I don't + think this causes any new version compatibility issues. + + Fixes bug 925609. (Already marked fixed, but this hits more cases.) + + Includes some merges with a whitespace-cleanup fix. + + Change-Id: Ia166e9184ed3e13753c5669a1006a3711738319a + + glance/db/sqlalchemy/migrate_repo/versions/003_add_disk_format.py | 2 +- + glance/db/sqlalchemy/migrate_repo/versions/005_size_big_integer.py | 2 +- + glance/db/sqlalchemy/migrate_repo/versions/006_key_to_name.py | 2 +- + 3 files changed, 3 insertions(+), 3 deletions(-) + +commit 96a470be64adcef97f235ca96ed3c59ed954a4c1 +Author: Dan Prince +Date: Sat Jan 12 15:38:09 2013 -0500 + + Remove Swift location/password from messages. + + Updates several exceptions and log messages in the Swift backend + so that they don't include Swift location URI's which may contain + passwords when used in Swift single tenant mode. + + Fixes LP Bug #1098962 (for Folsom). + + Change-Id: Ia97a95ce6ed5d98a76515eea8817e309bcf0889a + + glance/store/swift.py | 21 ++++++++++----------- + 1 file changed, 10 insertions(+), 11 deletions(-) + +commit fd04efb679a4521a2105ecb6645e40a60dfa3aeb +Merge: 3c56950 bca6e26 +Author: Jenkins +Date: Wed Jan 16 17:41:03 2013 +0000 + + Merge "wsgi.Middleware forward-compatibility with webob 1.2b1 or later" into stable/folsom + +commit 3c569509c999d377ca3ff470d03e0b294850508c +Merge: 514b4b4 5e5e722 +Author: Jenkins +Date: Thu Jan 10 01:12:44 2013 +0000 + + Merge "Verify size in addition to checksum of uploaded image" into stable/folsom + +commit 514b4b49fdba873518f8736b280d6691f34d3426 +Author: Eoghan Glynn +Date: Thu Jan 3 14:11:45 2013 +0000 + + Log error on failure to load paste deploy app. + + Fixes bug 1091294 + + Avoids possible silent failure of service launch when say a + dependency such as keystone is missing (but required by the + configured paste_deploy flavor). + + Change-Id: I9a63d24bcf0a93277829d24073268210d2c063d3 + + glance/common/config.py | 8 +++++--- + 1 file changed, 5 insertions(+), 3 deletions(-) + +commit 5e5e722d353e0670d2aa06cbac8e138617c10806 +Author: Eoghan Glynn +Date: Thu Dec 20 15:33:56 2012 +0000 + + Verify size in addition to checksum of uploaded image + + Fixes bug 1092584 + + Previously only the supplied checksum was verified against the actual + checksum calculated by the backend store, with the image being killed + on mismatch. + + Now we also similarly verify the supplied image size, if provided. + + Change-Id: I87fa3ff77715111f1095f3ebe64cd699776ec27e + + glance/api/v1/images.py | 28 ++++++++++++++++------------ + glance/tests/functional/v1/test_api.py | 19 +++++++++++++++++++ + glance/tests/unit/v1/test_api.py | 24 +++++++++++++++++++++--- + 3 files changed, 56 insertions(+), 15 deletions(-) + +commit 35260a7f0b9336d65aac9ffb0aa4b099a35a54aa +Author: Mark McLoughlin +Date: Thu Nov 29 21:28:17 2012 +0000 + + Bump next version to 2012.2.3 + + 2012.2.2 has been released without Glance, so prepare for 2012.2.3. + + Change-Id: I3a6221b579f612418ffce5fd9ba89720699f2e06 + + glance/version.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit bca6e2661f44b4d27cba7431903ee1ce67205372 +Author: Sascha Peilicke +Date: Mon Dec 10 19:20:26 2012 +0100 + + wsgi.Middleware forward-compatibility with webob 1.2b1 or later + + Response.request is None by default + (http://docs.webob.org/en/latest/news.html#b1), but is used in the + CacheFilter WSGI middleware. + + Backport of https://review.openstack.org/#/c/17794/ + + Change-Id: I28f5ca92fe517f4f56af934799db32650e079ba7 + + glance/common/wsgi.py | 1 + + 1 file changed, 1 insertion(+) + +commit 199783cec5d42203740c8fe2272b7037315ce941 +Author: Mark McLoughlin +Date: Thu Nov 29 21:28:17 2012 +0000 + + Bump next version to 2012.2.2 + + Change-Id: I3375a323b0c9fb3f9b6350b9a6163a08beb0f083 + + glance/version.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + commit a4062940b804f524ada38df3c62c14b9c98f82bc Merge: 91aaa48 49408e9 Author: Jenkins diff -Nru glance-2012.2.1/PKG-INFO glance-2012.2.4/PKG-INFO --- glance-2012.2.1/PKG-INFO 2012-12-03 22:28:21.000000000 +0000 +++ glance-2012.2.4/PKG-INFO 2013-04-11 19:11:56.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: glance -Version: 2012.2.1 +Version: 2012.2.4 Summary: The Glance project provides services for discovering, registering, and retrieving virtual machine images Home-page: http://glance.openstack.org/ Author: OpenStack diff -Nru glance-2012.2.1/bin/glance-cache-manage glance-2012.2.4/bin/glance-cache-manage --- glance-2012.2.1/bin/glance-cache-manage 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/bin/glance-cache-manage 2013-04-11 19:09:50.000000000 +0000 @@ -26,6 +26,7 @@ import os import sys import time +import warnings # If ../glance/__init__.py exists, add ../ to Python search path, so that # it will override what happens to be installed in /usr/(local/)lib/python... @@ -37,9 +38,13 @@ gettext.install('glance', unicode=1) -from glance import client as glance_client +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + from glance import client as glance_client + from glance.common import exception from glance.common import utils +from glance.openstack.common import timeutils from glance.version import version_info as version @@ -102,10 +107,19 @@ print pretty_table.make_header() for image in images: + last_modified = image['last_modified'] + last_modified = timeutils.iso8601_from_timestamp(last_modified) + + last_accessed = image['last_accessed'] + if last_accessed == 0: + last_accessed = "N/A" + else: + last_accessed = timeutils.iso8601_from_timestamp(last_accessed) + print pretty_table.make_row( image['image_id'], - image['last_accessed'], - image['last_modified'], + last_accessed, + last_modified, image['size'], image['hits']) diff -Nru glance-2012.2.1/debian/changelog glance-2012.2.4/debian/changelog --- glance-2012.2.1/debian/changelog 2013-03-13 20:41:17.000000000 +0000 +++ glance-2012.2.4/debian/changelog 2013-10-22 18:42:45.000000000 +0000 @@ -1,3 +1,67 @@ +glance (2012.2.4-0ubuntu1.1) quantal-security; urgency=low + + * SECURITY UPDATE: enforce 'download_image' policy in cache middleware + - debian/patches/CVE-2013-4428.patch: fix confusing behavior when using + download_image. Ie, return 403 rather than empty content (LP: #1235378) + - CVE-2013-4428 + + -- Jamie Strandboge Tue, 22 Oct 2013 13:42:27 -0500 + +glance (2012.2.4-0ubuntu1) quantal-proposed; urgency=low + + [ Adam Gandelman ] + * Dropped patches, applied upstream: + - debian/patches/CVE-2013-1840.patch: [dd849a9] + * Resynchronize with stable/folsom (dbd3d3d7) (LP: #1179707): + - [cfaa2d8] repeated deletion on image member does not result in 404 + LP: 1157427 + - [5b4d21d] glance-cache-prefetcher explodes when no auth parameters were + configured LP: 1157765 + - [dd849a9] v1 api returns location as header for cached images LP: 1135541 + - [04f88c8] 500 error returned when an Admin tries to delete membership of + image from a non-existent /invalid tenant LP: 1060868 + - [5597697] Fragile Test: + glance.tests.functional.test_bin_glance:TestBinGlance.test_update_copying_from + LP: 1107768 + - [5183360] filesystem store does not clean up after premature termination + of image upload LP: 1104924 + - [03dc862] mismatched image size or checksum leaves behind dangling image + data LP: 1122299 + - [12d28c3] UserWarning on deprecation of legacy glance client inappropriate + for internal usage LP: 1129445 + - [afe6166] 'glance-cache-manage list-cached' does not show 'last accessed' + and 'last modified' fields in human-readable format' LP: 1102334 + - [ee13560] Fix broken JSON schemas in v2 tests + + [ Chuck Short ] + * debian/patches/disable-swift-tests.patch: Refreshed. + + -- Adam Gandelman Thu, 25 Apr 2013 17:39:57 -0400 + +glance (2012.2.3-0ubuntu2) quantal-proposed; urgency=low + + * Resync with latest security update. + * SECURITY UPDATE: fix information disclosure via Glance v1 API + - debian/patches/CVE-2013-1840.patch: adjust api/middleware/cache.py to + not show image_meta['location'] + - CVE-2013-1840 + + -- James Page Fri, 22 Mar 2013 11:48:52 +0000 + +glance (2012.2.3-0ubuntu1) quantal-proposed; urgency=low + + * Dropped patches, applied upstream: + - debian/patches/CVE-2013-0212.patch: [96a470b] + * Resynchronize with stable/folsom (98d9928a) (LP: #1116671): + - [96a470b] glance image-download can display backend Swift password + - [4c96080] install throws errors about SADeprecationWarning LP: 925609 + - [bca6e26] wsgi.Middleware forward-compatibility with webob 1.2b1 or later + - [5e5e722] Supplied image size should be verified against actual size + LP: 1092584 + - [514b4b4] silent failure when loading the paste deploy app LP: 1091294 + + -- Adam Gandelman Tue, 05 Feb 2013 14:02:33 -0400 + glance (2012.2.1-0ubuntu1.2) quantal-security; urgency=low * SECURITY UPDATE: fix information disclosure via Glance v1 API diff -Nru glance-2012.2.1/debian/patches/CVE-2013-0212.patch glance-2012.2.4/debian/patches/CVE-2013-0212.patch --- glance-2012.2.1/debian/patches/CVE-2013-0212.patch 2013-01-29 15:12:40.000000000 +0000 +++ glance-2012.2.4/debian/patches/CVE-2013-0212.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,78 +0,0 @@ -Origin: supplied by upstream via pre-disclosure -Description: information leak via error message when using swift -Bug: https://bugs.launchpad.net/bugs/1098962 - -Index: glance-2012.2/glance/store/swift.py -=================================================================== ---- glance-2012.2.orig/glance/store/swift.py 2013-01-28 13:38:04.000000000 -0600 -+++ glance-2012.2/glance/store/swift.py 2013-01-28 13:46:26.000000000 -0600 -@@ -136,7 +136,7 @@ - "like so: " - "swift+http://user:pass@authurl.com/v1/container/obj" - ) -- LOG.error(_("Invalid store uri %(uri)s: %(reason)s") % locals()) -+ LOG.error(_("Invalid store URI: %(reason)s") % locals()) - raise exception.BadStoreUri(message=reason) - - pieces = urlparse.urlparse(uri) -@@ -162,8 +162,7 @@ - if creds: - cred_parts = creds.split(':') - if len(cred_parts) != 2: -- reason = (_("Badly formed credentials '%(creds)s' in Swift " -- "URI") % locals()) -+ reason = (_("Badly formed credentials in Swift URI.")) - LOG.error(reason) - raise exception.BadStoreUri() - user, key = cred_parts -@@ -181,7 +180,7 @@ - path_parts.insert(0, netloc) - self.auth_or_store_url = '/'.join(path_parts) - except IndexError: -- reason = _("Badly formed Swift URI: %s") % uri -+ reason = _("Badly formed Swift URI.") - LOG.error(reason) - raise exception.BadStoreUri() - -@@ -293,8 +292,8 @@ - except swiftclient.ClientException, e: - if e.http_status == httplib.NOT_FOUND: - uri = location.get_store_uri() -- raise exception.NotFound(_("Swift could not find image at " -- "uri %(uri)s") % locals()) -+ msg = _("Swift could not find image at URI.") -+ raise exception.NotFound(msg) - else: - raise - -@@ -543,7 +542,7 @@ - except swiftclient.ClientException, e: - if e.http_status == httplib.CONFLICT: - raise exception.Duplicate(_("Swift already has an image at " -- "location %s") % location.get_uri()) -+ "this location.")) - msg = (_("Failed to add object to Swift.\n" - "Got error from Swift: %(e)s") % locals()) - LOG.error(msg) -@@ -596,8 +595,8 @@ - except swiftclient.ClientException, e: - if e.http_status == httplib.NOT_FOUND: - uri = location.get_store_uri() -- raise exception.NotFound(_("Swift could not find image at " -- "uri %(uri)s") % locals()) -+ msg = _("Swift could not find image at URI.") -+ raise exception.NotFound(msg) - else: - raise - -@@ -637,8 +636,8 @@ - except swiftclient.ClientException, e: - if e.http_status == httplib.NOT_FOUND: - uri = location.get_store_uri() -- raise exception.NotFound(_("Swift could not find image at " -- "uri %(uri)s") % locals()) -+ msg = _("Swift could not find image at URI.") -+ raise exception.NotFound(msg) - else: - raise - diff -Nru glance-2012.2.1/debian/patches/CVE-2013-1840.patch glance-2012.2.4/debian/patches/CVE-2013-1840.patch --- glance-2012.2.1/debian/patches/CVE-2013-1840.patch 2013-03-13 20:38:35.000000000 +0000 +++ glance-2012.2.4/debian/patches/CVE-2013-1840.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,22 +0,0 @@ -commit 13eecc3e19e9528d52e71f57bd15b2cf6e0c1af2 -Author: Stuart McLaren -Date: Thu Mar 7 17:11:35 2013 +0000 - - folsom patch - - Change-Id: Ib0dbef4ce5d1aa5303f63d0aca635eee49e40284 - -diff --git a/glance/api/middleware/cache.py b/glance/api/middleware/cache.py -index 8e24ef0..dcd59b6 100644 ---- a/glance/api/middleware/cache.py -+++ b/glance/api/middleware/cache.py -@@ -111,6 +111,9 @@ class CacheFilter(wsgi.Middleware): - - def _process_v1_request(self, request, image_id, image_iterator): - image_meta = registry.get_image_metadata(request.context, image_id) -+ # Don't display location -+ if 'location' in image_meta: -+ del image_meta['location'] - - if not image_meta['size']: - # override image size metadata with the actual cached diff -Nru glance-2012.2.1/debian/patches/CVE-2013-4428.patch glance-2012.2.4/debian/patches/CVE-2013-4428.patch --- glance-2012.2.1/debian/patches/CVE-2013-4428.patch 1970-01-01 00:00:00.000000000 +0000 +++ glance-2012.2.4/debian/patches/CVE-2013-4428.patch 2013-10-22 18:42:08.000000000 +0000 @@ -0,0 +1,350 @@ +From: Zhi Yan Liu +Date: Mon, 7 Oct 2013 03:44:33 +0000 (+0800) +Subject: Adding 'download_image' policy enforcement to image cache middleware +X-Git-Url: https://review.openstack.org/gitweb?p=openstack%2Fglance.git;a=commitdiff_plain;h=feb735412021b771d4fe8b5706506abe6677899b + +Adding 'download_image' policy enforcement to image cache middleware + +Currently image cache middleware not care 'download_image' policy, the +enforcement caused user receive empty content but with HTTP 200 code +rather than 403 when client attempt to download image using v2 API. And +the real Forbidden exception be logged in glance-api log which image +application action raised. The end user is confused by this behavior. + +Fixes bug: 1235378 + +Related-Id: Ibaa7ccf8613ee3cce4cb6a72e3206a2c94122222 +Change-Id: I6ce09c764436da52ed0a5219c33ae0fb542dd3f8 +Signed-off-by: Zhi Yan Liu +(cherry picked from commit a50bfbf490fd354d08abd25b67aaab83b2a17a85) +--- + +diff --git a/glance/api/middleware/cache.py b/glance/api/middleware/cache.py +index dcd59b6..d4ea0ba 100644 +--- a/glance/api/middleware/cache.py ++++ b/glance/api/middleware/cache.py +@@ -28,6 +28,7 @@ import re + + import webob + ++from glance.api import policy + from glance.api.v1 import images + from glance.common import exception + from glance.common import utils +@@ -52,6 +53,7 @@ class CacheFilter(wsgi.Middleware): + def __init__(self, app): + self.cache = image_cache.ImageCache() + self.serializer = images.ImageSerializer() ++ self.policy = policy.Enforcer() + LOG.info(_("Initialized image cache middleware")) + super(CacheFilter, self).__init__(app) + +@@ -75,6 +77,13 @@ class CacheFilter(wsgi.Middleware): + else: + return (version, method, image_id) + ++ def _enforce(self, req, action): ++ """Authorize an action against our policies""" ++ try: ++ self.policy.enforce(req.context, action, {}) ++ except exception.Forbidden as e: ++ raise webob.exc.HTTPForbidden(explanation=unicode(e), request=req) ++ + def process_request(self, request): + """ + For requests for an image file, we check the local image +@@ -97,6 +106,11 @@ class CacheFilter(wsgi.Middleware): + if request.method != 'GET' or not self.cache.is_cached(image_id): + return None + ++ try: ++ self._enforce(request, 'download_image') ++ except webob.exc.HTTPForbidden: ++ return None ++ + LOG.debug(_("Cache hit for image '%s'"), image_id) + image_iterator = self.get_from_cache(image_id) + method = getattr(self, '_process_%s_request' % version) +@@ -176,6 +190,13 @@ class CacheFilter(wsgi.Middleware): + if not image_checksum: + LOG.error(_("Checksum header is missing.")) + ++ # NOTE(zhiyan): image_cache return a generator object and set to ++ # response.app_iter, it will be called by eventlet.wsgi later. ++ # So we need enforce policy firstly but do it by application ++ # since eventlet.wsgi could not catch webob.exc.HTTPForbidden and ++ # return 403 error to client then. ++ self._enforce(resp.request, 'download_image') ++ + resp.app_iter = self.cache.get_caching_iter(image_id, image_checksum, + resp.app_iter) + return resp +diff --git a/glance/common/wsgi.py b/glance/common/wsgi.py +index d324861..9e38aba 100644 +--- a/glance/common/wsgi.py ++++ b/glance/common/wsgi.py +@@ -325,7 +325,10 @@ class Middleware(object): + return response + response = req.get_response(self.application) + response.request = req +- return self.process_response(response) ++ try: ++ return self.process_response(response) ++ except webob.exc.HTTPException as e: ++ return e + + + class Debug(Middleware): +diff --git a/glance/tests/functional/test_cache_middleware.py b/glance/tests/functional/test_cache_middleware.py +index 2dcf7a8..cc088d2 100644 +--- a/glance/tests/functional/test_cache_middleware.py ++++ b/glance/tests/functional/test_cache_middleware.py +@@ -223,6 +223,121 @@ class BaseCacheMiddlewareTest(object): + + self.stop_servers() + ++ @skip_if_disabled ++ def test_cache_middleware_trans_v1_without_download_image_policy(self): ++ """ ++ Ensure the image v1 API image transfer applied 'download_image' ++ policy enforcement. ++ """ ++ self.cleanup() ++ self.start_servers(**self.__dict__.copy()) ++ ++ # Add an image and verify a 200 OK is returned ++ image_data = "*" * FIVE_KB ++ headers = minimal_headers('Image1') ++ path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) ++ http = httplib2.Http() ++ response, content = http.request(path, 'POST', headers=headers, ++ body=image_data) ++ self.assertEqual(response.status, 201) ++ data = json.loads(content) ++ self.assertEqual(data['image']['checksum'], ++ hashlib.md5(image_data).hexdigest()) ++ self.assertEqual(data['image']['size'], FIVE_KB) ++ self.assertEqual(data['image']['name'], "Image1") ++ self.assertEqual(data['image']['is_public'], True) ++ ++ image_id = data['image']['id'] ++ ++ # Verify image not in cache ++ image_cached_path = os.path.join(self.api_server.image_cache_dir, ++ image_id) ++ self.assertFalse(os.path.exists(image_cached_path)) ++ ++ rules = {"context_is_admin": "role:admin", "default": "", ++ "download_image": "!"} ++ self.set_policy_rules(rules) ++ ++ # Grab the image ++ path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port, ++ image_id) ++ http = httplib2.Http() ++ response, content = http.request(path, 'GET') ++ self.assertEqual(response.status, 403) ++ ++ # Now, we delete the image from the server and verify that ++ # the image cache no longer contains the deleted image ++ path = "http://%s:%d/v1/images/%s" % ("127.0.0.1", self.api_port, ++ image_id) ++ http = httplib2.Http() ++ response, content = http.request(path, 'DELETE') ++ self.assertEqual(response.status, 200) ++ ++ self.assertFalse(os.path.exists(image_cached_path)) ++ ++ self.stop_servers() ++ ++ @skip_if_disabled ++ def test_cache_middleware_trans_v2_without_download_image_policy(self): ++ """ ++ Ensure the image v2 API image transfer applied 'download_image' ++ policy enforcement. ++ """ ++ self.cleanup() ++ self.start_servers(**self.__dict__.copy()) ++ ++ # Add an image and verify success ++ path = "http://%s:%d/v2/images" % ("0.0.0.0", self.api_port) ++ http = httplib2.Http() ++ headers = {'content-type': 'application/json'} ++ image_entity = { ++ 'name': 'Image1', ++ 'visibility': 'public', ++ 'container_format': 'bare', ++ 'disk_format': 'raw', ++ } ++ response, content = http.request(path, 'POST', ++ headers=headers, ++ body=json.dumps(image_entity)) ++ self.assertEqual(response.status, 201) ++ data = json.loads(content) ++ image_id = data['id'] ++ ++ path = "http://%s:%d/v2/images/%s/file" % ("0.0.0.0", self.api_port, ++ image_id) ++ headers = {'content-type': 'application/octet-stream'} ++ image_data = "*" * FIVE_KB ++ response, content = http.request(path, 'PUT', ++ headers=headers, ++ body=image_data) ++ self.assertEqual(response.status, 201) ++ ++ # Verify image not in cache ++ image_cached_path = os.path.join(self.api_server.image_cache_dir, ++ image_id) ++ self.assertFalse(os.path.exists(image_cached_path)) ++ ++ rules = {"context_is_admin": "role:admin", "default": "", ++ "download_image": "!"} ++ self.set_policy_rules(rules) ++ ++ # Grab the image ++ http = httplib2.Http() ++ response, content = http.request(path, 'GET') ++ self.assertEqual(response.status, 403) ++ ++ # Now, we delete the image from the server and verify that ++ # the image cache no longer contains the deleted image ++ path = "http://%s:%d/v2/images/%s" % ("0.0.0.0", self.api_port, ++ image_id) ++ http = httplib2.Http() ++ response, content = http.request(path, 'DELETE') ++ self.assertEqual(response.status, 204) ++ ++ self.assertFalse(os.path.exists(image_cached_path)) ++ ++ self.stop_servers() ++ + + class BaseCacheManageMiddlewareTest(object): + +diff --git a/glance/tests/unit/test_cache_middleware.py b/glance/tests/unit/test_cache_middleware.py +index 0f97b86..91890bc 100644 +--- a/glance/tests/unit/test_cache_middleware.py ++++ b/glance/tests/unit/test_cache_middleware.py +@@ -16,7 +16,9 @@ + import webob + + import glance.api.middleware.cache ++from glance import context + from glance.tests.unit import base ++from glance.tests.unit import utils as unit_test_utils + + + class ChecksumTestCacheFilter(glance.api.middleware.cache.CacheFilter): +@@ -27,9 +29,16 @@ class ChecksumTestCacheFilter(glance.api.middleware.cache.CacheFilter): + self.image_checksum = image_checksum + + self.cache = DummyCache() ++ self.policy = unit_test_utils.FakePolicyEnforcer() + + + class TestCacheMiddleware(base.IsolatedUnitTest): ++ def setUp(self): ++ super(TestCacheMiddleware, self).setUp() ++ self.context = context.RequestContext(is_admin=True) ++ self.request = webob.Request.blank('') ++ self.request.context = self.context ++ + def test_no_match_detail(self): + req = webob.Request.blank('/v1/images/detail') + out = glance.api.middleware.cache.CacheFilter._match_request(req) +@@ -48,7 +57,7 @@ class TestCacheMiddleware(base.IsolatedUnitTest): + def test_checksum_v1_header(self): + cache_filter = ChecksumTestCacheFilter() + headers = {"x-image-meta-checksum": "1234567890"} +- resp = webob.Response(headers=headers) ++ resp = webob.Response(request=self.request, headers=headers) + cache_filter._process_GET_response(resp, None) + + self.assertEqual("1234567890", cache_filter.cache.image_checksum) +@@ -59,14 +68,84 @@ class TestCacheMiddleware(base.IsolatedUnitTest): + "x-image-meta-checksum": "1234567890", + "Content-MD5": "abcdefghi" + } +- resp = webob.Response(headers=headers) ++ resp = webob.Response(request=self.request, headers=headers) + cache_filter._process_GET_response(resp, None) + + self.assertEqual("abcdefghi", cache_filter.cache.image_checksum) + + def test_checksum_missing_header(self): + cache_filter = ChecksumTestCacheFilter() +- resp = webob.Response() ++ resp = webob.Response(request=self.request) + cache_filter._process_GET_response(resp, None) + + self.assertEqual(None, cache_filter.cache.image_checksum) ++ ++ ++class FakeImageSerializer(object): ++ def show(self, response, raw_response): ++ return True ++ ++ ++class ProcessRequestTestCacheFilter(glance.api.middleware.cache.CacheFilter): ++ def __init__(self): ++ self.serializer = FakeImageSerializer() ++ ++ class DummyCache(object): ++ def __init__(self): ++ self.deleted_images = [] ++ ++ def is_cached(self, image_id): ++ return True ++ ++ def get_caching_iter(self, image_id, image_checksum, app_iter): ++ pass ++ ++ def delete_cached_image(self, image_id): ++ self.deleted_images.append(image_id) ++ ++ def get_image_size(self, image_id): ++ pass ++ ++ self.cache = DummyCache() ++ self.policy = unit_test_utils.FakePolicyEnforcer() ++ ++ ++class TestCacheMiddlewareProcessRequest(base.IsolatedUnitTest): ++ def test_process_request_without_download_image_policy(self): ++ """ ++ Test for cache middleware skip processing when request ++ context has not 'download_image' role. ++ """ ++ image_id = 'test1' ++ request = webob.Request.blank('/v1/images/%s' % image_id) ++ request.context = context.RequestContext() ++ ++ cache_filter = ProcessRequestTestCacheFilter() ++ ++ rules = {'download_image': '!'} ++ self.set_policy_rules(rules) ++ cache_filter.policy = glance.api.policy.Enforcer() ++ ++ self.assertEqual(None, cache_filter.process_request(request)) ++ ++ ++class TestCacheMiddlewareProcessResponse(base.IsolatedUnitTest): ++ def test_process_response_without_download_image_policy(self): ++ """ ++ Test for cache middleware raise webob.exc.HTTPForbidden directly ++ when request context has not 'download_image' role. ++ """ ++ cache_filter = ProcessRequestTestCacheFilter() ++ rules = {'download_image': '!'} ++ self.set_policy_rules(rules) ++ cache_filter.policy = glance.api.policy.Enforcer() ++ ++ image_id = 'test1' ++ request = webob.Request.blank('/v1/images/%s' % image_id) ++ request.context = context.RequestContext() ++ request.environ['api.cache.image_id'] = 'test1' ++ request.environ['api.cache.method'] = 'GET' ++ resp = webob.Response(request=request) ++ self.assertRaises(webob.exc.HTTPForbidden, ++ cache_filter.process_response, resp) ++ self.assertEqual([''], resp.app_iter) diff -Nru glance-2012.2.1/debian/patches/disable-swift-tests.patch glance-2012.2.4/debian/patches/disable-swift-tests.patch --- glance-2012.2.1/debian/patches/disable-swift-tests.patch 2012-12-04 17:21:20.000000000 +0000 +++ glance-2012.2.4/debian/patches/disable-swift-tests.patch 2013-05-14 17:34:57.000000000 +0000 @@ -1,6 +1,6 @@ -diff -Naupr glance-2012.2.orig/glance/tests/functional/test_bin_glance.py glance-2012.2/glance/tests/functional/test_bin_glance.py ---- glance-2012.2.orig/glance/tests/functional/test_bin_glance.py 2012-09-25 02:15:06.000000000 -0500 -+++ glance-2012.2/glance/tests/functional/test_bin_glance.py 2012-09-26 10:44:37.666976531 -0500 +diff -Naurp glance-2012.2.4.orig/glance/tests/functional/test_bin_glance.py glance-2012.2.4/glance/tests/functional/test_bin_glance.py +--- glance-2012.2.4.orig/glance/tests/functional/test_bin_glance.py 2013-03-21 10:20:46.000000000 -0500 ++++ glance-2012.2.4/glance/tests/functional/test_bin_glance.py 2013-03-21 10:35:22.183948477 -0500 @@ -21,6 +21,7 @@ import BaseHTTPServer import datetime import httplib2 @@ -9,10 +9,10 @@ import os import tempfile import thread -@@ -308,7 +309,10 @@ class TestBinGlance(functional.Functiona - 'Minimum Disk Required (GB): 0', - ] - lines = out.split("\n") +@@ -316,7 +317,10 @@ class TestBinGlance(functional.Functiona + # with 'Status: saving' if we didn't wait long enough + time.sleep(0.10) + - self.assertTrue(set(lines) >= set(expected_lines)) + try: + self.assertTrue(set(lines) >= set(expected_lines)) @@ -21,7 +21,7 @@ self.stop_servers() -@@ -318,7 +322,10 @@ class TestBinGlance(functional.Functiona +@@ -326,7 +330,10 @@ class TestBinGlance(functional.Functiona Tests creating an queued image then subsequently updating with a copy-from source """ @@ -33,9 +33,9 @@ @requires(teardown=teardown_http) def test_update_location(self): -diff -Naupr glance-2012.2.orig/glance/tests/functional/v1/test_multiprocessing.py glance-2012.2/glance/tests/functional/v1/test_multiprocessing.py ---- glance-2012.2.orig/glance/tests/functional/v1/test_multiprocessing.py 2012-09-25 02:15:03.000000000 -0500 -+++ glance-2012.2/glance/tests/functional/v1/test_multiprocessing.py 2012-09-26 10:38:41.858968048 -0500 +diff -Naurp glance-2012.2.4.orig/glance/tests/functional/v1/test_multiprocessing.py glance-2012.2.4/glance/tests/functional/v1/test_multiprocessing.py +--- glance-2012.2.4.orig/glance/tests/functional/v1/test_multiprocessing.py 2013-03-21 10:20:46.000000000 -0500 ++++ glance-2012.2.4/glance/tests/functional/v1/test_multiprocessing.py 2013-03-21 10:34:00.423948437 -0500 @@ -17,6 +17,7 @@ import time @@ -52,9 +52,9 @@ self.cleanup() self.start_servers(**self.__dict__.copy()) -diff -Naupr glance-2012.2.orig/glance/tests/functional/v2/test_images.py glance-2012.2/glance/tests/functional/v2/test_images.py ---- glance-2012.2.orig/glance/tests/functional/v2/test_images.py 2012-09-25 02:15:03.000000000 -0500 -+++ glance-2012.2/glance/tests/functional/v2/test_images.py 2012-09-26 10:43:22.610974741 -0500 +diff -Naurp glance-2012.2.4.orig/glance/tests/functional/v2/test_images.py glance-2012.2.4/glance/tests/functional/v2/test_images.py +--- glance-2012.2.4.orig/glance/tests/functional/v2/test_images.py 2013-03-21 10:20:46.000000000 -0500 ++++ glance-2012.2.4/glance/tests/functional/v2/test_images.py 2013-03-21 10:34:00.423948437 -0500 @@ -18,6 +18,7 @@ import json @@ -77,9 +77,9 @@ self.assertEqual(response.text, 'ZZZZZ') # Uploading duplicate data should be rejected with a 409 -diff -Naupr glance-2012.2.orig/glance/tests/unit/test_clients.py glance-2012.2/glance/tests/unit/test_clients.py ---- glance-2012.2.orig/glance/tests/unit/test_clients.py 2012-09-25 02:15:06.000000000 -0500 -+++ glance-2012.2/glance/tests/unit/test_clients.py 2012-09-26 10:36:58.102965574 -0500 +diff -Naurp glance-2012.2.4.orig/glance/tests/unit/test_clients.py glance-2012.2.4/glance/tests/unit/test_clients.py +--- glance-2012.2.4.orig/glance/tests/unit/test_clients.py 2013-03-21 10:20:46.000000000 -0500 ++++ glance-2012.2.4/glance/tests/unit/test_clients.py 2013-03-21 10:34:00.427948437 -0500 @@ -1811,7 +1811,11 @@ class TestClient(base.IsolatedUnitTest): tmp_file.write(image_data_fixture) tmp_file.close() @@ -93,9 +93,9 @@ new_image_id = new_image['id'] if os.path.exists(tmp_image_filepath): -diff -Naupr glance-2012.2.orig/glance/tests/unit/test_swift_store.py glance-2012.2/glance/tests/unit/test_swift_store.py ---- glance-2012.2.orig/glance/tests/unit/test_swift_store.py 2012-09-25 02:15:06.000000000 -0500 -+++ glance-2012.2/glance/tests/unit/test_swift_store.py 2012-09-26 11:56:17.831079055 -0500 +diff -Naurp glance-2012.2.4.orig/glance/tests/unit/test_swift_store.py glance-2012.2.4/glance/tests/unit/test_swift_store.py +--- glance-2012.2.4.orig/glance/tests/unit/test_swift_store.py 2013-03-21 10:20:46.000000000 -0500 ++++ glance-2012.2.4/glance/tests/unit/test_swift_store.py 2013-03-21 10:34:00.427948437 -0500 @@ -33,6 +33,7 @@ from glance.store import BackendExceptio from glance.store.location import get_location_from_uri import glance.store.swift @@ -239,9 +239,9 @@ def test_delete_non_existing(self): """ Test that trying to delete a swift that doesn't exist -diff -Naupr glance-2012.2.orig/glance/tests/unit/v2/test_image_data_resource.py glance-2012.2/glance/tests/unit/v2/test_image_data_resource.py ---- glance-2012.2.orig/glance/tests/unit/v2/test_image_data_resource.py 2012-09-25 02:15:03.000000000 -0500 -+++ glance-2012.2/glance/tests/unit/v2/test_image_data_resource.py 2012-09-26 12:01:05.523085914 -0500 +diff -Naurp glance-2012.2.4.orig/glance/tests/unit/v2/test_image_data_resource.py glance-2012.2.4/glance/tests/unit/v2/test_image_data_resource.py +--- glance-2012.2.4.orig/glance/tests/unit/v2/test_image_data_resource.py 2013-03-21 10:20:46.000000000 -0500 ++++ glance-2012.2.4/glance/tests/unit/v2/test_image_data_resource.py 2013-03-21 10:34:00.427948437 -0500 @@ -16,6 +16,7 @@ import StringIO diff -Nru glance-2012.2.1/debian/patches/series glance-2012.2.4/debian/patches/series --- glance-2012.2.1/debian/patches/series 2013-03-13 20:38:08.000000000 +0000 +++ glance-2012.2.4/debian/patches/series 2013-10-22 18:42:14.000000000 +0000 @@ -1,5 +1,4 @@ sql_conn.patch disable-swift-tests.patch disable-network-for-docs.patch -CVE-2013-0212.patch -CVE-2013-1840.patch +CVE-2013-4428.patch diff -Nru glance-2012.2.1/glance/api/middleware/cache.py glance-2012.2.4/glance/api/middleware/cache.py --- glance-2012.2.1/glance/api/middleware/cache.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/api/middleware/cache.py 2013-04-11 19:09:50.000000000 +0000 @@ -111,6 +111,9 @@ def _process_v1_request(self, request, image_id, image_iterator): image_meta = registry.get_image_metadata(request.context, image_id) + # Don't display location + if 'location' in image_meta: + del image_meta['location'] if not image_meta['size']: # override image size metadata with the actual cached diff -Nru glance-2012.2.1/glance/api/v1/images.py glance-2012.2.4/glance/api/v1/images.py --- glance-2012.2.1/glance/api/v1/images.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/api/v1/images.py 2013-04-11 19:09:50.000000000 +0000 @@ -436,19 +436,24 @@ utils.CooperativeReader(image_data), image_meta['size']) - # Verify any supplied checksum value matches checksum + def _kill_mismatched(image_meta, attr, actual): + supplied = image_meta.get(attr) + if supplied and supplied != actual: + msg = _("Supplied %(attr)s (%(supplied)s) and " + "%(attr)s generated from uploaded image " + "(%(actual)s) did not match. Setting image " + "status to 'killed'.") % locals() + LOG.error(msg) + self._safe_kill(req, image_id) + self._initiate_deletion(req, location, image_id) + raise HTTPBadRequest(explanation=msg, + content_type="text/plain", + request=req) + + # Verify any supplied size/checksum value matches size/checksum # returned from store when adding image - supplied_checksum = image_meta.get('checksum') - if supplied_checksum and supplied_checksum != checksum: - msg = _("Supplied checksum (%(supplied_checksum)s) and " - "checksum generated from uploaded image " - "(%(checksum)s) did not match. Setting image " - "status to 'killed'.") % locals() - LOG.error(msg) - self._safe_kill(req, image_id) - raise HTTPBadRequest(explanation=msg, - content_type="text/plain", - request=req) + _kill_mismatched(image_meta, 'size', size) + _kill_mismatched(image_meta, 'checksum', checksum) # Update the database with the checksum returned # from the backend store @@ -806,6 +811,13 @@ return {'image_meta': image_meta} + @staticmethod + def _initiate_deletion(req, location, id): + if CONF.delayed_delete: + schedule_delayed_delete_from_backend(location, id) + else: + safe_delete_from_backend(location, req.context, id) + @utils.mutating def delete(self, req, id): """ @@ -852,11 +864,7 @@ # to delete the image if the backend doesn't yet store it. # See https://bugs.launchpad.net/glance/+bug/747799 if image['location']: - if CONF.delayed_delete: - schedule_delayed_delete_from_backend(image['location'], id) - else: - safe_delete_from_backend(image['location'], - req.context, id) + self._initiate_deletion(req, image['location'], id) except exception.NotFound, e: msg = ("Failed to find image to delete: %(e)s" % locals()) for line in msg.split('\n'): diff -Nru glance-2012.2.1/glance/common/auth.py glance-2012.2.4/glance/common/auth.py --- glance-2012.2.1/glance/common/auth.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/common/auth.py 2013-04-11 19:09:50.000000000 +0000 @@ -85,14 +85,14 @@ # Ensure that supplied credential parameters are as required for required in ('username', 'password', 'auth_url', 'strategy'): - if required not in self.creds: + if self.creds.get(required) is None: raise exception.MissingCredentialError(required=required) if self.creds['strategy'] != 'keystone': raise exception.BadAuthStrategy(expected='keystone', received=self.creds['strategy']) # For v2.0 also check tenant is present if self.creds['auth_url'].rstrip('/').endswith('v2.0'): - if 'tenant' not in self.creds: + if self.creds.get("tenant") is None: raise exception.MissingCredentialError(required='tenant') def authenticate(self): diff -Nru glance-2012.2.1/glance/common/config.py glance-2012.2.4/glance/common/config.py --- glance-2012.2.1/glance/common/config.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/common/config.py 2013-04-11 19:09:50.000000000 +0000 @@ -192,6 +192,8 @@ return app except (LookupError, ImportError), e: - raise RuntimeError("Unable to load %(app_name)s from " - "configuration file %(conf_file)s." - "\nGot: %(e)r" % locals()) + msg = _("Unable to load %(app_name)s from " + "configuration file %(conf_file)s." + "\nGot: %(e)r") % locals() + logger.error(msg) + raise RuntimeError(msg) diff -Nru glance-2012.2.1/glance/common/wsgi.py glance-2012.2.4/glance/common/wsgi.py --- glance-2012.2.1/glance/common/wsgi.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/common/wsgi.py 2013-04-11 19:09:50.000000000 +0000 @@ -324,6 +324,7 @@ if response: return response response = req.get_response(self.application) + response.request = req return self.process_response(response) diff -Nru glance-2012.2.1/glance/db/sqlalchemy/api.py glance-2012.2.4/glance/db/sqlalchemy/api.py --- glance-2012.2.1/glance/db/sqlalchemy/api.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/db/sqlalchemy/api.py 2013-04-11 19:09:50.000000000 +0000 @@ -710,11 +710,15 @@ return memb_ref -def image_member_find(context, image_id=None, member=None, session=None): +def image_member_find(context, image_id=None, member=None, session=None, + show_deleted=True): """Find all members that meet the given criteria :param image_id: identifier of image entity :param member: tenant to which membership has been granted + :param session: an existing sqlalchemy session to use + :param show_deleted: set to False to override use of the context to + determine if deleted memberships are included """ session = session or get_session() @@ -726,7 +730,7 @@ query = query.filter_by(image_id=image_id) if member is not None: query = query.filter_by(member=member) - if not can_show_deleted(context): + if not (show_deleted and can_show_deleted(context)): query = query.filter_by(deleted=False) return query.all() diff -Nru glance-2012.2.1/glance/db/sqlalchemy/migrate_repo/versions/003_add_disk_format.py glance-2012.2.4/glance/db/sqlalchemy/migrate_repo/versions/003_add_disk_format.py --- glance-2012.2.1/glance/db/sqlalchemy/migrate_repo/versions/003_add_disk_format.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/db/sqlalchemy/migrate_repo/versions/003_add_disk_format.py 2013-04-11 19:09:50.000000000 +0000 @@ -43,7 +43,7 @@ Column('deleted', Boolean(), nullable=False, default=False, index=True), mysql_engine='InnoDB', - useexisting=True) + extend_existing=True) return images diff -Nru glance-2012.2.1/glance/db/sqlalchemy/migrate_repo/versions/005_size_big_integer.py glance-2012.2.4/glance/db/sqlalchemy/migrate_repo/versions/005_size_big_integer.py --- glance-2012.2.1/glance/db/sqlalchemy/migrate_repo/versions/005_size_big_integer.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/db/sqlalchemy/migrate_repo/versions/005_size_big_integer.py 2013-04-11 19:09:50.000000000 +0000 @@ -44,7 +44,7 @@ Column('deleted', Boolean(), nullable=False, default=False, index=True), mysql_engine='InnoDB', - useexisting=True) + extend_existing=True) return images diff -Nru glance-2012.2.1/glance/db/sqlalchemy/migrate_repo/versions/006_key_to_name.py glance-2012.2.4/glance/db/sqlalchemy/migrate_repo/versions/006_key_to_name.py --- glance-2012.2.1/glance/db/sqlalchemy/migrate_repo/versions/006_key_to_name.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/db/sqlalchemy/migrate_repo/versions/006_key_to_name.py 2013-04-11 19:09:50.000000000 +0000 @@ -56,7 +56,7 @@ index=True), UniqueConstraint('image_id', 'name'), mysql_engine='InnoDB', - useexisting=True) + extend_existing=True) return image_properties diff -Nru glance-2012.2.1/glance/openstack/common/timeutils.py glance-2012.2.4/glance/openstack/common/timeutils.py --- glance-2012.2.1/glance/openstack/common/timeutils.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/openstack/common/timeutils.py 2013-04-11 19:09:50.000000000 +0000 @@ -84,6 +84,11 @@ return datetime.datetime.utcnow() +def iso8601_from_timestamp(timestamp): + """Returns a iso8601 formated date from timestamp""" + return isotime(datetime.datetime.utcfromtimestamp(timestamp)) + + utcnow.override_time = None @@ -120,7 +125,36 @@ def unmarshall_time(tyme): - """Unmarshall a datetime dict.""" - return datetime.datetime(day=tyme['day'], month=tyme['month'], - year=tyme['year'], hour=tyme['hour'], minute=tyme['minute'], - second=tyme['second'], microsecond=tyme['microsecond']) + return datetime.datetime(day=tyme['day'], + month=tyme['month'], + year=tyme['year'], + hour=tyme['hour'], + minute=tyme['minute'], + second=tyme['second'], + microsecond=tyme['microsecond']) + + +def delta_seconds(before, after): + """ + Compute the difference in seconds between two date, time, or + datetime objects (as a float, to microsecond resolution). + """ + delta = after - before + try: + return delta.total_seconds() + except AttributeError: + return ((delta.days * 24 * 3600) + delta.seconds + + float(delta.microseconds) / (10 ** 6)) + + +def is_soon(dt, window): + """ + Determines if time is going to happen in the next window seconds. + + :params dt: the time + :params window: minimum seconds to remain to consider the time not soon + + :return: True if expiration is within the given duration + """ + soon = (utcnow() + datetime.timedelta(seconds=window)) + return normalize_time(dt) < soon diff -Nru glance-2012.2.1/glance/registry/api/v1/members.py glance-2012.2.4/glance/registry/api/v1/members.py --- glance-2012.2.1/glance/registry/api/v1/members.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/registry/api/v1/members.py 2013-04-11 19:09:50.000000000 +0000 @@ -274,17 +274,21 @@ raise webob.exc.HTTPForbidden(msg) # Look up an existing membership - try: - session = self.db_api.get_session() - members = self.db_api.image_member_find(req.context, - image_id=image_id, - member=id, - session=session) + session = self.db_api.get_session() + members = self.db_api.image_member_find(req.context, + image_id=image_id, + member=id, + session=session, + show_deleted=False) + if members: self.db_api.image_member_delete(req.context, members[0], session=session) - except exception.NotFound: - pass + else: + msg = _("%(id)s is not a member of image %(image_id)s") + LOG.debug(msg % locals()) + msg = _("Membership could not be found.") + raise webob.exc.HTTPNotFound(explanation=msg) # Make an appropriate result msg = _("Successfully deleted a membership from image %(id)s") diff -Nru glance-2012.2.1/glance/store/filesystem.py glance-2012.2.4/glance/store/filesystem.py --- glance-2012.2.1/glance/store/filesystem.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/store/filesystem.py 2013-04-11 19:09:50.000000000 +0000 @@ -209,20 +209,26 @@ checksum.update(buf) f.write(buf) except IOError as e: - if e.errno in [errno.EFBIG, errno.ENOSPC]: - try: - os.unlink(filepath) - except Exception: - msg = _('Unable to remove partial image data for image %s') - LOG.error(msg % image_id) - raise exception.StorageFull() - elif e.errno == errno.EACCES: - raise exception.StorageWriteDenied() - else: - raise + if e.errno != errno.EACCES: + self._delete_partial(filepath, image_id) + exceptions = {errno.EFBIG: exception.StorageFull(), + errno.ENOSPC: exception.StorageFull(), + errno.EACCES: exception.StorageWriteDenied()} + raise exceptions.get(e.errno, e) + except: + self._delete_partial(filepath, image_id) + raise checksum_hex = checksum.hexdigest() LOG.debug(_("Wrote %(bytes_written)d bytes to %(filepath)s with " "checksum %(checksum_hex)s") % locals()) return ('file://%s' % filepath, bytes_written, checksum_hex) + + @staticmethod + def _delete_partial(filepath, id): + try: + os.unlink(filepath) + except Exception as e: + msg = _('Unable to remove partial image data for image %s: %s') + LOG.error(msg % (id, e)) diff -Nru glance-2012.2.1/glance/store/swift.py glance-2012.2.4/glance/store/swift.py --- glance-2012.2.1/glance/store/swift.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/store/swift.py 2013-04-11 19:09:50.000000000 +0000 @@ -136,7 +136,7 @@ "like so: " "swift+http://user:pass@authurl.com/v1/container/obj" ) - LOG.error(_("Invalid store uri %(uri)s: %(reason)s") % locals()) + LOG.error(_("Invalid store URI: %(reason)s") % locals()) raise exception.BadStoreUri(message=reason) pieces = urlparse.urlparse(uri) @@ -162,8 +162,7 @@ if creds: cred_parts = creds.split(':') if len(cred_parts) != 2: - reason = (_("Badly formed credentials '%(creds)s' in Swift " - "URI") % locals()) + reason = (_("Badly formed credentials in Swift URI.")) LOG.error(reason) raise exception.BadStoreUri() user, key = cred_parts @@ -181,7 +180,7 @@ path_parts.insert(0, netloc) self.auth_or_store_url = '/'.join(path_parts) except IndexError: - reason = _("Badly formed Swift URI: %s") % uri + reason = _("Badly formed Swift URI.") LOG.error(reason) raise exception.BadStoreUri() @@ -293,8 +292,8 @@ except swiftclient.ClientException, e: if e.http_status == httplib.NOT_FOUND: uri = location.get_store_uri() - raise exception.NotFound(_("Swift could not find image at " - "uri %(uri)s") % locals()) + msg = _("Swift could not find image at URI.") + raise exception.NotFound(msg) else: raise @@ -543,7 +542,7 @@ except swiftclient.ClientException, e: if e.http_status == httplib.CONFLICT: raise exception.Duplicate(_("Swift already has an image at " - "location %s") % location.get_uri()) + "this location.")) msg = (_("Failed to add object to Swift.\n" "Got error from Swift: %(e)s") % locals()) LOG.error(msg) @@ -596,8 +595,8 @@ except swiftclient.ClientException, e: if e.http_status == httplib.NOT_FOUND: uri = location.get_store_uri() - raise exception.NotFound(_("Swift could not find image at " - "uri %(uri)s") % locals()) + msg = _("Swift could not find image at URI.") + raise exception.NotFound(msg) else: raise @@ -637,8 +636,8 @@ except swiftclient.ClientException, e: if e.http_status == httplib.NOT_FOUND: uri = location.get_store_uri() - raise exception.NotFound(_("Swift could not find image at " - "uri %(uri)s") % locals()) + msg = _("Swift could not find image at URI.") + raise exception.NotFound(msg) else: raise diff -Nru glance-2012.2.1/glance/tests/functional/test_bin_glance.py glance-2012.2.4/glance/tests/functional/test_bin_glance.py --- glance-2012.2.1/glance/tests/functional/test_bin_glance.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/tests/functional/test_bin_glance.py 2013-04-11 19:09:50.000000000 +0000 @@ -291,10 +291,6 @@ # 3. Verify image is now active and of the correct size cmd = "bin/glance --port=%d show %s" % (api_port, image_id) - exitcode, out, err = execute(cmd) - - self.assertEqual(0, exitcode) - expected_lines = [ 'URI: http://0.0.0.0:%s/v1/images/%s' % (api_port, image_id), 'Id: %s' % image_id, @@ -307,7 +303,19 @@ 'Minimum Ram Required (MB): 0', 'Minimum Disk Required (GB): 0', ] - lines = out.split("\n") + + for _ in range(0, 9): + exitcode, out, err = execute(cmd) + self.assertEqual(0, exitcode) + lines = out.split("\n") + + if "Status: active" in lines: + break + + # Yeah. This totally isn't a race condition. Randomly fails + # with 'Status: saving' if we didn't wait long enough + time.sleep(0.10) + self.assertTrue(set(lines) >= set(expected_lines)) self.stop_servers() diff -Nru glance-2012.2.1/glance/tests/functional/v1/test_api.py glance-2012.2.4/glance/tests/functional/v1/test_api.py --- glance-2012.2.1/glance/tests/functional/v1/test_api.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/tests/functional/v1/test_api.py 2013-04-11 19:09:50.000000000 +0000 @@ -20,6 +20,7 @@ import datetime import hashlib import json +import os import tempfile import httplib2 @@ -1451,3 +1452,42 @@ self.assertEqual('tenant2', response['x-image-meta-owner']) self.stop_servers() + + def _do_test_mismatched_attribute(self, attribute, value): + """ + Test mismatched attribute. + """ + self.cleanup() + self.start_servers(**self.__dict__.copy()) + + image_data = "*" * FIVE_KB + headers = minimal_headers('Image1') + headers[attribute] = value + path = "http://%s:%d/v1/images" % ("127.0.0.1", self.api_port) + http = httplib2.Http() + response, content = http.request(path, 'POST', headers=headers, + body=image_data) + self.assertEqual(response.status, 400) + + images_dir = os.path.join(self.test_dir, 'images') + image_count = len([name for name in os.listdir(images_dir) + if os.path.isfile(name)]) + self.assertEquals(image_count, 0) + + self.stop_servers() + + @skip_if_disabled + def test_mismatched_size(self): + """ + Test mismatched size. + """ + self._do_test_mismatched_attribute('x-image-meta-size', + str(FIVE_KB + 1)) + + @skip_if_disabled + def test_mismatched_checksum(self): + """ + Test mismatched checksum. + """ + self._do_test_mismatched_attribute('x-image-meta-checksum', + 'foobar') diff -Nru glance-2012.2.1/glance/tests/functional/v1/test_bin_glance_cache_manage.py glance-2012.2.4/glance/tests/functional/v1/test_bin_glance_cache_manage.py --- glance-2012.2.1/glance/tests/functional/v1/test_bin_glance_cache_manage.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/tests/functional/v1/test_bin_glance_cache_manage.py 2013-04-11 19:09:50.000000000 +0000 @@ -17,6 +17,7 @@ """Functional test case that utilizes the bin/glance-cache-manage CLI tool""" +import datetime import hashlib import httplib2 import json @@ -76,6 +77,16 @@ self.assertEqual(0, exitcode) return image_id in out + def iso_date(self, image_id): + """ + Return True if supplied image ID is cached, False otherwise + """ + cmd = "bin/glance-cache-manage --port=%d list-cached" % self.api_port + + exitcode, out, err = execute(cmd) + + return datetime.date.today().isoformat() in out + def test_no_cache_enabled(self): """ Test that cache index command works @@ -132,6 +143,8 @@ self.assertTrue(self.is_image_cached(ids[1]), "%s is not cached." % ids[1]) + self.assertTrue(self.iso_date(ids[1])) + self.stop_servers() def test_queue(self): diff -Nru glance-2012.2.1/glance/tests/unit/test_auth.py glance-2012.2.4/glance/tests/unit/test_auth.py --- glance-2012.2.1/glance/tests/unit/test_auth.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/tests/unit/test_auth.py 2013-04-11 19:09:50.000000000 +0000 @@ -138,7 +138,18 @@ 'username': 'user1', 'password': 'pass', 'auth_url': 'http://localhost/v2.0/' - } # v2.0: missing tenant + }, # v2.0: missing tenant + { + 'username': None, + 'password': 'pass', + 'auth_url': 'http://localhost/v2.0/' + }, # None parameter + { + 'username': 'user1', + 'password': 'pass', + 'auth_url': 'http://localhost/v2.0/', + 'tenant': None + } # None tenant ] for creds in bad_creds: try: diff -Nru glance-2012.2.1/glance/tests/unit/test_filesystem_store.py glance-2012.2.4/glance/tests/unit/test_filesystem_store.py --- glance-2012.2.1/glance/tests/unit/test_filesystem_store.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/tests/unit/test_filesystem_store.py 2013-04-11 19:09:50.000000000 +0000 @@ -17,10 +17,14 @@ """Tests the filesystem backend store""" +import __builtin__ import errno import hashlib +import os import StringIO +import mox + from glance.common import exception from glance.common import utils from glance.store.filesystem import Store, ChunkedFile @@ -132,51 +136,82 @@ self.store.add, image_id, image_file, 0) - def _do_test_add_failure(self, errno, exception): + def _do_test_add_write_failure(self, errno, exception): ChunkedFile.CHUNKSIZE = 1024 image_id = utils.generate_uuid() file_size = 1024 * 5 # 5K file_contents = "*" * file_size + path = os.path.join(self.test_dir, image_id) location = "file://%s/%s" % (self.test_dir, image_id) image_file = StringIO.StringIO(file_contents) - def fake_IO_Error(size): - e = IOError() - e.errno = errno - raise e - - self.stubs.Set(image_file, 'read', fake_IO_Error) - self.assertRaises(exception, - self.store.add, - image_id, image_file, 0) + m = mox.Mox() + m.StubOutWithMock(__builtin__, 'open') + e = IOError() + e.errno = errno + open(path, 'wb').AndRaise(e) + m.ReplayAll() + + try: + self.assertRaises(exception, + self.store.add, + image_id, image_file, 0) + self.assertFalse(os.path.exists(path)) + finally: + m.VerifyAll() + m.UnsetStubs() def test_add_storage_full(self): """ Tests that adding an image without enough space on disk raises an appropriate exception """ - self._do_test_add_failure(errno.ENOSPC, exception.StorageFull) + self._do_test_add_write_failure(errno.ENOSPC, exception.StorageFull) def test_add_file_too_big(self): """ Tests that adding an excessively large image file raises an appropriate exception """ - self._do_test_add_failure(errno.EFBIG, exception.StorageFull) + self._do_test_add_write_failure(errno.EFBIG, exception.StorageFull) def test_add_storage_write_denied(self): """ Tests that adding an image with insufficient filestore permissions raises an appropriate exception """ - self._do_test_add_failure(errno.EACCES, exception.StorageWriteDenied) + self._do_test_add_write_failure(errno.EACCES, + exception.StorageWriteDenied) def test_add_other_failure(self): """ Tests that a non-space-related IOError does not raise a StorageFull exception. """ - self._do_test_add_failure(errno.ENOTDIR, IOError) + self._do_test_add_write_failure(errno.ENOTDIR, IOError) + + def test_add_cleanup_on_read_failure(self): + """ + Tests the partial image file is cleaned up after a read + failure. + """ + ChunkedFile.CHUNKSIZE = 1024 + image_id = utils.generate_uuid() + file_size = 1024 * 5 # 5K + file_contents = "*" * file_size + path = os.path.join(self.test_dir, image_id) + location = "file://%s/%s" % (self.test_dir, image_id) + image_file = StringIO.StringIO(file_contents) + + def fake_Error(size): + raise AttributeError() + + self.stubs.Set(image_file, 'read', fake_Error) + + self.assertRaises(AttributeError, + self.store.add, + image_id, image_file, 0) + self.assertFalse(os.path.exists(path)) def test_delete(self): """ diff -Nru glance-2012.2.1/glance/tests/unit/v1/test_api.py glance-2012.2.4/glance/tests/unit/v1/test_api.py --- glance-2012.2.1/glance/tests/unit/v1/test_api.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/tests/unit/v1/test_api.py 2013-04-11 19:09:50.000000000 +0000 @@ -1905,6 +1905,41 @@ res = req.get_response(self.api) self.assertEquals(res.status_int, webob.exc.HTTPUnauthorized.code) + def test_delete_member_invalid(self): + """ + Tests deleting a invalid/non existing member raises right exception + """ + self.api = test_utils.FakeAuthMiddleware(rserver.API(self.mapper), + is_admin=True) + req = webob.Request.blank('/images/%s/members/pattieblack' % UUID2) + req.method = 'DELETE' + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code) + self.assertTrue('Membership could not be found' in res.body) + + def test_delete_member_existing(self): + """ + Tests deleting an existing member is handled correctly + """ + member = dict(image_id=UUID2, member='pattieblack', can_share=False) + db_api.image_member_create(self.context, member) + + self.api = test_utils.FakeAuthMiddleware(rserver.API(self.mapper), + is_admin=True) + req = webob.Request.blank('/images/%s/members/pattieblack' % UUID2) + req.method = 'DELETE' + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPNoContent.code) + + req = webob.Request.blank('/images/%s/members/pattieblack' % UUID2) + req.method = 'DELETE' + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code) + self.assertTrue('Membership could not be found' in res.body) + class TestGlanceAPI(base.IsolatedUnitTest): def setUp(self): @@ -2189,12 +2224,11 @@ self.assertEqual(res.status_int, 200) self.assertEqual(len(res.body), 0) - def test_add_image_checksum_mismatch(self): + def _do_test_add_image_attribute_mismatch(self, attributes): fixture_headers = { - 'x-image-meta-checksum': 'asdf', - 'x-image-meta-size': '4', 'x-image-meta-name': 'fake image #3', } + fixture_headers.update(attributes) req = webob.Request.blank("/images") req.method = 'POST' @@ -2206,6 +2240,25 @@ res = req.get_response(self.api) self.assertEquals(res.status_int, 400) + def test_add_image_checksum_mismatch(self): + attributes = { + 'x-image-meta-checksum': 'asdf', + } + self._do_test_add_image_attribute_mismatch(attributes) + + def test_add_image_size_mismatch(self): + attributes = { + 'x-image-meta-size': str(len("XXXX") + 1), + } + self._do_test_add_image_attribute_mismatch(attributes) + + def test_add_image_checksum_and_size_mismatch(self): + attributes = { + 'x-image-meta-checksum': 'asdf', + 'x-image-meta-size': str(len("XXXX") + 1), + } + self._do_test_add_image_attribute_mismatch(attributes) + def test_add_image_bad_store(self): """Tests raises BadRequest for invalid store header""" fixture_headers = {'x-image-meta-store': 'bad', diff -Nru glance-2012.2.1/glance/tests/unit/v2/test_images_resource.py glance-2012.2.4/glance/tests/unit/v2/test_images_resource.py --- glance-2012.2.1/glance/tests/unit/v2/test_images_resource.py 2012-11-30 20:19:33.000000000 +0000 +++ glance-2012.2.4/glance/tests/unit/v2/test_images_resource.py 2013-04-11 19:09:50.000000000 +0000 @@ -1105,7 +1105,6 @@ custom_image_properties = { 'pants': { 'type': 'string', - 'required': True, 'enum': ['on', 'off'], }, } @@ -1541,7 +1540,6 @@ custom_image_properties = { 'color': { 'type': 'string', - 'required': True, 'enum': ['red', 'green'], }, } diff -Nru glance-2012.2.1/glance/version.py glance-2012.2.4/glance/version.py --- glance-2012.2.1/glance/version.py 2012-12-03 22:26:30.000000000 +0000 +++ glance-2012.2.4/glance/version.py 2013-04-11 19:09:50.000000000 +0000 @@ -17,6 +17,6 @@ from glance.openstack.common import version as common_version -NEXT_VERSION = '2012.2.1' +NEXT_VERSION = '2012.2.4' version_info = common_version.VersionInfo('glance', pre_version=NEXT_VERSION) diff -Nru glance-2012.2.1/glance/versioninfo glance-2012.2.4/glance/versioninfo --- glance-2012.2.1/glance/versioninfo 2012-12-03 22:28:19.000000000 +0000 +++ glance-2012.2.4/glance/versioninfo 2013-04-11 19:11:54.000000000 +0000 @@ -1 +1 @@ -2012.2.1 +2012.2.4 diff -Nru glance-2012.2.1/glance.egg-info/PKG-INFO glance-2012.2.4/glance.egg-info/PKG-INFO --- glance-2012.2.1/glance.egg-info/PKG-INFO 2012-12-03 22:28:21.000000000 +0000 +++ glance-2012.2.4/glance.egg-info/PKG-INFO 2013-04-11 19:11:56.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: glance -Version: 2012.2.1 +Version: 2012.2.4 Summary: The Glance project provides services for discovering, registering, and retrieving virtual machine images Home-page: http://glance.openstack.org/ Author: OpenStack