Browse Source

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
tags/17.0.0.0rc1
Brian Rosmaita 9 months ago
parent
commit
0b24dbd620
32 changed files with 511 additions and 35 deletions
  1. 7
    1
      api-ref/source/v2/images-images-v2.inc
  2. 21
    0
      api-ref/source/v2/images-parameters.yaml
  3. 2
    0
      api-ref/source/v2/samples/image-create-response.json
  4. 2
    0
      api-ref/source/v2/samples/image-details-deactivate-response.json
  5. 2
    0
      api-ref/source/v2/samples/image-show-response.json
  6. 2
    0
      api-ref/source/v2/samples/image-update-response.json
  7. 4
    0
      api-ref/source/v2/samples/images-list-response.json
  8. 18
    0
      api-ref/source/v2/samples/schemas-image-show-response.json
  9. 18
    0
      api-ref/source/v2/samples/schemas-images-list-response.json
  10. 2
    0
      glance/api/authorization.py
  11. 17
    2
      glance/api/v2/images.py
  12. 34
    0
      glance/common/config.py
  13. 4
    0
      glance/db/__init__.py
  14. 3
    1
      glance/db/simple/api.py
  15. 26
    0
      glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate02_empty.py
  16. 25
    0
      glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract02_empty.py
  17. 33
    0
      glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand02_add_os_hash_.py
  18. 4
    0
      glance/db/sqlalchemy/api.py
  19. 4
    1
      glance/db/sqlalchemy/models.py
  20. 4
    1
      glance/domain/__init__.py
  21. 2
    0
      glance/domain/proxy.py
  22. 10
    1
      glance/location.py
  23. 41
    0
      glance/tests/functional/db/migrations/test_rocky_expand02.py
  24. 59
    24
      glance/tests/functional/v2/test_images.py
  25. 2
    0
      glance/tests/functional/v2/test_schemas.py
  26. 3
    0
      glance/tests/unit/test_policy.py
  27. 24
    0
      glance/tests/unit/utils.py
  28. 79
    1
      glance/tests/unit/v2/test_images_resource.py
  29. 2
    1
      glance/tests/unit/v2/test_schemas_resource.py
  30. 1
    1
      lower-constraints.txt
  31. 55
    0
      releasenotes/notes/multihash-081466a98601da20.yaml
  32. 1
    1
      requirements.txt

+ 7
- 1
api-ref/source/v2/images-images-v2.inc View File

@@ -202,6 +202,8 @@ Response Parameters
202 202
    - min_disk: min_disk
203 203
    - min_ram: min_ram
204 204
    - name: name
205
+   - os_hash_algo: os_hash_algo
206
+   - os_hash_value: os_hash_value
205 207
    - owner: owner
206 208
    - protected: protected
207 209
    - schema: schema-image
@@ -266,6 +268,8 @@ Response Parameters
266 268
    - min_disk: min_disk
267 269
    - min_ram: min_ram
268 270
    - name: name
271
+   - os_hash_algo: os_hash_algo
272
+   - os_hash_value: os_hash_value
269 273
    - owner: owner
270 274
    - protected: protected
271 275
    - schema: schema-image
@@ -584,8 +588,10 @@ Response Parameters
584 588
    - id: id
585 589
    - min_disk: min_disk
586 590
    - min_ram: min_ram
587
-   - owner: owner
588 591
    - name: name
592
+   - owner: owner
593
+   - os_hash_algo: os_hash_algo
594
+   - os_hash_value: os_hash_value
589 595
    - protected: protected
590 596
    - schema: schema-image
591 597
    - self: self

+ 21
- 0
api-ref/source/v2/images-parameters.yaml View File

@@ -484,6 +484,27 @@ next:
484 484
   in: body
485 485
   required: true
486 486
   type: string
487
+os_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
497
+os_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
487 508
 owner:
488 509
   description: |
489 510
     An identifier for the owner of the image, usually the project (also

+ 2
- 0
api-ref/source/v2/samples/image-create-response.json View File

@@ -15,6 +15,8 @@
15 15
     "id": "b2173dd3-7ad6-4362-baa6-a68bce3565cb",
16 16
     "file": "/v2/images/b2173dd3-7ad6-4362-baa6-a68bce3565cb/file",
17 17
     "checksum": null,
18
+    "os_hash_algo": null,
19
+    "os_hash_value": null,
18 20
     "owner": "bab7d5c60cd041a0a36f7c4b6e1dd978",
19 21
     "virtual_size": null,
20 22
     "min_ram": 0,

+ 2
- 0
api-ref/source/v2/samples/image-details-deactivate-response.json View File

@@ -13,6 +13,8 @@
13 13
     "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
14 14
     "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
15 15
     "checksum": "64d7c1cd2b6f60c92c14662941cb7913",
16
+    "os_hash_algo": "sha512",
17
+    "os_hash_value": "073b4523583784fbe01daff81eba092a262ec37ba6d04dd3f52e4cd5c93eb8258af44881345ecda0e49f3d8cc6d2df6b050ff3e72681d723234aff9d17d0cf09"
16 18
     "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
17 19
     "size": 13167616,
18 20
     "min_ram": 0,

+ 2
- 0
api-ref/source/v2/samples/image-show-response.json View File

@@ -13,6 +13,8 @@
13 13
     "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
14 14
     "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
15 15
     "checksum": "64d7c1cd2b6f60c92c14662941cb7913",
16
+    "os_hash_algo": "sha512",
17
+    "os_hash_value": "073b4523583784fbe01daff81eba092a262ec37ba6d04dd3f52e4cd5c93eb8258af44881345ecda0e49f3d8cc6d2df6b050ff3e72681d723234aff9d17d0cf09"
16 18
     "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
17 19
     "size": 13167616,
18 20
     "min_ram": 0,

+ 2
- 0
api-ref/source/v2/samples/image-update-response.json View File

@@ -9,6 +9,8 @@
9 9
     "min_ram": 512,
10 10
     "name": "Fedora 17",
11 11
     "owner": "02a7fb2dd4ef434c8a628c511dcbbeb6",
12
+    "os_hash_algo": "sha512",
13
+    "os_hash_value": "ef7d1ed957ffafefb324d50ebc6685ed03d0e64549762ba94a1c44e92270cdbb69d7437dd1e101d00dd41684aaecccad1edc5c2e295e66d4733025b052497844"
12 14
     "protected": false,
13 15
     "schema": "/v2/schemas/image",
14 16
     "self": "/v2/images/2b61ed2b-f800-4da0-99ff-396b742b8646",

+ 4
- 0
api-ref/source/v2/samples/images-list-response.json View File

@@ -15,6 +15,8 @@
15 15
             "id": "1bea47ed-f6a9-463b-b423-14b9cca9ad27",
16 16
             "file": "/v2/images/1bea47ed-f6a9-463b-b423-14b9cca9ad27/file",
17 17
             "checksum": "64d7c1cd2b6f60c92c14662941cb7913",
18
+            "os_hash_algo": "sha512",
19
+            "os_hash_value": "073b4523583784fbe01daff81eba092a262ec37ba6d04dd3f52e4cd5c93eb8258af44881345ecda0e49f3d8cc6d2df6b050ff3e72681d723234aff9d17d0cf09"
18 20
             "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
19 21
             "size": 13167616,
20 22
             "min_ram": 0,
@@ -36,6 +38,8 @@
36 38
             "id": "781b3762-9469-4cec-b58d-3349e5de4e9c",
37 39
             "file": "/v2/images/781b3762-9469-4cec-b58d-3349e5de4e9c/file",
38 40
             "checksum": "afab0f79bac770d61d24b4d0560b5f70",
41
+            "os_hash_algo": "sha512",
42
+            "os_hash_value": "ea3e20140df1cc65f53d4c5b9ee3b38d0d6868f61bbe2230417b0f98cef0e0c7c37f0ebc5c6456fa47f013de48b452617d56c15fdba25e100379bd0e81ee15ec"
39 43
             "owner": "5ef70662f8b34079a6eddb8da9d75fe8",
40 44
             "size": 476704768,
41 45
             "min_ram": 0,

+ 18
- 0
api-ref/source/v2/samples/schemas-image-show-response.json View File

@@ -145,6 +145,24 @@
145 145
             "is_base": false,
146 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 166
         "os_version": {
149 167
             "description": "Operating system version as specified by the distributor",
150 168
             "is_base": false,

+ 18
- 0
api-ref/source/v2/samples/schemas-images-list-response.json View File

@@ -166,6 +166,24 @@
166 166
                         "is_base": false,
167 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 187
                     "os_version": {
170 188
                         "description": "Operating system version as specified by the distributor",
171 189
                         "is_base": false,

+ 2
- 0
glance/api/authorization.py View File

@@ -315,6 +315,8 @@ class ImmutableImageProxy(object):
315 315
     min_disk = _immutable_attr('base', 'min_disk')
316 316
     min_ram = _immutable_attr('base', 'min_ram')
317 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 320
     os_hidden = _immutable_attr('base', 'os_hidden')
319 321
     locations = _immutable_attr('base', 'locations', proxy=ImmutableLocations)
320 322
     checksum = _immutable_attr('base', 'checksum')

+ 17
- 2
glance/api/v2/images.py View File

@@ -446,7 +446,8 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
446 446
     _disallowed_properties = ('direct_url', 'self', 'file', 'schema')
447 447
     _readonly_properties = ('created_at', 'updated_at', 'status', 'checksum',
448 448
                             'size', 'virtual_size', 'direct_url', 'self',
449
-                            'file', 'schema', 'id')
449
+                            'file', 'schema', 'id', 'os_hash_algo',
450
+                            'os_hash_value')
450 451
     _reserved_properties = ('location', 'deleted', 'deleted_at')
451 452
     _base_properties = ('checksum', 'created_at', 'container_format',
452 453
                         'disk_format', 'id', 'min_disk', 'min_ram', 'name',
@@ -884,7 +885,8 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
884 885
             attributes = ['name', 'disk_format', 'container_format',
885 886
                           'visibility', 'size', 'virtual_size', 'status',
886 887
                           'checksum', 'protected', 'min_ram', 'min_disk',
887
-                          'owner', 'os_hidden']
888
+                          'owner', 'os_hidden', 'os_hash_algo',
889
+                          'os_hash_value']
888 890
             for key in attributes:
889 891
                 image_view[key] = getattr(image, key)
890 892
             image_view['id'] = image.image_id
@@ -1018,6 +1020,19 @@ def get_base_properties():
1018 1020
             'description': _('md5 hash of image contents.'),
1019 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 1036
         'owner': {
1022 1037
             'type': ['null', 'string'],
1023 1038
             'description': _('Owner of the image'),

+ 34
- 0
glance/common/config.py View File

@@ -191,6 +191,40 @@ Possible values:
191 191
 Related options:
192 192
     * image_property_quota
193 193
 
194
+""")),
195
+    cfg.StrOpt('hashing_algorithm',
196
+               default='sha512',
197
+               help=_(""""
198
+Secure hashing algorithm used for computing the 'os_hash_value' property.
199
+
200
+This option configures the Glance "multihash", which consists of two
201
+image properties: the 'os_hash_algo' and the 'os_hash_value'.  The
202
+'os_hash_algo' will be populated by the value of this configuration
203
+option, and the 'os_hash_value' will be populated by the hexdigest computed
204
+when the algorithm is applied to the uploaded or imported image data.
205
+
206
+The value must be a valid secure hash algorithm name recognized by the
207
+python 'hashlib' library.  You can determine what these are by examining
208
+the 'hashlib.algorithms_available' data member of the version of the
209
+library being used in your Glance installation.  For interoperability
210
+purposes, however, we recommend that you use the set of secure hash
211
+names supplies by the 'hashlib.algorithms_guaranteed' data member because
212
+those algorithms are guaranteed to be supported by the 'hashlib' library
213
+on all platforms.  Thus, any image consumer using 'hashlib' locally should
214
+be able to verify the 'os_hash_value' of the image.
215
+
216
+The default value of 'sha512' is a performant secure hash algorithm.
217
+
218
+If this option is misconfigured, any attempts to store image data will fail.
219
+For that reason, we recommend using the default value.
220
+
221
+Possible values:
222
+    * Any secure hash algorithm name recognized by the Python 'hashlib'
223
+      library
224
+
225
+Related options:
226
+    * None
227
+
194 228
 """)),
195 229
     cfg.IntOpt('image_member_quota', default=128,
196 230
                help=_("""

+ 4
- 0
glance/db/__init__.py View File

@@ -130,6 +130,8 @@ class ImageRepo(object):
130 130
             protected=db_image['protected'],
131 131
             locations=location_strategy.get_ordered_locations(locations),
132 132
             checksum=db_image['checksum'],
133
+            os_hash_algo=db_image['os_hash_algo'],
134
+            os_hash_value=db_image['os_hash_value'],
133 135
             owner=db_image['owner'],
134 136
             disk_format=db_image['disk_format'],
135 137
             container_format=db_image['container_format'],
@@ -162,6 +164,8 @@ class ImageRepo(object):
162 164
             'protected': image.protected,
163 165
             'locations': locations,
164 166
             'checksum': image.checksum,
167
+            'os_hash_algo': image.os_hash_algo,
168
+            'os_hash_value': image.os_hash_value,
165 169
             'owner': image.owner,
166 170
             'disk_format': image.disk_format,
167 171
             'container_format': image.container_format,

+ 3
- 1
glance/db/simple/api.py View File

@@ -225,6 +225,8 @@ def _image_format(image_id, **values):
225 225
         'size': None,
226 226
         'virtual_size': None,
227 227
         'checksum': None,
228
+        'os_hash_algo': None,
229
+        'os_hash_value': None,
228 230
         'tags': [],
229 231
         'created_at': dt,
230 232
         'updated_at': dt,
@@ -735,7 +737,7 @@ def image_create(context, image_values, v1_mode=False):
735 737
                         'protected', 'is_public', 'container_format',
736 738
                         'disk_format', 'created_at', 'updated_at', 'deleted',
737 739
                         'deleted_at', 'properties', 'tags', 'visibility',
738
-                        'os_hidden'])
740
+                        'os_hidden', 'os_hash_algo', 'os_hash_value'])
739 741
 
740 742
     incorrect_keys = set(image_values.keys()) - allowed_keys
741 743
     if incorrect_keys:

+ 26
- 0
glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate02_empty.py View File

@@ -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
+
17
+def has_migrations(engine):
18
+    """Returns true if at least one data row can be migrated."""
19
+
20
+    return False
21
+
22
+
23
+def migrate(engine):
24
+    """Return the number of rows migrated."""
25
+
26
+    return 0

+ 25
- 0
glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract02_empty.py View File

@@ -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.
18
+revision = 'rocky_contract02'
19
+down_revision = 'rocky_contract01'
20
+branch_labels = None
21
+depends_on = 'rocky_expand02'
22
+
23
+
24
+def upgrade():
25
+    pass

+ 33
- 0
glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand02_add_os_hash_.py View File

@@ -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
+
18
+from alembic import op
19
+from sqlalchemy import Column, String
20
+
21
+# revision identifiers, used by Alembic.
22
+revision = 'rocky_expand02'
23
+down_revision = 'rocky_expand01'
24
+branch_labels = None
25
+depends_on = None
26
+
27
+
28
+def 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'])

+ 4
- 0
glance/db/sqlalchemy/api.py View File

@@ -468,6 +468,10 @@ def _make_conditions_from_filters(filters, is_public=None):
468 468
         checksum = filters.pop('checksum')
469 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 475
     for (k, v) in filters.pop('properties', {}).items():
472 476
         prop_filters = _make_image_property_condition(key=k, value=v)
473 477
         prop_conditions.append(prop_filters)

+ 4
- 1
glance/db/sqlalchemy/models.py View File

@@ -120,7 +120,8 @@ class Image(BASE, GlanceBase):
120 120
                       Index('owner_image_idx', 'owner'),
121 121
                       Index('created_at_image_idx', 'created_at'),
122 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 126
     id = Column(String(36), primary_key=True,
126 127
                 default=lambda: str(uuid.uuid4()))
@@ -134,6 +135,8 @@ class Image(BASE, GlanceBase):
134 135
                         name='image_visibility'), nullable=False,
135 136
                         server_default='shared')
136 137
     checksum = Column(String(32))
138
+    os_hash_algo = Column(String(64))
139
+    os_hash_value = Column(String(128))
137 140
     min_disk = Column(Integer, nullable=False, default=0)
138 141
     min_ram = Column(Integer, nullable=False, default=0)
139 142
     owner = Column(String(255))

+ 4
- 1
glance/domain/__init__.py View File

@@ -48,7 +48,8 @@ def _import_delayed_delete():
48 48
 
49 49
 class ImageFactory(object):
50 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 53
     _reserved_properties = ['owner', 'locations', 'deleted', 'deleted_at',
53 54
                             'direct_url', 'self', 'file', 'schema']
54 55
 
@@ -127,6 +128,8 @@ class Image(object):
127 128
         self.protected = kwargs.pop('protected', False)
128 129
         self.locations = kwargs.pop('locations', [])
129 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 133
         self.owner = kwargs.pop('owner', None)
131 134
         self._disk_format = kwargs.pop('disk_format', None)
132 135
         self._container_format = kwargs.pop('container_format', None)

+ 2
- 0
glance/domain/proxy.py View File

@@ -175,6 +175,8 @@ class Image(object):
175 175
     os_hidden = _proxy('base', 'os_hidden')
176 176
     locations = _proxy('base', 'locations')
177 177
     checksum = _proxy('base', 'checksum')
178
+    os_hash_algo = _proxy('base', 'os_hash_algo')
179
+    os_hash_value = _proxy('base', 'os_hash_value')
178 180
     owner = _proxy('base', 'owner')
179 181
     disk_format = _proxy('base', 'disk_format')
180 182
     container_format = _proxy('base', 'container_format')

+ 10
- 1
glance/location.py View File

@@ -428,12 +428,19 @@ class ImageProxy(glance.domain.proxy.Image):
428 428
         else:
429 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 438
             CONF,
433 439
             self.image.image_id,
434 440
             utils.LimitingReader(utils.CooperativeReader(data),
435 441
                                  CONF.image_size_cap),
436 442
             size,
443
+            hashing_algo,
437 444
             context=self.context,
438 445
             verifier=verifier)
439 446
 
@@ -454,6 +461,8 @@ class ImageProxy(glance.domain.proxy.Image):
454 461
                                  'status': 'active'}]
455 462
         self.image.size = size
456 463
         self.image.checksum = checksum
464
+        self.image.os_hash_value = multihash
465
+        self.image.os_hash_algo = hashing_algo
457 466
         self.image.status = 'active'
458 467
 
459 468
     def get_data(self, offset=0, chunk_size=None):

+ 41
- 0
glance/tests/functional/db/migrations/test_rocky_expand02.py View File

@@ -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
+
14
+from oslo_db.sqlalchemy import test_base
15
+from oslo_db.sqlalchemy import utils as db_utils
16
+
17
+from glance.tests.functional.db import test_migrations
18
+
19
+
20
+class 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
+
39
+class TestRockyExpand02MySQL(TestRockyExpand02Mixin,
40
+                             test_base.MySQLOpportunisticTestCase):
41
+    pass

+ 59
- 24
glance/tests/functional/v2/test_images.py View File

@@ -13,6 +13,7 @@
13 13
 #    License for the specific language governing permissions and limitations
14 14
 #    under the License.
15 15
 
16
+import hashlib
16 17
 import os
17 18
 import signal
18 19
 import uuid
@@ -158,6 +159,8 @@ class TestImages(functional.FunctionalTest):
158 159
             u'container_format',
159 160
             u'owner',
160 161
             u'checksum',
162
+            u'os_hash_algo',
163
+            u'os_hash_value',
161 164
             u'size',
162 165
             u'virtual_size',
163 166
         ])
@@ -186,23 +189,29 @@ class TestImages(functional.FunctionalTest):
186 189
         self.assertEqual(1, len(images))
187 190
         self.assertEqual(image_id, images[0]['id'])
188 191
 
189
-        def _verify_image_checksum_and_status(checksum=None, status=None):
190
-            # Checksum should be populated and status should be active
192
+        def _verify_image_hashes_and_status(
193
+                checksum=None, os_hash_value=None, status=None):
191 194
             path = self._url('/v2/images/%s' % image_id)
192 195
             response = requests.get(path, headers=self._headers())
193 196
             self.assertEqual(http.OK, response.status_code)
194 197
             image = jsonutils.loads(response.text)
195 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 204
             self.assertEqual(status, image['status'])
197 205
 
198 206
         # Upload some image data to staging area
199 207
         path = self._url('/v2/images/%s/stage' % image_id)
200 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 211
         self.assertEqual(http.NO_CONTENT, response.status_code)
203 212
 
204
-        # Verify image is in uploading state and checksum is None
205
-        _verify_image_checksum_and_status(status='uploading')
213
+        # Verify image is in uploading state, hashes are None
214
+        _verify_image_hashes_and_status(status='uploading')
206 215
 
207 216
         # Import image to store
208 217
         path = self._url('/v2/images/%s/import' % image_id)
@@ -225,9 +234,11 @@ class TestImages(functional.FunctionalTest):
225 234
                                    status='active',
226 235
                                    max_sec=2,
227 236
                                    delay_sec=0.2)
228
-        _verify_image_checksum_and_status(
229
-            checksum='8f113e38d28a79a5a451b16048cc2b72',
230
-            status='active')
237
+        expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
238
+        expect_h = six.text_type(hashlib.sha512(image_data).hexdigest())
239
+        _verify_image_hashes_and_status(checksum=expect_c,
240
+                                        os_hash_value=expect_h,
241
+                                        status='active')
231 242
 
232 243
         # Ensure the size is updated to reflect the data uploaded
233 244
         path = self._url('/v2/images/%s' % image_id)
@@ -300,6 +311,8 @@ class TestImages(functional.FunctionalTest):
300 311
             u'container_format',
301 312
             u'owner',
302 313
             u'checksum',
314
+            u'os_hash_algo',
315
+            u'os_hash_value',
303 316
             u'size',
304 317
             u'virtual_size',
305 318
         ])
@@ -328,17 +341,22 @@ class TestImages(functional.FunctionalTest):
328 341
         self.assertEqual(1, len(images))
329 342
         self.assertEqual(image_id, images[0]['id'])
330 343
 
331
-        def _verify_image_checksum_and_status(checksum=None, status=None):
332
-            # Checksum should be populated and status should be active
344
+        def _verify_image_hashes_and_status(
345
+                checksum=None, os_hash_value=None, status=None):
333 346
             path = self._url('/v2/images/%s' % image_id)
334 347
             response = requests.get(path, headers=self._headers())
335 348
             self.assertEqual(http.OK, response.status_code)
336 349
             image = jsonutils.loads(response.text)
337 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 356
             self.assertEqual(status, image['status'])
339 357
 
340
-        # Verify image is in queued state and checksum is None
341
-        _verify_image_checksum_and_status(status='queued')
358
+        # Verify image is in queued state and hashes are None
359
+        _verify_image_hashes_and_status(status='queued')
342 360
 
343 361
         # Import image to store
344 362
         path = self._url('/v2/images/%s/import' % image_id)
@@ -346,10 +364,11 @@ class TestImages(functional.FunctionalTest):
346 364
             'content-type': 'application/json',
347 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 369
         data = jsonutils.dumps({'method': {
350 370
             'name': 'web-download',
351
-            'uri': 'https://www.openstack.org/assets/openstack-logo/'
352
-                   '2016R/OpenStack-Logo-Horizontal.eps.zip'
371
+            'uri': image_data_uri
353 372
         }})
354 373
         response = requests.post(path, headers=headers, data=data)
355 374
         self.assertEqual(http.ACCEPTED, response.status_code)
@@ -364,9 +383,12 @@ class TestImages(functional.FunctionalTest):
364 383
                                    max_sec=20,
365 384
                                    delay_sec=0.2,
366 385
                                    start_delay_sec=1)
367
-        _verify_image_checksum_and_status(
368
-            checksum='bcd65f8922f61a9e6a20572ad7aa2bdd',
369
-            status='active')
386
+        with requests.get(image_data_uri) as r:
387
+            expect_c = six.text_type(hashlib.md5(r.content).hexdigest())
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 393
         # Deleting image should work
372 394
         path = self._url('/v2/images/%s' % image_id)
@@ -428,6 +450,8 @@ class TestImages(functional.FunctionalTest):
428 450
             u'container_format',
429 451
             u'owner',
430 452
             u'checksum',
453
+            u'os_hash_algo',
454
+            u'os_hash_value',
431 455
             u'size',
432 456
             u'virtual_size',
433 457
             u'locations',
@@ -493,6 +517,8 @@ class TestImages(functional.FunctionalTest):
493 517
             u'container_format',
494 518
             u'owner',
495 519
             u'checksum',
520
+            u'os_hash_algo',
521
+            u'os_hash_value',
496 522
             u'size',
497 523
             u'virtual_size',
498 524
             u'locations',
@@ -722,23 +748,28 @@ class TestImages(functional.FunctionalTest):
722 748
         response = requests.get(path, headers=headers)
723 749
         self.assertEqual(http.NO_CONTENT, response.status_code)
724 750
 
725
-        def _verify_image_checksum_and_status(checksum, status):
726
-            # Checksum should be populated and status should be active
751
+        def _verify_image_hashes_and_status(checksum, os_hash_value, status):
752
+            # hashes should be populated and status should be active
727 753
             path = self._url('/v2/images/%s' % image_id)
728 754
             response = requests.get(path, headers=self._headers())
729 755
             self.assertEqual(http.OK, response.status_code)
730 756
             image = jsonutils.loads(response.text)
731 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 761
             self.assertEqual(status, image['status'])
733 762
 
734 763
         # Upload some image data
735 764
         path = self._url('/v2/images/%s/file' % image_id)
736 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 768
         self.assertEqual(http.NO_CONTENT, response.status_code)
739 769
 
740
-        expected_checksum = '8f113e38d28a79a5a451b16048cc2b72'
741
-        _verify_image_checksum_and_status(expected_checksum, 'active')
770
+        expect_c = six.text_type(hashlib.md5(image_data).hexdigest())
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 774
         # `disk_format` and `container_format` cannot
744 775
         # be replaced when the image is active.
@@ -757,7 +788,7 @@ class TestImages(functional.FunctionalTest):
757 788
         path = self._url('/v2/images/%s/file' % image_id)
758 789
         response = requests.get(path, headers=self._headers())
759 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 792
         self.assertEqual('ZZZZZ', response.text)
762 793
 
763 794
         # Uploading duplicate data should be rejected with a 409. The
@@ -766,7 +797,7 @@ class TestImages(functional.FunctionalTest):
766 797
         headers = self._headers({'Content-Type': 'application/octet-stream'})
767 798
         response = requests.put(path, headers=headers, data='XXX')
768 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 802
         # Ensure the size is updated to reflect the data uploaded
772 803
         path = self._url('/v2/images/%s' % image_id)
@@ -944,6 +975,8 @@ class TestImages(functional.FunctionalTest):
944 975
             u'container_format',
945 976
             u'owner',
946 977
             u'checksum',
978
+            u'os_hash_algo',
979
+            u'os_hash_value',
947 980
             u'size',
948 981
             u'virtual_size',
949 982
             u'locations',
@@ -1009,6 +1042,8 @@ class TestImages(functional.FunctionalTest):
1009 1042
             u'container_format',
1010 1043
             u'owner',
1011 1044
             u'checksum',
1045
+            u'os_hash_algo',
1046
+            u'os_hash_value',
1012 1047
             u'size',
1013 1048
             u'virtual_size',
1014 1049
             u'locations',

+ 2
- 0
glance/tests/functional/v2/test_schemas.py View File

@@ -38,6 +38,8 @@ class TestSchemas(functional.FunctionalTest):
38 38
             'name',
39 39
             'visibility',
40 40
             'checksum',
41
+            'os_hash_algo',
42
+            'os_hash_value',
41 43
             'created_at',
42 44
             'updated_at',
43 45
             'tags',

+ 3
- 0
glance/tests/unit/test_policy.py View File

@@ -15,6 +15,7 @@
15 15
 #    under the License.
16 16
 
17 17
 import collections
18
+import hashlib
18 19
 import os.path
19 20
 
20 21
 import mock
@@ -66,6 +67,8 @@ class ImageStub(object):
66 67
         self.status = status
67 68
         self.extra_properties = extra_properties
68 69
         self.checksum = 'c2e5db72bd7fd153f53ede5da5a06de3'
70
+        self.os_hash_algo = 'sha512'
71
+        self.os_hash_value = hashlib.sha512(b'glance').hexdigest()
69 72
         self.created_at = '2013-09-28T15:27:36Z'
70 73
         self.updated_at = '2013-09-28T15:27:37Z'
71 74
         self.locations = []

+ 24
- 0
glance/tests/unit/utils.py View File

@@ -238,6 +238,30 @@ class FakeStoreAPI(object):
238 238
         checksum = 'Z'
239 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 265
     def check_location_metadata(self, val, key=''):
242 266
         store.check_location_metadata(val)
243 267
 

+ 79
- 1
glance/tests/unit/v2/test_images_resource.py View File

@@ -15,6 +15,7 @@
15 15
 
16 16
 import datetime
17 17
 import eventlet
18
+import hashlib
18 19
 import uuid
19 20
 
20 21
 import glance_store as store
@@ -56,6 +57,10 @@ TENANT4 = 'c6c87f25-8a94-47ed-8c83-053c25f42df4'
56 57
 CHKSUM = '93264c3edf5972c9f1cb309543d38a5c'
57 58
 CHKSUM1 = '43254c3edf6972c9f1cb309543d38a8c'
58 59
 
60
+FAKEHASHALGO = 'fake-name-for-sha512'
61
+MULTIHASH1 = hashlib.sha512(b'glance').hexdigest()
62
+MULTIHASH2 = hashlib.sha512(b'image_service').hexdigest()
63
+
59 64
 
60 65
 def _db_fixture(id, **kwargs):
61 66
     obj = {
@@ -64,6 +69,8 @@ def _db_fixture(id, **kwargs):
64 69
         'visibility': 'shared',
65 70
         'properties': {},
66 71
         'checksum': None,
72
+        'os_hash_algo': FAKEHASHALGO,
73
+        'os_hash_value': None,
67 74
         'owner': None,
68 75
         'status': 'queued',
69 76
         'tags': [],
@@ -87,6 +94,8 @@ def _domain_fixture(id, **kwargs):
87 94
         'name': None,
88 95
         'visibility': 'private',
89 96
         'checksum': None,
97
+        'os_hash_algo': None,
98
+        'os_hash_value': None,
90 99
         'owner': None,
91 100
         'status': 'queued',
92 101
         'size': None,
@@ -149,6 +158,7 @@ class TestImagesController(base.IsolatedUnitTest):
149 158
     def _create_images(self):
150 159
         self.images = [
151 160
             _db_fixture(UUID1, owner=TENANT1, checksum=CHKSUM,
161
+                        os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
152 162
                         name='1', size=256, virtual_size=1024,
153 163
                         visibility='public',
154 164
                         locations=[{'url': '%s/%s' % (BASE_URI, UUID1),
@@ -157,6 +167,7 @@ class TestImagesController(base.IsolatedUnitTest):
157 167
                         container_format='bare',
158 168
                         status='active'),
159 169
             _db_fixture(UUID2, owner=TENANT1, checksum=CHKSUM1,
170
+                        os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH2,
160 171
                         name='2', size=512, virtual_size=2048,
161 172
                         visibility='public',
162 173
                         disk_format='raw',
@@ -166,6 +177,7 @@ class TestImagesController(base.IsolatedUnitTest):
166 177
                         properties={'hypervisor_type': 'kvm', 'foo': 'bar',
167 178
                                     'bar': 'foo'}),
168 179
             _db_fixture(UUID3, owner=TENANT3, checksum=CHKSUM1,
180
+                        os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH2,
169 181
                         name='3', size=512, virtual_size=2048,
170 182
                         visibility='public', tags=['windows', '64bit', 'x86']),
171 183
             _db_fixture(UUID4, owner=TENANT4, name='4',
@@ -291,6 +303,34 @@ class TestImagesController(base.IsolatedUnitTest):
291 303
         output = self.controller.index(req, filters={'checksum': '236231827'})
292 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 334
     def test_index_size_max_filter(self):
295 335
         request = unit_test_utils.get_fake_request('/images?size_max=512')
296 336
         output = self.controller.index(request, filters={'size_max': 512})
@@ -2776,6 +2816,8 @@ class TestImagesDeserializer(test_utils.BaseTestCase):
2776 2816
             'id': '00000000-0000-0000-0000-000000000000',
2777 2817
             'status': 'active',
2778 2818
             'checksum': 'abcdefghijklmnopqrstuvwxyz012345',
2819
+            'os_hash_algo': 'supersecure',
2820
+            'os_hash_value': 'a' * 32 + 'b' * 32 + 'c' * 32 + 'd' * 32,
2779 2821
             'size': 9001,
2780 2822
             'virtual_size': 9001,
2781 2823
             'created_at': ISOTIME,
@@ -3435,7 +3477,9 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3435 3477
                             visibility='public', container_format='ami',
3436 3478
                             tags=['one', 'two'], disk_format='ami',
3437 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 3484
             # NOTE(bcwaldon): This second fixture depends on default behavior
3441 3485
             # and sets most values to None
@@ -3456,6 +3500,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3456 3500
                     'size': 1024,
3457 3501
                     'virtual_size': 3072,
3458 3502
                     'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3503
+                    'os_hash_algo': FAKEHASHALGO,
3504
+                    'os_hash_value': MULTIHASH1,
3459 3505
                     'container_format': 'ami',
3460 3506
                     'disk_format': 'ami',
3461 3507
                     'min_ram': 128,
@@ -3485,6 +3531,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3485 3531
                     'min_ram': None,
3486 3532
                     'min_disk': None,
3487 3533
                     'checksum': None,
3534
+                    'os_hash_algo': None,
3535
+                    'os_hash_value': None,
3488 3536
                     'disk_format': None,
3489 3537
                     'virtual_size': None,
3490 3538
                     'container_format': None,
@@ -3564,6 +3612,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3564 3612
             'size': 1024,
3565 3613
             'virtual_size': 3072,
3566 3614
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3615
+            'os_hash_algo': FAKEHASHALGO,
3616
+            'os_hash_value': MULTIHASH1,
3567 3617
             'container_format': 'ami',
3568 3618
             'disk_format': 'ami',
3569 3619
             'min_ram': 128,
@@ -3601,6 +3651,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3601 3651
             'min_ram': None,
3602 3652
             'min_disk': None,
3603 3653
             'checksum': None,
3654
+            'os_hash_algo': None,
3655
+            'os_hash_value': None,
3604 3656
             'disk_format': None,
3605 3657
             'virtual_size': None,
3606 3658
             'container_format': None,
@@ -3621,6 +3673,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3621 3673
             'size': 1024,
3622 3674
             'virtual_size': 3072,
3623 3675
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3676
+            'os_hash_algo': FAKEHASHALGO,
3677
+            'os_hash_value': MULTIHASH1,
3624 3678
             'container_format': 'ami',
3625 3679
             'disk_format': 'ami',
3626 3680
             'min_ram': 128,
@@ -3687,6 +3741,8 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3687 3741
             'size': 1024,
3688 3742
             'virtual_size': 3072,
3689 3743
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3744
+            'os_hash_algo': FAKEHASHALGO,
3745
+            'os_hash_value': MULTIHASH1,
3690 3746
             'container_format': 'ami',
3691 3747
             'disk_format': 'ami',
3692 3748
             'min_ram': 128,
@@ -3733,6 +3789,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3733 3789
                 'min_ram': 128,
3734 3790
                 'min_disk': 10,
3735 3791
                 'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
3792
+                'os_hash_algo': FAKEHASHALGO,
3793
+                'os_hash_value': MULTIHASH1,
3736 3794
                 'extra_properties': {'lang': u'Fran\u00E7ais',
3737 3795
                                      u'dispos\u00E9': u'f\u00E2ch\u00E9'},
3738 3796
             }),
@@ -3752,6 +3810,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3752 3810
                     u'size': 1024,
3753 3811
                     u'virtual_size': 3072,
3754 3812
                     u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
3813
+                    u'os_hash_algo': six.text_type(FAKEHASHALGO),
3814
+                    u'os_hash_value': six.text_type(MULTIHASH1),
3755 3815
                     u'container_format': u'ami',
3756 3816
                     u'disk_format': u'ami',
3757 3817
                     u'min_ram': 128,
@@ -3790,6 +3850,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3790 3850
             u'size': 1024,
3791 3851
             u'virtual_size': 3072,
3792 3852
             u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
3853
+            u'os_hash_algo': six.text_type(FAKEHASHALGO),
3854
+            u'os_hash_value': six.text_type(MULTIHASH1),
3793 3855
             u'container_format': u'ami',
3794 3856
             u'disk_format': u'ami',
3795 3857
             u'min_ram': 128,
@@ -3822,6 +3884,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3822 3884
             u'size': 1024,
3823 3885
             u'virtual_size': 3072,
3824 3886
             u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
3887
+            u'os_hash_algo': six.text_type(FAKEHASHALGO),
3888
+            u'os_hash_value': six.text_type(MULTIHASH1),
3825 3889
             u'container_format': u'ami',
3826 3890
             u'disk_format': u'ami',
3827 3891
             u'min_ram': 128,
@@ -3856,6 +3920,8 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3856 3920
             u'size': 1024,
3857 3921
             u'virtual_size': 3072,
3858 3922
             u'checksum': u'ca425b88f047ce8ec45ee90e813ada91',
3923
+            u'os_hash_algo': six.text_type(FAKEHASHALGO),
3924
+            u'os_hash_value': six.text_type(MULTIHASH1),
3859 3925
             u'container_format': u'ami',
3860 3926
             u'disk_format': u'ami',
3861 3927
             u'min_ram': 128,
@@ -3895,6 +3961,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
3895 3961
         self.fixture = _domain_fixture(
3896 3962
             UUID2, name='image-2', owner=TENANT2,
3897 3963
             checksum='ca425b88f047ce8ec45ee90e813ada91',
3964
+            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
3898 3965
             created_at=DATETIME, updated_at=DATETIME, size=1024,
3899 3966
             virtual_size=3072, extra_properties=props)
3900 3967
 
@@ -3907,6 +3974,8 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
3907 3974
             'protected': False,
3908 3975
             'os_hidden': False,
3909 3976
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3977
+            'os_hash_algo': FAKEHASHALGO,
3978
+            'os_hash_value': MULTIHASH1,
3910 3979
             'tags': [],
3911 3980
             'size': 1024,
3912 3981
             'virtual_size': 3072,
@@ -3936,6 +4005,8 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
3936 4005
             'protected': False,
3937 4006
             'os_hidden': False,
3938 4007
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
4008
+            'os_hash_algo': FAKEHASHALGO,
4009
+            'os_hash_value': MULTIHASH1,
3939 4010
             'tags': [],
3940 4011
             'size': 1024,
3941 4012
             'virtual_size': 3072,
@@ -3964,6 +4035,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
3964 4035
         self.fixture = _domain_fixture(
3965 4036
             UUID2, name='image-2', owner=TENANT2,
3966 4037
             checksum='ca425b88f047ce8ec45ee90e813ada91',
4038
+            os_hash_algo=FAKEHASHALGO, os_hash_value=MULTIHASH1,
3967 4039
             created_at=DATETIME, updated_at=DATETIME, size=1024,
3968 4040
             virtual_size=3072, extra_properties={'marx': 'groucho'})
3969 4041
 
@@ -3977,6 +4049,8 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
3977 4049
             'protected': False,
3978 4050
             'os_hidden': False,
3979 4051
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
4052
+            'os_hash_algo': FAKEHASHALGO,
4053
+            'os_hash_value': MULTIHASH1,
3980 4054
             'marx': 'groucho',
3981 4055
             'tags': [],
3982 4056
             'size': 1024,
@@ -4012,6 +4086,8 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
4012 4086
             'protected': False,
4013 4087
             'os_hidden': False,
4014 4088
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
4089
+            'os_hash_algo': FAKEHASHALGO,
4090
+            'os_hash_value': MULTIHASH1,
4015 4091
             'marx': 123,
4016 4092
             'tags': [],
4017 4093
             'size': 1024,
@@ -4042,6 +4118,8 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
4042 4118
             'protected': False,
4043 4119
             'os_hidden': False,
4044 4120
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
4121
+            'os_hash_algo': FAKEHASHALGO,
4122
+            'os_hash_value': MULTIHASH1,
4045 4123
             'tags': [],
4046 4124
             'size': 1024,
4047 4125
             'virtual_size': 3072,

+ 2
- 1
glance/tests/unit/v2/test_schemas_resource.py View File

@@ -33,7 +33,8 @@ class TestSchemasController(test_utils.BaseTestCase):
33 33
                         'disk_format', 'updated_at', 'visibility', 'self',
34 34
                         'file', 'container_format', 'schema', 'id', 'size',
35 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 38
         self.assertEqual(expected, set(output['properties'].keys()))
38 39
 
39 40
     def test_image_has_correct_statuses(self):

+ 1
- 1
lower-constraints.txt View File

@@ -35,7 +35,7 @@ future==0.16.0
35 35
 futurist==1.2.0
36 36
 gitdb2==2.0.3
37 37
 GitPython==2.1.8
38
-glance-store==0.22.0
38
+glance-store==0.26.1
39 39
 greenlet==0.4.13
40 40
 hacking==0.12.0
41 41
 httplib2==0.9.1

+ 55
- 0
releasenotes/notes/multihash-081466a98601da20.yaml View File

@@ -0,0 +1,55 @@
1
+---
2
+features:
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.
27
+issues:
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.
43
+security:
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.

+ 1
- 1
requirements.txt View File

@@ -46,7 +46,7 @@ retrying!=1.3.0,>=1.2.3 # Apache-2.0
46 46
 osprofiler>=1.4.0 # Apache-2.0
47 47
 
48 48
 # Glance Store
49
-glance-store>=0.22.0 # Apache-2.0
49
+glance-store>=0.26.1 # Apache-2.0
50 50
 
51 51
 
52 52
 debtcollector>=1.2.0 # Apache-2.0

Loading…
Cancel
Save