Browse Source

Hide old images

Added new boolean column "os_hidden" in images table. Images where
"os_hidden" = True will be omitted from the image list presented
to the user. This will apply to all image visibilities. However,
the images will continue to be discoverable. User can use
filter "os_hidden=true" in GET v2/images call to see all hidden
images.

Implements: blueprint hidden-images
Change-Id: If8f02ca94fdb8e1ac7a81853cd392988900172d1
tags/17.0.0.0b3
Abhishek Kekane 11 months ago
parent
commit
a308c44406

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

@@ -315,6 +315,7 @@ 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_hidden = _immutable_attr('base', 'os_hidden')
318 319
     locations = _immutable_attr('base', 'locations', proxy=ImmutableLocations)
319 320
     checksum = _immutable_attr('base', 'checksum')
320 321
     owner = _immutable_attr('base', 'owner')

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

@@ -169,6 +169,14 @@ class ImagesController(object):
169 169
             filters = {}
170 170
         filters['deleted'] = False
171 171
 
172
+        os_hidden = filters.get('os_hidden', 'false').lower()
173
+        if os_hidden not in ['true', 'false']:
174
+            message = _("Invalid value '%s' for 'os_hidden' filter."
175
+                        " Valid values are 'true' or 'false'.") % os_hidden
176
+            raise webob.exc.HTTPBadRequest(explanation=message)
177
+        # ensure the type of os_hidden is boolean
178
+        filters['os_hidden'] = os_hidden == 'true'
179
+
172 180
         protected = filters.get('protected')
173 181
         if protected is not None:
174 182
             if protected not in ['true', 'false']:
@@ -443,7 +451,7 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
443 451
     _base_properties = ('checksum', 'created_at', 'container_format',
444 452
                         'disk_format', 'id', 'min_disk', 'min_ram', 'name',
445 453
                         'size', 'virtual_size', 'status', 'tags', 'owner',
446
-                        'updated_at', 'visibility', 'protected')
454
+                        'updated_at', 'visibility', 'protected', 'os_hidden')
447 455
     _available_sort_keys = ('name', 'status', 'container_format',
448 456
                             'disk_format', 'size', 'id', 'created_at',
449 457
                             'updated_at')
@@ -876,7 +884,7 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
876 884
             attributes = ['name', 'disk_format', 'container_format',
877 885
                           'visibility', 'size', 'virtual_size', 'status',
878 886
                           'checksum', 'protected', 'min_ram', 'min_disk',
879
-                          'owner']
887
+                          'owner', 'os_hidden']
880 888
             for key in attributes:
881 889
                 image_view[key] = getattr(image, key)
882 890
             image_view['id'] = image.image_id
@@ -999,6 +1007,11 @@ def get_base_properties():
999 1007
             'type': 'boolean',
1000 1008
             'description': _('If true, image will not be deletable.'),
1001 1009
         },
1010
+        'os_hidden': {
1011
+            'type': 'boolean',
1012
+            'description': _('If true, image will not appear in default '
1013
+                             'image list response.'),
1014
+        },
1002 1015
         'checksum': {
1003 1016
             'type': ['null', 'string'],
1004 1017
             'readOnly': True,

+ 3
- 1
glance/db/__init__.py View File

@@ -136,7 +136,8 @@ class ImageRepo(object):
136 136
             size=db_image['size'],
137 137
             virtual_size=db_image['virtual_size'],
138 138
             extra_properties=properties,
139
-            tags=db_tags
139
+            tags=db_tags,
140
+            os_hidden=db_image['os_hidden'],
140 141
         )
141 142
 
142 143
     def _format_image_to_db(self, image):
@@ -168,6 +169,7 @@ class ImageRepo(object):
168 169
             'virtual_size': image.virtual_size,
169 170
             'visibility': image.visibility,
170 171
             'properties': dict(image.extra_properties),
172
+            'os_hidden': image.os_hidden
171 173
         }
172 174
 
173 175
     def add(self, image):

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

@@ -230,6 +230,7 @@ def _image_format(image_id, **values):
230 230
         'updated_at': dt,
231 231
         'deleted_at': None,
232 232
         'deleted': False,
233
+        'os_hidden': False
233 234
     }
234 235
 
235 236
     locations = values.pop('locations', None)
@@ -258,6 +259,7 @@ def _filter_images(images, filters, context,
258 259
         status = None
259 260
 
260 261
     visibility = filters.pop('visibility', None)
262
+    os_hidden = filters.pop('os_hidden', False)
261 263
 
262 264
     for image in images:
263 265
         member = image_member_find(context, image_id=image['id'],
@@ -267,6 +269,7 @@ def _filter_images(images, filters, context,
267 269
         image_is_public = image['visibility'] == 'public'
268 270
         image_is_community = image['visibility'] == 'community'
269 271
         image_is_shared = image['visibility'] == 'shared'
272
+        image_is_hidden = image['os_hidden'] == True
270 273
         acts_as_admin = context.is_admin and not admin_as_user
271 274
         can_see = (image_is_public
272 275
                    or image_is_community
@@ -299,6 +302,10 @@ def _filter_images(images, filters, context,
299 302
             if not image_is_public == is_public:
300 303
                 continue
301 304
 
305
+        if os_hidden:
306
+            if image_is_hidden:
307
+                continue
308
+
302 309
         to_add = True
303 310
         for k, value in six.iteritems(filters):
304 311
             key = k
@@ -727,7 +734,8 @@ def image_create(context, image_values, v1_mode=False):
727 734
                         'virtual_size', 'checksum', 'locations', 'owner',
728 735
                         'protected', 'is_public', 'container_format',
729 736
                         'disk_format', 'created_at', 'updated_at', 'deleted',
730
-                        'deleted_at', 'properties', 'tags', 'visibility'])
737
+                        'deleted_at', 'properties', 'tags', 'visibility',
738
+                        'os_hidden'])
731 739
 
732 740
     incorrect_keys = set(image_values.keys()) - allowed_keys
733 741
     if incorrect_keys:

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

@@ -0,0 +1,26 @@
1
+# Copyright (C) 2018 RedHat Inc.
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_contract01_empty.py View File

@@ -0,0 +1,25 @@
1
+# Copyright (C) 2018 RedHat Inc.
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_contract01'
19
+down_revision = 'queens_contract01'
20
+branch_labels = None
21
+depends_on = 'rocky_expand01'
22
+
23
+
24
+def upgrade():
25
+    pass

+ 32
- 0
glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand01_add_os_hidden.py View File

@@ -0,0 +1,32 @@
1
+# Copyright (C) 2018 RedHat Inc.
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_hidden column to images table"""
17
+
18
+from alembic import op
19
+from sqlalchemy import Boolean, Column, sql
20
+
21
+# revision identifiers, used by Alembic.
22
+revision = 'rocky_expand01'
23
+down_revision = 'queens_expand01'
24
+branch_labels = None
25
+depends_on = None
26
+
27
+
28
+def upgrade():
29
+    h_col = Column('os_hidden', Boolean, default=False, nullable=False,
30
+                   server_default=sql.expression.false())
31
+    op.add_column('images', h_col)
32
+    op.create_index('os_hidden_image_idx', 'images', ['os_hidden'])

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

@@ -460,6 +460,10 @@ def _make_conditions_from_filters(filters, is_public=None):
460 460
         else:
461 461
             image_conditions.append(models.Image.visibility != 'public')
462 462
 
463
+    if 'os_hidden' in filters:
464
+        os_hidden = filters.pop('os_hidden')
465
+        image_conditions.append(models.Image.os_hidden == os_hidden)
466
+
463 467
     if 'checksum' in filters:
464 468
         checksum = filters.pop('checksum')
465 469
         image_conditions.append(models.Image.checksum == checksum)

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

@@ -119,7 +119,8 @@ class Image(BASE, GlanceBase):
119 119
                       Index('ix_images_deleted', 'deleted'),
120 120
                       Index('owner_image_idx', 'owner'),
121 121
                       Index('created_at_image_idx', 'created_at'),
122
-                      Index('updated_at_image_idx', 'updated_at'))
122
+                      Index('updated_at_image_idx', 'updated_at'),
123
+                      Index('os_hidden_image_idx', 'os_hidden'))
123 124
 
124 125
     id = Column(String(36), primary_key=True,
125 126
                 default=lambda: str(uuid.uuid4()))
@@ -138,6 +139,8 @@ class Image(BASE, GlanceBase):
138 139
     owner = Column(String(255))
139 140
     protected = Column(Boolean, nullable=False, default=False,
140 141
                        server_default=sql.expression.false())
142
+    os_hidden = Column(Boolean, nullable=False, default=False,
143
+                       server_default=sql.expression.false())
141 144
 
142 145
 
143 146
 class ImageProperty(BASE, GlanceBase):

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

@@ -71,7 +71,8 @@ class ImageFactory(object):
71 71
     def new_image(self, image_id=None, name=None, visibility='shared',
72 72
                   min_disk=0, min_ram=0, protected=False, owner=None,
73 73
                   disk_format=None, container_format=None,
74
-                  extra_properties=None, tags=None, **other_args):
74
+                  extra_properties=None, tags=None, os_hidden=False,
75
+                  **other_args):
75 76
         extra_properties = extra_properties or {}
76 77
         self._check_readonly(other_args)
77 78
         self._check_unexpected(other_args)
@@ -89,6 +90,7 @@ class ImageFactory(object):
89 90
                      min_ram=min_ram, protected=protected,
90 91
                      owner=owner, disk_format=disk_format,
91 92
                      container_format=container_format,
93
+                     os_hidden=os_hidden,
92 94
                      extra_properties=extra_properties, tags=tags or [])
93 95
 
94 96
 
@@ -119,6 +121,7 @@ class Image(object):
119 121
         self.updated_at = updated_at
120 122
         self.name = kwargs.pop('name', None)
121 123
         self.visibility = kwargs.pop('visibility', 'shared')
124
+        self.os_hidden = kwargs.pop('os_hidden', False)
122 125
         self.min_disk = kwargs.pop('min_disk', 0)
123 126
         self.min_ram = kwargs.pop('min_ram', 0)
124 127
         self.protected = kwargs.pop('protected', False)

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

@@ -172,6 +172,7 @@ class Image(object):
172 172
     min_disk = _proxy('base', 'min_disk')
173 173
     min_ram = _proxy('base', 'min_ram')
174 174
     protected = _proxy('base', 'protected')
175
+    os_hidden = _proxy('base', 'os_hidden')
175 176
     locations = _proxy('base', 'locations')
176 177
     checksum = _proxy('base', 'checksum')
177 178
     owner = _proxy('base', 'owner')

+ 39
- 0
glance/tests/functional/db/migrations/test_rocky_expand01.py View File

@@ -0,0 +1,39 @@
1
+#    Copyright (c) 2018 RedHat, Inc.
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 TestRockyExpand01Mixin(test_migrations.AlembicMigrationsMixin):
21
+
22
+    def _get_revisions(self, config):
23
+        return test_migrations.AlembicMigrationsMixin._get_revisions(
24
+            self, config, head='rocky_expand01')
25
+
26
+    def _pre_upgrade_rocky_expand01(self, engine):
27
+        images = db_utils.get_table(engine, 'images')
28
+        self.assertNotIn('os_hidden', images.c)
29
+
30
+    def _check_rocky_expand01(self, engine, data):
31
+        # check that after migration, 'os_hidden' column is introduced
32
+        images = db_utils.get_table(engine, 'images')
33
+        self.assertIn('os_hidden', images.c)
34
+        self.assertFalse(images.c.os_hidden.nullable)
35
+
36
+
37
+class TestRockyExpand01MySQL(TestRockyExpand01Mixin,
38
+                             test_base.MySQLOpportunisticTestCase):
39
+    pass

+ 267
- 0
glance/tests/functional/v2/test_images.py View File

@@ -175,6 +175,7 @@ class TestImages(functional.FunctionalTest):
175 175
             u'visibility',
176 176
             u'self',
177 177
             u'protected',
178
+            u'os_hidden',
178 179
             u'id',
179 180
             u'file',
180 181
             u'min_disk',
@@ -316,6 +317,7 @@ class TestImages(functional.FunctionalTest):
316 317
             u'visibility',
317 318
             u'self',
318 319
             u'protected',
320
+            u'os_hidden',
319 321
             u'id',
320 322
             u'file',
321 323
             u'min_disk',
@@ -441,6 +443,7 @@ class TestImages(functional.FunctionalTest):
441 443
             u'visibility',
442 444
             u'self',
443 445
             u'protected',
446
+            u'os_hidden',
444 447
             u'id',
445 448
             u'file',
446 449
             u'min_disk',
@@ -505,6 +508,7 @@ class TestImages(functional.FunctionalTest):
505 508
             u'visibility',
506 509
             u'self',
507 510
             u'protected',
511
+            u'os_hidden',
508 512
             u'id',
509 513
             u'file',
510 514
             u'min_disk',
@@ -925,6 +929,269 @@ class TestImages(functional.FunctionalTest):
925 929
 
926 930
         self.stop_servers()
927 931
 
932
+    def test_hidden_images(self):
933
+        # Image list should be empty
934
+        self.api_server.show_multiple_locations = True
935
+        self.start_servers(**self.__dict__.copy())
936
+        path = self._url('/v2/images')
937
+        response = requests.get(path, headers=self._headers())
938
+        self.assertEqual(http.OK, response.status_code)
939
+        images = jsonutils.loads(response.text)['images']
940
+        self.assertEqual(0, len(images))
941
+
942
+        # Create an image
943
+        path = self._url('/v2/images')
944
+        headers = self._headers({'content-type': 'application/json'})
945
+        data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel',
946
+                                'disk_format': 'aki',
947
+                                'container_format': 'aki',
948
+                                'protected': False})
949
+        response = requests.post(path, headers=headers, data=data)
950
+        self.assertEqual(http.CREATED, response.status_code)
951
+
952
+        # Returned image entity should have a generated id and status
953
+        image = jsonutils.loads(response.text)
954
+        image_id = image['id']
955
+        checked_keys = set([
956
+            u'status',
957
+            u'name',
958
+            u'tags',
959
+            u'created_at',
960
+            u'updated_at',
961
+            u'visibility',
962
+            u'self',
963
+            u'protected',
964
+            u'os_hidden',
965
+            u'id',
966
+            u'file',
967
+            u'min_disk',
968
+            u'type',
969
+            u'min_ram',
970
+            u'schema',
971
+            u'disk_format',
972
+            u'container_format',
973
+            u'owner',
974
+            u'checksum',
975
+            u'size',
976
+            u'virtual_size',
977
+            u'locations',
978
+        ])
979
+        self.assertEqual(checked_keys, set(image.keys()))
980
+
981
+        # Returned image entity should have os_hidden as False
982
+        expected_image = {
983
+            'status': 'queued',
984
+            'name': 'image-1',
985
+            'tags': [],
986
+            'visibility': 'shared',
987
+            'self': '/v2/images/%s' % image_id,
988
+            'protected': False,
989
+            'os_hidden': False,
990
+            'file': '/v2/images/%s/file' % image_id,
991
+            'min_disk': 0,
992
+            'type': 'kernel',
993
+            'min_ram': 0,
994
+            'schema': '/v2/schemas/image',
995
+        }
996
+        for key, value in expected_image.items():
997
+            self.assertEqual(value, image[key], key)
998
+
999
+        # Image list should now have one entry
1000
+        path = self._url('/v2/images')
1001
+        response = requests.get(path, headers=self._headers())
1002
+        self.assertEqual(http.OK, response.status_code)
1003
+        images = jsonutils.loads(response.text)['images']
1004
+        self.assertEqual(1, len(images))
1005
+        self.assertEqual(image_id, images[0]['id'])
1006
+
1007
+        # Create another image wiht hidden true
1008
+        path = self._url('/v2/images')
1009
+        headers = self._headers({'content-type': 'application/json'})
1010
+        data = jsonutils.dumps({'name': 'image-2', 'type': 'kernel',
1011
+                                'disk_format': 'aki',
1012
+                                'container_format': 'aki',
1013
+                                'os_hidden': True})
1014
+        response = requests.post(path, headers=headers, data=data)
1015
+        self.assertEqual(http.CREATED, response.status_code)
1016
+
1017
+        # Returned image entity should have a generated id and status
1018
+        image = jsonutils.loads(response.text)
1019
+        image2_id = image['id']
1020
+        checked_keys = set([
1021
+            u'status',
1022
+            u'name',
1023
+            u'tags',
1024
+            u'created_at',
1025
+            u'updated_at',
1026
+            u'visibility',
1027
+            u'self',
1028
+            u'protected',
1029
+            u'os_hidden',
1030
+            u'id',
1031
+            u'file',
1032
+            u'min_disk',
1033
+            u'type',
1034
+            u'min_ram',
1035
+            u'schema',
1036
+            u'disk_format',
1037
+            u'container_format',
1038
+            u'owner',
1039
+            u'checksum',
1040
+            u'size',
1041
+            u'virtual_size',
1042
+            u'locations',
1043
+        ])
1044
+        self.assertEqual(checked_keys, set(image.keys()))
1045
+
1046
+        # Returned image entity should have os_hidden as True
1047
+        expected_image = {
1048
+            'status': 'queued',
1049
+            'name': 'image-2',
1050
+            'tags': [],
1051
+            'visibility': 'shared',
1052
+            'self': '/v2/images/%s' % image2_id,
1053
+            'protected': False,
1054
+            'os_hidden': True,
1055
+            'file': '/v2/images/%s/file' % image2_id,
1056
+            'min_disk': 0,
1057
+            'type': 'kernel',
1058
+            'min_ram': 0,
1059
+            'schema': '/v2/schemas/image',
1060
+        }
1061
+        for key, value in expected_image.items():
1062
+            self.assertEqual(value, image[key], key)
1063
+
1064
+        # Image list should now have one entries
1065
+        path = self._url('/v2/images')
1066
+        response = requests.get(path, headers=self._headers())
1067
+        self.assertEqual(http.OK, response.status_code)
1068
+        images = jsonutils.loads(response.text)['images']
1069
+        self.assertEqual(1, len(images))
1070
+        self.assertEqual(image_id, images[0]['id'])
1071
+
1072
+        # Image list should list should show one image based on the filter
1073
+        # 'hidden=false'
1074
+        path = self._url('/v2/images?os_hidden=false')
1075
+        response = requests.get(path, headers=self._headers())
1076
+        self.assertEqual(http.OK, response.status_code)
1077
+        images = jsonutils.loads(response.text)['images']
1078
+        self.assertEqual(1, len(images))
1079
+        self.assertEqual(image_id, images[0]['id'])
1080
+
1081
+        # Image list should list should show one image based on the filter
1082
+        # 'hidden=true'
1083
+        path = self._url('/v2/images?os_hidden=true')
1084
+        response = requests.get(path, headers=self._headers())
1085
+        self.assertEqual(http.OK, response.status_code)
1086
+        images = jsonutils.loads(response.text)['images']
1087
+        self.assertEqual(1, len(images))
1088
+        self.assertEqual(image2_id, images[0]['id'])
1089
+
1090
+        # Image list should return 400 based on the filter
1091
+        # 'hidden=abcd'
1092
+        path = self._url('/v2/images?os_hidden=abcd')
1093
+        response = requests.get(path, headers=self._headers())
1094
+        self.assertEqual(http.BAD_REQUEST, response.status_code)
1095
+
1096
+        def _verify_image_checksum_and_status(checksum, status):
1097
+            # Checksum should be populated and status should be active
1098
+            path = self._url('/v2/images/%s' % image_id)
1099
+            response = requests.get(path, headers=self._headers())
1100
+            self.assertEqual(http.OK, response.status_code)
1101
+            image = jsonutils.loads(response.text)
1102
+            self.assertEqual(checksum, image['checksum'])
1103
+            self.assertEqual(status, image['status'])
1104
+
1105
+        # Upload some image data to image-1
1106
+        path = self._url('/v2/images/%s/file' % image_id)
1107
+        headers = self._headers({'Content-Type': 'application/octet-stream'})
1108
+        response = requests.put(path, headers=headers, data='ZZZZZ')
1109
+        self.assertEqual(http.NO_CONTENT, response.status_code)
1110
+
1111
+        expected_checksum = '8f113e38d28a79a5a451b16048cc2b72'
1112
+        _verify_image_checksum_and_status(expected_checksum, 'active')
1113
+
1114
+        # Upload some image data to image-2
1115
+        path = self._url('/v2/images/%s/file' % image2_id)
1116
+        headers = self._headers({'Content-Type': 'application/octet-stream'})
1117
+        response = requests.put(path, headers=headers, data='ZZZZZ')
1118
+        self.assertEqual(http.NO_CONTENT, response.status_code)
1119
+
1120
+        expected_checksum = '8f113e38d28a79a5a451b16048cc2b72'
1121
+        _verify_image_checksum_and_status(expected_checksum, 'active')
1122
+
1123
+        # Hide image-1
1124
+        path = self._url('/v2/images/%s' % image_id)
1125
+        media_type = 'application/openstack-images-v2.1-json-patch'
1126
+        headers = self._headers({'content-type': media_type})
1127
+        data = jsonutils.dumps([
1128
+            {'op': 'replace', 'path': '/os_hidden', 'value': True},
1129
+        ])
1130
+        response = requests.patch(path, headers=headers, data=data)
1131
+        self.assertEqual(http.OK, response.status_code, response.text)
1132
+
1133
+        # Returned image entity should reflect the changes
1134
+        image = jsonutils.loads(response.text)
1135
+        self.assertTrue(image['os_hidden'])
1136
+
1137
+        # Image list should now have 0 entries
1138
+        path = self._url('/v2/images')
1139
+        response = requests.get(path, headers=self._headers())
1140
+        self.assertEqual(http.OK, response.status_code)
1141
+        images = jsonutils.loads(response.text)['images']
1142
+        self.assertEqual(0, len(images))
1143
+
1144
+        # Image list should list should show image-1, and image-2 based
1145
+        # on the filter 'hidden=true'
1146
+        path = self._url('/v2/images?os_hidden=true')
1147
+        response = requests.get(path, headers=self._headers())
1148
+        self.assertEqual(http.OK, response.status_code)
1149
+        images = jsonutils.loads(response.text)['images']
1150
+        self.assertEqual(2, len(images))
1151
+        self.assertEqual(image2_id, images[0]['id'])
1152
+        self.assertEqual(image_id, images[1]['id'])
1153
+
1154
+        # Un-Hide image-1
1155
+        path = self._url('/v2/images/%s' % image_id)
1156
+        media_type = 'application/openstack-images-v2.1-json-patch'
1157
+        headers = self._headers({'content-type': media_type})
1158
+        data = jsonutils.dumps([
1159
+            {'op': 'replace', 'path': '/os_hidden', 'value': False},
1160
+        ])
1161
+        response = requests.patch(path, headers=headers, data=data)
1162
+        self.assertEqual(http.OK, response.status_code, response.text)
1163
+
1164
+        # Returned image entity should reflect the changes
1165
+        image = jsonutils.loads(response.text)
1166
+        self.assertFalse(image['os_hidden'])
1167
+
1168
+        # Image list should now have 1 entry
1169
+        path = self._url('/v2/images')
1170
+        response = requests.get(path, headers=self._headers())
1171
+        self.assertEqual(http.OK, response.status_code)
1172
+        images = jsonutils.loads(response.text)['images']
1173
+        self.assertEqual(1, len(images))
1174
+        self.assertEqual(image_id, images[0]['id'])
1175
+
1176
+        # Deleting image-1 should work
1177
+        path = self._url('/v2/images/%s' % image_id)
1178
+        response = requests.delete(path, headers=self._headers())
1179
+        self.assertEqual(http.NO_CONTENT, response.status_code)
1180
+
1181
+        # Deleting image-2 should work
1182
+        path = self._url('/v2/images/%s' % image2_id)
1183
+        response = requests.delete(path, headers=self._headers())
1184
+        self.assertEqual(http.NO_CONTENT, response.status_code)
1185
+
1186
+        # Image list should now be empty
1187
+        path = self._url('/v2/images')
1188
+        response = requests.get(path, headers=self._headers())
1189
+        self.assertEqual(http.OK, response.status_code)
1190
+        images = jsonutils.loads(response.text)['images']
1191
+        self.assertEqual(0, len(images))
1192
+
1193
+        self.stop_servers()
1194
+
928 1195
     def test_update_readonly_prop(self):
929 1196
         self.start_servers(**self.__dict__.copy())
930 1197
         # Create an image (with two deployer-defined properties)

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

@@ -55,6 +55,7 @@ class TestSchemas(functional.FunctionalTest):
55 55
             'min_ram',
56 56
             'min_disk',
57 57
             'protected',
58
+            'os_hidden',
58 59
         ])
59 60
         self.assertEqual(expected, set(image_schema['properties'].keys()))
60 61
 

+ 6
- 2
glance/tests/unit/test_policy.py View File

@@ -53,7 +53,8 @@ class ImageRepoStub(object):
53 53
 class ImageStub(object):
54 54
     def __init__(self, image_id=None, visibility='private',
55 55
                  container_format='bear', disk_format='raw',
56
-                 status='active', extra_properties=None):
56
+                 status='active', extra_properties=None,
57
+                 os_hidden=False):
57 58
 
58 59
         if extra_properties is None:
59 60
             extra_properties = {}
@@ -76,6 +77,7 @@ class ImageStub(object):
76 77
         self.size = 0
77 78
         self.virtual_size = 0
78 79
         self.tags = []
80
+        self.os_hidden = os_hidden
79 81
 
80 82
     def delete(self):
81 83
         self.status = 'deleted'
@@ -85,8 +87,10 @@ class ImageFactoryStub(object):
85 87
     def new_image(self, image_id=None, name=None, visibility='private',
86 88
                   min_disk=0, min_ram=0, protected=False, owner=None,
87 89
                   disk_format=None, container_format=None,
88
-                  extra_properties=None, tags=None, **other_args):
90
+                  extra_properties=None, hidden=False, tags=None,
91
+                  **other_args):
89 92
         self.visibility = visibility
93
+        self.hidden = hidden
90 94
         return 'new_image'
91 95
 
92 96
 

+ 27
- 0
glance/tests/unit/v2/test_images_resource.py View File

@@ -264,6 +264,12 @@ class TestImagesController(base.IsolatedUnitTest):
264 264
         expected = set([UUID1])
265 265
         self.assertEqual(expected, actual)
266 266
 
267
+    def test_index_with_invalid_hidden_filter(self):
268
+        request = unit_test_utils.get_fake_request('/images?os_hidden=abcd')
269
+        self.assertRaises(webob.exc.HTTPBadRequest,
270
+                          self.controller.index, request,
271
+                          filters={'os_hidden': 'abcd'})
272
+
267 273
     def test_index_with_checksum_filter_single_image(self):
268 274
         req = unit_test_utils.get_fake_request('/images?checksum=%s' % CHKSUM)
269 275
         output = self.controller.index(req, filters={'checksum': CHKSUM})
@@ -884,6 +890,12 @@ class TestImagesController(base.IsolatedUnitTest):
884 890
         # NOTE(markwash): don't send a notification if nothing is updated
885 891
         self.assertEqual(0, len(output_logs))
886 892
 
893
+    def test_update_queued_image_with_hidden(self):
894
+        request = unit_test_utils.get_fake_request()
895
+        changes = [{'op': 'replace', 'path': ['os_hidden'], 'value': 'true'}]
896
+        self.assertRaises(webob.exc.HTTPForbidden, self.controller.update,
897
+                          request, UUID3, changes=changes)
898
+
887 899
     def test_update_with_bad_min_disk(self):
888 900
         request = unit_test_utils.get_fake_request()
889 901
         changes = [{'op': 'replace', 'path': ['min_disk'], 'value': -42}]
@@ -3439,6 +3451,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3439 3451
                     'status': 'queued',
3440 3452
                     'visibility': 'public',
3441 3453
                     'protected': False,
3454
+                    'os_hidden': False,
3442 3455
                     'tags': set(['one', 'two']),
3443 3456
                     'size': 1024,
3444 3457
                     'virtual_size': 3072,
@@ -3459,6 +3472,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3459 3472
                     'status': 'queued',
3460 3473
                     'visibility': 'private',
3461 3474
                     'protected': False,
3475
+                    'os_hidden': False,
3462 3476
                     'tags': set([]),
3463 3477
                     'created_at': ISOTIME,
3464 3478
                     'updated_at': ISOTIME,
@@ -3545,6 +3559,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3545 3559
             'status': 'queued',
3546 3560
             'visibility': 'public',
3547 3561
             'protected': False,
3562
+            'os_hidden': False,
3548 3563
             'tags': set(['one', 'two']),
3549 3564
             'size': 1024,
3550 3565
             'virtual_size': 3072,
@@ -3573,6 +3588,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3573 3588
             'status': 'queued',
3574 3589
             'visibility': 'private',
3575 3590
             'protected': False,
3591
+            'os_hidden': False,
3576 3592
             'tags': [],
3577 3593
             'created_at': ISOTIME,
3578 3594
             'updated_at': ISOTIME,
@@ -3600,6 +3616,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3600 3616
             'status': 'queued',
3601 3617
             'visibility': 'public',
3602 3618
             'protected': False,
3619
+            'os_hidden': False,
3603 3620
             'tags': ['one', 'two'],
3604 3621
             'size': 1024,
3605 3622
             'virtual_size': 3072,
@@ -3665,6 +3682,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3665 3682
             'status': 'queued',
3666 3683
             'visibility': 'public',
3667 3684
             'protected': False,
3685
+            'os_hidden': False,
3668 3686
             'tags': set(['one', 'two']),
3669 3687
             'size': 1024,
3670 3688
             'virtual_size': 3072,
@@ -3729,6 +3747,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3729 3747
                     u'status': u'queued',
3730 3748
                     u'visibility': u'public',
3731 3749
                     u'protected': False,
3750
+                    u'os_hidden': False,
3732 3751
                     u'tags': [u'\u2160', u'\u2161'],
3733 3752
                     u'size': 1024,
3734 3753
                     u'virtual_size': 3072,
@@ -3766,6 +3785,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3766 3785
             u'status': u'queued',
3767 3786
             u'visibility': u'public',
3768 3787
             u'protected': False,
3788
+            u'os_hidden': False,
3769 3789
             u'tags': set([u'\u2160', u'\u2161']),
3770 3790
             u'size': 1024,
3771 3791
             u'virtual_size': 3072,
@@ -3797,6 +3817,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3797 3817
             u'status': u'queued',
3798 3818
             u'visibility': u'public',
3799 3819
             u'protected': False,
3820
+            u'os_hidden': False,
3800 3821
             u'tags': [u'\u2160', u'\u2161'],
3801 3822
             u'size': 1024,
3802 3823
             u'virtual_size': 3072,
@@ -3830,6 +3851,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3830 3851
             u'status': u'queued',
3831 3852
             u'visibility': u'public',
3832 3853
             u'protected': False,
3854
+            u'os_hidden': False,
3833 3855
             u'tags': set([u'\u2160', u'\u2161']),
3834 3856
             u'size': 1024,
3835 3857
             u'virtual_size': 3072,
@@ -3883,6 +3905,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
3883 3905
             'status': 'queued',
3884 3906
             'visibility': 'private',
3885 3907
             'protected': False,
3908
+            'os_hidden': False,
3886 3909
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3887 3910
             'tags': [],
3888 3911
             'size': 1024,
@@ -3911,6 +3934,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
3911 3934
             'status': 'queued',
3912 3935
             'visibility': 'private',
3913 3936
             'protected': False,
3937
+            'os_hidden': False,
3914 3938
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3915 3939
             'tags': [],
3916 3940
             'size': 1024,
@@ -3951,6 +3975,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
3951 3975
             'status': 'queued',
3952 3976
             'visibility': 'private',
3953 3977
             'protected': False,
3978
+            'os_hidden': False,
3954 3979
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3955 3980
             'marx': 'groucho',
3956 3981
             'tags': [],
@@ -3985,6 +4010,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
3985 4010
             'status': 'queued',
3986 4011
             'visibility': 'private',
3987 4012
             'protected': False,
4013
+            'os_hidden': False,
3988 4014
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3989 4015
             'marx': 123,
3990 4016
             'tags': [],
@@ -4014,6 +4040,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
4014 4040
             'status': 'queued',
4015 4041
             'visibility': 'private',
4016 4042
             'protected': False,
4043
+            'os_hidden': False,
4017 4044
             'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
4018 4045
             'tags': [],
4019 4046
             'size': 1024,

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

@@ -33,7 +33,7 @@ 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'])
36
+                        'locations', 'owner', 'virtual_size', 'os_hidden'])
37 37
         self.assertEqual(expected, set(output['properties'].keys()))
38 38
 
39 39
     def test_image_has_correct_statuses(self):

Loading…
Cancel
Save