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
This commit is contained in:
Abhishek Kekane 2018-06-27 16:55:52 +00:00
parent 2f859cee90
commit a308c44406
17 changed files with 465 additions and 9 deletions

View File

@ -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')

View File

@ -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,

View File

@ -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):

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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'])

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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')

View File

@ -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

View File

@ -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)

View File

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

View File

@ -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'

View File

@ -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,

View File

@ -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):