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)
downloadglance-0b24dbd620f88b4d36bf6e0f8975f10aa8709b86.tar.gz
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
-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 f8ee98190..aead71e21 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
- min_disk: min_disk
- min_ram: min_ram
- name: name
+ - os_hash_algo: os_hash_algo
+ - os_hash_value: os_hash_value
- owner: owner
- protected: protected
- schema: schema-image
@@ -266,6 +268,8 @@ Response Parameters
- min_disk: min_disk
- min_ram: min_ram
- name: name
+ - os_hash_algo: os_hash_algo
+ - os_hash_value: os_hash_value
- owner: owner
- protected: protected
- schema: schema-image
@@ -584,8 +588,10 @@ Response Parameters
- id: id
- min_disk: min_disk
- min_ram: min_ram
- - owner: owner
- name: name
+ - owner: owner
+ - os_hash_algo: os_hash_algo
+ - os_hash_value: os_hash_value
- protected: protected
- schema: schema-image
- self: self
diff --git a/api-ref/source/v2/images-parameters.yaml b/api-ref/source/v2/images-parameters.yaml
index bb5420a55..060f6f9b1 100644
--- a/api-ref/source/v2/images-parameters.yaml
+++ b/api-ref/source/v2/images-parameters.yaml
@@ -484,6 +484,27 @@ next:
in: body
required: true
type: string
+os_hash_algo:
+ description: |
+ The algorithm used to compute a secure hash of the image data for this
+ image. The result of the computation is displayed as the value of the
+ ``os_hash_value`` property. The value might be ``null`` (JSON null
+ data type). The algorithm used is chosen by the cloud operator; it
+ may not be configured by end users. *(Since Image API v2.7)*
+ in: body
+ required: true
+ type: string
+os_hash_value:
+ description: |
+ The hexdigest of the secure hash of the image data computed using the
+ algorithm whose name is the value of the ``os_hash_algo`` property.
+ The value might be ``null`` (JSON null data type) if data has not
+ yet been associated with this image, or if the image was created using
+ a version of the Image Service API prior to version 2.7.
+ *(Since Image API v2.7)*
+ in: body
+ required: true
+ type: string
owner:
description: |
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 dd2289ee2..afcb822dc 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 @@
"id": "b2173dd3-7ad6-4362-baa6-a68bce3565cb",
"file": "/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb/file",
"checksum": null,
+ "os_hash_algo": null,
+ "os_hash_value": null,
"owner": "bab7d5c60cd041a0a36f7c4b6e1dd978",
"virtual_size": null,
"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 43d41c335..66a5d5b1d 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 @@
"id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
"file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
"checksum": "64d7c1cd2b6f60c92c14662941cb7913",
+ "os_hash_algo": "sha512",
+ "os_hash_value": "073b4523583784fbe01daff81eba092a262ec37ba6d04dd3f52e4cd5c93eb8258af44881345ecda0e49f3d8cc6d2df6b050ff3e72681d723234aff9d17d0cf09"
"owner": "5ef70662f8b34079a6eddb8da9d75fe8",
"size": 13167616,
"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 07055355c..9660d4fd0 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 @@
"id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
"file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
"checksum": "64d7c1cd2b6f60c92c14662941cb7913",
+ "os_hash_algo": "sha512",
+ "os_hash_value": "073b4523583784fbe01daff81eba092a262ec37ba6d04dd3f52e4cd5c93eb8258af44881345ecda0e49f3d8cc6d2df6b050ff3e72681d723234aff9d17d0cf09"
"owner": "5ef70662f8b34079a6eddb8da9d75fe8",
"size": 13167616,
"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 c337290da..3211cc83d 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 @@
"min_ram": 512,
"name": "Fedora 17",
"owner": "02a7fb2dd4ef434c8a628c511dcbbeb6",
+ "os_hash_algo": "sha512",
+ "os_hash_value": "ef7d1ed957ffafefb324d50ebc6685ed03d0e64549762ba94a1c44e92270cdbb69d7437dd1e101d00dd41684aaecccad1edc5c2e295e66d4733025b052497844"
"protected": false,
"schema": "/v2/schemas/image",
"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 8e2097a09..c44504b70 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 @@
"id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
"file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
"checksum": "64d7c1cd2b6f60c92c14662941cb7913",
+ "os_hash_algo": "sha512",
+ "os_hash_value": "073b4523583784fbe01daff81eba092a262ec37ba6d04dd3f52e4cd5c93eb8258af44881345ecda0e49f3d8cc6d2df6b050ff3e72681d723234aff9d17d0cf09"
"owner": "5ef70662f8b34079a6eddb8da9d75fe8",
"size": 13167616,
"min_ram": 0,
@@ -36,6 +38,8 @@
"id": "781b3762-9469-4cec-b58d-3349e5de4e9c",
"file": "/v2/images/781b3762-9469-4cec-b58d-3349e5de4e9c/file",
"checksum": "afab0f79bac770d61d24b4d0560b5f70",
+ "os_hash_algo": "sha512",
+ "os_hash_value": "ea3e20140df1cc65f53d4c5b9ee3b38d0d6868f61bbe2230417b0f98cef0e0c7c37f0ebc5c6456fa47f013de48b452617d56c15fdba25e100379bd0e81ee15ec"
"owner": "5ef70662f8b34079a6eddb8da9d75fe8",
"size": 476704768,
"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 329ccb9a8..5232ac7d3 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 @@
"is_base": false,
"type": "string"
},
+ "os_hash_algo": {
+ "description": "Algorithm to calculate the os_hash_value",
+ "maxLength": 64,
+ "readOnly": true,
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "os_hash_value": {
+ "description": "Hexdigest of the image contents using the algorithm specified by the os_hash_algo",
+ "maxLength": 128,
+ "readOnly": true,
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"os_version": {
"description": "Operating system version as specified by the distributor",
"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 47992fdbe..3365b88e8 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 @@
"is_base": false,
"type": "string"
},
+ "os_hash_algo": {
+ "description": "Algorithm to calculate the os_hash_value",
+ "maxLength": 64,
+ "readOnly": true,
+ "type": [
+ "null",
+ "string"
+ ]
+ },
+ "os_hash_value": {
+ "description": "Hexdigest of the image contents using the algorithm specified by the os_hash_algo",
+ "maxLength": 128,
+ "readOnly": true,
+ "type": [
+ "null",
+ "string"
+ ]
+ },
"os_version": {
"description": "Operating system version as specified by the distributor",
"is_base": false,
diff --git a/glance/api/authorization.py b/glance/api/authorization.py
index 694584518..d077dc610 100644
--- a/glance/api/authorization.py
+++ b/glance/api/authorization.py
@@ -315,6 +315,8 @@ class ImmutableImageProxy(object):
min_disk = _immutable_attr('base', 'min_disk')
min_ram = _immutable_attr('base', 'min_ram')
protected = _immutable_attr('base', 'protected')
+ os_hash_algo = _immutable_attr('base', 'os_hash_algo')
+ os_hash_value = _immutable_attr('base', 'os_hash_value')
os_hidden = _immutable_attr('base', 'os_hidden')
locations = _immutable_attr('base', 'locations', proxy=ImmutableLocations)
checksum = _immutable_attr('base', 'checksum')
diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py
index 0d7242f90..9d70a24d3 100644
--- a/glance/api/v2/images.py
+++ b/glance/api/v2/images.py
@@ -446,7 +446,8 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
_disallowed_properties = ('direct_url', 'self', 'file', 'schema')
_readonly_properties = ('created_at', 'updated_at', 'status', 'checksum',
'size', 'virtual_size', 'direct_url', 'self',
- 'file', 'schema', 'id')
+ 'file', 'schema', 'id', 'os_hash_algo',
+ 'os_hash_value')
_reserved_properties = ('location', 'deleted', 'deleted_at')
_base_properties = ('checksum', 'created_at', 'container_format',
'disk_format', 'id', 'min_disk', 'min_ram', 'name',
@@ -884,7 +885,8 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
attributes = ['name', 'disk_format', 'container_format',
'visibility', 'size', 'virtual_size', 'status',
'checksum', 'protected', 'min_ram', 'min_disk',
- 'owner', 'os_hidden']
+ 'owner', 'os_hidden', 'os_hash_algo',
+ 'os_hash_value']
for key in attributes:
image_view[key] = getattr(image, key)
image_view['id'] = image.image_id
@@ -1018,6 +1020,19 @@ def get_base_properties():
'description': _('md5 hash of image contents.'),
'maxLength': 32,
},
+ 'os_hash_algo': {
+ 'type': ['null', 'string'],
+ 'readOnly': True,
+ 'description': _('Algorithm to calculate the os_hash_value'),
+ 'maxLength': 64,
+ },
+ 'os_hash_value': {
+ 'type': ['null', 'string'],
+ 'readOnly': True,
+ 'description': _('Hexdigest of the image contents using the '
+ 'algorithm specified by the os_hash_algo'),
+ 'maxLength': 128,
+ },
'owner': {
'type': ['null', 'string'],
'description': _('Owner of the image'),
diff --git a/glance/common/config.py b/glance/common/config.py
index 4ab65be6d..57a066b6c 100644
--- a/glance/common/config.py
+++ b/glance/common/config.py
@@ -192,6 +192,40 @@ Related options:
* image_property_quota
""")),
+ cfg.StrOpt('hashing_algorithm',
+ default='sha512',
+ help=_(""""
+Secure hashing algorithm used for computing the 'os_hash_value' property.
+
+This option configures the Glance "multihash", which consists of two
+image properties: the 'os_hash_algo' and the 'os_hash_value'. The
+'os_hash_algo' will be populated by the value of this configuration
+option, and the 'os_hash_value' will be populated by the hexdigest computed
+when the algorithm is applied to the uploaded or imported image data.
+
+The value must be a valid secure hash algorithm name recognized by the
+python 'hashlib' library. You can determine what these are by examining
+the 'hashlib.algorithms_available' data member of the version of the
+library being used in your Glance installation. For interoperability
+purposes, however, we recommend that you use the set of secure hash
+names supplies by the 'hashlib.algorithms_guaranteed' data member because
+those algorithms are guaranteed to be supported by the 'hashlib' library
+on all platforms. Thus, any image consumer using 'hashlib' locally should
+be able to verify the 'os_hash_value' of the image.
+
+The default value of 'sha512' is a performant secure hash algorithm.
+
+If this option is misconfigured, any attempts to store image data will fail.
+For that reason, we recommend using the default value.
+
+Possible values:
+ * Any secure hash algorithm name recognized by the Python 'hashlib'
+ library
+
+Related options:
+ * None
+
+""")),
cfg.IntOpt('image_member_quota', default=128,
help=_("""
Maximum number of image members per image.
diff --git a/glance/db/__init__.py b/glance/db/__init__.py
index 862adbd1d..465cecb56 100644
--- a/glance/db/__init__.py
+++ b/glance/db/__init__.py
@@ -130,6 +130,8 @@ class ImageRepo(object):
protected=db_image['protected'],
locations=location_strategy.get_ordered_locations(locations),
checksum=db_image['checksum'],
+ os_hash_algo=db_image['os_hash_algo'],
+ os_hash_value=db_image['os_hash_value'],
owner=db_image['owner'],
disk_format=db_image['disk_format'],
container_format=db_image['container_format'],
@@ -162,6 +164,8 @@ class ImageRepo(object):
'protected': image.protected,
'locations': locations,
'checksum': image.checksum,
+ 'os_hash_algo': image.os_hash_algo,
+ 'os_hash_value': image.os_hash_value,
'owner': image.owner,
'disk_format': image.disk_format,
'container_format': image.container_format,
diff --git a/glance/db/simple/api.py b/glance/db/simple/api.py
index e9ae30c46..66b64e70e 100644
--- a/glance/db/simple/api.py
+++ b/glance/db/simple/api.py
@@ -225,6 +225,8 @@ def _image_format(image_id, **values):
'size': None,
'virtual_size': None,
'checksum': None,
+ 'os_hash_algo': None,
+ 'os_hash_value': None,
'tags': [],
'created_at': dt,
'updated_at': dt,
@@ -735,7 +737,7 @@ def image_create(context, image_values, v1_mode=False):
'protected', 'is_public', 'container_format',
'disk_format', 'created_at', 'updated_at', 'deleted',
'deleted_at', 'properties', 'tags', 'visibility',
- 'os_hidden'])
+ 'os_hidden', 'os_hash_algo', 'os_hash_value'])
incorrect_keys = set(image_values.keys()) - allowed_keys
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 000000000..62769e82f
--- /dev/null
+++ b/glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate02_empty.py
@@ -0,0 +1,26 @@
+# Copyright (C) 2018 Verizon Wireless
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+def has_migrations(engine):
+ """Returns true if at least one data row can be migrated."""
+
+ return False
+
+
+def migrate(engine):
+ """Return the number of rows migrated."""
+
+ 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 000000000..919b3ed52
--- /dev/null
+++ b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract02_empty.py
@@ -0,0 +1,25 @@
+# Copyright (C) 2018 Verizon Wireless
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+
+# revision identifiers, used by Alembic.
+revision = 'rocky_contract02'
+down_revision = 'rocky_contract01'
+branch_labels = None
+depends_on = 'rocky_expand02'
+
+
+def upgrade():
+ 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 000000000..c800d0f50
--- /dev/null
+++ b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand02_add_os_hash_.py
@@ -0,0 +1,33 @@
+# Copyright (C) 2018 Verizon Wireless
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""add os_hash_algo and os_hash_value columns to images table"""
+
+from alembic import op
+from sqlalchemy import Column, String
+
+# revision identifiers, used by Alembic.
+revision = 'rocky_expand02'
+down_revision = 'rocky_expand01'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ algo_col = Column('os_hash_algo', String(length=64), nullable=True)
+ value_col = Column('os_hash_value', String(length=128), nullable=True)
+ op.add_column('images', algo_col)
+ op.add_column('images', value_col)
+ 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 0e68e4690..9cddd2bfc 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):
checksum = filters.pop('checksum')
image_conditions.append(models.Image.checksum == checksum)
+ if 'os_hash_value' in filters:
+ os_hash_value = filters.pop('os_hash_value')
+ image_conditions.append(models.Image.os_hash_value == os_hash_value)
+
for (k, v) in filters.pop('properties', {}).items():
prop_filters = _make_image_property_condition(key=k, value=v)
prop_conditions.append(prop_filters)
diff --git a/glance/db/sqlalchemy/models.py b/glance/db/sqlalchemy/models.py
index 08a3db551..ed61e23f4 100644
--- a/glance/db/sqlalchemy/models.py
+++ b/glance/db/sqlalchemy/models.py
@@ -120,7 +120,8 @@ class Image(BASE, GlanceBase):
Index('owner_image_idx', 'owner'),
Index('created_at_image_idx', 'created_at'),
Index('updated_at_image_idx', 'updated_at'),
- Index('os_hidden_image_idx', 'os_hidden'))
+ Index('os_hidden_image_idx', 'os_hidden'),
+ Index('os_hash_value_image_idx', 'os_hash_value'))
id = Column(String(36), primary_key=True,
default=lambda: str(uuid.uuid4()))
@@ -134,6 +135,8 @@ class Image(BASE, GlanceBase):
name='image_visibility'), nullable=False,
server_default='shared')
checksum = Column(String(32))
+ os_hash_algo = Column(String(64))
+ os_hash_value = Column(String(128))
min_disk = Column(Integer, nullable=False, default=0)
min_ram = Column(Integer, nullable=False, default=0)
owner = Column(String(255))
diff --git a/glance/domain/__init__.py b/glance/domain/__init__.py
index d72f3867a..6d78c3dd9 100644
--- a/glance/domain/__init__.py
+++ b/glance/domain/__init__.py
@@ -48,7 +48,8 @@ def _import_delayed_delete():
class ImageFactory(object):
_readonly_properties = ['created_at', 'updated_at', 'status', 'checksum',
- 'size', 'virtual_size']
+ 'os_hash_algo', 'os_hash_value', 'size',
+ 'virtual_size']
_reserved_properties = ['owner', 'locations', 'deleted', 'deleted_at',
'direct_url', 'self', 'file', 'schema']
@@ -127,6 +128,8 @@ class Image(object):
self.protected = kwargs.pop('protected', False)
self.locations = kwargs.pop('locations', [])
self.checksum = kwargs.pop('checksum', None)
+ self.os_hash_algo = kwargs.pop('os_hash_algo', None)
+ self.os_hash_value = kwargs.pop('os_hash_value', None)
self.owner = kwargs.pop('owner', None)
self._disk_format = kwargs.pop('disk_format', None)
self._container_format = kwargs.pop('container_format', None)
diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py
index 53e500f5f..7bfd45812 100644
--- a/glance/domain/proxy.py
+++ b/glance/domain/proxy.py
@@ -175,6 +175,8 @@ class Image(object):
os_hidden = _proxy('base', 'os_hidden')
locations = _proxy('base', 'locations')
checksum = _proxy('base', 'checksum')
+ os_hash_algo = _proxy('base', 'os_hash_algo')
+ os_hash_value = _proxy('base', 'os_hash_value')
owner = _proxy('base', 'owner')
disk_format = _proxy('base', 'disk_format')
container_format = _proxy('base', 'container_format')
diff --git a/glance/location.py b/glance/location.py
index bde3b1355..345dc6273 100644
--- a/glance/location.py
+++ b/glance/location.py
@@ -428,12 +428,19 @@ class ImageProxy(glance.domain.proxy.Image):
else:
verifier = None
- location, size, checksum, loc_meta = self.store_api.add_to_backend(
+ hashing_algo = CONF['hashing_algorithm']
+
+ (location,
+ size,
+ checksum,
+ multihash,
+ loc_meta) = self.store_api.add_to_backend_with_multihash(
CONF,
self.image.image_id,
utils.LimitingReader(utils.CooperativeReader(data),
CONF.image_size_cap),
size,
+ hashing_algo,
context=self.context,
verifier=verifier)
@@ -454,6 +461,8 @@ class ImageProxy(glance.domain.proxy.Image):
'status': 'active'}]
self.image.size = size
self.image.checksum = checksum
+ self.image.os_hash_value = multihash
+ self.image.os_hash_algo = hashing_algo
self.image.status = 'active'
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 000000000..5bb44e697
--- /dev/null
+++ b/glance/tests/functional/db/migrations/test_rocky_expand02.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2018 Verizon Wireless
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from oslo_db.sqlalchemy import test_base
+from oslo_db.sqlalchemy import utils as db_utils
+
+from glance.tests.functional.db import test_migrations
+
+
+class TestRockyExpand02Mixin(test_migrations.AlembicMigrationsMixin):
+
+ def _get_revisions(self, config):
+ return test_migrations.AlembicMigrationsMixin._get_revisions(
+ self, config, head='rocky_expand02')
+
+ def _pre_upgrade_rocky_expand02(self, engine):
+ images = db_utils.get_table(engine, 'images')
+ self.assertNotIn('os_hash_algo', images.c)
+ self.assertNotIn('os_hash_value', images.c)
+
+ def _check_rocky_expand02(self, engine, data):
+ images = db_utils.get_table(engine, 'images')
+ self.assertIn('os_hash_algo', images.c)
+ self.assertTrue(images.c.os_hash_algo.nullable)
+ self.assertIn('os_hash_value', images.c)
+ self.assertTrue(images.c.os_hash_value.nullable)
+
+
+class TestRockyExpand02MySQL(TestRockyExpand02Mixin,
+ test_base.MySQLOpportunisticTestCase):
+ pass
diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py
index 52e27308c..b9eec6273 100644
--- a/glance/tests/functional/v2/test_images.py
+++ b/glance/tests/functional/v2/test_images.py
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+import hashlib
import os
import signal
import uuid
@@ -158,6 +159,8 @@ class TestImages(functional.FunctionalTest):
u'container_format',
u'owner',
u'checksum',
+ u'os_hash_algo',
+ u'os_hash_value',
u'size',
u'virtual_size',
])
@@ -186,23 +189,29 @@ class TestImages(functional.FunctionalTest):
self.assertEqual(1, len(images))
self.assertEqual(image_id, images[0]['id'])
- def _verify_image_checksum_and_status(checksum=None, status=None):
- # Checksum should be populated and status should be active
+ def _verify_image_hashes_and_status(
+ checksum=None, os_hash_value=None, status=None):
path = self._url('/v2/images/%s' % image_id)
response = requests.get(path, headers=self._headers())
self.assertEqual(http.OK, response.status_code)
image = jsonutils.loads(response.text)
self.assertEqual(checksum, image['checksum'])
+ if os_hash_value:
+ # make sure we're using the hashing_algorithm we expect
+ self.assertEqual(six.text_type('sha512'),
+ image['os_hash_algo'])
+ self.assertEqual(os_hash_value, image['os_hash_value'])
self.assertEqual(status, image['status'])
# Upload some image data to staging area
path = self._url('/v2/images/%s/stage' % image_id)
headers = self._headers({'Content-Type': 'application/octet-stream'})
- response = requests.put(path, headers=headers, data='ZZZZZ')
+ image_data = b'ZZZZZ'
+ response = requests.put(path, headers=headers, data=image_data)
self.assertEqual(http.NO_CONTENT, response.status_code)
- # Verify image is in uploading state and checksum is None
- _verify_image_checksum_and_status(status='uploading')
+ # Verify image is in uploading state, hashes are None
+ _verify_image_hashes_and_status(status='uploading')
# Import image to store
path = self._url('/v2/images/%s/import' % image_id)
@@ -225,9 +234,11 @@ class TestImages(functional.FunctionalTest):
status='active',
max_sec=2,
delay_sec=0.2)
- _verify_image_checksum_and_status(
- checksum='8f113e38d28a79a5a451b16048cc2b72',
- status='active')
+ expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
+ expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
+ _verify_image_hashes_and_status(checksum=expect_c,
+ os_hash_value=expect_h,
+ status='active')
# Ensure the size is updated to reflect the data uploaded
path = self._url('/v2/images/%s' % image_id)
@@ -300,6 +311,8 @@ class TestImages(functional.FunctionalTest):
u'container_format',
u'owner',
u'checksum',
+ u'os_hash_algo',
+ u'os_hash_value',
u'size',
u'virtual_size',
])
@@ -328,17 +341,22 @@ class TestImages(functional.FunctionalTest):
self.assertEqual(1, len(images))
self.assertEqual(image_id, images[0]['id'])
- def _verify_image_checksum_and_status(checksum=None, status=None):
- # Checksum should be populated and status should be active
+ def _verify_image_hashes_and_status(
+ checksum=None, os_hash_value=None, status=None):
path = self._url('/v2/images/%s' % image_id)
response = requests.get(path, headers=self._headers())
self.assertEqual(http.OK, response.status_code)
image = jsonutils.loads(response.text)
self.assertEqual(checksum, image['checksum'])
+ if os_hash_value:
+ # make sure we're using the hashing_algorithm we expect
+ self.assertEqual(six.text_type('sha512'),
+ image['os_hash_algo'])
+ self.assertEqual(os_hash_value, image['os_hash_value'])
self.assertEqual(status, image['status'])
- # Verify image is in queued state and checksum is None
- _verify_image_checksum_and_status(status='queued')
+ # Verify image is in queued state and hashes are None
+ _verify_image_hashes_and_status(status='queued')
# Import image to store
path = self._url('/v2/images/%s/import' % image_id)
@@ -346,10 +364,11 @@ class TestImages(functional.FunctionalTest):
'content-type': 'application/json',
'X-Roles': 'admin',
})
+ image_data_uri = ('https://www.openstack.org/assets/openstack-logo/'
+ '2016R/OpenStack-Logo-Horizontal.eps.zip')
data = jsonutils.dumps({'method': {
'name': 'web-download',
- 'uri': 'https://www.openstack.org/assets/openstack-logo/'
- '2016R/OpenStack-Logo-Horizontal.eps.zip'
+ 'uri': image_data_uri
}})
response = requests.post(path, headers=headers, data=data)
self.assertEqual(http.ACCEPTED, response.status_code)
@@ -364,9 +383,12 @@ class TestImages(functional.FunctionalTest):
max_sec=20,
delay_sec=0.2,
start_delay_sec=1)
- _verify_image_checksum_and_status(
- checksum='bcd65f8922f61a9e6a20572ad7aa2bdd',
- status='active')
+ with requests.get(image_data_uri) as r:
+ expect_c = six.text_type(hashlib.md5(r.content).hexdigest())
+ expect_h = six.text_type(hashlib.sha512(r.content).hexdigest())
+ _verify_image_hashes_and_status(checksum=expect_c,
+ os_hash_value=expect_h,
+ status='active')
# Deleting image should work
path = self._url('/v2/images/%s' % image_id)
@@ -428,6 +450,8 @@ class TestImages(functional.FunctionalTest):
u'container_format',
u'owner',
u'checksum',
+ u'os_hash_algo',
+ u'os_hash_value',
u'size',
u'virtual_size',
u'locations',
@@ -493,6 +517,8 @@ class TestImages(functional.FunctionalTest):
u'container_format',
u'owner',
u'checksum',
+ u'os_hash_algo',
+ u'os_hash_value',
u'size',
u'virtual_size',
u'locations',
@@ -722,23 +748,28 @@ class TestImages(functional.FunctionalTest):
response = requests.get(path, headers=headers)
self.assertEqual(http.NO_CONTENT, response.status_code)
- def _verify_image_checksum_and_status(checksum, status):
- # Checksum should be populated and status should be active
+ def _verify_image_hashes_and_status(checksum, os_hash_value, status):
+ # hashes should be populated and status should be active
path = self._url('/v2/images/%s' % image_id)
response = requests.get(path, headers=self._headers())
self.assertEqual(http.OK, response.status_code)
image = jsonutils.loads(response.text)
self.assertEqual(checksum, image['checksum'])
+ # make sure we're using the default algo
+ self.assertEqual(six.text_type('sha512'), image['os_hash_algo'])
+ self.assertEqual(os_hash_value, image['os_hash_value'])
self.assertEqual(status, image['status'])
# Upload some image data
path = self._url('/v2/images/%s/file' % image_id)
headers = self._headers({'Content-Type': 'application/octet-stream'})
- response = requests.put(path, headers=headers, data='ZZZZZ')
+ image_data = b'ZZZZZ'
+ response = requests.put(path, headers=headers, data=image_data)
self.assertEqual(http.NO_CONTENT, response.status_code)
- expected_checksum = '8f113e38d28a79a5a451b16048cc2b72'
- _verify_image_checksum_and_status(expected_checksum, 'active')
+ expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
+ expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
+ _verify_image_hashes_and_status(expect_c, expect_h, 'active')
# `disk_format` and `container_format` cannot
# be replaced when the image is active.
@@ -757,7 +788,7 @@ class TestImages(functional.FunctionalTest):
path = self._url('/v2/images/%s/file' % image_id)
response = requests.get(path, headers=self._headers())
self.assertEqual(http.OK, response.status_code)
- self.assertEqual(expected_checksum, response.headers['Content-MD5'])
+ self.assertEqual(expect_c, response.headers['Content-MD5'])
self.assertEqual('ZZZZZ', response.text)
# Uploading duplicate data should be rejected with a 409. The
@@ -766,7 +797,7 @@ class TestImages(functional.FunctionalTest):
headers = self._headers({'Content-Type': 'application/octet-stream'})
response = requests.put(path, headers=headers, data='XXX')
self.assertEqual(http.CONFLICT, response.status_code)
- _verify_image_checksum_and_status(expected_checksum, 'active')
+ _verify_image_hashes_and_status(expect_c, expect_h, 'active')
# Ensure the size is updated to reflect the data uploaded
path = self._url('/v2/images/%s' % image_id)
@@ -944,6 +975,8 @@ class TestImages(functional.FunctionalTest):
u'container_format',
u'owner',
u'checksum',
+ u'os_hash_algo',
+ u'os_hash_value',
u'size',
u'virtual_size',
u'locations',
@@ -1009,6 +1042,8 @@ class TestImages(functional.FunctionalTest):
u'container_format',
u'owner',
u'checksum',
+ u'os_hash_algo',
+ u'os_hash_value',
u'size',
u'virtual_size',
u'locations',
diff --git a/glance/tests/functional/v2/test_schemas.py b/glance/tests/functional/v2/test_schemas.py
index 51bb3660a..9e93b44c0 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):
'name',
'visibility',
'checksum',
+ 'os_hash_algo',
+ 'os_hash_value',
'created_at',
'updated_at',
'tags',
diff --git a/glance/tests/unit/test_policy.py b/glance/tests/unit/test_policy.py
index aefa29d42..b5abca70f 100644
--- a/glance/tests/unit/test_policy.py
+++ b/glance/tests/unit/test_policy.py
@@ -15,6 +15,7 @@
# under the License.
import collections
+import hashlib
import os.path
import mock
@@ -66,6 +67,8 @@ class ImageStub(object):
self.status = status
self.extra_properties = extra_properties
self.checksum = 'c2e5db72bd7fd153f53ede5da5a06de3'
+ self.os_hash_algo = 'sha512'
+ self.os_hash_value = hashlib.sha512(b'glance').hexdigest()
self.created_at = '2013-09-28T15:27:36Z'
self.updated_at = '2013-09-28T15:27:37Z'
self.locations = []
diff --git a/glance/tests/unit/utils.py b/glance/tests/unit/utils.py
index 56d754d7f..6f30f666d 100644
--- a/glance/tests/unit/utils.py
+++ b/glance/tests/unit/utils.py
@@ -238,6 +238,30 @@ class FakeStoreAPI(object):
checksum = 'Z'
return (image_id, size, checksum, self.store_metadata)
+ def add_to_backend_with_multihash(
+ self, conf, image_id, data, size, hashing_algo,
+ scheme=None, context=None, verifier=None):
+ store_max_size = 7
+ current_store_size = 2
+ for location in self.data.keys():
+ if image_id in location:
+ raise exception.Duplicate()
+ if not size:
+ # 'data' is a string wrapped in a LimitingReader|CooperativeReader
+ # pipeline, so peek under the hood of those objects to get at the
+ # string itself.
+ size = len(data.data.fd)
+ if (current_store_size + size) > store_max_size:
+ raise exception.StorageFull()
+ if context.user == USER2:
+ raise exception.Forbidden()
+ if context.user == USER3:
+ raise exception.StorageWriteDenied()
+ self.data[image_id] = (data, size)
+ checksum = 'Z'
+ multihash = 'ZZ'
+ return (image_id, size, checksum, multihash, self.store_metadata)
+
def check_location_metadata(self, val, key=''):
store.check_location_metadata(val)
diff --git a/glance/tests/unit/v2/test_images_resource.py b/glance/tests/unit/v2/test_images_resource.py
index 7990e97f7..0b6b486d6 100644
--- a/glance/tests/unit/v2/test_images_resource.py
+++ b/glance/tests/unit/v2/test_images_resource.py
@@ -15,6 +15,7 @@
import datetime
import eventlet
+import hashlib
import uuid
import glance_store as store
@@ -56,6 +57,10 @@ TENANT4 = 'c6c87f25-8a94-47ed-8c83-053c25f42df4'
CHKSUM = '93264c3edf5972c9f1cb309543d38a5c'
CHKSUM1 = '43254c3edf6972c9f1cb309543d38a8c'
+FAKEHASHALGO = 'fake-name-for-sha512'
+MULTIHASH1 = hashlib.sha512(b'glance').hexdigest()
+MULTIHASH2 = hashlib.sha512(b'image_service').hexdigest()
+
def _db_fixture(id, **kwargs):
obj = {
@@ -64,6 +69,8 @@ def _db_fixture(id, **kwargs):
'visibility': 'shared',
'properties': {},
'checksum': None,
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': None,
'owner': None,
'status': 'queued',
'tags': [],
@@ -87,6 +94,8 @@ def _domain_fixture(id, **kwargs):
'name': None,
'visibility': 'private',
'checksum': None,
+ 'os_hash_algo': None,
+ 'os_hash_value': None,
'owner': None,
'status': 'queued',
'size': None,
@@ -149,6 +158,7 @@ class TestImagesController(base.IsolatedUnitTest):
def _create_images(self):
self.images = [
_db_fixture(UUID1, owner=TENANT1, checksum=CHKSUM,
+ os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
name='1', size=256, virtual_size=1024,
visibility='public',
locations=[{'url': '%s/%s' % (BASE_URI, UUID1),
@@ -157,6 +167,7 @@ class TestImagesController(base.IsolatedUnitTest):
container_format='bare',
status='active'),
_db_fixture(UUID2, owner=TENANT1, checksum=CHKSUM1,
+ os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH2,
name='2', size=512, virtual_size=2048,
visibility='public',
disk_format='raw',
@@ -166,6 +177,7 @@ class TestImagesController(base.IsolatedUnitTest):
properties={'hypervisor_type': 'kvm', 'foo': 'bar',
'bar': 'foo'}),
_db_fixture(UUID3, owner=TENANT3, checksum=CHKSUM1,
+ os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH2,
name='3', size=512, virtual_size=2048,
visibility='public', tags=['windows', '64bit', 'x86']),
_db_fixture(UUID4, owner=TENANT4, name='4',
@@ -291,6 +303,34 @@ class TestImagesController(base.IsolatedUnitTest):
output = self.controller.index(req, filters={'checksum': '236231827'})
self.assertEqual(0, len(output['images']))
+ def test_index_with_os_hash_value_filter_single_image(self):
+ req = unit_test_utils.get_fake_request(
+ '/images?os_hash_value=%s' % MULTIHASH1)
+ output = self.controller.index(req,
+ filters={'os_hash_value': MULTIHASH1})
+ self.assertEqual(1, len(output['images']))
+ actual = list([image.image_id for image in output['images']])
+ expected = [UUID1]
+ self.assertEqual(expected, actual)
+
+ def test_index_with_os_hash_value_filter_multiple_images(self):
+ req = unit_test_utils.get_fake_request(
+ '/images?os_hash_value=%s' % MULTIHASH2)
+ output = self.controller.index(req,
+ filters={'os_hash_value': MULTIHASH2})
+ self.assertEqual(2, len(output['images']))
+ actual = list([image.image_id for image in output['images']])
+ expected = [UUID3, UUID2]
+ self.assertEqual(expected, actual)
+
+ def test_index_with_non_existent_os_hash_value(self):
+ fake_hash_value = hashlib.sha512(b'not_used_in_fixtures').hexdigest()
+ req = unit_test_utils.get_fake_request(
+ '/images?os_hash_value=%s' % fake_hash_value)
+ output = self.controller.index(req,
+ filters={'checksum': fake_hash_value})
+ self.assertEqual(0, len(output['images']))
+
def test_index_size_max_filter(self):
request = unit_test_utils.get_fake_request('/images?size_max=512')
output = self.controller.index(request, filters={'size_max': 512})
@@ -2776,6 +2816,8 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
'id': '00000000-0000-0000-0000-000000000000',
'status': 'active',
'checksum': 'abcdefghijklmnopqrstuvwxyz012345',
+ 'os_hash_algo': 'supersecure',
+ 'os_hash_value': 'a' * 32 + 'b' * 32 + 'c' * 32 + 'd' * 32,
'size': 9001,
'virtual_size': 9001,
'created_at': ISOTIME,
@@ -3435,7 +3477,9 @@ class TestImagesSerializer(test_utils.BaseTestCase):
visibility='public', container_format='ami',
tags=['one', 'two'], disk_format='ami',
min_ram=128, min_disk=10,
- checksum='ca425b88f047ce8ec45ee90e813ada91'),
+ checksum='ca425b88f047ce8ec45ee90e813ada91',
+ os_hash_algo=FAKEHASHALGO,
+ os_hash_value=MULTIHASH1),
# NOTE(bcwaldon): This second fixture depends on default behavior
# and sets most values to None
@@ -3456,6 +3500,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'size': 1024,
'virtual_size': 3072,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': MULTIHASH1,
'container_format': 'ami',
'disk_format': 'ami',
'min_ram': 128,
@@ -3485,6 +3531,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'min_ram': None,
'min_disk': None,
'checksum': None,
+ 'os_hash_algo': None,
+ 'os_hash_value': None,
'disk_format': None,
'virtual_size': None,
'container_format': None,
@@ -3564,6 +3612,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'size': 1024,
'virtual_size': 3072,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': MULTIHASH1,
'container_format': 'ami',
'disk_format': 'ami',
'min_ram': 128,
@@ -3601,6 +3651,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'min_ram': None,
'min_disk': None,
'checksum': None,
+ 'os_hash_algo': None,
+ 'os_hash_value': None,
'disk_format': None,
'virtual_size': None,
'container_format': None,
@@ -3621,6 +3673,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'size': 1024,
'virtual_size': 3072,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': MULTIHASH1,
'container_format': 'ami',
'disk_format': 'ami',
'min_ram': 128,
@@ -3687,6 +3741,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
'size': 1024,
'virtual_size': 3072,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': MULTIHASH1,
'container_format': 'ami',
'disk_format': 'ami',
'min_ram': 128,
@@ -3733,6 +3789,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
'min_ram': 128,
'min_disk': 10,
'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': MULTIHASH1,
'extra_properties': {'lang': u'Fran\u00E7ais',
u'dispos\u00E9': u'f\u00E2ch\u00E9'},
}),
@@ -3752,6 +3810,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
u'size': 1024,
u'virtual_size': 3072,
u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
+ u'os_hash_algo': six.text_type(FAKEHASHALGO),
+ u'os_hash_value': six.text_type(MULTIHASH1),
u'container_format': u'ami',
u'disk_format': u'ami',
u'min_ram': 128,
@@ -3790,6 +3850,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
u'size': 1024,
u'virtual_size': 3072,
u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
+ u'os_hash_algo': six.text_type(FAKEHASHALGO),
+ u'os_hash_value': six.text_type(MULTIHASH1),
u'container_format': u'ami',
u'disk_format': u'ami',
u'min_ram': 128,
@@ -3822,6 +3884,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
u'size': 1024,
u'virtual_size': 3072,
u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
+ u'os_hash_algo': six.text_type(FAKEHASHALGO),
+ u'os_hash_value': six.text_type(MULTIHASH1),
u'container_format': u'ami',
u'disk_format': u'ami',
u'min_ram': 128,
@@ -3856,6 +3920,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
u'size': 1024,
u'virtual_size': 3072,
u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
+ u'os_hash_algo': six.text_type(FAKEHASHALGO),
+ u'os_hash_value': six.text_type(MULTIHASH1),
u'container_format': u'ami',
u'disk_format': u'ami',
u'min_ram': 128,
@@ -3895,6 +3961,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
self.fixture = _domain_fixture(
UUID2, name='image-2', owner=TENANT2,
checksum='ca425b88f047ce8ec45ee90e813ada91',
+ os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
created_at=DATETIME, updated_at=DATETIME, size=1024,
virtual_size=3072, extra_properties=props)
@@ -3907,6 +3974,8 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
'protected': False,
'os_hidden': False,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': MULTIHASH1,
'tags': [],
'size': 1024,
'virtual_size': 3072,
@@ -3936,6 +4005,8 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
'protected': False,
'os_hidden': False,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': MULTIHASH1,
'tags': [],
'size': 1024,
'virtual_size': 3072,
@@ -3964,6 +4035,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
self.fixture = _domain_fixture(
UUID2, name='image-2', owner=TENANT2,
checksum='ca425b88f047ce8ec45ee90e813ada91',
+ os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
created_at=DATETIME, updated_at=DATETIME, size=1024,
virtual_size=3072, extra_properties={'marx': 'groucho'})
@@ -3977,6 +4049,8 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
'protected': False,
'os_hidden': False,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': MULTIHASH1,
'marx': 'groucho',
'tags': [],
'size': 1024,
@@ -4012,6 +4086,8 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
'protected': False,
'os_hidden': False,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': MULTIHASH1,
'marx': 123,
'tags': [],
'size': 1024,
@@ -4042,6 +4118,8 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
'protected': False,
'os_hidden': False,
'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
+ 'os_hash_algo': FAKEHASHALGO,
+ 'os_hash_value': MULTIHASH1,
'tags': [],
'size': 1024,
'virtual_size': 3072,
diff --git a/glance/tests/unit/v2/test_schemas_resource.py b/glance/tests/unit/v2/test_schemas_resource.py
index e25627807..adcc9361e 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):
'disk_format', 'updated_at', 'visibility', 'self',
'file', 'container_format', 'schema', 'id', 'size',
'direct_url', 'min_ram', 'min_disk', 'protected',
- 'locations', 'owner', 'virtual_size', 'os_hidden'])
+ 'locations', 'owner', 'virtual_size', 'os_hidden',
+ 'os_hash_algo', 'os_hash_value'])
self.assertEqual(expected, set(output['properties'].keys()))
def test_image_has_correct_statuses(self):
diff --git a/lower-constraints.txt b/lower-constraints.txt
index 706e4b2eb..d032306b2 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -35,7 +35,7 @@ future==0.16.0
futurist==1.2.0
gitdb2==2.0.3
GitPython==2.1.8
-glance-store==0.22.0
+glance-store==0.26.1
greenlet==0.4.13
hacking==0.12.0
httplib2==0.9.1
diff --git a/releasenotes/notes/multihash-081466a98601da20.yaml b/releasenotes/notes/multihash-081466a98601da20.yaml
new file mode 100644
index 000000000..df3c470ed
--- /dev/null
+++ b/releasenotes/notes/multihash-081466a98601da20.yaml
@@ -0,0 +1,55 @@
+---
+features:
+ - |
+ This release implements the Glance spec `Secure Hash Algorithm Support
+ <https://specs.openstack.org/openstack/glance-specs/specs/rocky/approved/glance/multihash.html>`_
+ (also known as "multihash"). This feature supplements the current
+ 'checksum' image property with a self-describing secure hash. The
+ self-description consists of two new image properties:
+
+ * ``os_hash_algo`` - this contains the name of the secure hash algorithm
+ used to generate the value on this image
+ * ``os_hash_value`` - this is the hexdigest computed by applying the
+ secure hash algorithm named in the ``os_hash_algo`` property to the
+ image data
+
+ These are read-only image properties and are not user-modifiable.
+
+ The secure hash algorithm used is an operator-configurable setting. See
+ the help text for 'hashing_algorithm' in the sample Glance configuration
+ file for more information.
+
+ The default secure hash algorithm is SHA-512. It should be suitable for
+ most applications.
+
+ The legacy 'checksum' image property, which provides an MD5 message
+ digest of the image data, is preserved for backward compatibility.
+issues:
+ - |
+ The ``os_hash_value`` image property, introduced as part of the
+ `Secure Hash Algorithm Support
+ <https://specs.openstack.org/openstack/glance-specs/specs/rocky/approved/glance/multihash.html>`_
+ ("multihash") feature, is limited to 128 characters. This is sufficient
+ to store 512 bits as a hexadecimal numeral.
+
+ - |
+ The "multihash" implemented in this release (`Secure Hash Algorithm Support
+ <https://specs.openstack.org/openstack/glance-specs/specs/rocky/approved/glance/multihash.html>`_)
+ is computed only for new images. There is no provision for computing
+ the multihash for existing images. Thus, users should expect to see
+ JSON 'null' values for the ``os_hash_algo`` and ``os_hash_value`` image
+ properties on images created prior to the installation of the Rocky
+ release at your site.
+security:
+ - |
+ This release implements the Glance spec `Secure Hash Algorithm Support
+ <https://specs.openstack.org/openstack/glance-specs/specs/rocky/approved/glance/multihash.html>`_,
+ which introduces a self-describing "multihash" to the image-show response.
+ This feature supplements the current 'checksum' image property with a
+ self-describing secure hash. The default hashing algorithm is SHA-512,
+ which is currently considered secure. In the event that algorithm is
+ compromised, you will immediately be able to begin using a different
+ algorithm (as long as it's supported by the Python 'hashlib' library and
+ has output that fits in 128 characters) by modifying the value of the
+ 'hashing_algorithm' configuration option and either restarting or issuing
+ a SIGHUP to Glance.
diff --git a/requirements.txt b/requirements.txt
index 51327ca1d..1b5027124 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -46,7 +46,7 @@ retrying!=1.3.0,>=1.2.3 # Apache-2.0
osprofiler>=1.4.0 # Apache-2.0
# Glance Store
-glance-store>=0.22.0 # Apache-2.0
+glance-store>=0.26.1 # Apache-2.0
debtcollector>=1.2.0 # Apache-2.0