summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAbhishek Kekane <akekane@redhat.com>2018-06-27 16:55:52 +0000
committerAbhishek Kekane <akekane@redhat.com>2018-07-25 16:37:56 +0000
commita308c444065307e99f18b521ed8d95714be24da7 (patch)
tree117ec263e580717556defff9a7f82d0882e42941
parent2f859cee9038472d7ea02175d0264469e6ebaba9 (diff)
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
Notes
Notes (review): Code-Review+2: Brian Rosmaita <rosmaita.fossdev@gmail.com> Workflow+1: Erno Kuvaja <jokke@usr.fi> Code-Review+2: Erno Kuvaja <jokke@usr.fi> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Fri, 27 Jul 2018 00:42:02 +0000 Reviewed-on: https://review.openstack.org/578755 Project: openstack/glance Branch: refs/heads/master
-rw-r--r--glance/api/authorization.py1
-rw-r--r--glance/api/v2/images.py17
-rw-r--r--glance/db/__init__.py4
-rw-r--r--glance/db/simple/api.py10
-rw-r--r--glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate01_empty.py26
-rw-r--r--glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract01_empty.py25
-rw-r--r--glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand01_add_os_hidden.py32
-rw-r--r--glance/db/sqlalchemy/api.py4
-rw-r--r--glance/db/sqlalchemy/models.py5
-rw-r--r--glance/domain/__init__.py5
-rw-r--r--glance/domain/proxy.py1
-rw-r--r--glance/tests/functional/db/migrations/test_rocky_expand01.py39
-rw-r--r--glance/tests/functional/v2/test_images.py267
-rw-r--r--glance/tests/functional/v2/test_schemas.py1
-rw-r--r--glance/tests/unit/test_policy.py8
-rw-r--r--glance/tests/unit/v2/test_images_resource.py27
-rw-r--r--glance/tests/unit/v2/test_schemas_resource.py2
17 files changed, 465 insertions, 9 deletions
diff --git a/glance/api/authorization.py b/glance/api/authorization.py
index 8a4fbc9..6945845 100644
--- a/glance/api/authorization.py
+++ b/glance/api/authorization.py
@@ -315,6 +315,7 @@ class ImmutableImageProxy(object):
315 min_disk = _immutable_attr('base', 'min_disk') 315 min_disk = _immutable_attr('base', 'min_disk')
316 min_ram = _immutable_attr('base', 'min_ram') 316 min_ram = _immutable_attr('base', 'min_ram')
317 protected = _immutable_attr('base', 'protected') 317 protected = _immutable_attr('base', 'protected')
318 os_hidden = _immutable_attr('base', 'os_hidden')
318 locations = _immutable_attr('base', 'locations', proxy=ImmutableLocations) 319 locations = _immutable_attr('base', 'locations', proxy=ImmutableLocations)
319 checksum = _immutable_attr('base', 'checksum') 320 checksum = _immutable_attr('base', 'checksum')
320 owner = _immutable_attr('base', 'owner') 321 owner = _immutable_attr('base', 'owner')
diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py
index 81510b0..0d7242f 100644
--- a/glance/api/v2/images.py
+++ b/glance/api/v2/images.py
@@ -169,6 +169,14 @@ class ImagesController(object):
169 filters = {} 169 filters = {}
170 filters['deleted'] = False 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 protected = filters.get('protected') 180 protected = filters.get('protected')
173 if protected is not None: 181 if protected is not None:
174 if protected not in ['true', 'false']: 182 if protected not in ['true', 'false']:
@@ -443,7 +451,7 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer):
443 _base_properties = ('checksum', 'created_at', 'container_format', 451 _base_properties = ('checksum', 'created_at', 'container_format',
444 'disk_format', 'id', 'min_disk', 'min_ram', 'name', 452 'disk_format', 'id', 'min_disk', 'min_ram', 'name',
445 'size', 'virtual_size', 'status', 'tags', 'owner', 453 'size', 'virtual_size', 'status', 'tags', 'owner',
446 'updated_at', 'visibility', 'protected') 454 'updated_at', 'visibility', 'protected', 'os_hidden')
447 _available_sort_keys = ('name', 'status', 'container_format', 455 _available_sort_keys = ('name', 'status', 'container_format',
448 'disk_format', 'size', 'id', 'created_at', 456 'disk_format', 'size', 'id', 'created_at',
449 'updated_at') 457 'updated_at')
@@ -876,7 +884,7 @@ class ResponseSerializer(wsgi.JSONResponseSerializer):
876 attributes = ['name', 'disk_format', 'container_format', 884 attributes = ['name', 'disk_format', 'container_format',
877 'visibility', 'size', 'virtual_size', 'status', 885 'visibility', 'size', 'virtual_size', 'status',
878 'checksum', 'protected', 'min_ram', 'min_disk', 886 'checksum', 'protected', 'min_ram', 'min_disk',
879 'owner'] 887 'owner', 'os_hidden']
880 for key in attributes: 888 for key in attributes:
881 image_view[key] = getattr(image, key) 889 image_view[key] = getattr(image, key)
882 image_view['id'] = image.image_id 890 image_view['id'] = image.image_id
@@ -999,6 +1007,11 @@ def get_base_properties():
999 'type': 'boolean', 1007 'type': 'boolean',
1000 'description': _('If true, image will not be deletable.'), 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 'checksum': { 1015 'checksum': {
1003 'type': ['null', 'string'], 1016 'type': ['null', 'string'],
1004 'readOnly': True, 1017 'readOnly': True,
diff --git a/glance/db/__init__.py b/glance/db/__init__.py
index 142f2cc..862adbd 100644
--- a/glance/db/__init__.py
+++ b/glance/db/__init__.py
@@ -136,7 +136,8 @@ class ImageRepo(object):
136 size=db_image['size'], 136 size=db_image['size'],
137 virtual_size=db_image['virtual_size'], 137 virtual_size=db_image['virtual_size'],
138 extra_properties=properties, 138 extra_properties=properties,
139 tags=db_tags 139 tags=db_tags,
140 os_hidden=db_image['os_hidden'],
140 ) 141 )
141 142
142 def _format_image_to_db(self, image): 143 def _format_image_to_db(self, image):
@@ -168,6 +169,7 @@ class ImageRepo(object):
168 'virtual_size': image.virtual_size, 169 'virtual_size': image.virtual_size,
169 'visibility': image.visibility, 170 'visibility': image.visibility,
170 'properties': dict(image.extra_properties), 171 'properties': dict(image.extra_properties),
172 'os_hidden': image.os_hidden
171 } 173 }
172 174
173 def add(self, image): 175 def add(self, image):
diff --git a/glance/db/simple/api.py b/glance/db/simple/api.py
index 1456878..e9ae30c 100644
--- a/glance/db/simple/api.py
+++ b/glance/db/simple/api.py
@@ -230,6 +230,7 @@ def _image_format(image_id, **values):
230 'updated_at': dt, 230 'updated_at': dt,
231 'deleted_at': None, 231 'deleted_at': None,
232 'deleted': False, 232 'deleted': False,
233 'os_hidden': False
233 } 234 }
234 235
235 locations = values.pop('locations', None) 236 locations = values.pop('locations', None)
@@ -258,6 +259,7 @@ def _filter_images(images, filters, context,
258 status = None 259 status = None
259 260
260 visibility = filters.pop('visibility', None) 261 visibility = filters.pop('visibility', None)
262 os_hidden = filters.pop('os_hidden', False)
261 263
262 for image in images: 264 for image in images:
263 member = image_member_find(context, image_id=image['id'], 265 member = image_member_find(context, image_id=image['id'],
@@ -267,6 +269,7 @@ def _filter_images(images, filters, context,
267 image_is_public = image['visibility'] == 'public' 269 image_is_public = image['visibility'] == 'public'
268 image_is_community = image['visibility'] == 'community' 270 image_is_community = image['visibility'] == 'community'
269 image_is_shared = image['visibility'] == 'shared' 271 image_is_shared = image['visibility'] == 'shared'
272 image_is_hidden = image['os_hidden'] == True
270 acts_as_admin = context.is_admin and not admin_as_user 273 acts_as_admin = context.is_admin and not admin_as_user
271 can_see = (image_is_public 274 can_see = (image_is_public
272 or image_is_community 275 or image_is_community
@@ -299,6 +302,10 @@ def _filter_images(images, filters, context,
299 if not image_is_public == is_public: 302 if not image_is_public == is_public:
300 continue 303 continue
301 304
305 if os_hidden:
306 if image_is_hidden:
307 continue
308
302 to_add = True 309 to_add = True
303 for k, value in six.iteritems(filters): 310 for k, value in six.iteritems(filters):
304 key = k 311 key = k
@@ -727,7 +734,8 @@ def image_create(context, image_values, v1_mode=False):
727 'virtual_size', 'checksum', 'locations', 'owner', 734 'virtual_size', 'checksum', 'locations', 'owner',
728 'protected', 'is_public', 'container_format', 735 'protected', 'is_public', 'container_format',
729 'disk_format', 'created_at', 'updated_at', 'deleted', 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 incorrect_keys = set(image_values.keys()) - allowed_keys 740 incorrect_keys = set(image_values.keys()) - allowed_keys
733 if incorrect_keys: 741 if incorrect_keys:
diff --git a/glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate01_empty.py b/glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate01_empty.py
new file mode 100644
index 0000000..1c65346
--- /dev/null
+++ b/glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate01_empty.py
@@ -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
17def has_migrations(engine):
18 """Returns true if at least one data row can be migrated."""
19
20 return False
21
22
23def migrate(engine):
24 """Return the number of rows migrated."""
25
26 return 0
diff --git a/glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract01_empty.py b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract01_empty.py
new file mode 100644
index 0000000..3caad9d
--- /dev/null
+++ b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract01_empty.py
@@ -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.
18revision = 'rocky_contract01'
19down_revision = 'queens_contract01'
20branch_labels = None
21depends_on = 'rocky_expand01'
22
23
24def upgrade():
25 pass
diff --git a/glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand01_add_os_hidden.py b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand01_add_os_hidden.py
new file mode 100644
index 0000000..aa89b2e
--- /dev/null
+++ b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand01_add_os_hidden.py
@@ -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
18from alembic import op
19from sqlalchemy import Boolean, Column, sql
20
21# revision identifiers, used by Alembic.
22revision = 'rocky_expand01'
23down_revision = 'queens_expand01'
24branch_labels = None
25depends_on = None
26
27
28def 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'])
diff --git a/glance/db/sqlalchemy/api.py b/glance/db/sqlalchemy/api.py
index 8fca7b7..0e68e46 100644
--- a/glance/db/sqlalchemy/api.py
+++ b/glance/db/sqlalchemy/api.py
@@ -460,6 +460,10 @@ def _make_conditions_from_filters(filters, is_public=None):
460 else: 460 else:
461 image_conditions.append(models.Image.visibility != 'public') 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 if 'checksum' in filters: 467 if 'checksum' in filters:
464 checksum = filters.pop('checksum') 468 checksum = filters.pop('checksum')
465 image_conditions.append(models.Image.checksum == checksum) 469 image_conditions.append(models.Image.checksum == checksum)
diff --git a/glance/db/sqlalchemy/models.py b/glance/db/sqlalchemy/models.py
index cc1b6eb..08a3db5 100644
--- a/glance/db/sqlalchemy/models.py
+++ b/glance/db/sqlalchemy/models.py
@@ -119,7 +119,8 @@ class Image(BASE, GlanceBase):
119 Index('ix_images_deleted', 'deleted'), 119 Index('ix_images_deleted', 'deleted'),
120 Index('owner_image_idx', 'owner'), 120 Index('owner_image_idx', 'owner'),
121 Index('created_at_image_idx', 'created_at'), 121 Index('created_at_image_idx', 'created_at'),
122 Index('updated_at_image_idx', 'updated_at')) 122 Index('updated_at_image_idx', 'updated_at'),
123 Index('os_hidden_image_idx', 'os_hidden'))
123 124
124 id = Column(String(36), primary_key=True, 125 id = Column(String(36), primary_key=True,
125 default=lambda: str(uuid.uuid4())) 126 default=lambda: str(uuid.uuid4()))
@@ -138,6 +139,8 @@ class Image(BASE, GlanceBase):
138 owner = Column(String(255)) 139 owner = Column(String(255))
139 protected = Column(Boolean, nullable=False, default=False, 140 protected = Column(Boolean, nullable=False, default=False,
140 server_default=sql.expression.false()) 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
143class ImageProperty(BASE, GlanceBase): 146class ImageProperty(BASE, GlanceBase):
diff --git a/glance/domain/__init__.py b/glance/domain/__init__.py
index 7cd6aad..d72f386 100644
--- a/glance/domain/__init__.py
+++ b/glance/domain/__init__.py
@@ -71,7 +71,8 @@ class ImageFactory(object):
71 def new_image(self, image_id=None, name=None, visibility='shared', 71 def new_image(self, image_id=None, name=None, visibility='shared',
72 min_disk=0, min_ram=0, protected=False, owner=None, 72 min_disk=0, min_ram=0, protected=False, owner=None,
73 disk_format=None, container_format=None, 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 extra_properties = extra_properties or {} 76 extra_properties = extra_properties or {}
76 self._check_readonly(other_args) 77 self._check_readonly(other_args)
77 self._check_unexpected(other_args) 78 self._check_unexpected(other_args)
@@ -89,6 +90,7 @@ class ImageFactory(object):
89 min_ram=min_ram, protected=protected, 90 min_ram=min_ram, protected=protected,
90 owner=owner, disk_format=disk_format, 91 owner=owner, disk_format=disk_format,
91 container_format=container_format, 92 container_format=container_format,
93 os_hidden=os_hidden,
92 extra_properties=extra_properties, tags=tags or []) 94 extra_properties=extra_properties, tags=tags or [])
93 95
94 96
@@ -119,6 +121,7 @@ class Image(object):
119 self.updated_at = updated_at 121 self.updated_at = updated_at
120 self.name = kwargs.pop('name', None) 122 self.name = kwargs.pop('name', None)
121 self.visibility = kwargs.pop('visibility', 'shared') 123 self.visibility = kwargs.pop('visibility', 'shared')
124 self.os_hidden = kwargs.pop('os_hidden', False)
122 self.min_disk = kwargs.pop('min_disk', 0) 125 self.min_disk = kwargs.pop('min_disk', 0)
123 self.min_ram = kwargs.pop('min_ram', 0) 126 self.min_ram = kwargs.pop('min_ram', 0)
124 self.protected = kwargs.pop('protected', False) 127 self.protected = kwargs.pop('protected', False)
diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py
index 9cc7bfe..53e500f 100644
--- a/glance/domain/proxy.py
+++ b/glance/domain/proxy.py
@@ -172,6 +172,7 @@ class Image(object):
172 min_disk = _proxy('base', 'min_disk') 172 min_disk = _proxy('base', 'min_disk')
173 min_ram = _proxy('base', 'min_ram') 173 min_ram = _proxy('base', 'min_ram')
174 protected = _proxy('base', 'protected') 174 protected = _proxy('base', 'protected')
175 os_hidden = _proxy('base', 'os_hidden')
175 locations = _proxy('base', 'locations') 176 locations = _proxy('base', 'locations')
176 checksum = _proxy('base', 'checksum') 177 checksum = _proxy('base', 'checksum')
177 owner = _proxy('base', 'owner') 178 owner = _proxy('base', 'owner')
diff --git a/glance/tests/functional/db/migrations/test_rocky_expand01.py b/glance/tests/functional/db/migrations/test_rocky_expand01.py
new file mode 100644
index 0000000..a94232c
--- /dev/null
+++ b/glance/tests/functional/db/migrations/test_rocky_expand01.py
@@ -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
14from oslo_db.sqlalchemy import test_base
15from oslo_db.sqlalchemy import utils as db_utils
16
17from glance.tests.functional.db import test_migrations
18
19
20class 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
37class TestRockyExpand01MySQL(TestRockyExpand01Mixin,
38 test_base.MySQLOpportunisticTestCase):
39 pass
diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py
index b875427..1160ee7 100644
--- a/glance/tests/functional/v2/test_images.py
+++ b/glance/tests/functional/v2/test_images.py
@@ -175,6 +175,7 @@ class TestImages(functional.FunctionalTest):
175 u'visibility', 175 u'visibility',
176 u'self', 176 u'self',
177 u'protected', 177 u'protected',
178 u'os_hidden',
178 u'id', 179 u'id',
179 u'file', 180 u'file',
180 u'min_disk', 181 u'min_disk',
@@ -316,6 +317,7 @@ class TestImages(functional.FunctionalTest):
316 u'visibility', 317 u'visibility',
317 u'self', 318 u'self',
318 u'protected', 319 u'protected',
320 u'os_hidden',
319 u'id', 321 u'id',
320 u'file', 322 u'file',
321 u'min_disk', 323 u'min_disk',
@@ -441,6 +443,7 @@ class TestImages(functional.FunctionalTest):
441 u'visibility', 443 u'visibility',
442 u'self', 444 u'self',
443 u'protected', 445 u'protected',
446 u'os_hidden',
444 u'id', 447 u'id',
445 u'file', 448 u'file',
446 u'min_disk', 449 u'min_disk',
@@ -505,6 +508,7 @@ class TestImages(functional.FunctionalTest):
505 u'visibility', 508 u'visibility',
506 u'self', 509 u'self',
507 u'protected', 510 u'protected',
511 u'os_hidden',
508 u'id', 512 u'id',
509 u'file', 513 u'file',
510 u'min_disk', 514 u'min_disk',
@@ -925,6 +929,269 @@ class TestImages(functional.FunctionalTest):
925 929
926 self.stop_servers() 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 def test_update_readonly_prop(self): 1195 def test_update_readonly_prop(self):
929 self.start_servers(**self.__dict__.copy()) 1196 self.start_servers(**self.__dict__.copy())
930 # Create an image (with two deployer-defined properties) 1197 # Create an image (with two deployer-defined properties)
diff --git a/glance/tests/functional/v2/test_schemas.py b/glance/tests/functional/v2/test_schemas.py
index 2257e95..51bb366 100644
--- a/glance/tests/functional/v2/test_schemas.py
+++ b/glance/tests/functional/v2/test_schemas.py
@@ -55,6 +55,7 @@ class TestSchemas(functional.FunctionalTest):
55 'min_ram', 55 'min_ram',
56 'min_disk', 56 'min_disk',
57 'protected', 57 'protected',
58 'os_hidden',
58 ]) 59 ])
59 self.assertEqual(expected, set(image_schema['properties'].keys())) 60 self.assertEqual(expected, set(image_schema['properties'].keys()))
60 61
diff --git a/glance/tests/unit/test_policy.py b/glance/tests/unit/test_policy.py
index b48d5a0..aefa29d 100644
--- a/glance/tests/unit/test_policy.py
+++ b/glance/tests/unit/test_policy.py
@@ -53,7 +53,8 @@ class ImageRepoStub(object):
53class ImageStub(object): 53class ImageStub(object):
54 def __init__(self, image_id=None, visibility='private', 54 def __init__(self, image_id=None, visibility='private',
55 container_format='bear', disk_format='raw', 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 if extra_properties is None: 59 if extra_properties is None:
59 extra_properties = {} 60 extra_properties = {}
@@ -76,6 +77,7 @@ class ImageStub(object):
76 self.size = 0 77 self.size = 0
77 self.virtual_size = 0 78 self.virtual_size = 0
78 self.tags = [] 79 self.tags = []
80 self.os_hidden = os_hidden
79 81
80 def delete(self): 82 def delete(self):
81 self.status = 'deleted' 83 self.status = 'deleted'
@@ -85,8 +87,10 @@ class ImageFactoryStub(object):
85 def new_image(self, image_id=None, name=None, visibility='private', 87 def new_image(self, image_id=None, name=None, visibility='private',
86 min_disk=0, min_ram=0, protected=False, owner=None, 88 min_disk=0, min_ram=0, protected=False, owner=None,
87 disk_format=None, container_format=None, 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 self.visibility = visibility 92 self.visibility = visibility
93 self.hidden = hidden
90 return 'new_image' 94 return 'new_image'
91 95
92 96
diff --git a/glance/tests/unit/v2/test_images_resource.py b/glance/tests/unit/v2/test_images_resource.py
index 5d86907..7990e97 100644
--- a/glance/tests/unit/v2/test_images_resource.py
+++ b/glance/tests/unit/v2/test_images_resource.py
@@ -264,6 +264,12 @@ class TestImagesController(base.IsolatedUnitTest):
264 expected = set([UUID1]) 264 expected = set([UUID1])
265 self.assertEqual(expected, actual) 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 def test_index_with_checksum_filter_single_image(self): 273 def test_index_with_checksum_filter_single_image(self):
268 req = unit_test_utils.get_fake_request('/images?checksum=%s' % CHKSUM) 274 req = unit_test_utils.get_fake_request('/images?checksum=%s' % CHKSUM)
269 output = self.controller.index(req, filters={'checksum': CHKSUM}) 275 output = self.controller.index(req, filters={'checksum': CHKSUM})
@@ -884,6 +890,12 @@ class TestImagesController(base.IsolatedUnitTest):
884 # NOTE(markwash): don't send a notification if nothing is updated 890 # NOTE(markwash): don't send a notification if nothing is updated
885 self.assertEqual(0, len(output_logs)) 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 def test_update_with_bad_min_disk(self): 899 def test_update_with_bad_min_disk(self):
888 request = unit_test_utils.get_fake_request() 900 request = unit_test_utils.get_fake_request()
889 changes = [{'op': 'replace', 'path': ['min_disk'], 'value': -42}] 901 changes = [{'op': 'replace', 'path': ['min_disk'], 'value': -42}]
@@ -3439,6 +3451,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3439 'status': 'queued', 3451 'status': 'queued',
3440 'visibility': 'public', 3452 'visibility': 'public',
3441 'protected': False, 3453 'protected': False,
3454 'os_hidden': False,
3442 'tags': set(['one', 'two']), 3455 'tags': set(['one', 'two']),
3443 'size': 1024, 3456 'size': 1024,
3444 'virtual_size': 3072, 3457 'virtual_size': 3072,
@@ -3459,6 +3472,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3459 'status': 'queued', 3472 'status': 'queued',
3460 'visibility': 'private', 3473 'visibility': 'private',
3461 'protected': False, 3474 'protected': False,
3475 'os_hidden': False,
3462 'tags': set([]), 3476 'tags': set([]),
3463 'created_at': ISOTIME, 3477 'created_at': ISOTIME,
3464 'updated_at': ISOTIME, 3478 'updated_at': ISOTIME,
@@ -3545,6 +3559,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3545 'status': 'queued', 3559 'status': 'queued',
3546 'visibility': 'public', 3560 'visibility': 'public',
3547 'protected': False, 3561 'protected': False,
3562 'os_hidden': False,
3548 'tags': set(['one', 'two']), 3563 'tags': set(['one', 'two']),
3549 'size': 1024, 3564 'size': 1024,
3550 'virtual_size': 3072, 3565 'virtual_size': 3072,
@@ -3573,6 +3588,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3573 'status': 'queued', 3588 'status': 'queued',
3574 'visibility': 'private', 3589 'visibility': 'private',
3575 'protected': False, 3590 'protected': False,
3591 'os_hidden': False,
3576 'tags': [], 3592 'tags': [],
3577 'created_at': ISOTIME, 3593 'created_at': ISOTIME,
3578 'updated_at': ISOTIME, 3594 'updated_at': ISOTIME,
@@ -3600,6 +3616,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3600 'status': 'queued', 3616 'status': 'queued',
3601 'visibility': 'public', 3617 'visibility': 'public',
3602 'protected': False, 3618 'protected': False,
3619 'os_hidden': False,
3603 'tags': ['one', 'two'], 3620 'tags': ['one', 'two'],
3604 'size': 1024, 3621 'size': 1024,
3605 'virtual_size': 3072, 3622 'virtual_size': 3072,
@@ -3665,6 +3682,7 @@ class TestImagesSerializer(test_utils.BaseTestCase):
3665 'status': 'queued', 3682 'status': 'queued',
3666 'visibility': 'public', 3683 'visibility': 'public',
3667 'protected': False, 3684 'protected': False,
3685 'os_hidden': False,
3668 'tags': set(['one', 'two']), 3686 'tags': set(['one', 'two']),
3669 'size': 1024, 3687 'size': 1024,
3670 'virtual_size': 3072, 3688 'virtual_size': 3072,
@@ -3729,6 +3747,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3729 u'status': u'queued', 3747 u'status': u'queued',
3730 u'visibility': u'public', 3748 u'visibility': u'public',
3731 u'protected': False, 3749 u'protected': False,
3750 u'os_hidden': False,
3732 u'tags': [u'\u2160', u'\u2161'], 3751 u'tags': [u'\u2160', u'\u2161'],
3733 u'size': 1024, 3752 u'size': 1024,
3734 u'virtual_size': 3072, 3753 u'virtual_size': 3072,
@@ -3766,6 +3785,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3766 u'status': u'queued', 3785 u'status': u'queued',
3767 u'visibility': u'public', 3786 u'visibility': u'public',
3768 u'protected': False, 3787 u'protected': False,
3788 u'os_hidden': False,
3769 u'tags': set([u'\u2160', u'\u2161']), 3789 u'tags': set([u'\u2160', u'\u2161']),
3770 u'size': 1024, 3790 u'size': 1024,
3771 u'virtual_size': 3072, 3791 u'virtual_size': 3072,
@@ -3797,6 +3817,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3797 u'status': u'queued', 3817 u'status': u'queued',
3798 u'visibility': u'public', 3818 u'visibility': u'public',
3799 u'protected': False, 3819 u'protected': False,
3820 u'os_hidden': False,
3800 u'tags': [u'\u2160', u'\u2161'], 3821 u'tags': [u'\u2160', u'\u2161'],
3801 u'size': 1024, 3822 u'size': 1024,
3802 u'virtual_size': 3072, 3823 u'virtual_size': 3072,
@@ -3830,6 +3851,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase):
3830 u'status': u'queued', 3851 u'status': u'queued',
3831 u'visibility': u'public', 3852 u'visibility': u'public',
3832 u'protected': False, 3853 u'protected': False,
3854 u'os_hidden': False,
3833 u'tags': set([u'\u2160', u'\u2161']), 3855 u'tags': set([u'\u2160', u'\u2161']),
3834 u'size': 1024, 3856 u'size': 1024,
3835 u'virtual_size': 3072, 3857 u'virtual_size': 3072,
@@ -3883,6 +3905,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
3883 'status': 'queued', 3905 'status': 'queued',
3884 'visibility': 'private', 3906 'visibility': 'private',
3885 'protected': False, 3907 'protected': False,
3908 'os_hidden': False,
3886 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 3909 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3887 'tags': [], 3910 'tags': [],
3888 'size': 1024, 3911 'size': 1024,
@@ -3911,6 +3934,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase):
3911 'status': 'queued', 3934 'status': 'queued',
3912 'visibility': 'private', 3935 'visibility': 'private',
3913 'protected': False, 3936 'protected': False,
3937 'os_hidden': False,
3914 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 3938 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3915 'tags': [], 3939 'tags': [],
3916 'size': 1024, 3940 'size': 1024,
@@ -3951,6 +3975,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
3951 'status': 'queued', 3975 'status': 'queued',
3952 'visibility': 'private', 3976 'visibility': 'private',
3953 'protected': False, 3977 'protected': False,
3978 'os_hidden': False,
3954 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 3979 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3955 'marx': 'groucho', 3980 'marx': 'groucho',
3956 'tags': [], 3981 'tags': [],
@@ -3985,6 +4010,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
3985 'status': 'queued', 4010 'status': 'queued',
3986 'visibility': 'private', 4011 'visibility': 'private',
3987 'protected': False, 4012 'protected': False,
4013 'os_hidden': False,
3988 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 4014 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
3989 'marx': 123, 4015 'marx': 123,
3990 'tags': [], 4016 'tags': [],
@@ -4014,6 +4040,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase):
4014 'status': 'queued', 4040 'status': 'queued',
4015 'visibility': 'private', 4041 'visibility': 'private',
4016 'protected': False, 4042 'protected': False,
4043 'os_hidden': False,
4017 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 4044 'checksum': 'ca425b88f047ce8ec45ee90e813ada91',
4018 'tags': [], 4045 'tags': [],
4019 'size': 1024, 4046 'size': 1024,
diff --git a/glance/tests/unit/v2/test_schemas_resource.py b/glance/tests/unit/v2/test_schemas_resource.py
index 55cf66f..e256278 100644
--- a/glance/tests/unit/v2/test_schemas_resource.py
+++ b/glance/tests/unit/v2/test_schemas_resource.py
@@ -33,7 +33,7 @@ class TestSchemasController(test_utils.BaseTestCase):
33 'disk_format', 'updated_at', 'visibility', 'self', 33 'disk_format', 'updated_at', 'visibility', 'self',
34 'file', 'container_format', 'schema', 'id', 'size', 34 'file', 'container_format', 'schema', 'id', 'size',
35 'direct_url', 'min_ram', 'min_disk', 'protected', 35 'direct_url', 'min_ram', 'min_disk', 'protected',
36 'locations', 'owner', 'virtual_size']) 36 'locations', 'owner', 'virtual_size', 'os_hidden'])
37 self.assertEqual(expected, set(output['properties'].keys())) 37 self.assertEqual(expected, set(output['properties'].keys()))
38 38
39 def test_image_has_correct_statuses(self): 39 def test_image_has_correct_statuses(self):