diff --git a/glance/api/authorization.py b/glance/api/authorization.py index 8a4fbc94ca..6945845186 100644 --- a/glance/api/authorization.py +++ b/glance/api/authorization.py @@ -315,6 +315,7 @@ class ImmutableImageProxy(object): min_disk = _immutable_attr('base', 'min_disk') min_ram = _immutable_attr('base', 'min_ram') protected = _immutable_attr('base', 'protected') + os_hidden = _immutable_attr('base', 'os_hidden') locations = _immutable_attr('base', 'locations', proxy=ImmutableLocations) checksum = _immutable_attr('base', 'checksum') owner = _immutable_attr('base', 'owner') diff --git a/glance/api/v2/images.py b/glance/api/v2/images.py index 81510b0e39..0d7242f902 100644 --- a/glance/api/v2/images.py +++ b/glance/api/v2/images.py @@ -169,6 +169,14 @@ class ImagesController(object): filters = {} filters['deleted'] = False + os_hidden = filters.get('os_hidden', 'false').lower() + if os_hidden not in ['true', 'false']: + message = _("Invalid value '%s' for 'os_hidden' filter." + " Valid values are 'true' or 'false'.") % os_hidden + raise webob.exc.HTTPBadRequest(explanation=message) + # ensure the type of os_hidden is boolean + filters['os_hidden'] = os_hidden == 'true' + protected = filters.get('protected') if protected is not None: if protected not in ['true', 'false']: @@ -443,7 +451,7 @@ class RequestDeserializer(wsgi.JSONRequestDeserializer): _base_properties = ('checksum', 'created_at', 'container_format', 'disk_format', 'id', 'min_disk', 'min_ram', 'name', 'size', 'virtual_size', 'status', 'tags', 'owner', - 'updated_at', 'visibility', 'protected') + 'updated_at', 'visibility', 'protected', 'os_hidden') _available_sort_keys = ('name', 'status', 'container_format', 'disk_format', 'size', 'id', 'created_at', 'updated_at') @@ -876,7 +884,7 @@ class ResponseSerializer(wsgi.JSONResponseSerializer): attributes = ['name', 'disk_format', 'container_format', 'visibility', 'size', 'virtual_size', 'status', 'checksum', 'protected', 'min_ram', 'min_disk', - 'owner'] + 'owner', 'os_hidden'] for key in attributes: image_view[key] = getattr(image, key) image_view['id'] = image.image_id @@ -999,6 +1007,11 @@ def get_base_properties(): 'type': 'boolean', 'description': _('If true, image will not be deletable.'), }, + 'os_hidden': { + 'type': 'boolean', + 'description': _('If true, image will not appear in default ' + 'image list response.'), + }, 'checksum': { 'type': ['null', 'string'], 'readOnly': True, diff --git a/glance/db/__init__.py b/glance/db/__init__.py index 142f2cc452..862adbd1d9 100644 --- a/glance/db/__init__.py +++ b/glance/db/__init__.py @@ -136,7 +136,8 @@ class ImageRepo(object): size=db_image['size'], virtual_size=db_image['virtual_size'], extra_properties=properties, - tags=db_tags + tags=db_tags, + os_hidden=db_image['os_hidden'], ) def _format_image_to_db(self, image): @@ -168,6 +169,7 @@ class ImageRepo(object): 'virtual_size': image.virtual_size, 'visibility': image.visibility, 'properties': dict(image.extra_properties), + 'os_hidden': image.os_hidden } def add(self, image): diff --git a/glance/db/simple/api.py b/glance/db/simple/api.py index 1456878504..e9ae30c46a 100644 --- a/glance/db/simple/api.py +++ b/glance/db/simple/api.py @@ -230,6 +230,7 @@ def _image_format(image_id, **values): 'updated_at': dt, 'deleted_at': None, 'deleted': False, + 'os_hidden': False } locations = values.pop('locations', None) @@ -258,6 +259,7 @@ def _filter_images(images, filters, context, status = None visibility = filters.pop('visibility', None) + os_hidden = filters.pop('os_hidden', False) for image in images: member = image_member_find(context, image_id=image['id'], @@ -267,6 +269,7 @@ def _filter_images(images, filters, context, image_is_public = image['visibility'] == 'public' image_is_community = image['visibility'] == 'community' image_is_shared = image['visibility'] == 'shared' + image_is_hidden = image['os_hidden'] == True acts_as_admin = context.is_admin and not admin_as_user can_see = (image_is_public or image_is_community @@ -299,6 +302,10 @@ def _filter_images(images, filters, context, if not image_is_public == is_public: continue + if os_hidden: + if image_is_hidden: + continue + to_add = True for k, value in six.iteritems(filters): key = k @@ -727,7 +734,8 @@ def image_create(context, image_values, v1_mode=False): 'virtual_size', 'checksum', 'locations', 'owner', 'protected', 'is_public', 'container_format', 'disk_format', 'created_at', 'updated_at', 'deleted', - 'deleted_at', 'properties', 'tags', 'visibility']) + 'deleted_at', 'properties', 'tags', 'visibility', + 'os_hidden']) incorrect_keys = set(image_values.keys()) - allowed_keys 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 0000000000..1c65346abc --- /dev/null +++ b/glance/db/sqlalchemy/alembic_migrations/data_migrations/rocky_migrate01_empty.py @@ -0,0 +1,26 @@ +# Copyright (C) 2018 RedHat Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +def has_migrations(engine): + """Returns true if at least one data row can be migrated.""" + + return False + + +def migrate(engine): + """Return the number of rows migrated.""" + + return 0 diff --git a/glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract01_empty.py b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract01_empty.py new file mode 100644 index 0000000000..3caad9da29 --- /dev/null +++ b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_contract01_empty.py @@ -0,0 +1,25 @@ +# Copyright (C) 2018 RedHat Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +# revision identifiers, used by Alembic. +revision = 'rocky_contract01' +down_revision = 'queens_contract01' +branch_labels = None +depends_on = 'rocky_expand01' + + +def upgrade(): + 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 0000000000..aa89b2e487 --- /dev/null +++ b/glance/db/sqlalchemy/alembic_migrations/versions/rocky_expand01_add_os_hidden.py @@ -0,0 +1,32 @@ +# Copyright (C) 2018 RedHat Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""add os_hidden column to images table""" + +from alembic import op +from sqlalchemy import Boolean, Column, sql + +# revision identifiers, used by Alembic. +revision = 'rocky_expand01' +down_revision = 'queens_expand01' +branch_labels = None +depends_on = None + + +def upgrade(): + h_col = Column('os_hidden', Boolean, default=False, nullable=False, + server_default=sql.expression.false()) + op.add_column('images', h_col) + 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 8fca7b7b9f..0e68e4690c 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): else: image_conditions.append(models.Image.visibility != 'public') + if 'os_hidden' in filters: + os_hidden = filters.pop('os_hidden') + image_conditions.append(models.Image.os_hidden == os_hidden) + if 'checksum' in filters: checksum = filters.pop('checksum') image_conditions.append(models.Image.checksum == checksum) diff --git a/glance/db/sqlalchemy/models.py b/glance/db/sqlalchemy/models.py index cc1b6eb53c..08a3db551d 100644 --- a/glance/db/sqlalchemy/models.py +++ b/glance/db/sqlalchemy/models.py @@ -119,7 +119,8 @@ class Image(BASE, GlanceBase): Index('ix_images_deleted', 'deleted'), Index('owner_image_idx', 'owner'), Index('created_at_image_idx', 'created_at'), - Index('updated_at_image_idx', 'updated_at')) + Index('updated_at_image_idx', 'updated_at'), + Index('os_hidden_image_idx', 'os_hidden')) id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4())) @@ -138,6 +139,8 @@ class Image(BASE, GlanceBase): owner = Column(String(255)) protected = Column(Boolean, nullable=False, default=False, server_default=sql.expression.false()) + os_hidden = Column(Boolean, nullable=False, default=False, + server_default=sql.expression.false()) class ImageProperty(BASE, GlanceBase): diff --git a/glance/domain/__init__.py b/glance/domain/__init__.py index 7cd6aadf53..d72f3867ac 100644 --- a/glance/domain/__init__.py +++ b/glance/domain/__init__.py @@ -71,7 +71,8 @@ class ImageFactory(object): def new_image(self, image_id=None, name=None, visibility='shared', min_disk=0, min_ram=0, protected=False, owner=None, disk_format=None, container_format=None, - extra_properties=None, tags=None, **other_args): + extra_properties=None, tags=None, os_hidden=False, + **other_args): extra_properties = extra_properties or {} self._check_readonly(other_args) self._check_unexpected(other_args) @@ -89,6 +90,7 @@ class ImageFactory(object): min_ram=min_ram, protected=protected, owner=owner, disk_format=disk_format, container_format=container_format, + os_hidden=os_hidden, extra_properties=extra_properties, tags=tags or []) @@ -119,6 +121,7 @@ class Image(object): self.updated_at = updated_at self.name = kwargs.pop('name', None) self.visibility = kwargs.pop('visibility', 'shared') + self.os_hidden = kwargs.pop('os_hidden', False) self.min_disk = kwargs.pop('min_disk', 0) self.min_ram = kwargs.pop('min_ram', 0) self.protected = kwargs.pop('protected', False) diff --git a/glance/domain/proxy.py b/glance/domain/proxy.py index 9cc7bfef15..53e500f5f7 100644 --- a/glance/domain/proxy.py +++ b/glance/domain/proxy.py @@ -172,6 +172,7 @@ class Image(object): min_disk = _proxy('base', 'min_disk') min_ram = _proxy('base', 'min_ram') protected = _proxy('base', 'protected') + os_hidden = _proxy('base', 'os_hidden') locations = _proxy('base', 'locations') checksum = _proxy('base', 'checksum') 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 0000000000..a94232c5d3 --- /dev/null +++ b/glance/tests/functional/db/migrations/test_rocky_expand01.py @@ -0,0 +1,39 @@ +# Copyright (c) 2018 RedHat, Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from oslo_db.sqlalchemy import test_base +from oslo_db.sqlalchemy import utils as db_utils + +from glance.tests.functional.db import test_migrations + + +class TestRockyExpand01Mixin(test_migrations.AlembicMigrationsMixin): + + def _get_revisions(self, config): + return test_migrations.AlembicMigrationsMixin._get_revisions( + self, config, head='rocky_expand01') + + def _pre_upgrade_rocky_expand01(self, engine): + images = db_utils.get_table(engine, 'images') + self.assertNotIn('os_hidden', images.c) + + def _check_rocky_expand01(self, engine, data): + # check that after migration, 'os_hidden' column is introduced + images = db_utils.get_table(engine, 'images') + self.assertIn('os_hidden', images.c) + self.assertFalse(images.c.os_hidden.nullable) + + +class TestRockyExpand01MySQL(TestRockyExpand01Mixin, + test_base.MySQLOpportunisticTestCase): + pass diff --git a/glance/tests/functional/v2/test_images.py b/glance/tests/functional/v2/test_images.py index b875427623..1160ee7638 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): u'visibility', u'self', u'protected', + u'os_hidden', u'id', u'file', u'min_disk', @@ -316,6 +317,7 @@ class TestImages(functional.FunctionalTest): u'visibility', u'self', u'protected', + u'os_hidden', u'id', u'file', u'min_disk', @@ -441,6 +443,7 @@ class TestImages(functional.FunctionalTest): u'visibility', u'self', u'protected', + u'os_hidden', u'id', u'file', u'min_disk', @@ -505,6 +508,7 @@ class TestImages(functional.FunctionalTest): u'visibility', u'self', u'protected', + u'os_hidden', u'id', u'file', u'min_disk', @@ -925,6 +929,269 @@ class TestImages(functional.FunctionalTest): self.stop_servers() + def test_hidden_images(self): + # Image list should be empty + self.api_server.show_multiple_locations = True + self.start_servers(**self.__dict__.copy()) + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.OK, response.status_code) + images = jsonutils.loads(response.text)['images'] + self.assertEqual(0, len(images)) + + # Create an image + path = self._url('/v2/images') + headers = self._headers({'content-type': 'application/json'}) + data = jsonutils.dumps({'name': 'image-1', 'type': 'kernel', + 'disk_format': 'aki', + 'container_format': 'aki', + 'protected': False}) + response = requests.post(path, headers=headers, data=data) + self.assertEqual(http.CREATED, response.status_code) + + # Returned image entity should have a generated id and status + image = jsonutils.loads(response.text) + image_id = image['id'] + checked_keys = set([ + u'status', + u'name', + u'tags', + u'created_at', + u'updated_at', + u'visibility', + u'self', + u'protected', + u'os_hidden', + u'id', + u'file', + u'min_disk', + u'type', + u'min_ram', + u'schema', + u'disk_format', + u'container_format', + u'owner', + u'checksum', + u'size', + u'virtual_size', + u'locations', + ]) + self.assertEqual(checked_keys, set(image.keys())) + + # Returned image entity should have os_hidden as False + expected_image = { + 'status': 'queued', + 'name': 'image-1', + 'tags': [], + 'visibility': 'shared', + 'self': '/v2/images/%s' % image_id, + 'protected': False, + 'os_hidden': False, + 'file': '/v2/images/%s/file' % image_id, + 'min_disk': 0, + 'type': 'kernel', + 'min_ram': 0, + 'schema': '/v2/schemas/image', + } + for key, value in expected_image.items(): + self.assertEqual(value, image[key], key) + + # Image list should now have one entry + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.OK, response.status_code) + images = jsonutils.loads(response.text)['images'] + self.assertEqual(1, len(images)) + self.assertEqual(image_id, images[0]['id']) + + # Create another image wiht hidden true + path = self._url('/v2/images') + headers = self._headers({'content-type': 'application/json'}) + data = jsonutils.dumps({'name': 'image-2', 'type': 'kernel', + 'disk_format': 'aki', + 'container_format': 'aki', + 'os_hidden': True}) + response = requests.post(path, headers=headers, data=data) + self.assertEqual(http.CREATED, response.status_code) + + # Returned image entity should have a generated id and status + image = jsonutils.loads(response.text) + image2_id = image['id'] + checked_keys = set([ + u'status', + u'name', + u'tags', + u'created_at', + u'updated_at', + u'visibility', + u'self', + u'protected', + u'os_hidden', + u'id', + u'file', + u'min_disk', + u'type', + u'min_ram', + u'schema', + u'disk_format', + u'container_format', + u'owner', + u'checksum', + u'size', + u'virtual_size', + u'locations', + ]) + self.assertEqual(checked_keys, set(image.keys())) + + # Returned image entity should have os_hidden as True + expected_image = { + 'status': 'queued', + 'name': 'image-2', + 'tags': [], + 'visibility': 'shared', + 'self': '/v2/images/%s' % image2_id, + 'protected': False, + 'os_hidden': True, + 'file': '/v2/images/%s/file' % image2_id, + 'min_disk': 0, + 'type': 'kernel', + 'min_ram': 0, + 'schema': '/v2/schemas/image', + } + for key, value in expected_image.items(): + self.assertEqual(value, image[key], key) + + # Image list should now have one entries + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.OK, response.status_code) + images = jsonutils.loads(response.text)['images'] + self.assertEqual(1, len(images)) + self.assertEqual(image_id, images[0]['id']) + + # Image list should list should show one image based on the filter + # 'hidden=false' + path = self._url('/v2/images?os_hidden=false') + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.OK, response.status_code) + images = jsonutils.loads(response.text)['images'] + self.assertEqual(1, len(images)) + self.assertEqual(image_id, images[0]['id']) + + # Image list should list should show one image based on the filter + # 'hidden=true' + path = self._url('/v2/images?os_hidden=true') + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.OK, response.status_code) + images = jsonutils.loads(response.text)['images'] + self.assertEqual(1, len(images)) + self.assertEqual(image2_id, images[0]['id']) + + # Image list should return 400 based on the filter + # 'hidden=abcd' + path = self._url('/v2/images?os_hidden=abcd') + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.BAD_REQUEST, response.status_code) + + def _verify_image_checksum_and_status(checksum, status): + # Checksum should be populated and status should be active + path = self._url('/v2/images/%s' % image_id) + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.OK, response.status_code) + image = jsonutils.loads(response.text) + self.assertEqual(checksum, image['checksum']) + self.assertEqual(status, image['status']) + + # Upload some image data to image-1 + path = self._url('/v2/images/%s/file' % image_id) + headers = self._headers({'Content-Type': 'application/octet-stream'}) + response = requests.put(path, headers=headers, data='ZZZZZ') + self.assertEqual(http.NO_CONTENT, response.status_code) + + expected_checksum = '8f113e38d28a79a5a451b16048cc2b72' + _verify_image_checksum_and_status(expected_checksum, 'active') + + # Upload some image data to image-2 + path = self._url('/v2/images/%s/file' % image2_id) + headers = self._headers({'Content-Type': 'application/octet-stream'}) + response = requests.put(path, headers=headers, data='ZZZZZ') + self.assertEqual(http.NO_CONTENT, response.status_code) + + expected_checksum = '8f113e38d28a79a5a451b16048cc2b72' + _verify_image_checksum_and_status(expected_checksum, 'active') + + # Hide image-1 + path = self._url('/v2/images/%s' % image_id) + media_type = 'application/openstack-images-v2.1-json-patch' + headers = self._headers({'content-type': media_type}) + data = jsonutils.dumps([ + {'op': 'replace', 'path': '/os_hidden', 'value': True}, + ]) + response = requests.patch(path, headers=headers, data=data) + self.assertEqual(http.OK, response.status_code, response.text) + + # Returned image entity should reflect the changes + image = jsonutils.loads(response.text) + self.assertTrue(image['os_hidden']) + + # Image list should now have 0 entries + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.OK, response.status_code) + images = jsonutils.loads(response.text)['images'] + self.assertEqual(0, len(images)) + + # Image list should list should show image-1, and image-2 based + # on the filter 'hidden=true' + path = self._url('/v2/images?os_hidden=true') + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.OK, response.status_code) + images = jsonutils.loads(response.text)['images'] + self.assertEqual(2, len(images)) + self.assertEqual(image2_id, images[0]['id']) + self.assertEqual(image_id, images[1]['id']) + + # Un-Hide image-1 + path = self._url('/v2/images/%s' % image_id) + media_type = 'application/openstack-images-v2.1-json-patch' + headers = self._headers({'content-type': media_type}) + data = jsonutils.dumps([ + {'op': 'replace', 'path': '/os_hidden', 'value': False}, + ]) + response = requests.patch(path, headers=headers, data=data) + self.assertEqual(http.OK, response.status_code, response.text) + + # Returned image entity should reflect the changes + image = jsonutils.loads(response.text) + self.assertFalse(image['os_hidden']) + + # Image list should now have 1 entry + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.OK, response.status_code) + images = jsonutils.loads(response.text)['images'] + self.assertEqual(1, len(images)) + self.assertEqual(image_id, images[0]['id']) + + # Deleting image-1 should work + path = self._url('/v2/images/%s' % image_id) + response = requests.delete(path, headers=self._headers()) + self.assertEqual(http.NO_CONTENT, response.status_code) + + # Deleting image-2 should work + path = self._url('/v2/images/%s' % image2_id) + response = requests.delete(path, headers=self._headers()) + self.assertEqual(http.NO_CONTENT, response.status_code) + + # Image list should now be empty + path = self._url('/v2/images') + response = requests.get(path, headers=self._headers()) + self.assertEqual(http.OK, response.status_code) + images = jsonutils.loads(response.text)['images'] + self.assertEqual(0, len(images)) + + self.stop_servers() + def test_update_readonly_prop(self): self.start_servers(**self.__dict__.copy()) # 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 2257e95de4..51bb3660a7 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): 'min_ram', 'min_disk', 'protected', + 'os_hidden', ]) self.assertEqual(expected, set(image_schema['properties'].keys())) diff --git a/glance/tests/unit/test_policy.py b/glance/tests/unit/test_policy.py index b48d5a08c5..aefa29d42e 100644 --- a/glance/tests/unit/test_policy.py +++ b/glance/tests/unit/test_policy.py @@ -53,7 +53,8 @@ class ImageRepoStub(object): class ImageStub(object): def __init__(self, image_id=None, visibility='private', container_format='bear', disk_format='raw', - status='active', extra_properties=None): + status='active', extra_properties=None, + os_hidden=False): if extra_properties is None: extra_properties = {} @@ -76,6 +77,7 @@ class ImageStub(object): self.size = 0 self.virtual_size = 0 self.tags = [] + self.os_hidden = os_hidden def delete(self): self.status = 'deleted' @@ -85,8 +87,10 @@ class ImageFactoryStub(object): def new_image(self, image_id=None, name=None, visibility='private', min_disk=0, min_ram=0, protected=False, owner=None, disk_format=None, container_format=None, - extra_properties=None, tags=None, **other_args): + extra_properties=None, hidden=False, tags=None, + **other_args): self.visibility = visibility + self.hidden = hidden return 'new_image' diff --git a/glance/tests/unit/v2/test_images_resource.py b/glance/tests/unit/v2/test_images_resource.py index 5d86907621..7990e97f7f 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): expected = set([UUID1]) self.assertEqual(expected, actual) + def test_index_with_invalid_hidden_filter(self): + request = unit_test_utils.get_fake_request('/images?os_hidden=abcd') + self.assertRaises(webob.exc.HTTPBadRequest, + self.controller.index, request, + filters={'os_hidden': 'abcd'}) + def test_index_with_checksum_filter_single_image(self): req = unit_test_utils.get_fake_request('/images?checksum=%s' % CHKSUM) output = self.controller.index(req, filters={'checksum': CHKSUM}) @@ -884,6 +890,12 @@ class TestImagesController(base.IsolatedUnitTest): # NOTE(markwash): don't send a notification if nothing is updated self.assertEqual(0, len(output_logs)) + def test_update_queued_image_with_hidden(self): + request = unit_test_utils.get_fake_request() + changes = [{'op': 'replace', 'path': ['os_hidden'], 'value': 'true'}] + self.assertRaises(webob.exc.HTTPForbidden, self.controller.update, + request, UUID3, changes=changes) + def test_update_with_bad_min_disk(self): request = unit_test_utils.get_fake_request() changes = [{'op': 'replace', 'path': ['min_disk'], 'value': -42}] @@ -3439,6 +3451,7 @@ class TestImagesSerializer(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'public', 'protected': False, + 'os_hidden': False, 'tags': set(['one', 'two']), 'size': 1024, 'virtual_size': 3072, @@ -3459,6 +3472,7 @@ class TestImagesSerializer(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'private', 'protected': False, + 'os_hidden': False, 'tags': set([]), 'created_at': ISOTIME, 'updated_at': ISOTIME, @@ -3545,6 +3559,7 @@ class TestImagesSerializer(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'public', 'protected': False, + 'os_hidden': False, 'tags': set(['one', 'two']), 'size': 1024, 'virtual_size': 3072, @@ -3573,6 +3588,7 @@ class TestImagesSerializer(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'private', 'protected': False, + 'os_hidden': False, 'tags': [], 'created_at': ISOTIME, 'updated_at': ISOTIME, @@ -3600,6 +3616,7 @@ class TestImagesSerializer(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'public', 'protected': False, + 'os_hidden': False, 'tags': ['one', 'two'], 'size': 1024, 'virtual_size': 3072, @@ -3665,6 +3682,7 @@ class TestImagesSerializer(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'public', 'protected': False, + 'os_hidden': False, 'tags': set(['one', 'two']), 'size': 1024, 'virtual_size': 3072, @@ -3729,6 +3747,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase): u'status': u'queued', u'visibility': u'public', u'protected': False, + u'os_hidden': False, u'tags': [u'\u2160', u'\u2161'], u'size': 1024, u'virtual_size': 3072, @@ -3766,6 +3785,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase): u'status': u'queued', u'visibility': u'public', u'protected': False, + u'os_hidden': False, u'tags': set([u'\u2160', u'\u2161']), u'size': 1024, u'virtual_size': 3072, @@ -3797,6 +3817,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase): u'status': u'queued', u'visibility': u'public', u'protected': False, + u'os_hidden': False, u'tags': [u'\u2160', u'\u2161'], u'size': 1024, u'virtual_size': 3072, @@ -3830,6 +3851,7 @@ class TestImagesSerializerWithUnicode(test_utils.BaseTestCase): u'status': u'queued', u'visibility': u'public', u'protected': False, + u'os_hidden': False, u'tags': set([u'\u2160', u'\u2161']), u'size': 1024, u'virtual_size': 3072, @@ -3883,6 +3905,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'private', 'protected': False, + 'os_hidden': False, 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 'tags': [], 'size': 1024, @@ -3911,6 +3934,7 @@ class TestImagesSerializerWithExtendedSchema(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'private', 'protected': False, + 'os_hidden': False, 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 'tags': [], 'size': 1024, @@ -3951,6 +3975,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'private', 'protected': False, + 'os_hidden': False, 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 'marx': 'groucho', 'tags': [], @@ -3985,6 +4010,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'private', 'protected': False, + 'os_hidden': False, 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 'marx': 123, 'tags': [], @@ -4014,6 +4040,7 @@ class TestImagesSerializerWithAdditionalProperties(test_utils.BaseTestCase): 'status': 'queued', 'visibility': 'private', 'protected': False, + 'os_hidden': False, 'checksum': 'ca425b88f047ce8ec45ee90e813ada91', 'tags': [], 'size': 1024, diff --git a/glance/tests/unit/v2/test_schemas_resource.py b/glance/tests/unit/v2/test_schemas_resource.py index 55cf66fc3b..e256278070 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): 'disk_format', 'updated_at', 'visibility', 'self', 'file', 'container_format', 'schema', 'id', 'size', 'direct_url', 'min_ram', 'min_disk', 'protected', - 'locations', 'owner', 'virtual_size']) + 'locations', 'owner', 'virtual_size', 'os_hidden']) self.assertEqual(expected, set(output['properties'].keys())) def test_image_has_correct_statuses(self):