summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Rosmaita <rosmaita.fossdev@gmail.com>2018-07-30 15:48:49 -0400
committerBrian Rosmaita <rosmaita.fossdev@gmail.com>2018-07-31 21:28:38 -0400
commit0b24dbd620f88b4d36bf6e0f8975f10aa8709b86 (patch)
tree237b02d7a9cb109a5030600de1a9a3cdf0cd2f9d
parentff77f59bd4376be3bed8f8c62258f9973b7ef1f2 (diff)
Multihash implementation for Glance
Partially implements blueprint multihash. Requires glance_store 0.26.1 Co-authored-by: Scott McClymont <scott.mcclymont@verizonwireless.com> Co-authored-by: Brian Rosmaita <rosmaita.fossdev@gmail.com> Change-Id: Ib28ea1f6c431db6434dbab2a234018e82d5a6d1a
Notes
Notes (review): Code-Review+2: Erno Kuvaja <jokke@usr.fi> Code-Review+2: Abhishek Kekane <akekane@redhat.com> Code-Review+2: Sean McGinnis <sean.mcginnis@gmail.com> Workflow+1: Sean McGinnis <sean.mcginnis@gmail.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Wed, 01 Aug 2018 16:44:45 +0000 Reviewed-on: https://review.openstack.org/587225 Project: openstack/glance Branch: refs/heads/master
-rw-r--r--api-ref/source/v2/images-images-v2.inc8
-rw-r--r--api-ref/source/v2/images-parameters.yaml21
-rw-r--r--api-ref/source/v2/samples/image-create-response.json2
-rw-r--r--api-ref/source/v2/samples/image-details-deactivate-response.json2
-rw-r--r--api-ref/source/v2/samples/image-show-response.json2
-rw-r--r--api-ref/source/v2/samples/image-update-response.json2
-rw-r--r--api-ref/source/v2/samples/images-list-response.json4
-rw-r--r--api-ref/source/v2/samples/schemas-image-show-response.json18
-rw-r--r--api-ref/source/v2/samples/schemas-images-list-response.json18
-rw-r--r--glance/api/authorization.py2
-rw-r--r--glance/api/v2/images.py19
-rw-r--r--glance/common/config.py34
-rw-r--r--glance/db/__init__.py4
-rw-r--r--glance/db/simple/api.py4
-rw-r--r--glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate02_empty.py26
-rw-r--r--glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract02_empty.py25
-rw-r--r--glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand02_add_os_hash_.py33
-rw-r--r--glance/db/sqlalchemy/api.py4
-rw-r--r--glance/db/sqlalchemy/models.py5
-rw-r--r--glance/domain/__init__.py5
-rw-r--r--glance/domain/proxy.py2
-rw-r--r--glance/location.py11
-rw-r--r--glance/tests/functional/db/migrations/test_rocky_expand02.py41
-rw-r--r--glance/tests/functional/v2/test_images.py83
-rw-r--r--glance/tests/functional/v2/test_schemas.py2
-rw-r--r--glance/tests/unit/test_policy.py3
-rw-r--r--glance/tests/unit/utils.py24
-rw-r--r--glance/tests/unit/v2/test_images_resource.py80
-rw-r--r--glance/tests/unit/v2/test_schemas_resource.py3
-rw-r--r--lower-constraints.txt2
-rw-r--r--releasenotes/notes/multihash-081466a98601da20.yaml55
-rw-r--r--requirements.txt2
32 files changed, 511 insertions, 35 deletions
diff --git a/api-ref/source/v2/images-images-v2.inc b/api-ref/source/v2/images-images-v2.inc
index f8ee981..aead71e 100644
--- a/api-ref/source/v2/images-images-v2.inc
+++ b/api-ref/source/v2/images-images-v2.inc
@@ -202,6 +202,8 @@ Response Parameters
202 - min_disk: min_disk 202 - min_disk: min_disk
203 - min_ram: min_ram 203 - min_ram: min_ram
204 - name: name 204 - name: name
205 - os_hash_algo: os_hash_algo
206 - os_hash_value: os_hash_value
205 - owner: owner 207 - owner: owner
206 - protected: protected 208 - protected: protected
207 - schema: schema-image 209 - schema: schema-image
@@ -266,6 +268,8 @@ Response Parameters
266 - min_disk: min_disk 268 - min_disk: min_disk
267 - min_ram: min_ram 269 - min_ram: min_ram
268 - name: name 270 - name: name
271 - os_hash_algo: os_hash_algo
272 - os_hash_value: os_hash_value
269 - owner: owner 273 - owner: owner
270 - protected: protected 274 - protected: protected
271 - schema: schema-image 275 - schema: schema-image
@@ -584,8 +588,10 @@ Response Parameters
584 - id: id 588 - id: id
585 - min_disk: min_disk 589 - min_disk: min_disk
586 - min_ram: min_ram 590 - min_ram: min_ram
587 - owner: owner
588 - name: name 591 - name: name
592 - owner: owner
593 - os_hash_algo: os_hash_algo
594 - os_hash_value: os_hash_value
589 - protected: protected 595 - protected: protected
590 - schema: schema-image 596 - schema: schema-image
591 - self: self 597 - self: self
diff --git a/api-ref/source/v2/images-parameters.yaml b/api-ref/source/v2/images-parameters.yaml
index bb5420a..060f6f9 100644
--- a/api-ref/source/v2/images-parameters.yaml
+++ b/api-ref/source/v2/images-parameters.yaml
@@ -484,6 +484,27 @@ next:
484 in: body 484 in: body
485 required: true 485 required: true
486 type: string 486 type: string
487os_hash_algo:
488 description: |
489 The algorithm used to compute a secure hash of the image data for this
490 image. The result of the computation is displayed as the value of the
491 ``os_hash_value`` property. The value might be ``null`` (JSON null
492 data type). The algorithm used is chosen by the cloud operator; it
493 may not be configured by end users. *(Since Image API v2.7)*
494 in: body
495 required: true
496 type: string
497os_hash_value:
498 description: |
499 The hexdigest of the secure hash of the image data computed using the
500 algorithm whose name is the value of the ``os_hash_algo`` property.
501 The value might be ``null`` (JSON null data type) if data has not
502 yet been associated with this image, or if the image was created using
503 a version of the Image Service API prior to version 2.7.
504 *(Since Image API v2.7)*
505 in: body
506 required: true
507 type: string
487owner: 508owner:
488 description: | 509 description: |
489 An identifier for the owner of the image, usually the project (also 510 An identifier for the owner of the image, usually the project (also
diff --git a/api-ref/source/v2/samples/image-create-response.json b/api-ref/source/v2/samples/image-create-response.json
index dd2289e..afcb822 100644
--- a/api-ref/source/v2/samples/image-create-response.json
+++ b/api-ref/source/v2/samples/image-create-response.json
@@ -15,6 +15,8 @@
15 "id": "b2173dd3-7ad6-4362-baa6-a68bce3565cb", 15 "id": "b2173dd3-7ad6-4362-baa6-a68bce3565cb",
16 "file": "/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb/file", 16 "file": "/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb/file",
17 "checksum": null, 17 "checksum": null,
18 "os_hash_algo": null,
19 "os_hash_value": null,
18 "owner": "bab7d5c60cd041a0a36f7c4b6e1dd978", 20 "owner": "bab7d5c60cd041a0a36f7c4b6e1dd978",
19 "virtual_size": null, 21 "virtual_size": null,
20 "min_ram": 0, 22 "min_ram": 0,
diff --git a/api-ref/source/v2/samples/image-details-deactivate-response.json b/api-ref/source/v2/samples/image-details-deactivate-response.json
index 43d41c3..66a5d5b 100644
--- a/api-ref/source/v2/samples/image-details-deactivate-response.json
+++ b/api-ref/source/v2/samples/image-details-deactivate-response.json
@@ -13,6 +13,8 @@
13 "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27", 13 "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
14 "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file", 14 "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
15 "checksum": "64d7c1cd2b6f60c92c14662941cb7913", 15 "checksum": "64d7c1cd2b6f60c92c14662941cb7913",
16 "os_hash_algo": "sha512",
17 "os_hash_value": "073b4523583784fbe01daff81eba092a262ec37ba6d04dd3f52e4cd5c93eb8258af44881345ecda0e49f3d8cc6d2df6b050ff3e72681d723234aff9d17d0cf09"
16 "owner": "5ef70662f8b34079a6eddb8da9d75fe8", 18 "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
17 "size": 13167616, 19 "size": 13167616,
18 "min_ram": 0, 20 "min_ram": 0,
diff --git a/api-ref/source/v2/samples/image-show-response.json b/api-ref/source/v2/samples/image-show-response.json
index 0705535..9660d4f 100644
--- a/api-ref/source/v2/samples/image-show-response.json
+++ b/api-ref/source/v2/samples/image-show-response.json
@@ -13,6 +13,8 @@
13 "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27", 13 "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
14 "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file", 14 "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
15 "checksum": "64d7c1cd2b6f60c92c14662941cb7913", 15 "checksum": "64d7c1cd2b6f60c92c14662941cb7913",
16 "os_hash_algo": "sha512",
17 "os_hash_value": "073b4523583784fbe01daff81eba092a262ec37ba6d04dd3f52e4cd5c93eb8258af44881345ecda0e49f3d8cc6d2df6b050ff3e72681d723234aff9d17d0cf09"
16 "owner": "5ef70662f8b34079a6eddb8da9d75fe8", 18 "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
17 "size": 13167616, 19 "size": 13167616,
18 "min_ram": 0, 20 "min_ram": 0,
diff --git a/api-ref/source/v2/samples/image-update-response.json b/api-ref/source/v2/samples/image-update-response.json
index c337290..3211cc8 100644
--- a/api-ref/source/v2/samples/image-update-response.json
+++ b/api-ref/source/v2/samples/image-update-response.json
@@ -9,6 +9,8 @@
9 "min_ram": 512, 9 "min_ram": 512,
10 "name": "Fedora 17", 10 "name": "Fedora 17",
11 "owner": "02a7fb2dd4ef434c8a628c511dcbbeb6", 11 "owner": "02a7fb2dd4ef434c8a628c511dcbbeb6",
12 "os_hash_algo": "sha512",
13 "os_hash_value": "ef7d1ed957ffafefb324d50ebc6685ed03d0e64549762ba94a1c44e92270cdbb69d7437dd1e101d00dd41684aaecccad1edc5c2e295e66d4733025b052497844"
12 "protected": false, 14 "protected": false,
13 "schema": "/v2/schemas/image", 15 "schema": "/v2/schemas/image",
14 "self": "/v2/images/2b61ed2b-f800-4da0-99ff-396b742b8646", 16 "self": "/v2/images/2b61ed2b-f800-4da0-99ff-396b742b8646",
diff --git a/api-ref/source/v2/samples/images-list-response.json b/api-ref/source/v2/samples/images-list-response.json
index 8e2097a..c44504b 100644
--- a/api-ref/source/v2/samples/images-list-response.json
+++ b/api-ref/source/v2/samples/images-list-response.json
@@ -15,6 +15,8 @@
15 "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27", 15 "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
16 "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file", 16 "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
17 "checksum": "64d7c1cd2b6f60c92c14662941cb7913", 17 "checksum": "64d7c1cd2b6f60c92c14662941cb7913",
18 "os_hash_algo": "sha512",
19 "os_hash_value": "073b4523583784fbe01daff81eba092a262ec37ba6d04dd3f52e4cd5c93eb8258af44881345ecda0e49f3d8cc6d2df6b050ff3e72681d723234aff9d17d0cf09"
18 "owner": "5ef70662f8b34079a6eddb8da9d75fe8", 20 "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
19 "size": 13167616, 21 "size": 13167616,
20 "min_ram": 0, 22 "min_ram": 0,
@@ -36,6 +38,8 @@
36 "id": "781b3762-9469-4cec-b58d-3349e5de4e9c", 38 "id": "781b3762-9469-4cec-b58d-3349e5de4e9c",
37 "file": "/v2/images/781b3762-9469-4cec-b58d-3349e5de4e9c/file", 39 "file": "/v2/images/781b3762-9469-4cec-b58d-3349e5de4e9c/file",
38 "checksum": "afab0f79bac770d61d24b4d0560b5f70", 40 "checksum": "afab0f79bac770d61d24b4d0560b5f70",
41 "os_hash_algo": "sha512",
42 "os_hash_value": "ea3e20140df1cc65f53d4c5b9ee3b38d0d6868f61bbe2230417b0f98cef0e0c7c37f0ebc5c6456fa47f013de48b452617d56c15fdba25e100379bd0e81ee15ec"
39 "owner": "5ef70662f8b34079a6eddb8da9d75fe8", 43 "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
40 "size": 476704768, 44 "size": 476704768,
41 "min_ram": 0, 45 "min_ram": 0,
diff --git a/api-ref/source/v2/samples/schemas-image-show-response.json b/api-ref/source/v2/samples/schemas-image-show-response.json
index 329ccb9..5232ac7 100644
--- a/api-ref/source/v2/samples/schemas-image-show-response.json
+++ b/api-ref/source/v2/samples/schemas-image-show-response.json
@@ -145,6 +145,24 @@
145 "is_base": false, 145 "is_base": false,
146 "type": "string" 146 "type": "string"
147 }, 147 },
148 "os_hash_algo": {
149 "description": "Algorithm to calculate the os_hash_value",
150 "maxLength": 64,
151 "readOnly": true,
152 "type": [
153 "null",
154 "string"
155 ]
156 },
157 "os_hash_value": {
158 "description": "Hexdigest of the image contents using the algorithm specified by the os_hash_algo",
159 "maxLength": 128,
160 "readOnly": true,
161 "type": [
162 "null",
163 "string"
164 ]
165 },
148 "os_version": { 166 "os_version": {
149 "description": "Operating system version as specified by the distributor", 167 "description": "Operating system version as specified by the distributor",
150 "is_base": false, 168 "is_base": false,
diff --git a/api-ref/source/v2/samples/schemas-images-list-response.json b/api-ref/source/v2/samples/schemas-images-list-response.json
index 47992fd..3365b88 100644
--- a/api-ref/source/v2/samples/schemas-images-list-response.json
+++ b/api-ref/source/v2/samples/schemas-images-list-response.json
@@ -166,6 +166,24 @@
166 "is_base": false, 166 "is_base": false,
167 "type": "string" 167 "type": "string"
168 }, 168 },
169 "os_hash_algo": {
170 "description": "Algorithm to calculate the os_hash_value",
171 "maxLength": 64,
172 "readOnly": true,
173 "type": [
174 "null",
175 "string"
176 ]
177 },
178 "os_hash_value": {
179 "description": "Hexdigest of the image contents using the algorithm specified by the os_hash_algo",
180 "maxLength": 128,
181 "readOnly": true,
182 "type": [
183 "null",
184 "string"
185 ]
186 },
169 "os_version": { 187 "os_version": {
170 "description": "Operating system version as specified by the distributor", 188 "description": "Operating system version as specified by the distributor",
171 "is_base": false, 189 "is_base": false,
diff --git a/glance/api/authorization.py b/glance/api/authorization.py
index 6945845..d077dc6 100644
--- a/glance/api/authorization.py
+++ b/glance/api/authorization.py
@@ -315,6 +315,8 @@ class ImmutableImageProxy(object):
315 min_disk = _immutable_attr('base', 'min_disk') 315 min_disk = _immutable_attr('base', 'min_disk')
316 min_ram = _immutable_attr('base', 'min_ram') 316 min_ram = _immutable_attr('base', 'min_ram')
317 protected = _immutable_attr('base', 'protected') 317 protected = _immutable_attr('base', 'protected')
318 os_hash_algo = _immutable_attr('base', 'os_hash_algo')
319 os_hash_value = _immutable_attr('base', 'os_hash_value')
318 os_hidden = _immutable_attr('base', 'os_hidden') 320 os_hidden = _immutable_attr('base', 'os_hidden')
319 locations = _immutable_attr('base', 'locations', proxy=ImmutableLocations) 321 locations = _immutable_attr('base', 'locations', proxy=ImmutableLocations)
320 checksum = _immutable_attr('base', 'checksum') 322 checksum = _immutable_attr('base', 'checksum')
diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py
index 0d7242f..9d70a24 100644
--- a/glance/api/v2/images.py
+++ b/glance/api/v2/images.py
@@ -446,7 +446,8 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
446 _disallowed_properties = ('direct_url', 'self', 'file', 'schema') 446 _disallowed_properties = ('direct_url', 'self', 'file', 'schema')
447 _readonly_properties = ('created_at', 'updated_at', 'status', 'checksum', 447 _readonly_properties = ('created_at', 'updated_at', 'status', 'checksum',
448 'size', 'virtual_size', 'direct_url', 'self', 448 'size', 'virtual_size', 'direct_url', 'self',
449 'file', 'schema', 'id') 449 'file', 'schema', 'id', 'os_hash_algo',
450 'os_hash_value')
450 _reserved_properties = ('location', 'deleted', 'deleted_at') 451 _reserved_properties = ('location', 'deleted', 'deleted_at')
451 _base_properties = ('checksum', 'created_at', 'container_format', 452 _base_properties = ('checksum', 'created_at', 'container_format',
452 'disk_format', 'id', 'min_disk', 'min_ram', 'name', 453 'disk_format', 'id', 'min_disk', 'min_ram', 'name',
@@ -884,7 +885,8 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
884 attributes = ['name', 'disk_format', 'container_format', 885 attributes = ['name', 'disk_format', 'container_format',
885 'visibility', 'size', 'virtual_size', 'status', 886 'visibility', 'size', 'virtual_size', 'status',
886 'checksum', 'protected', 'min_ram', 'min_disk', 887 'checksum', 'protected', 'min_ram', 'min_disk',
887 'owner', 'os_hidden'] 888 'owner', 'os_hidden', 'os_hash_algo',
889 'os_hash_value']
888 for key in attributes: 890 for key in attributes:
889 image_view[key] = getattr(image, key) 891 image_view[key] = getattr(image, key)
890 image_view['id'] = image.image_id 892 image_view['id'] = image.image_id
@@ -1018,6 +1020,19 @@ def get_base_properties():
1018 'description': _('md5 hash of image contents.'), 1020 'description': _('md5 hash of image contents.'),
1019 'maxLength': 32, 1021 'maxLength': 32,
1020 }, 1022 },
1023 'os_hash_algo': {
1024 'type': ['null', 'string'],
1025 'readOnly': True,
1026 'description': _('Algorithm to calculate the os_hash_value'),
1027 'maxLength': 64,
1028 },
1029 'os_hash_value': {
1030 'type': ['null', 'string'],
1031 'readOnly': True,
1032 'description': _('Hexdigest of the image contents using the '
1033 'algorithm specified by the os_hash_algo'),
1034 'maxLength': 128,
1035 },
1021 'owner': { 1036 'owner': {
1022 'type': ['null', 'string'], 1037 'type': ['null', 'string'],
1023 'description': _('Owner of the image'), 1038 'description': _('Owner of the image'),
diff --git a/glance/common/config.py b/glance/common/config.py
index 4ab65be..57a066b 100644
--- a/glance/common/config.py
+++ b/glance/common/config.py
@@ -192,6 +192,40 @@ Related options:
192 * image_property_quota 192 * image_property_quota
193 193
194""")), 194""")),
195 cfg.StrOpt('hashing_algorithm',
196 default='sha512',
197 help=_(""""
198Secure hashing algorithm used for computing the 'os_hash_value' property.
199
200This option configures the Glance "multihash", which consists of two
201image properties: the 'os_hash_algo' and the 'os_hash_value'. The
202'os_hash_algo' will be populated by the value of this configuration
203option, and the 'os_hash_value' will be populated by the hexdigest computed
204when the algorithm is applied to the uploaded or imported image data.
205
206The value must be a valid secure hash algorithm name recognized by the
207python 'hashlib' library. You can determine what these are by examining
208the 'hashlib.algorithms_available' data member of the version of the
209library being used in your Glance installation. For interoperability
210purposes, however, we recommend that you use the set of secure hash
211names supplies by the 'hashlib.algorithms_guaranteed' data member because
212those algorithms are guaranteed to be supported by the 'hashlib' library
213on all platforms. Thus, any image consumer using 'hashlib' locally should
214be able to verify the 'os_hash_value' of the image.
215
216The default value of 'sha512' is a performant secure hash algorithm.
217
218If this option is misconfigured, any attempts to store image data will fail.
219For that reason, we recommend using the default value.
220
221Possible values:
222 * Any secure hash algorithm name recognized by the Python 'hashlib'
223 library
224
225Related options:
226 * None
227
228""")),
195 cfg.IntOpt('image_member_quota', default=128, 229 cfg.IntOpt('image_member_quota', default=128,
196 help=_(""" 230 help=_("""
197Maximum number of image members per image. 231Maximum number of image members per image.
diff --git a/glance/db/__init__.py b/glance/db/__init__.py
index 862adbd..465cecb 100644
--- a/glance/db/__init__.py
+++ b/glance/db/__init__.py
@@ -130,6 +130,8 @@ class ImageRepo(object):
130 protected=db_image['protected'], 130 protected=db_image['protected'],
131 locations=location_strategy.get_ordered_locations(locations), 131 locations=location_strategy.get_ordered_locations(locations),
132 checksum=db_image['checksum'], 132 checksum=db_image['checksum'],
133 os_hash_algo=db_image['os_hash_algo'],
134 os_hash_value=db_image['os_hash_value'],
133 owner=db_image['owner'], 135 owner=db_image['owner'],
134 disk_format=db_image['disk_format'], 136 disk_format=db_image['disk_format'],
135 container_format=db_image['container_format'], 137 container_format=db_image['container_format'],
@@ -162,6 +164,8 @@ class ImageRepo(object):
162 'protected': image.protected, 164 'protected': image.protected,
163 'locations': locations, 165 'locations': locations,
164 'checksum': image.checksum, 166 'checksum': image.checksum,
167 'os_hash_algo': image.os_hash_algo,
168 'os_hash_value': image.os_hash_value,
165 'owner': image.owner, 169 'owner': image.owner,
166 'disk_format': image.disk_format, 170 'disk_format': image.disk_format,
167 'container_format': image.container_format, 171 'container_format': image.container_format,
diff --git a/glance/db/simple/api.py b/glance/db/simple/api.py
index e9ae30c..66b64e7 100644
--- a/glance/db/simple/api.py
+++ b/glance/db/simple/api.py
@@ -225,6 +225,8 @@ def _image_format(image_id, **values):
225 'size': None, 225 'size': None,
226 'virtual_size': None, 226 'virtual_size': None,
227 'checksum': None, 227 'checksum': None,
228 'os_hash_algo': None,
229 'os_hash_value': None,
228 'tags': [], 230 'tags': [],
229 'created_at': dt, 231 'created_at': dt,
230 'updated_at': dt, 232 'updated_at': dt,
@@ -735,7 +737,7 @@ def image_create(context, image_values, v1_mode=False):
735 'protected', 'is_public', 'container_format', 737 'protected', 'is_public', 'container_format',
736 'disk_format', 'created_at', 'updated_at', 'deleted', 738 'disk_format', 'created_at', 'updated_at', 'deleted',
737 'deleted_at', 'properties', 'tags', 'visibility', 739 'deleted_at', 'properties', 'tags', 'visibility',
738 'os_hidden']) 740 'os_hidden', 'os_hash_algo', 'os_hash_value'])
739 741
740 incorrect_keys = set(image_values.keys()) - allowed_keys 742 incorrect_keys = set(image_values.keys()) - allowed_keys
741 if incorrect_keys: 743 if incorrect_keys:
diff --git a/glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate02_empty.py b/glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate02_empty.py
new file mode 100644
index 0000000..62769e8
--- /dev/null
+++ b/glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate02_empty.py
@@ -0,0 +1,26 @@
1# Copyright (C) 2018 Verizon Wireless
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16
17def has_migrations(engine):
18 """Returns true if at least one data row can be migrated."""
19
20 return False
21
22
23def migrate(engine):
24 """Return the number of rows migrated."""
25
26 return 0
diff --git a/glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract02_empty.py b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract02_empty.py
new file mode 100644
index 0000000..919b3ed
--- /dev/null
+++ b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract02_empty.py
@@ -0,0 +1,25 @@
1# Copyright (C) 2018 Verizon Wireless
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16
17# revision identifiers, used by Alembic.
18revision = 'rocky_contract02'
19down_revision = 'rocky_contract01'
20branch_labels = None
21depends_on = 'rocky_expand02'
22
23
24def upgrade():
25 pass
diff --git a/glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand02_add_os_hash_.py b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand02_add_os_hash_.py
new file mode 100644
index 0000000..c800d0f
--- /dev/null
+++ b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand02_add_os_hash_.py
@@ -0,0 +1,33 @@
1# Copyright (C) 2018 Verizon Wireless
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16"""add os_hash_algo and os_hash_value columns to images table"""
17
18from alembic import op
19from sqlalchemy import Column, String
20
21# revision identifiers, used by Alembic.
22revision = 'rocky_expand02'
23down_revision = 'rocky_expand01'
24branch_labels = None
25depends_on = None
26
27
28def upgrade():
29 algo_col = Column('os_hash_algo', String(length=64), nullable=True)
30 value_col = Column('os_hash_value', String(length=128), nullable=True)
31 op.add_column('images', algo_col)
32 op.add_column('images', value_col)
33 op.create_index('os_hash_value_image_idx', 'images', ['os_hash_value'])
diff --git a/glance/db/sqlalchemy/api.py b/glance/db/sqlalchemy/api.py
index 0e68e46..9cddd2b 100644
--- a/glance/db/sqlalchemy/api.py
+++ b/glance/db/sqlalchemy/api.py
@@ -468,6 +468,10 @@ def _make_conditions_from_filters(filters, is_public=None):
468 checksum = filters.pop('checksum') 468 checksum = filters.pop('checksum')
469 image_conditions.append(models.Image.checksum == checksum) 469 image_conditions.append(models.Image.checksum == checksum)
470 470
471 if 'os_hash_value' in filters:
472 os_hash_value = filters.pop('os_hash_value')
473 image_conditions.append(models.Image.os_hash_value == os_hash_value)
474
471 for (k, v) in filters.pop('properties', {}).items(): 475 for (k, v) in filters.pop('properties', {}).items():
472 prop_filters = _make_image_property_condition(key=k, value=v) 476 prop_filters = _make_image_property_condition(key=k, value=v)
473 prop_conditions.append(prop_filters) 477 prop_conditions.append(prop_filters)
diff --git a/glance/db/sqlalchemy/models.py b/glance/db/sqlalchemy/models.py
index 08a3db5..ed61e23 100644
--- a/glance/db/sqlalchemy/models.py
+++ b/glance/db/sqlalchemy/models.py
@@ -120,7 +120,8 @@ class Image(BASE, GlanceBase):
120 Index('owner_image_idx', 'owner'), 120 Index('owner_image_idx', 'owner'),
121 Index('created_at_image_idx', 'created_at'), 121 Index('created_at_image_idx', 'created_at'),
122 Index('updated_at_image_idx', 'updated_at'), 122 Index('updated_at_image_idx', 'updated_at'),
123 Index('os_hidden_image_idx', 'os_hidden')) 123 Index('os_hidden_image_idx', 'os_hidden'),
124 Index('os_hash_value_image_idx', 'os_hash_value'))
124 125
125 id = Column(String(36), primary_key=True, 126 id = Column(String(36), primary_key=True,
126 default=lambda: str(uuid.uuid4())) 127 default=lambda: str(uuid.uuid4()))
@@ -134,6 +135,8 @@ class Image(BASE, GlanceBase):
134 name='image_visibility'), nullable=False, 135 name='image_visibility'), nullable=False,
135 server_default='shared') 136 server_default='shared')
136 checksum = Column(String(32)) 137 checksum = Column(String(32))
138 os_hash_algo = Column(String(64))
139 os_hash_value = Column(String(128))
137 min_disk = Column(Integer, nullable=False, default=0) 140 min_disk = Column(Integer, nullable=False, default=0)
138 min_ram = Column(Integer, nullable=False, default=0) 141 min_ram = Column(Integer, nullable=False, default=0)
139 owner = Column(String(255)) 142 owner = Column(String(255))
diff --git a/glance/domain/__init__.py b/glance/domain/__init__.py
index d72f386..6d78c3d 100644
--- a/glance/domain/__init__.py
+++ b/glance/domain/__init__.py
@@ -48,7 +48,8 @@ def _import_delayed_delete():
48 48
49class ImageFactory(object): 49class ImageFactory(object):
50 _readonly_properties = ['created_at', 'updated_at', 'status', 'checksum', 50 _readonly_properties = ['created_at', 'updated_at', 'status', 'checksum',
51 'size', 'virtual_size'] 51 'os_hash_algo', 'os_hash_value', 'size',
52 'virtual_size']
52 _reserved_properties = ['owner', 'locations', 'deleted', 'deleted_at', 53 _reserved_properties = ['owner', 'locations', 'deleted', 'deleted_at',
53 'direct_url', 'self', 'file', 'schema'] 54 'direct_url', 'self', 'file', 'schema']
54 55
@@ -127,6 +128,8 @@ class Image(object):
127 self.protected = kwargs.pop('protected', False) 128 self.protected = kwargs.pop('protected', False)
128 self.locations = kwargs.pop('locations', []) 129 self.locations = kwargs.pop('locations', [])
129 self.checksum = kwargs.pop('checksum', None) 130 self.checksum = kwargs.pop('checksum', None)
131 self.os_hash_algo = kwargs.pop('os_hash_algo', None)
132 self.os_hash_value = kwargs.pop('os_hash_value', None)
130 self.owner = kwargs.pop('owner', None) 133 self.owner = kwargs.pop('owner', None)
131 self._disk_format = kwargs.pop('disk_format', None) 134 self._disk_format = kwargs.pop('disk_format', None)
132 self._container_format = kwargs.pop('container_format', None) 135 self._container_format = kwargs.pop('container_format', None)
diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py
index 53e500f..7bfd458 100644
--- a/glance/domain/proxy.py
+++ b/glance/domain/proxy.py
@@ -175,6 +175,8 @@ class Image(object):
175 os_hidden = _proxy('base', 'os_hidden') 175 os_hidden = _proxy('base', 'os_hidden')
176 locations = _proxy('base', 'locations') 176 locations = _proxy('base', 'locations')
177 checksum = _proxy('base', 'checksum') 177 checksum = _proxy('base', 'checksum')
178 os_hash_algo = _proxy('base', 'os_hash_algo')
179 os_hash_value = _proxy('base', 'os_hash_value')
178 owner = _proxy('base', 'owner') 180 owner = _proxy('base', 'owner')
179 disk_format = _proxy('base', 'disk_format') 181 disk_format = _proxy('base', 'disk_format')
180 container_format = _proxy('base', 'container_format') 182 container_format = _proxy('base', 'container_format')
diff --git a/glance/location.py b/glance/location.py
index bde3b13..345dc62 100644
--- a/glance/location.py
+++ b/glance/location.py
@@ -428,12 +428,19 @@ class ImageProxy(glance.domain.proxy.Image):
428 else: 428 else:
429 verifier = None 429 verifier = None
430 430
431 location, size, checksum, loc_meta = self.store_api.add_to_backend( 431 hashing_algo = CONF['hashing_algorithm']
432
433 (location,
434 size,
435 checksum,
436 multihash,
437 loc_meta) = self.store_api.add_to_backend_with_multihash(
432 CONF, 438 CONF,
433 self.image.image_id, 439 self.image.image_id,
434 utils.LimitingReader(utils.CooperativeReader(data), 440 utils.LimitingReader(utils.CooperativeReader(data),
435 CONF.image_size_cap), 441 CONF.image_size_cap),
436 size, 442 size,
443 hashing_algo,
437 context=self.context, 444 context=self.context,
438 verifier=verifier) 445 verifier=verifier)
439 446
@@ -454,6 +461,8 @@ class ImageProxy(glance.domain.proxy.Image):
454 'status': 'active'}] 461 'status': 'active'}]
455 self.image.size = size 462 self.image.size = size
456 self.image.checksum = checksum 463 self.image.checksum = checksum
464 self.image.os_hash_value = multihash
465 self.image.os_hash_algo = hashing_algo
457 self.image.status = 'active' 466 self.image.status = 'active'
458 467
459 def get_data(self, offset=0, chunk_size=None): 468 def get_data(self, offset=0, chunk_size=None):
diff --git a/glance/tests/functional/db/migrations/test_rocky_expand02.py b/glance/tests/functional/db/migrations/test_rocky_expand02.py
new file mode 100644
index 0000000..5bb44e6
--- /dev/null
+++ b/glance/tests/functional/db/migrations/test_rocky_expand02.py
@@ -0,0 +1,41 @@
1# Copyright (c) 2018 Verizon Wireless
2# Licensed under the Apache License, Version 2.0 (the "License"); you may
3# not use this file except in compliance with the License. You may obtain
4# a copy of the License at
5#
6# http://www.apache.org/licenses/LICENSE-2.0
7#
8# Unless required by applicable law or agreed to in writing, software
9# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11# License for the specific language governing permissions and limitations
12# under the License.
13
14from oslo_db.sqlalchemy import test_base
15from oslo_db.sqlalchemy import utils as db_utils
16
17from glance.tests.functional.db import test_migrations
18
19
20class TestRockyExpand02Mixin(test_migrations.AlembicMigrationsMixin):
21
22 def _get_revisions(self, config):
23 return test_migrations.AlembicMigrationsMixin._get_revisions(
24 self, config, head='rocky_expand02')
25
26 def _pre_upgrade_rocky_expand02(self, engine):
27 images = db_utils.get_table(engine, 'images')
28 self.assertNotIn('os_hash_algo', images.c)
29 self.assertNotIn('os_hash_value', images.c)
30
31 def _check_rocky_expand02(self, engine, data):
32 images = db_utils.get_table(engine, 'images')
33 self.assertIn('os_hash_algo', images.c)
34 self.assertTrue(images.c.os_hash_algo.nullable)
35 self.assertIn('os_hash_value', images.c)
36 self.assertTrue(images.c.os_hash_value.nullable)
37
38
39class TestRockyExpand02MySQL(TestRockyExpand02Mixin,
40 test_base.MySQLOpportunisticTestCase):
41 pass
diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py
index 52e2730..b9eec62 100644
--- a/glance/tests/functional/v2/test_images.py
+++ b/glance/tests/functional/v2/test_images.py
@@ -13,6 +13,7 @@
13# License for the specific language governing permissions and limitations 13# License for the specific language governing permissions and limitations
14# under the License. 14# under the License.
15 15
16import hashlib
16import os 17import os
17import signal 18import signal
18import uuid 19import uuid
@@ -158,6 +159,8 @@ class TestImages(functional.FunctionalTest):
158 u'container_format', 159 u'container_format',
159 u'owner', 160 u'owner',
160 u'checksum', 161 u'checksum',
162 u'os_hash_algo',
163 u'os_hash_value',
161 u'size', 164 u'size',
162 u'virtual_size', 165 u'virtual_size',
163 ]) 166 ])
@@ -186,23 +189,29 @@ class TestImages(functional.FunctionalTest):
186 self.assertEqual(1, len(images)) 189 self.assertEqual(1, len(images))
187 self.assertEqual(image_id, images[0]['id']) 190 self.assertEqual(image_id, images[0]['id'])
188 191
189 def _verify_image_checksum_and_status(checksum=None, status=None): 192 def _verify_image_hashes_and_status(
190 # Checksum should be populated and status should be active 193 checksum=None, os_hash_value=None, status=None):
191 path = self._url('/v2/images/%s' % image_id) 194 path = self._url('/v2/images/%s' % image_id)
192 response = requests.get(path, headers=self._headers()) 195 response = requests.get(path, headers=self._headers())
193 self.assertEqual(http.OK, response.status_code) 196 self.assertEqual(http.OK, response.status_code)
194 image = jsonutils.loads(response.text) 197 image = jsonutils.loads(response.text)
195 self.assertEqual(checksum, image['checksum']) 198 self.assertEqual(checksum, image['checksum'])
199 if os_hash_value:
200 # make sure we're using the hashing_algorithm we expect
201 self.assertEqual(six.text_type('sha512'),
202 image['os_hash_algo'])
203 self.assertEqual(os_hash_value, image['os_hash_value'])
196 self.assertEqual(status, image['status']) 204 self.assertEqual(status, image['status'])
197 205
198 # Upload some image data to staging area 206 # Upload some image data to staging area
199 path = self._url('/v2/images/%s/stage' % image_id) 207 path = self._url('/v2/images/%s/stage' % image_id)
200 headers = self._headers({'Content-Type': 'application/octet-stream'}) 208 headers = self._headers({'Content-Type': 'application/octet-stream'})
201 response = requests.put(path, headers=headers, data='ZZZZZ') 209 image_data = b'ZZZZZ'
210 response = requests.put(path, headers=headers, data=image_data)
202 self.assertEqual(http.NO_CONTENT, response.status_code) 211 self.assertEqual(http.NO_CONTENT, response.status_code)
203 212
204 # Verify image is in uploading state and checksum is None 213 # Verify image is in uploading state, hashes are None
205 _verify_image_checksum_and_status(status='uploading') 214 _verify_image_hashes_and_status(status='uploading')
206 215
207 # Import image to store 216 # Import image to store
208 path = self._url('/v2/images/%s/import' % image_id) 217 path = self._url('/v2/images/%s/import' % image_id)
@@ -225,9 +234,11 @@ class TestImages(functional.FunctionalTest):
225 status='active', 234 status='active',
226 max_sec=2, 235 max_sec=2,
227 delay_sec=0.2) 236 delay_sec=0.2)
228 _verify_image_checksum_and_status( 237 expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
229 checksum='8f113e38d28a79a5a451b16048cc2b72', 238 expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
230 status='active') 239 _verify_image_hashes_and_status(checksum=expect_c,
240 os_hash_value=expect_h,
241 status='active')
231 242
232 # Ensure the size is updated to reflect the data uploaded 243 # Ensure the size is updated to reflect the data uploaded
233 path = self._url('/v2/images/%s' % image_id) 244 path = self._url('/v2/images/%s' % image_id)
@@ -300,6 +311,8 @@ class TestImages(functional.FunctionalTest):
300 u'container_format', 311 u'container_format',
301 u'owner', 312 u'owner',
302 u'checksum', 313 u'checksum',
314 u'os_hash_algo',
315 u'os_hash_value',
303 u'size', 316 u'size',
304 u'virtual_size', 317 u'virtual_size',
305 ]) 318 ])
@@ -328,17 +341,22 @@ class TestImages(functional.FunctionalTest):
328 self.assertEqual(1, len(images)) 341 self.assertEqual(1, len(images))
329 self.assertEqual(image_id, images[0]['id']) 342 self.assertEqual(image_id, images[0]['id'])
330 343
331 def _verify_image_checksum_and_status(checksum=None, status=None): 344 def _verify_image_hashes_and_status(
332 # Checksum should be populated and status should be active 345 checksum=None, os_hash_value=None, status=None):
333 path = self._url('/v2/images/%s' % image_id) 346 path = self._url('/v2/images/%s' % image_id)
334 response = requests.get(path, headers=self._headers()) 347 response = requests.get(path, headers=self._headers())
335 self.assertEqual(http.OK, response.status_code) 348 self.assertEqual(http.OK, response.status_code)
336 image = jsonutils.loads(response.text) 349 image = jsonutils.loads(response.text)
337 self.assertEqual(checksum, image['checksum']) 350 self.assertEqual(checksum, image['checksum'])
351 if os_hash_value:
352 # make sure we're using the hashing_algorithm we expect
353 self.assertEqual(six.text_type('sha512'),
354 image['os_hash_algo'])
355 self.assertEqual(os_hash_value, image['os_hash_value'])
338 self.assertEqual(status, image['status']) 356 self.assertEqual(status, image['status'])
339 357
340 # Verify image is in queued state and checksum is None 358 # Verify image is in queued state and hashes are None
341 _verify_image_checksum_and_status(status='queued') 359 _verify_image_hashes_and_status(status='queued')
342 360
343 # Import image to store 361 # Import image to store
344 path = self._url('/v2/images/%s/import' % image_id) 362 path = self._url('/v2/images/%s/import' % image_id)
@@ -346,10 +364,11 @@ class TestImages(functional.FunctionalTest):
346 'content-type': 'application/json', 364 'content-type': 'application/json',
347 'X-Roles': 'admin', 365 'X-Roles': 'admin',
348 }) 366 })
367 image_data_uri = ('https://www.openstack.org/assets/openstack-logo/'
368 '2016R/OpenStack-Logo-Horizontal.eps.zip')
349 data = jsonutils.dumps({'method': { 369 data = jsonutils.dumps({'method': {
350 'name': 'web-download', 370 'name': 'web-download',
351 'uri': 'https://www.openstack.org/assets/openstack-logo/' 371 'uri': image_data_uri
352 '2016R/OpenStack-Logo-Horizontal.eps.zip'
353 }}) 372 }})
354 response = requests.post(path, headers=headers, data=data) 373 response = requests.post(path, headers=headers, data=data)
355 self.assertEqual(http.ACCEPTED, response.status_code) 374 self.assertEqual(http.ACCEPTED, response.status_code)
@@ -364,9 +383,12 @@ class TestImages(functional.FunctionalTest):
364 max_sec=20, 383 max_sec=20,
365 delay_sec=0.2, 384 delay_sec=0.2,
366 start_delay_sec=1) 385 start_delay_sec=1)
367 _verify_image_checksum_and_status( 386 with requests.get(image_data_uri) as r:
368 checksum='bcd65f8922f61a9e6a20572ad7aa2bdd', 387 expect_c = six.text_type(hashlib.md5(r.content).hexdigest())
369 status='active') 388 expect_h = six.text_type(hashlib.sha512(r.content).hexdigest())
389 _verify_image_hashes_and_status(checksum=expect_c,
390 os_hash_value=expect_h,
391 status='active')
370 392
371 # Deleting image should work 393 # Deleting image should work
372 path = self._url('/v2/images/%s' % image_id) 394 path = self._url('/v2/images/%s' % image_id)
@@ -428,6 +450,8 @@ class TestImages(functional.FunctionalTest):
428 u'container_format', 450 u'container_format',
429 u'owner', 451 u'owner',
430 u'checksum', 452 u'checksum',
453 u'os_hash_algo',
454 u'os_hash_value',
431 u'size', 455 u'size',
432 u'virtual_size', 456 u'virtual_size',
433 u'locations', 457 u'locations',
@@ -493,6 +517,8 @@ class TestImages(functional.FunctionalTest):
493 u'container_format', 517 u'container_format',
494 u'owner', 518 u'owner',
495 u'checksum', 519 u'checksum',
520 u'os_hash_algo',
521 u'os_hash_value',
496 u'size', 522 u'size',
497 u'virtual_size', 523 u'virtual_size',
498 u'locations', 524 u'locations',
@@ -722,23 +748,28 @@ class TestImages(functional.FunctionalTest):
722 response = requests.get(path, headers=headers) 748 response = requests.get(path, headers=headers)
723 self.assertEqual(http.NO_CONTENT, response.status_code) 749 self.assertEqual(http.NO_CONTENT, response.status_code)
724 750
725 def _verify_image_checksum_and_status(checksum, status): 751 def _verify_image_hashes_and_status(checksum, os_hash_value, status):
726 # Checksum should be populated and status should be active 752 # hashes should be populated and status should be active
727 path = self._url('/v2/images/%s' % image_id) 753 path = self._url('/v2/images/%s' % image_id)
728 response = requests.get(path, headers=self._headers()) 754 response = requests.get(path, headers=self._headers())
729 self.assertEqual(http.OK, response.status_code) 755 self.assertEqual(http.OK, response.status_code)
730 image = jsonutils.loads(response.text) 756 image = jsonutils.loads(response.text)
731 self.assertEqual(checksum, image['checksum']) 757 self.assertEqual(checksum, image['checksum'])
758 # make sure we're using the default algo
759 self.assertEqual(six.text_type('sha512'), image['os_hash_algo'])
760 self.assertEqual(os_hash_value, image['os_hash_value'])
732 self.assertEqual(status, image['status']) 761 self.assertEqual(status, image['status'])
733 762
734 # Upload some image data 763 # Upload some image data
735 path = self._url('/v2/images/%s/file' % image_id) 764 path = self._url('/v2/images/%s/file' % image_id)
736 headers = self._headers({'Content-Type': 'application/octet-stream'}) 765 headers = self._headers({'Content-Type': 'application/octet-stream'})
737 response = requests.put(path, headers=headers, data='ZZZZZ') 766 image_data = b'ZZZZZ'
767 response = requests.put(path, headers=headers, data=image_data)
738 self.assertEqual(http.NO_CONTENT, response.status_code) 768 self.assertEqual(http.NO_CONTENT, response.status_code)
739 769
740 expected_checksum = '8f113e38d28a79a5a451b16048cc2b72' 770 expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
741 _verify_image_checksum_and_status(expected_checksum, 'active') 771 expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
772 _verify_image_hashes_and_status(expect_c, expect_h, 'active')
742 773
743 # `disk_format` and `container_format` cannot 774 # `disk_format` and `container_format` cannot
744 # be replaced when the image is active. 775 # be replaced when the image is active.
@@ -757,7 +788,7 @@ class TestImages(functional.FunctionalTest):
757 path = self._url('/v2/images/%s/file' % image_id) 788 path = self._url('/v2/images/%s/file' % image_id)
758 response = requests.get(path, headers=self._headers()) 789 response = requests.get(path, headers=self._headers())
759 self.assertEqual(http.OK, response.status_code) 790 self.assertEqual(http.OK, response.status_code)
760 self.assertEqual(expected_checksum, response.headers['Content-MD5']) 791 self.assertEqual(expect_c, response.headers['Content-MD5'])
761 self.assertEqual('ZZZZZ', response.text) 792 self.assertEqual('ZZZZZ', response.text)
762 793
763 # Uploading duplicate data should be rejected with a 409. The 794 # Uploading duplicate data should be rejected with a 409. The
@@ -766,7 +797,7 @@ class TestImages(functional.FunctionalTest):
766 headers = self._headers({'Content-Type': 'application/octet-stream'}) 797 headers = self._headers({'Content-Type': 'application/octet-stream'})
767 response = requests.put(path, headers=headers, data='XXX') 798 response = requests.put(path, headers=headers, data='XXX')
768 self.assertEqual(http.CONFLICT, response.status_code) 799 self.assertEqual(http.CONFLICT, response.status_code)
769 _verify_image_checksum_and_status(expected_checksum, 'active') 800 _verify_image_hashes_and_status(expect_c, expect_h, 'active')
770 801
771 # Ensure the size is updated to reflect the data uploaded 802 # Ensure the size is updated to reflect the data uploaded
772 path = self._url('/v2/images/%s' % image_id) 803 path = self._url('/v2/images/%s' % image_id)
@@ -944,6 +975,8 @@ class TestImages(functional.FunctionalTest):
944 u'container_format', 975 u'container_format',
945 u'owner', 976 u'owner',
946 u'checksum', 977 u'checksum',
978 u'os_hash_algo',
979 u'os_hash_value',
947 u'size', 980 u'size',
948 u'virtual_size', 981 u'virtual_size',
949 u'locations', 982 u'locations',
@@ -1009,6 +1042,8 @@ class TestImages(functional.FunctionalTest):
1009 u'container_format', 1042 u'container_format',
1010 u'owner', 1043 u'owner',
1011 u'checksum', 1044 u'checksum',
1045 u'os_hash_algo',
1046 u'os_hash_value',
1012 u'size', 1047 u'size',
1013 u'virtual_size', 1048 u'virtual_size',
1014 u'locations', 1049 u'locations',
diff --git a/glance/tests/functional/v2/test_schemas.py b/glance/tests/functional/v2/test_schemas.py
index 51bb366..9e93b44 100644
--- a/glance/tests/functional/v2/test_schemas.py
+++ b/glance/tests/functional/v2/test_schemas.py
@@ -38,6 +38,8 @@ class TestSchemas(functional.FunctionalTest):
38 'name', 38 'name',
39 'visibility', 39 'visibility',
40 'checksum', 40 'checksum',
41 'os_hash_algo',
42 'os_hash_value',
41 'created_at', 43 'created_at',
42 'updated_at', 44 'updated_at',
43 'tags', 45 'tags',
diff --git a/glance/tests/unit/test_policy.py b/glance/tests/unit/test_policy.py
index aefa29d..b5abca7 100644
--- a/glance/tests/unit/test_policy.py
+++ b/glance/tests/unit/test_policy.py
@@ -15,6 +15,7 @@
15# under the License. 15# under the License.
16 16
17import collections 17import collections
18import hashlib
18import os.path 19import os.path
19 20
20import mock 21import mock
@@ -66,6 +67,8 @@ class ImageStub(object):
66 self.status = status 67 self.status = status
67 self.extra_properties = extra_properties 68 self.extra_properties = extra_properties
68 self.checksum = 'c2e5db72bd7fd153f53ede5da5a06de3' 69 self.checksum = 'c2e5db72bd7fd153f53ede5da5a06de3'
70 self.os_hash_algo = 'sha512'
71 self.os_hash_value = hashlib.sha512(b'glance').hexdigest()
69 self.created_at = '2013-09-28T15:27:36Z' 72 self.created_at = '2013-09-28T15:27:36Z'
70 self.updated_at = '2013-09-28T15:27:37Z' 73 self.updated_at = '2013-09-28T15:27:37Z'
71 self.locations = [] 74 self.locations = []
diff --git a/glance/tests/unit/utils.py b/glance/tests/unit/utils.py
index 56d754d..6f30f66 100644
--- a/glance/tests/unit/utils.py
+++ b/glance/tests/unit/utils.py
@@ -238,6 +238,30 @@ class FakeStoreAPI(object):
238 checksum = 'Z' 238 checksum = 'Z'
239 return (image_id, size, checksum, self.store_metadata) 239 return (image_id, size, checksum, self.store_metadata)
240 240
241 def add_to_backend_with_multihash(
242 self, conf, image_id, data, size, hashing_algo,
243 scheme=None, context=None, verifier=None):
244 store_max_size = 7
245 current_store_size = 2
246 for location in self.data.keys():
247 if image_id in location:
248 raise exception.Duplicate()
249 if not size:
250 # 'data' is a string wrapped in a LimitingReader|CooperativeReader
251 # pipeline, so peek under the hood of those objects to get at the
252 # string itself.
253 size = len(data.data.fd)
254 if (current_store_size + size) > store_max_size:
255 raise exception.StorageFull()
256 if context.user == USER2:
257 raise exception.Forbidden()
258 if context.user == USER3:
259 raise exception.StorageWriteDenied()
260 self.data[image_id] = (data, size)
261 checksum = 'Z'
262 multihash = 'ZZ'
263 return (image_id, size, checksum, multihash, self.store_metadata)
264
241 def check_location_metadata(self, val, key=''): 265 def check_location_metadata(self, val, key=''):
242 store.check_location_metadata(val) 266 store.check_location_metadata(val)
243 267
diff --git a/glance/tests/unit/v2/test_images_resource.py b/glance/tests/unit/v2/test_images_resource.py
index 7990e97..0b6b486 100644
--- a/glance/tests/unit/v2/test_images_resource.py
+++ b/glance/tests/unit/v2/test_images_resource.py
@@ -15,6 +15,7 @@
15 15
16import datetime 16import datetime
17import eventlet 17import eventlet
18import hashlib
18import uuid 19import uuid
19 20
20import glance_store as store 21import glance_store as store
@@ -56,6 +57,10 @@ TENANT4 = 'c6c87f25-8a94-47ed-8c83-053c25f42df4'
56CHKSUM = '93264c3edf5972c9f1cb309543d38a5c' 57CHKSUM = '93264c3edf5972c9f1cb309543d38a5c'
57CHKSUM1 = '43254c3edf6972c9f1cb309543d38a8c' 58CHKSUM1 = '43254c3edf6972c9f1cb309543d38a8c'
58 59
60FAKEHASHALGO = 'fake-name-for-sha512'
61MULTIHASH1 = hashlib.sha512(b'glance').hexdigest()
62MULTIHASH2 = hashlib.sha512(b'image_service').hexdigest()
63
59 64
60def _db_fixture(id, **kwargs): 65def _db_fixture(id, **kwargs):
61 obj = { 66 obj = {
@@ -64,6 +69,8 @@ def _db_fixture(id, **kwargs):
64 'visibility': 'shared', 69 'visibility': 'shared',
65 'properties': {}, 70 'properties': {},
66 'checksum': None, 71 'checksum': None,
72 'os_hash_algo': FAKEHASHALGO,
73 'os_hash_value': None,
67 'owner': None, 74 'owner': None,
68 'status': 'queued', 75 'status': 'queued',
69 'tags': [], 76 'tags': [],
@@ -87,6 +94,8 @@ def _domain_fixture(id, **kwargs):
87 'name': None, 94 'name': None,
88 'visibility': 'private', 95 'visibility': 'private',
89 'checksum': None, 96 'checksum': None,
97 'os_hash_algo': None,
98 'os_hash_value': None,
90 'owner': None, 99 'owner': None,
91 'status': 'queued', 100 'status': 'queued',
92 'size': None, 101 'size': None,
@@ -149,6 +158,7 @@ class TestImagesController(base.IsolatedUnitTest):
149 def _create_images(self): 158 def _create_images(self):
150 self.images = [ 159 self.images = [
151 _db_fixture(UUID1, owner=TENANT1, checksum=CHKSUM, 160 _db_fixture(UUID1, owner=TENANT1, checksum=CHKSUM,
161 os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
152 name='1', size=256, virtual_size=1024, 162 name='1', size=256, virtual_size=1024,
153 visibility='public', 163 visibility='public',
154 locations=[{'url': '%s/%s' % (BASE_URI, UUID1), 164 locations=[{'url': '%s/%s' % (BASE_URI, UUID1),
@@ -157,6 +167,7 @@ class TestImagesController(base.IsolatedUnitTest):
157 container_format='bare', 167 container_format='bare',
158 status='active'), 168 status='active'),
159 _db_fixture(UUID2, owner=TENANT1, checksum=CHKSUM1, 169 _db_fixture(UUID2, owner=TENANT1, checksum=CHKSUM1,
170 os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH2,
160 name='2', size=512, virtual_size=2048, 171 name='2', size=512, virtual_size=2048,
161 visibility='public', 172 visibility='public',
162 disk_format='raw', 173 disk_format='raw',
@@ -166,6 +177,7 @@ class TestImagesController(base.IsolatedUnitTest):
166 properties={'hypervisor_type': 'kvm', 'foo': 'bar', 177 properties={'hypervisor_type': 'kvm', 'foo': 'bar',
167 'bar': 'foo'}), 178 'bar': 'foo'}),
168 _db_fixture(UUID3, owner=TENANT3, checksum=CHKSUM1, 179 _db_fixture(UUID3, owner=TENANT3, checksum=CHKSUM1,
180 os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH2,
169 name='3', size=512, virtual_size=2048, 181 name='3', size=512, virtual_size=2048,
170 visibility='public', tags=['windows', '64bit', 'x86']), 182 visibility='public', tags=['windows', '64bit', 'x86']),
171 _db_fixture(UUID4, owner=TENANT4, name='4', 183 _db_fixture(UUID4, owner=TENANT4, name='4',
@@ -291,6 +303,34 @@ class TestImagesController(base.IsolatedUnitTest):
291 output = self.controller.index(req, filters={'checksum': '236231827'}) 303 output = self.controller.index(req, filters={'checksum': '236231827'})
292 self.assertEqual(0, len(output['images'])) 304 self.assertEqual(0, len(output['images']))
293 305
306 def test_index_with_os_hash_value_filter_single_image(self):
307 req = unit_test_utils.get_fake_request(
308 '/images?os_hash_value=%s' % MULTIHASH1)
309 output = self.controller.index(req,
310 filters={'os_hash_value': MULTIHASH1})
311 self.assertEqual(1, len(output['images']))
312 actual = list([image.image_id for image in output['images']])
313 expected = [UUID1]
314 self.assertEqual(expected, actual)
315
316 def test_index_with_os_hash_value_filter_multiple_images(self):
317 req = unit_test_utils.get_fake_request(
318 '/images?os_hash_value=%s' % MULTIHASH2)
319 output = self.controller.index(req,
320 filters={'os_hash_value': MULTIHASH2})
321 self.assertEqual(2, len(output['images']))
322 actual = list([image.image_id for image in output['images']])
323 expected = [UUID3, UUID2]
324 self.assertEqual(expected, actual)
325
326 def test_index_with_non_existent_os_hash_value(self):
327 fake_hash_value = hashlib.sha512(b'not_used_in_fixtures').hexdigest()
328 req = unit_test_utils.get_fake_request(
329 '/images?os_hash_value=%s' % fake_hash_value)
330 output = self.controller.index(req,
331 filters={'checksum': fake_hash_value})
332 self.assertEqual(0, len(output['images']))
333
294 def test_index_size_max_filter(self): 334 def test_index_size_max_filter(self):
295 request = unit_test_utils.get_fake_request('/images?size_max=512') 335 request = unit_test_utils.get_fake_request('/images?size_max=512')
296 output = self.controller.index(request, filters={'size_max': 512}) 336 output = self.controller.index(request, filters={'size_max': 512})
@@ -2776,6 +2816,8 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
2776 'id': '00000000-0000-0000-0000-000000000000', 2816 'id': '00000000-0000-0000-0000-000000000000',
2777 'status': 'active', 2817 'status': 'active',
2778 'checksum': 'abcdefghijklmnopqrstuvwxyz012345', 2818 'checksum': 'abcdefghijklmnopqrstuvwxyz012345',
2819 'os_hash_algo': 'supersecure',
2820 'os_hash_value': 'a' * 32 + 'b' * 32 + 'c' * 32 + 'd' * 32,
2779 'size': 9001, 2821 'size': 9001,
2780 'virtual_size': 9001, 2822 'virtual_size': 9001,
2781 'created_at': ISOTIME, 2823 'created_at': ISOTIME,
@@ -3435,7 +3477,9 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3435 visibility='public', container_format='ami', 3477 visibility='public', container_format='ami',
3436 tags=['one', 'two'], disk_format='ami', 3478 tags=['one', 'two'], disk_format='ami',
3437 min_ram=128, min_disk=10, 3479 min_ram=128, min_disk=10,
3438 checksum='ca425b88f047ce8ec45ee90e813ada91'), 3480 checksum='ca425b88f047ce8ec45ee90e813ada91',
3481 os_hash_algo=FAKEHASHALGO,
3482 os_hash_value=MULTIHASH1),
3439 3483
3440 # NOTE(bcwaldon): This second fixture depends on default behavior 3484 # NOTE(bcwaldon): This second fixture depends on default behavior
3441 # and sets most values to None 3485 # and sets most values to None
@@ -3456,6 +3500,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3456 'size': 1024, 3500 'size': 1024,
3457 'virtual_size': 3072, 3501 'virtual_size': 3072,
3458 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 3502 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3503 'os_hash_algo': FAKEHASHALGO,
3504 'os_hash_value': MULTIHASH1,
3459 'container_format': 'ami', 3505 'container_format': 'ami',
3460 'disk_format': 'ami', 3506 'disk_format': 'ami',
3461 'min_ram': 128, 3507 'min_ram': 128,
@@ -3485,6 +3531,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3485 'min_ram': None, 3531 'min_ram': None,
3486 'min_disk': None, 3532 'min_disk': None,
3487 'checksum': None, 3533 'checksum': None,
3534 'os_hash_algo': None,
3535 'os_hash_value': None,
3488 'disk_format': None, 3536 'disk_format': None,
3489 'virtual_size': None, 3537 'virtual_size': None,
3490 'container_format': None, 3538 'container_format': None,
@@ -3564,6 +3612,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3564 'size': 1024, 3612 'size': 1024,
3565 'virtual_size': 3072, 3613 'virtual_size': 3072,
3566 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 3614 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3615 'os_hash_algo': FAKEHASHALGO,
3616 'os_hash_value': MULTIHASH1,
3567 'container_format': 'ami', 3617 'container_format': 'ami',
3568 'disk_format': 'ami', 3618 'disk_format': 'ami',
3569 'min_ram': 128, 3619 'min_ram': 128,
@@ -3601,6 +3651,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3601 'min_ram': None, 3651 'min_ram': None,
3602 'min_disk': None, 3652 'min_disk': None,
3603 'checksum': None, 3653 'checksum': None,
3654 'os_hash_algo': None,
3655 'os_hash_value': None,
3604 'disk_format': None, 3656 'disk_format': None,
3605 'virtual_size': None, 3657 'virtual_size': None,
3606 'container_format': None, 3658 'container_format': None,
@@ -3621,6 +3673,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3621 'size': 1024, 3673 'size': 1024,
3622 'virtual_size': 3072, 3674 'virtual_size': 3072,
3623 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 3675 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3676 'os_hash_algo': FAKEHASHALGO,
3677 'os_hash_value': MULTIHASH1,
3624 'container_format': 'ami', 3678 'container_format': 'ami',
3625 'disk_format': 'ami', 3679 'disk_format': 'ami',
3626 'min_ram': 128, 3680 'min_ram': 128,
@@ -3687,6 +3741,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3687 'size': 1024, 3741 'size': 1024,
3688 'virtual_size': 3072, 3742 'virtual_size': 3072,
3689 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 3743 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3744 'os_hash_algo': FAKEHASHALGO,
3745 'os_hash_value': MULTIHASH1,
3690 'container_format': 'ami', 3746 'container_format': 'ami',
3691 'disk_format': 'ami', 3747 'disk_format': 'ami',
3692 'min_ram': 128, 3748 'min_ram': 128,
@@ -3733,6 +3789,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3733 'min_ram': 128, 3789 'min_ram': 128,
3734 'min_disk': 10, 3790 'min_disk': 10,
3735 'checksum': u'ca425b88f047ce8ec45ee90e813ada91', 3791 'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
3792 'os_hash_algo': FAKEHASHALGO,
3793 'os_hash_value': MULTIHASH1,
3736 'extra_properties': {'lang': u'Fran\u00E7ais', 3794 'extra_properties': {'lang': u'Fran\u00E7ais',
3737 u'dispos\u00E9': u'f\u00E2ch\u00E9'}, 3795 u'dispos\u00E9': u'f\u00E2ch\u00E9'},
3738 }), 3796 }),
@@ -3752,6 +3810,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3752 u'size': 1024, 3810 u'size': 1024,
3753 u'virtual_size': 3072, 3811 u'virtual_size': 3072,
3754 u'checksum': u'ca425b88f047ce8ec45ee90e813ada91', 3812 u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
3813 u'os_hash_algo': six.text_type(FAKEHASHALGO),
3814 u'os_hash_value': six.text_type(MULTIHASH1),
3755 u'container_format': u'ami', 3815 u'container_format': u'ami',
3756 u'disk_format': u'ami', 3816 u'disk_format': u'ami',
3757 u'min_ram': 128, 3817 u'min_ram': 128,
@@ -3790,6 +3850,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3790 u'size': 1024, 3850 u'size': 1024,
3791 u'virtual_size': 3072, 3851 u'virtual_size': 3072,
3792 u'checksum': u'ca425b88f047ce8ec45ee90e813ada91', 3852 u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
3853 u'os_hash_algo': six.text_type(FAKEHASHALGO),
3854 u'os_hash_value': six.text_type(MULTIHASH1),
3793 u'container_format': u'ami', 3855 u'container_format': u'ami',
3794 u'disk_format': u'ami', 3856 u'disk_format': u'ami',
3795 u'min_ram': 128, 3857 u'min_ram': 128,
@@ -3822,6 +3884,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3822 u'size': 1024, 3884 u'size': 1024,
3823 u'virtual_size': 3072, 3885 u'virtual_size': 3072,
3824 u'checksum': u'ca425b88f047ce8ec45ee90e813ada91', 3886 u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
3887 u'os_hash_algo': six.text_type(FAKEHASHALGO),
3888 u'os_hash_value': six.text_type(MULTIHASH1),
3825 u'container_format': u'ami', 3889 u'container_format': u'ami',
3826 u'disk_format': u'ami', 3890 u'disk_format': u'ami',
3827 u'min_ram': 128, 3891 u'min_ram': 128,
@@ -3856,6 +3920,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3856 u'size': 1024, 3920 u'size': 1024,
3857 u'virtual_size': 3072, 3921 u'virtual_size': 3072,
3858 u'checksum': u'ca425b88f047ce8ec45ee90e813ada91', 3922 u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
3923 u'os_hash_algo': six.text_type(FAKEHASHALGO),
3924 u'os_hash_value': six.text_type(MULTIHASH1),
3859 u'container_format': u'ami', 3925 u'container_format': u'ami',
3860 u'disk_format': u'ami', 3926 u'disk_format': u'ami',
3861 u'min_ram': 128, 3927 u'min_ram': 128,
@@ -3895,6 +3961,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
3895 self.fixture = _domain_fixture( 3961 self.fixture = _domain_fixture(
3896 UUID2, name='image-2', owner=TENANT2, 3962 UUID2, name='image-2', owner=TENANT2,
3897 checksum='ca425b88f047ce8ec45ee90e813ada91', 3963 checksum='ca425b88f047ce8ec45ee90e813ada91',
3964 os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
3898 created_at=DATETIME, updated_at=DATETIME, size=1024, 3965 created_at=DATETIME, updated_at=DATETIME, size=1024,
3899 virtual_size=3072, extra_properties=props) 3966 virtual_size=3072, extra_properties=props)
3900 3967
@@ -3907,6 +3974,8 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
3907 'protected': False, 3974 'protected': False,
3908 'os_hidden': False, 3975 'os_hidden': False,
3909 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 3976 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3977 'os_hash_algo': FAKEHASHALGO,
3978 'os_hash_value': MULTIHASH1,
3910 'tags': [], 3979 'tags': [],
3911 'size': 1024, 3980 'size': 1024,
3912 'virtual_size': 3072, 3981 'virtual_size': 3072,
@@ -3936,6 +4005,8 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
3936 'protected': False, 4005 'protected': False,
3937 'os_hidden': False, 4006 'os_hidden': False,
3938 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 4007 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
4008 'os_hash_algo': FAKEHASHALGO,
4009 'os_hash_value': MULTIHASH1,
3939 'tags': [], 4010 'tags': [],
3940 'size': 1024, 4011 'size': 1024,
3941 'virtual_size': 3072, 4012 'virtual_size': 3072,
@@ -3964,6 +4035,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
3964 self.fixture = _domain_fixture( 4035 self.fixture = _domain_fixture(
3965 UUID2, name='image-2', owner=TENANT2, 4036 UUID2, name='image-2', owner=TENANT2,
3966 checksum='ca425b88f047ce8ec45ee90e813ada91', 4037 checksum='ca425b88f047ce8ec45ee90e813ada91',
4038 os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
3967 created_at=DATETIME, updated_at=DATETIME, size=1024, 4039 created_at=DATETIME, updated_at=DATETIME, size=1024,
3968 virtual_size=3072, extra_properties={'marx': 'groucho'}) 4040 virtual_size=3072, extra_properties={'marx': 'groucho'})
3969 4041
@@ -3977,6 +4049,8 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
3977 'protected': False, 4049 'protected': False,
3978 'os_hidden': False, 4050 'os_hidden': False,
3979 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 4051 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
4052 'os_hash_algo': FAKEHASHALGO,
4053 'os_hash_value': MULTIHASH1,
3980 'marx': 'groucho', 4054 'marx': 'groucho',
3981 'tags': [], 4055 'tags': [],
3982 'size': 1024, 4056 'size': 1024,
@@ -4012,6 +4086,8 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
4012 'protected': False, 4086 'protected': False,
4013 'os_hidden': False, 4087 'os_hidden': False,
4014 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 4088 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
4089 'os_hash_algo': FAKEHASHALGO,
4090 'os_hash_value': MULTIHASH1,
4015 'marx': 123, 4091 'marx': 123,
4016 'tags': [], 4092 'tags': [],
4017 'size': 1024, 4093 'size': 1024,
@@ -4042,6 +4118,8 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
4042 'protected': False, 4118 'protected': False,
4043 'os_hidden': False, 4119 'os_hidden': False,
4044 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 4120 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
4121 'os_hash_algo': FAKEHASHALGO,
4122 'os_hash_value': MULTIHASH1,
4045 'tags': [], 4123 'tags': [],
4046 'size': 1024, 4124 'size': 1024,
4047 'virtual_size': 3072, 4125 'virtual_size': 3072,
diff --git a/glance/tests/unit/v2/test_schemas_resource.py b/glance/tests/unit/v2/test_schemas_resource.py
index e256278..adcc936 100644
--- a/glance/tests/unit/v2/test_schemas_resource.py
+++ b/glance/tests/unit/v2/test_schemas_resource.py
@@ -33,7 +33,8 @@ class TestSchemasController(test_utils.BaseTestCase):
33 'disk_format', 'updated_at', 'visibility', 'self', 33 'disk_format', 'updated_at', 'visibility', 'self',
34 'file', 'container_format', 'schema', 'id', 'size', 34 'file', 'container_format', 'schema', 'id', 'size',
35 'direct_url', 'min_ram', 'min_disk', 'protected', 35 'direct_url', 'min_ram', 'min_disk', 'protected',
36 'locations', 'owner', 'virtual_size', 'os_hidden']) 36 'locations', 'owner', 'virtual_size', 'os_hidden',
37 'os_hash_algo', 'os_hash_value'])
37 self.assertEqual(expected, set(output['properties'].keys())) 38 self.assertEqual(expected, set(output['properties'].keys()))
38 39
39 def test_image_has_correct_statuses(self): 40 def test_image_has_correct_statuses(self):
diff --git a/lower-constraints.txt b/lower-constraints.txt
index 706e4b2..d032306 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -35,7 +35,7 @@ future==0.16.0
35futurist==1.2.0 35futurist==1.2.0
36gitdb2==2.0.3 36gitdb2==2.0.3
37GitPython==2.1.8 37GitPython==2.1.8
38glance-store==0.22.0 38glance-store==0.26.1
39greenlet==0.4.13 39greenlet==0.4.13
40hacking==0.12.0 40hacking==0.12.0
41httplib2==0.9.1 41httplib2==0.9.1
diff --git a/releasenotes/notes/multihash-081466a98601da20.yaml b/releasenotes/notes/multihash-081466a98601da20.yaml
new file mode 100644
index 0000000..df3c470
--- /dev/null
+++ b/releasenotes/notes/multihash-081466a98601da20.yaml
@@ -0,0 +1,55 @@
1---
2features:
3 - |
4 This release implements the Glance spec `Secure Hash Algorithm Support
5 <https://specs.openstack.org/openstack/glance-specs/specs/rocky/approved/glance/multihash.html>`_
6 (also known as "multihash"). This feature supplements the current
7 'checksum' image property with a self-describing secure hash. The
8 self-description consists of two new image properties:
9
10 * ``os_hash_algo`` - this contains the name of the secure hash algorithm
11 used to generate the value on this image
12 * ``os_hash_value`` - this is the hexdigest computed by applying the
13 secure hash algorithm named in the ``os_hash_algo`` property to the
14 image data
15
16 These are read-only image properties and are not user-modifiable.
17
18 The secure hash algorithm used is an operator-configurable setting. See
19 the help text for 'hashing_algorithm' in the sample Glance configuration
20 file for more information.
21
22 The default secure hash algorithm is SHA-512. It should be suitable for
23 most applications.
24
25 The legacy 'checksum' image property, which provides an MD5 message
26 digest of the image data, is preserved for backward compatibility.
27issues:
28 - |
29 The ``os_hash_value`` image property, introduced as part of the
30 `Secure Hash Algorithm Support
31 <https://specs.openstack.org/openstack/glance-specs/specs/rocky/approved/glance/multihash.html>`_
32 ("multihash") feature, is limited to 128 characters. This is sufficient
33 to store 512 bits as a hexadecimal numeral.
34
35 - |
36 The "multihash" implemented in this release (`Secure Hash Algorithm Support
37 <https://specs.openstack.org/openstack/glance-specs/specs/rocky/approved/glance/multihash.html>`_)
38 is computed only for new images. There is no provision for computing
39 the multihash for existing images. Thus, users should expect to see
40 JSON 'null' values for the ``os_hash_algo`` and ``os_hash_value`` image
41 properties on images created prior to the installation of the Rocky
42 release at your site.
43security:
44 - |
45 This release implements the Glance spec `Secure Hash Algorithm Support
46 <https://specs.openstack.org/openstack/glance-specs/specs/rocky/approved/glance/multihash.html>`_,
47 which introduces a self-describing "multihash" to the image-show response.
48 This feature supplements the current 'checksum' image property with a
49 self-describing secure hash. The default hashing algorithm is SHA-512,
50 which is currently considered secure. In the event that algorithm is
51 compromised, you will immediately be able to begin using a different
52 algorithm (as long as it's supported by the Python 'hashlib' library and
53 has output that fits in 128 characters) by modifying the value of the
54 'hashing_algorithm' configuration option and either restarting or issuing
55 a SIGHUP to Glance.
diff --git a/requirements.txt b/requirements.txt
index 51327ca..1b50271 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -46,7 +46,7 @@ retrying!=1.3.0,>=1.2.3 # Apache-2.0
46osprofiler>=1.4.0 # Apache-2.0 46osprofiler>=1.4.0 # Apache-2.0
47 47
48# Glance Store 48# Glance Store
49glance-store>=0.22.0 # Apache-2.0 49glance-store>=0.26.1 # Apache-2.0
50 50
51 51
52debtcollector>=1.2.0 # Apache-2.0 52debtcollector>=1.2.0 # Apache-2.0