Hello community,

here is the log from the commit of package python-glanceclient for 
openSUSE:Factory checked in at 2019-02-11 21:24:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-glanceclient (Old)
 and      /work/SRC/openSUSE:Factory/.python-glanceclient.new.28833 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-glanceclient"

Mon Feb 11 21:24:53 2019 rev:28 rq:672978 version:2.13.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-glanceclient/python-glanceclient.changes  
2018-09-07 15:37:15.326674606 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-glanceclient.new.28833/python-glanceclient.changes
       2019-02-11 21:24:56.427094944 +0100
@@ -1,0 +2,9 @@
+Wed Feb  6 15:00:28 UTC 2019 - [email protected]
+
+- update to version 2.13.1
+  - Refactor periodic "tips" jobs
+  - import zuul job settings from project-config
+  - Use "multihash" for data download validation
+  - Don't quote colon in HTTP headers
+
+-------------------------------------------------------------------

Old:
----
  python-glanceclient-2.12.1.tar.gz

New:
----
  python-glanceclient-2.13.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-glanceclient.spec ++++++
--- /var/tmp/diff_new_pack.ziIYFL/_old  2019-02-11 21:24:56.899094690 +0100
+++ /var/tmp/diff_new_pack.ziIYFL/_new  2019-02-11 21:24:56.899094690 +0100
@@ -1,7 +1,7 @@
 #
 # spec file for package python-glanceclient
 #
-# Copyright (c) 2018 SUSE LINUX GmbH, Nuernberg, Germany.
+# Copyright (c) 2019 SUSE LINUX GmbH, Nuernberg, Germany.
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -12,18 +12,18 @@
 # license that conforms to the Open Source Definition (Version 1.9)
 # published by the Open Source Initiative.
 
-# Please submit bugfixes or comments via http://bugs.opensuse.org/
+# Please submit bugfixes or comments via https://bugs.opensuse.org/
 #
 
 
 Name:           python-glanceclient
-Version:        2.12.1
+Version:        2.13.1
 Release:        0
 Summary:        Python API and CLI for OpenStack Glance
 License:        Apache-2.0
 Group:          Development/Languages/Python
 URL:            https://launchpad.net/python-glanceclient
-Source0:        
https://files.pythonhosted.org/packages/source/p/python-glanceclient/python-glanceclient-2.12.1.tar.gz
+Source0:        
https://files.pythonhosted.org/packages/source/p/python-glanceclient/python-glanceclient-2.13.1.tar.gz
 BuildRequires:  openstack-macros
 BuildRequires:  python-devel
 BuildRequires:  python2-PrettyTable >= 0.7.1
@@ -94,7 +94,7 @@
 This package contains auto-generated documentation.
 
 %prep
-%autosetup -p1 -n python-glanceclient-2.12.1
+%autosetup -p1 -n python-glanceclient-2.13.1
 %py_req_cleanup
 sed -i 's/^warning-is-error.*/warning-is-error = 0/g' setup.cfg
 
@@ -102,8 +102,8 @@
 %python_build
 
 # generate html docs
-PBR_VERSION=2.12.1 sphinx-build -b html doc/source doc/build/html
-PBR_VERSION=2.12.1 sphinx-build -b man doc/source doc/build/man
+PBR_VERSION=2.13.1 sphinx-build -b html doc/source doc/build/html
+PBR_VERSION=2.13.1 sphinx-build -b man doc/source doc/build/man
 # remove the sphinx-build leftovers
 rm -rf doc/build/html/.{doctrees,buildinfo}
 

++++++ python-glanceclient-2.12.1.tar.gz -> python-glanceclient-2.13.1.tar.gz 
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-glanceclient-2.12.1/.zuul.yaml 
new/python-glanceclient-2.13.1/.zuul.yaml
--- old/python-glanceclient-2.12.1/.zuul.yaml   2018-07-26 23:20:46.000000000 
+0200
+++ new/python-glanceclient-2.13.1/.zuul.yaml   2018-12-12 04:54:05.000000000 
+0100
@@ -70,6 +70,7 @@
 - job:
     name: glanceclient-tox-keystone-tips-base
     parent: tox
+    abstract: true
     description: Abstract job for glanceclient vs. keystone
     required-projects:
       - name: openstack/keystoneauth
@@ -93,6 +94,7 @@
 - job:
     name: glanceclient-tox-oslo-tips-base
     parent: tox
+    abstract: true
     description: Abstract job for glanceclient vs. oslo
     required-projects:
       - name: openstack/oslo.i18n
@@ -122,6 +124,13 @@
         USE_PYTHON3: true
 
 - project:
+    templates:
+      - openstack-python-jobs
+      - openstack-python35-jobs
+      - release-notes-jobs
+      - publish-openstack-sphinx-docs
+      - check-requirements
+      - lib-forward-testing
     check:
       jobs:
         - glanceclient-dsvm-functional-v1
@@ -134,10 +143,29 @@
         - openstack-tox-lower-constraints
     periodic:
       jobs:
-        - glanceclient-tox-py27-keystone-tips
-        - glanceclient-tox-py35-keystone-tips
-        - glanceclient-tox-py27-oslo-tips
-        - glanceclient-tox-py35-oslo-tips
+        # NOTE(rosmaita): we only want the "tips" jobs to be run against
+        # master, hence the 'branches' qualifiers below.  Without them, when
+        # a stable branch is cut, the tests would be run against the stable
+        # branch as well, which is pointless because these libraries are
+        # frozen (more or less) in the stable branches.
+        #
+        # The "tips" jobs can be removed from the stable branch .zuul.yaml
+        # files if someone is so inclined, but that would require manual
+        # maintenance, so we do not do it by default.  Another option is
+        # to define these jobs in the openstack-infra/project-config repo.
+        # That would make us less agile in adjusting these tests, so we
+        # aren't doing that either.
+        - glanceclient-tox-py27-keystone-tips:
+            branches: master
+        - glanceclient-tox-py35-keystone-tips:
+            branches: master
+        - glanceclient-tox-py27-oslo-tips:
+            branches: master
+        - glanceclient-tox-py35-oslo-tips:
+            branches: master
     experimental:
       jobs:
         - glanceclient-dsvm-functional-py3
+    post:
+      jobs:
+        - openstack-tox-cover
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-glanceclient-2.12.1/AUTHORS 
new/python-glanceclient-2.13.1/AUTHORS
--- old/python-glanceclient-2.12.1/AUTHORS      2018-07-26 23:24:34.000000000 
+0200
+++ new/python-glanceclient-2.13.1/AUTHORS      2018-12-12 04:56:32.000000000 
+0100
@@ -213,7 +213,6 @@
 Zhi Yan Liu <[email protected]>
 ZhiQiang Fan <[email protected]>
 Zhiqiang Fan <[email protected]>
-Zuul <[email protected]>
 amalaba <[email protected]>
 bhagyashris <[email protected]>
 caishan <[email protected]>
@@ -224,6 +223,7 @@
 haobing1 <[email protected]>
 iccha-sethi <[email protected]>
 iccha.sethi <[email protected]>
+imacdonn <[email protected]>
 isethi <[email protected]>
 jaypipes <[email protected]>
 ji-xuepeng <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-glanceclient-2.12.1/ChangeLog 
new/python-glanceclient-2.13.1/ChangeLog
--- old/python-glanceclient-2.12.1/ChangeLog    2018-07-26 23:24:34.000000000 
+0200
+++ new/python-glanceclient-2.13.1/ChangeLog    2018-12-12 04:56:32.000000000 
+0100
@@ -1,6 +1,18 @@
 CHANGES
 =======
 
+2.13.1
+------
+
+* Don't quote colon in HTTP headers
+
+2.13.0
+------
+
+* Use "multihash" for data download validation
+* Refactor periodic "tips" jobs
+* import zuul job settings from project-config
+
 2.12.1
 ------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-glanceclient-2.12.1/PKG-INFO 
new/python-glanceclient-2.13.1/PKG-INFO
--- old/python-glanceclient-2.12.1/PKG-INFO     2018-07-26 23:24:35.000000000 
+0200
+++ new/python-glanceclient-2.13.1/PKG-INFO     2018-12-12 04:56:32.000000000 
+0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: python-glanceclient
-Version: 2.12.1
+Version: 2.13.1
 Summary: OpenStack Image API Client Library
 Home-page: https://docs.openstack.org/python-glanceclient/latest/
 Author: OpenStack
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/glanceclient/common/http.py 
new/python-glanceclient-2.13.1/glanceclient/common/http.py
--- old/python-glanceclient-2.12.1/glanceclient/common/http.py  2018-07-26 
23:20:46.000000000 +0200
+++ new/python-glanceclient-2.13.1/glanceclient/common/http.py  2018-12-12 
04:53:59.000000000 +0100
@@ -66,7 +66,11 @@
     for h, v in headers.items():
         if v is not None:
             # if the item is token, do not quote '+' as well.
-            safe = '=+/' if h in TOKEN_HEADERS else '/'
+            # NOTE(imacdonn): urlparse.quote() is intended for quoting the
+            # path part of a URL, but headers like x-image-meta-location
+            # include an entire URL. We should avoid encoding the colon in
+            # this case (bug #1788942)
+            safe = '=+/' if h in TOKEN_HEADERS else '/:'
             if six.PY2:
                 # incoming items may be unicode, so get them into something
                 # the py2 version of urllib can handle before percent encoding
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/glanceclient/common/utils.py 
new/python-glanceclient-2.13.1/glanceclient/common/utils.py
--- old/python-glanceclient-2.12.1/glanceclient/common/utils.py 2018-07-26 
23:20:46.000000000 +0200
+++ new/python-glanceclient-2.13.1/glanceclient/common/utils.py 2018-12-12 
04:53:59.000000000 +0100
@@ -449,6 +449,26 @@
                       (md5sum, checksum))
 
 
+def serious_integrity_iter(iter, hasher, hash_value):
+    """Check image data integrity using the Glance "multihash".
+
+    :param iter: iterable containing the image data
+    :param hasher: a hashlib object
+    :param hash_value: hexdigest of the image data
+    :raises: IOError if the hashdigest of the data is not hash_value
+    """
+    for chunk in iter:
+        yield chunk
+        if isinstance(chunk, six.string_types):
+            chunk = six.b(chunk)
+        hasher.update(chunk)
+    computed = hasher.hexdigest()
+    if computed != hash_value:
+        raise IOError(errno.EPIPE,
+                      'Corrupt image download. Hash was %s expected %s' %
+                      (computed, hash_value))
+
+
 def memoized_property(fn):
     attr_name = '_lazy_once_' + fn.__name__
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/glanceclient/tests/unit/test_http.py 
new/python-glanceclient-2.13.1/glanceclient/tests/unit/test_http.py
--- old/python-glanceclient-2.12.1/glanceclient/tests/unit/test_http.py 
2018-07-26 23:20:46.000000000 +0200
+++ new/python-glanceclient-2.13.1/glanceclient/tests/unit/test_http.py 
2018-12-12 04:53:59.000000000 +0100
@@ -216,7 +216,11 @@
 
     def test_headers_encoding(self):
         value = u'ni\xf1o'
-        headers = {"test": value, "none-val": None, "Name": "value"}
+        fake_location = b'http://web_server:80/images/fake.img'
+        headers = {"test": value,
+                   "none-val": None,
+                   "Name": "value",
+                   "x-image-meta-location": fake_location}
         encoded = http.encode_headers(headers)
         # Bug #1766235: According to RFC 8187, headers must be
         # encoded as 7-bit ASCII, so expect to see only displayable
@@ -225,6 +229,8 @@
         self.assertNotIn("none-val", encoded)
         self.assertNotIn(b"none-val", encoded)
         self.assertEqual(b"value", encoded[b"Name"])
+        # Bug #1788942: Colons in URL should not get percent-encoded
+        self.assertEqual(fake_location, encoded[b"x-image-meta-location"])
 
     @mock.patch('keystoneauth1.adapter.Adapter.request')
     def test_http_duplicate_content_type_headers(self, mock_ksarq):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/glanceclient/tests/unit/test_shell.py 
new/python-glanceclient-2.13.1/glanceclient/tests/unit/test_shell.py
--- old/python-glanceclient-2.12.1/glanceclient/tests/unit/test_shell.py        
2018-07-26 23:20:46.000000000 +0200
+++ new/python-glanceclient-2.13.1/glanceclient/tests/unit/test_shell.py        
2018-12-12 04:53:59.000000000 +0100
@@ -965,7 +965,9 @@
             self.requests = self.useFixture(rm_fixture.Fixture())
             self.requests.get('http://example.com/v2/images/%s/file' % id,
                               headers=headers, raw=fake)
-
+            self.requests.get('http://example.com/v2/images/%s' % id,
+                              headers={'Content-type': 'application/json'},
+                              json=image_show_fixture)
             shell = openstack_shell.OpenStackImagesShell()
             argstr = ('--os-image-api-version 2 --os-auth-token faketoken '
                       '--os-image-url http://example.com '
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/glanceclient/tests/unit/v2/fixtures.py 
new/python-glanceclient-2.13.1/glanceclient/tests/unit/v2/fixtures.py
--- old/python-glanceclient-2.12.1/glanceclient/tests/unit/v2/fixtures.py       
2018-07-26 23:20:46.000000000 +0200
+++ new/python-glanceclient-2.13.1/glanceclient/tests/unit/v2/fixtures.py       
2018-12-12 04:53:59.000000000 +0100
@@ -14,6 +14,9 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import hashlib
+
+
 UUID = "3fc2ba62-9a02-433e-b565-d493ffc69034"
 
 image_list_fixture = {
@@ -65,7 +68,9 @@
     "tags": [],
     "updated_at": "2015-07-24T12:18:13Z",
     "virtual_size": "null",
-    "visibility": "shared"
+    "visibility": "shared",
+    "os_hash_algo": "sha384",
+    "os_hash_value": hashlib.sha384(b'DATA').hexdigest()
 }
 
 image_create_fixture = {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/glanceclient/tests/unit/v2/test_images.py 
new/python-glanceclient-2.13.1/glanceclient/tests/unit/v2/test_images.py
--- old/python-glanceclient-2.12.1/glanceclient/tests/unit/v2/test_images.py    
2018-07-26 23:20:46.000000000 +0200
+++ new/python-glanceclient-2.13.1/glanceclient/tests/unit/v2/test_images.py    
2018-12-12 04:53:59.000000000 +0100
@@ -14,6 +14,7 @@
 #    under the License.
 
 import errno
+import hashlib
 import mock
 import testtools
 
@@ -193,7 +194,43 @@
             'A',
         ),
     },
-    '/v2/images/66fb18d6-db27-11e1-a1eb-080027cbe205/file': {
+    '/v2/images/5cc4bebc-db27-11e1-a1eb-080027cbe205': {
+        'GET': (
+            {},
+            {},
+        ),
+    },
+    '/v2/images/headeronly-db27-11e1-a1eb-080027cbe205/file': {
+        'GET': (
+            {
+                'content-md5': 'wrong'
+            },
+            'BB',
+        ),
+    },
+    '/v2/images/headeronly-db27-11e1-a1eb-080027cbe205': {
+        'GET': (
+            {},
+            {},
+        ),
+    },
+    '/v2/images/chkonly-db27-11e1-a1eb-080027cbe205/file': {
+        'GET': (
+            {
+                'content-md5': 'wrong'
+            },
+            'BB',
+        ),
+    },
+    '/v2/images/chkonly-db27-11e1-a1eb-080027cbe205': {
+        'GET': (
+            {},
+            {
+                'checksum': 'wrong',
+            },
+        ),
+    },
+    '/v2/images/multihash-db27-11e1-a1eb-080027cbe205/file': {
         'GET': (
             {
                 'content-md5': 'wrong'
@@ -201,7 +238,67 @@
             'BB',
         ),
     },
-    '/v2/images/1b1c6366-dd57-11e1-af0f-02163e68b1d8/file': {
+    '/v2/images/multihash-db27-11e1-a1eb-080027cbe205': {
+        'GET': (
+            {},
+            {
+                'checksum': 'wrong',
+                'os_hash_algo': 'md5',
+                'os_hash_value': 'junk'
+            },
+        ),
+    },
+    '/v2/images/badalgo-db27-11e1-a1eb-080027cbe205/file': {
+        'GET': (
+            {
+                'content-md5': hashlib.md5(b'BB').hexdigest()
+            },
+            'BB',
+        ),
+    },
+    '/v2/images/badalgo-db27-11e1-a1eb-080027cbe205': {
+        'GET': (
+            {},
+            {
+                'checksum': hashlib.md5(b'BB').hexdigest(),
+                'os_hash_algo': 'not_an_algo',
+                'os_hash_value': 'whatever'
+            },
+        ),
+    },
+    '/v2/images/bad-multihash-value-good-checksum/file': {
+        'GET': (
+            {
+                'content-md5': hashlib.md5(b'GOODCHECKSUM').hexdigest()
+            },
+            'GOODCHECKSUM',
+        ),
+    },
+    '/v2/images/bad-multihash-value-good-checksum': {
+        'GET': (
+            {},
+            {
+                'checksum': hashlib.md5(b'GOODCHECKSUM').hexdigest(),
+                'os_hash_algo': 'sha512',
+                'os_hash_value': 'badmultihashvalue'
+            },
+        ),
+    },
+    '/v2/images/headeronly-dd57-11e1-af0f-02163e68b1d8/file': {
+        'GET': (
+            {
+                'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae'
+            },
+            'CCC',
+        ),
+    },
+    '/v2/images/headeronly-dd57-11e1-af0f-02163e68b1d8': {
+        'GET': (
+            {},
+            {},
+        ),
+    },
+    '/v2/images/chkonly-dd57-11e1-af0f-02163e68b1d8/file': {
         'GET': (
             {
                 'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae'
@@ -209,6 +306,32 @@
             'CCC',
         ),
     },
+    '/v2/images/chkonly-dd57-11e1-af0f-02163e68b1d8': {
+        'GET': (
+            {},
+            {
+                'checksum': 'defb99e69a9f1f6e06f15006b1f166ae',
+            },
+        ),
+    },
+    '/v2/images/multihash-dd57-11e1-af0f-02163e68b1d8/file': {
+        'GET': (
+            {
+                'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae'
+            },
+            'CCC',
+        ),
+    },
+    '/v2/images/multihash-dd57-11e1-af0f-02163e68b1d8': {
+        'GET': (
+            {},
+            {
+                'checksum': 'defb99e69a9f1f6e06f15006b1f166ae',
+                'os_hash_algo': 'sha384',
+                'os_hash_value': hashlib.sha384(b'CCC').hexdigest()
+            },
+        ),
+    },
     '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a/actions/reactivate': {
         'POST': ({}, None)
     },
@@ -846,12 +969,24 @@
         self.assertEqual('A', body)
 
     def test_data_with_wrong_checksum(self):
-        body = self.controller.data('66fb18d6-db27-11e1-a1eb-080027cbe205',
+        body = self.controller.data('headeronly-db27-11e1-a1eb-080027cbe205',
                                     do_checksum=False)
         body = ''.join([b for b in body])
         self.assertEqual('BB', body)
+        body = self.controller.data('headeronly-db27-11e1-a1eb-080027cbe205')
+        try:
+            body = ''.join([b for b in body])
+            self.fail('data did not raise an error.')
+        except IOError as e:
+            self.assertEqual(errno.EPIPE, e.errno)
+            msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong'
+            self.assertIn(msg, str(e))
 
-        body = self.controller.data('66fb18d6-db27-11e1-a1eb-080027cbe205')
+        body = self.controller.data('chkonly-db27-11e1-a1eb-080027cbe205',
+                                    do_checksum=False)
+        body = ''.join([b for b in body])
+        self.assertEqual('BB', body)
+        body = self.controller.data('chkonly-db27-11e1-a1eb-080027cbe205')
         try:
             body = ''.join([b for b in body])
             self.fail('data did not raise an error.')
@@ -860,15 +995,103 @@
             msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong'
             self.assertIn(msg, str(e))
 
-    def test_data_with_checksum(self):
-        body = self.controller.data('1b1c6366-dd57-11e1-af0f-02163e68b1d8',
+        body = self.controller.data('multihash-db27-11e1-a1eb-080027cbe205',
                                     do_checksum=False)
         body = ''.join([b for b in body])
-        self.assertEqual('CCC', body)
+        self.assertEqual('BB', body)
+        body = self.controller.data('multihash-db27-11e1-a1eb-080027cbe205')
+        try:
+            body = ''.join([b for b in body])
+            self.fail('data did not raise an error.')
+        except IOError as e:
+            self.assertEqual(errno.EPIPE, e.errno)
+            msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected junk'
+            self.assertIn(msg, str(e))
+
+        body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205',
+                                    do_checksum=False)
+        body = ''.join([b for b in body])
+        self.assertEqual('BB', body)
+        try:
+            body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205')
+            self.fail('bad os_hash_algo did not raise an error.')
+        except ValueError as e:
+            msg = 'unsupported hash type not_an_algo'
+            self.assertIn(msg, str(e))
+
+    def test_data_with_checksum(self):
+        for prefix in ['headeronly', 'chkonly', 'multihash']:
+            body = self.controller.data(prefix +
+                                        '-dd57-11e1-af0f-02163e68b1d8',
+                                        do_checksum=False)
+            body = ''.join([b for b in body])
+            self.assertEqual('CCC', body)
+
+            body = self.controller.data(prefix +
+                                        '-dd57-11e1-af0f-02163e68b1d8')
+            body = ''.join([b for b in body])
+            self.assertEqual('CCC', body)
+
+    def test_data_with_checksum_and_fallback(self):
+        # make sure the allow_md5_fallback option does not cause any
+        # incorrect behavior when fallback is not needed
+        for prefix in ['headeronly', 'chkonly', 'multihash']:
+            body = self.controller.data(prefix +
+                                        '-dd57-11e1-af0f-02163e68b1d8',
+                                        do_checksum=False,
+                                        allow_md5_fallback=True)
+            body = ''.join([b for b in body])
+            self.assertEqual('CCC', body)
 
-        body = self.controller.data('1b1c6366-dd57-11e1-af0f-02163e68b1d8')
+            body = self.controller.data(prefix +
+                                        '-dd57-11e1-af0f-02163e68b1d8',
+                                        allow_md5_fallback=True)
+            body = ''.join([b for b in body])
+            self.assertEqual('CCC', body)
+
+    def test_data_with_bad_hash_algo_and_fallback(self):
+        # shouldn't matter when do_checksum is False
+        body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205',
+                                    do_checksum=False,
+                                    allow_md5_fallback=True)
+        body = ''.join([b for b in body])
+        self.assertEqual('BB', body)
+
+        # default value for do_checksum is True
+        body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205',
+                                    allow_md5_fallback=True)
+        body = ''.join([b for b in body])
+        self.assertEqual('BB', body)
+
+    def test_neg_data_with_bad_hash_value_and_fallback_enabled(self):
+        # make sure download fails when good hash_algo but bad hash_value
+        # even when correct checksum is present regardless of
+        # allow_md5_fallback setting
+        body = self.controller.data('bad-multihash-value-good-checksum',
+                                    allow_md5_fallback=False)
+        try:
+            body = ''.join([b for b in body])
+            self.fail('bad os_hash_value did not raise an error.')
+        except IOError as e:
+            self.assertEqual(errno.EPIPE, e.errno)
+            msg = 'expected badmultihashvalue'
+            self.assertIn(msg, str(e))
+
+        body = self.controller.data('bad-multihash-value-good-checksum',
+                                    allow_md5_fallback=True)
+        try:
+            body = ''.join([b for b in body])
+            self.fail('bad os_hash_value did not raise an error.')
+        except IOError as e:
+            self.assertEqual(errno.EPIPE, e.errno)
+            msg = 'expected badmultihashvalue'
+            self.assertIn(msg, str(e))
+
+        # download should succeed when do_checksum is off, though
+        body = self.controller.data('bad-multihash-value-good-checksum',
+                                    do_checksum=False)
         body = ''.join([b for b in body])
-        self.assertEqual('CCC', body)
+        self.assertEqual('GOODCHECKSUM', body)
 
     def test_image_import(self):
         uri = 'http://example.com/image.qcow'
@@ -883,7 +1106,7 @@
     def test_download_no_data(self):
         resp = utils.FakeResponse(headers={}, status_code=204)
         self.controller.controller.http_client.get = mock.Mock(
-            return_value=(resp, None))
+            return_value=(resp, {}))
         self.controller.data('image_id')
 
     def test_download_forbidden(self):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/glanceclient/tests/unit/v2/test_shell_v2.py 
new/python-glanceclient-2.13.1/glanceclient/tests/unit/v2/test_shell_v2.py
--- old/python-glanceclient-2.12.1/glanceclient/tests/unit/v2/test_shell_v2.py  
2018-07-26 23:20:46.000000000 +0200
+++ new/python-glanceclient-2.13.1/glanceclient/tests/unit/v2/test_shell_v2.py  
2018-12-12 04:54:05.000000000 +0100
@@ -1729,7 +1729,8 @@
 
     def test_image_download(self):
         args = self._make_args(
-            {'id': 'IMG-01', 'file': 'test', 'progress': True})
+            {'id': 'IMG-01', 'file': 'test', 'progress': True,
+             'allow_md5_fallback': False})
 
         with mock.patch.object(self.gc.images, 'data') as mocked_data, \
                 mock.patch.object(utils, '_extract_request_id'):
@@ -1737,14 +1738,27 @@
                 [c for c in 'abcdef'])
 
             test_shell.do_image_download(self.gc, args)
-            mocked_data.assert_called_once_with('IMG-01')
+            mocked_data.assert_called_once_with('IMG-01',
+                                                allow_md5_fallback=False)
+
+        # check that non-default value is being passed correctly
+        args.allow_md5_fallback = True
+        with mock.patch.object(self.gc.images, 'data') as mocked_data, \
+                mock.patch.object(utils, '_extract_request_id'):
+            mocked_data.return_value = utils.RequestIdProxy(
+                [c for c in 'abcdef'])
+
+            test_shell.do_image_download(self.gc, args)
+            mocked_data.assert_called_once_with('IMG-01',
+                                                allow_md5_fallback=True)
 
     @mock.patch.object(utils, 'exit')
     @mock.patch('sys.stdout', autospec=True)
     def test_image_download_no_file_arg(self, mocked_stdout,
                                         mocked_utils_exit):
         # Indicate that no file name was given as command line argument
-        args = self._make_args({'id': '1234', 'file': None, 'progress': False})
+        args = self._make_args({'id': '1234', 'file': None, 'progress': False,
+                                'allow_md5_fallback': False})
         # Indicate that no file is specified for output redirection
         mocked_stdout.isatty = lambda: True
         test_shell.do_image_download(self.gc, args)
@@ -1835,7 +1849,8 @@
     def test_do_image_download_with_forbidden_id(self, mocked_print_err,
                                                  mocked_stdout):
         args = self._make_args({'id': 'IMG-01', 'file': None,
-                                'progress': False})
+                                'progress': False,
+                                'allow_md5_fallback': False})
         mocked_stdout.isatty = lambda: False
         with mock.patch.object(self.gc.images, 'data') as mocked_data:
             mocked_data.side_effect = exc.HTTPForbidden
@@ -1852,7 +1867,8 @@
     @mock.patch.object(utils, 'print_err')
     def test_do_image_download_with_500(self, mocked_print_err, mocked_stdout):
         args = self._make_args({'id': 'IMG-01', 'file': None,
-                                'progress': False})
+                                'progress': False,
+                                'allow_md5_fallback': False})
         mocked_stdout.isatty = lambda: False
         with mock.patch.object(self.gc.images, 'data') as mocked_data:
             mocked_data.side_effect = exc.HTTPInternalServerError
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-glanceclient-2.12.1/glanceclient/v2/images.py 
new/python-glanceclient-2.13.1/glanceclient/v2/images.py
--- old/python-glanceclient-2.12.1/glanceclient/v2/images.py    2018-07-26 
23:20:46.000000000 +0200
+++ new/python-glanceclient-2.13.1/glanceclient/v2/images.py    2018-12-12 
04:53:59.000000000 +0100
@@ -13,6 +13,7 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import hashlib
 import json
 from oslo_utils import encodeutils
 from requests import codes
@@ -197,13 +198,39 @@
         return self._get(image_id)
 
     @utils.add_req_id_to_object()
-    def data(self, image_id, do_checksum=True):
+    def data(self, image_id, do_checksum=True, allow_md5_fallback=False):
         """Retrieve data of an image.
 
-        :param image_id:    ID of the image to download.
-        :param do_checksum: Enable/disable checksum validation.
-        :returns: An iterable body or None
+        When do_checksum is enabled, validation proceeds as follows:
+
+        1. if the image has a 'os_hash_value' property, the algorithm
+           specified in the image's 'os_hash_algo' property will be used
+           to validate against the 'os_hash_value' value.  If the
+           specified hash algorithm is not available AND allow_md5_fallback
+           is True, then continue to step #2
+        2. else if the image has a checksum property, MD5 is used to
+           validate against the 'checksum' value
+        3. else if the download response has a 'content-md5' header, MD5
+           is used to validate against the header value
+        4. if none of 1-3 obtain, the data is **not validated** (this is
+           compatible with legacy behavior)
+
+        :param image_id:    ID of the image to download
+        :param do_checksum: Enable/disable checksum validation
+        :param allow_md5_fallback:
+            Use the MD5 checksum for validation if the algorithm specified by
+            the image's 'os_hash_algo' property is not available
+        :returns: An iterable body or ``None``
         """
+        if do_checksum:
+            # doing this first to prevent race condition if image record
+            # is deleted during the image download
+            url = '/v2/images/%s' % image_id
+            resp, image_meta = self.http_client.get(url)
+            meta_checksum = image_meta.get('checksum', None)
+            meta_hash_value = image_meta.get('os_hash_value', None)
+            meta_hash_algo = image_meta.get('os_hash_algo', None)
+
         url = '/v2/images/%s/file' % image_id
         resp, body = self.http_client.get(url)
         if resp.status_code == codes.no_content:
@@ -212,8 +239,32 @@
         checksum = resp.headers.get('content-md5', None)
         content_length = int(resp.headers.get('content-length', 0))
 
-        if do_checksum and checksum is not None:
-            body = utils.integrity_iter(body, checksum)
+        check_md5sum = do_checksum
+        if do_checksum and meta_hash_value is not None:
+            try:
+                hasher = hashlib.new(str(meta_hash_algo))
+                body = utils.serious_integrity_iter(body,
+                                                    hasher,
+                                                    meta_hash_value)
+                check_md5sum = False
+            except ValueError as ve:
+                if (str(ve).startswith('unsupported hash type') and
+                        allow_md5_fallback):
+                    check_md5sum = True
+                else:
+                    raise
+
+        if do_checksum and check_md5sum:
+            if meta_checksum is not None:
+                body = utils.integrity_iter(body, meta_checksum)
+            elif checksum is not None:
+                body = utils.integrity_iter(body, checksum)
+            else:
+                # NOTE(rosmaita): this preserves legacy behavior to return the
+                # image data when checksumming is requested but there's no
+                # 'content-md5' header in the response.  Just want to make it
+                # clear that we're doing this on purpose.
+                pass
 
         return utils.IterableWithLength(body, content_length), resp
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/python-glanceclient-2.12.1/glanceclient/v2/shell.py 
new/python-glanceclient-2.13.1/glanceclient/v2/shell.py
--- old/python-glanceclient-2.12.1/glanceclient/v2/shell.py     2018-07-26 
23:20:46.000000000 +0200
+++ new/python-glanceclient-2.13.1/glanceclient/v2/shell.py     2018-12-12 
04:54:05.000000000 +0100
@@ -490,6 +490,17 @@
         utils.print_dict(stores_info)
 
 
[email protected]('--allow-md5-fallback', action='store_true',
+           default=utils.env('OS_IMAGE_ALLOW_MD5_FALLBACK', default=False),
+           help=_('If os_hash_algo and os_hash_value properties are available '
+                  'on the image, they will be used to validate the downloaded '
+                  'image data. If the indicated secure hash algorithm is not '
+                  'available on the client, the download will fail. Use this '
+                  'flag to indicate that in such a case the legacy MD5 image '
+                  'checksum should be used to validate the downloaded data. '
+                  'You can also set the enviroment variable '
+                  'OS_IMAGE_ALLOW_MD5_FALLBACK to any value to activate this '
+                  'option.'))
 @utils.arg('--file', metavar='<FILE>',
            help=_('Local file to save downloaded image data to. '
                   'If this is not specified and there is no redirection '
@@ -506,7 +517,8 @@
         utils.exit(msg)
 
     try:
-        body = gc.images.data(args.id)
+        body = gc.images.data(args.id,
+                              allow_md5_fallback=args.allow_md5_fallback)
     except (exc.HTTPForbidden, exc.HTTPException) as e:
         msg = "Unable to download image '%s'. (%s)" % (args.id, e)
         utils.exit(msg)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/python_glanceclient.egg-info/PKG-INFO 
new/python-glanceclient-2.13.1/python_glanceclient.egg-info/PKG-INFO
--- old/python-glanceclient-2.12.1/python_glanceclient.egg-info/PKG-INFO        
2018-07-26 23:24:34.000000000 +0200
+++ new/python-glanceclient-2.13.1/python_glanceclient.egg-info/PKG-INFO        
2018-12-12 04:56:32.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: python-glanceclient
-Version: 2.12.1
+Version: 2.13.1
 Summary: OpenStack Image API Client Library
 Home-page: https://docs.openstack.org/python-glanceclient/latest/
 Author: OpenStack
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/python_glanceclient.egg-info/SOURCES.txt 
new/python-glanceclient-2.13.1/python_glanceclient.egg-info/SOURCES.txt
--- old/python-glanceclient-2.12.1/python_glanceclient.egg-info/SOURCES.txt     
2018-07-26 23:24:35.000000000 +0200
+++ new/python-glanceclient-2.13.1/python_glanceclient.egg-info/SOURCES.txt     
2018-12-12 04:56:32.000000000 +0100
@@ -121,6 +121,7 @@
 releasenotes/notes/http-headers-per-rfc-8187-aafa3199f863be81.yaml
 releasenotes/notes/log-request-id-e7f67a23a0ed5c7b.yaml
 releasenotes/notes/multi-store-support-acc7ad0e7e8b6f99.yaml
+releasenotes/notes/multihash-download-verification-596e91bf7b68e7db.yaml
 releasenotes/notes/multihash-support-f1474590cf3ef5cf.yaml
 releasenotes/notes/pike-relnote-2c77b01aa8799f35.yaml
 releasenotes/notes/return-request-id-to-caller-47f4c0a684b1d88e.yaml
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/python_glanceclient.egg-info/pbr.json 
new/python-glanceclient-2.13.1/python_glanceclient.egg-info/pbr.json
--- old/python-glanceclient-2.12.1/python_glanceclient.egg-info/pbr.json        
2018-07-26 23:24:34.000000000 +0200
+++ new/python-glanceclient-2.13.1/python_glanceclient.egg-info/pbr.json        
2018-12-12 04:56:32.000000000 +0100
@@ -1 +1 @@
-{"git_version": "b79154c", "is_release": true}
\ No newline at end of file
+{"git_version": "e0673a1", "is_release": true}
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/python-glanceclient-2.12.1/releasenotes/notes/multihash-download-verification-596e91bf7b68e7db.yaml
 
new/python-glanceclient-2.13.1/releasenotes/notes/multihash-download-verification-596e91bf7b68e7db.yaml
--- 
old/python-glanceclient-2.12.1/releasenotes/notes/multihash-download-verification-596e91bf7b68e7db.yaml
     1970-01-01 01:00:00.000000000 +0100
+++ 
new/python-glanceclient-2.13.1/releasenotes/notes/multihash-download-verification-596e91bf7b68e7db.yaml
     2018-12-12 04:53:59.000000000 +0100
@@ -0,0 +1,41 @@
+---
+features:
+  - |
+    This release adds verification of image data downloads using the Glance
+    "multihash" feature introduced in the OpenStack Rocky release.  When
+    the ``os_hash_value`` is populated on an image, the glanceclient will
+    verify this value by computing the hexdigest of the downloaded data
+    using the algorithm specified by the image's ``os_hash_algo`` property.
+
+    Because the secure hash algorithm specified is determined by the cloud
+    provider, it is possible that the ``os_hash_algo`` may identify an
+    algorithm not available in the version of the Python ``hashlib`` library
+    used by the client.  In such a case the download will fail due to an
+    unsupported hash type.  In the event this occurs, a new option,
+    ``--allow-md5-fallback``, is introduced to the ``image-download`` command.
+    When present, this option will allow the glanceclient to use the legacy
+    MD5 checksum to verify the downloaded data if the secure hash algorithm
+    specified by the ``os_hash_algo`` image property is not supported.
+
+    Note that the fallback is *not* used in the case where the algorithm is
+    supported but the hexdigest of the downloaded data does not match the
+    ``os_hash_value``.  In that case the download fails regardless of whether
+    the option is present or not.
+
+    Whether using the ``--allow-md5-fallback`` option is a good idea depends
+    upon the user's expectations for the verification.  MD5 is an insecure
+    hashing algorithm, so if you are interested in making sure that the
+    downloaded image data has not been replaced by a datastream carefully
+    crafted to have the same MD5 checksum, then you should not use the
+    fallback.  If, however, you are using Glance in a trusted environment
+    and your interest is simply to verify that no bits have flipped during
+    the data transfer, the MD5 fallback is sufficient for that purpose.  That
+    being said, it is our recommendation that the multihash should be used
+    whenever possible.
+security:
+  - |
+    This release of the glanceclient uses the Glance "multihash" feature,
+    introduced in Rocky, to use a secure hashing algorithm to verify the
+    integrity of downloaded data.  Legacy images without the "multihash"
+    image properties (``os_hash_algo`` and ``os_hash_value``) are verified
+    using the MD5 ``checksum`` image property.


Reply via email to