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:
parent
2f859cee90
commit
a308c44406
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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'])
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -55,6 +55,7 @@ class TestSchemas(functional.FunctionalTest):
|
|||
'min_ram',
|
||||
'min_disk',
|
||||
'protected',
|
||||
'os_hidden',
|
||||
])
|
||||
self.assertEqual(expected, set(image_schema['properties'].keys()))
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue