Merge "Support for Glance v2"

This commit is contained in:
Jenkins 2016-09-16 02:13:50 +00:00 committed by Gerrit Code Review
commit 5d5bc86c97
24 changed files with 1130 additions and 239 deletions

View File

@ -963,6 +963,22 @@ web-server (e.g. http://<HOST_IP>/dashboard) and restart glance-api process.
was removed.
``IMAGES_ALLOW_LOCATION``
--------------------------------
.. versionadded:: 10.0.0(Newton)
Default: ``False``
If set to ``True``, this setting allows users to specify an image location
(URL) as the image source when creating or updating images. Depending on
the Glance version, the ability to set an image location is controlled by
policies and/or the Glance configuration. Therefore IMAGES_ALLOW_LOCATION
should only be set to ``True`` if Glance is configured to allow specifying a
location. This setting has no effect when the Keystone catalog doesn't contain
a Glance v2 endpoint.
``OPENSTACK_KEYSTONE_BACKEND``
------------------------------

View File

@ -56,13 +56,157 @@ except ImportError:
pass
class Image(base.APIResourceWrapper):
_attrs = {"architecture", "container_format", "disk_format", "created_at",
"owner", "size", "id", "status", "updated_at", "checksum",
"visibility", "name", "is_public", "protected", "min_disk",
"min_ram"}
_ext_attrs = {"file", "locations", "schema", "tags", "virtual_size",
"kernel_id", "ramdisk_id", "image_url"}
def __init__(self, apiresource):
super(Image, self).__init__(apiresource)
def __getattribute__(self, attr):
# Because Glance v2 treats custom properties as normal
# attributes, we need to be more flexible than the resource
# wrappers usually allow. In v1 they were defined under a
# "properties" attribute.
if VERSIONS.active >= 2 and attr == "properties":
return {k: v for (k, v) in self._apiresource.items()
if self.property_visible(k)}
try:
return object.__getattribute__(self, attr)
except AttributeError:
return getattr(self._apiresource, attr)
@property
def name(self):
return getattr(self._apiresource, 'name', None)
@property
def size(self):
image_size = getattr(self._apiresource, 'size', 0)
if image_size is None:
return 0
return image_size
@size.setter
def size(self, value):
self._apiresource.size = value
@property
def is_public(self):
# Glance v2 no longer has a 'is_public' attribute, but uses a
# 'visibility' attribute instead.
return (getattr(self._apiresource, 'is_public', None) or
getattr(self._apiresource, 'visibility', None) == "public")
def property_visible(self, prop_name, show_ext_attrs=False):
if show_ext_attrs:
return prop_name not in self._attrs
else:
return prop_name not in (self._attrs | self._ext_attrs)
def to_dict(self, show_ext_attrs=False):
# When using v1 Image objects (including when running unit tests
# for v2), self._apiresource is not iterable. In that case,
# the properties are included in the apiresource dict, so
# just return that dict.
if not isinstance(self._apiresource, collections.Iterable):
return self._apiresource.to_dict()
image_dict = super(Image, self).to_dict()
image_dict['is_public'] = self.is_public
image_dict['properties'] = {
k: self._apiresource[k] for k in self._apiresource
if self.property_visible(k, show_ext_attrs=show_ext_attrs)}
return image_dict
def __eq__(self, other_image):
return self._apiresource == other_image._apiresource
def __ne__(self, other_image):
return not self.__eq__(other_image)
@memoized
def glanceclient(request, version='1'):
def glanceclient(request, version=None):
api_version = VERSIONS.get_active_version()
url = base.url_for(request, 'image')
insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False)
cacert = getattr(settings, 'OPENSTACK_SSL_CACERT', None)
return glance_client.Client(version, url, token=request.user.token.id,
insecure=insecure, cacert=cacert)
# TODO(jpichon): Temporarily keep both till we update the API calls
# to stop hardcoding a version in this file. Once that's done we
# can get rid of the deprecated 'version' parameter.
if version is None:
return api_version['client'].Client(url, token=request.user.token.id,
insecure=insecure, cacert=cacert)
else:
return glance_client.Client(version, url, token=request.user.token.id,
insecure=insecure, cacert=cacert)
# Note: Glance is adding more than just public and private in Newton or later
PUBLIC_TO_VISIBILITY_MAP = {
None: None,
True: 'public',
False: 'private'
}
def _normalize_is_public_filter(filters):
if not filters:
return
if VERSIONS.active >= 2:
if 'is_public' in filters:
visibility = PUBLIC_TO_VISIBILITY_MAP[filters['is_public']]
del filters['is_public']
if visibility is not None:
filters['visibility'] = visibility
elif 'visibility' in filters:
filters['is_public'] = (
getattr(filters, 'visibility', None) == "public")
del filter['visibility']
def _normalize_list_input(filters, **kwargs):
_normalize_is_public_filter(filters)
if VERSIONS.active < 2:
# Glance v1 client processes some keywords specifically.
# Others, it just takes as a nested dict called filters.
# This results in the following being passed into the glance client:
# {
# 'is_public': u'true',
# 'sort_key': u'name',
# 'sort_dir': u'asc',
# 'filters': {
# u'min_disk': u'0',
# u'name': u'mysql',
# 'properties': {
# u'os_shutdown_timeout': u'1'
# }
# }
# }
v1_keywords = ['page_size', 'limit', 'sort_dir', 'sort_key', 'marker',
'is_public', 'return_req_id', 'paginate']
filters = {}
properties = {}
for key, value in iter(kwargs.items()):
if key in v1_keywords:
continue
else:
filters[key] = value
del kwargs[key]
if properties:
filters['properties'] = properties
if filters:
kwargs['filters'] = filters
def image_delete(request, image_id):
@ -74,22 +218,12 @@ def image_get(request, image_id):
with supplied identifier.
"""
image = glanceclient(request).images.get(image_id)
if not hasattr(image, 'name'):
image.name = None
return image
def is_image_public(im):
is_public_v1 = getattr(im, 'is_public', None)
if is_public_v1 is not None:
return is_public_v1
else:
return im.visibility == 'public'
return Image(image)
def image_list_detailed(request, marker=None, sort_dir='desc',
sort_key='created_at', filters=None, paginate=False,
reversed_order=False):
reversed_order=False, **kwargs):
"""Thin layer above glanceclient, for handling pagination issues.
It provides iterating both forward and backward on top of ascetic
@ -144,7 +278,9 @@ def image_list_detailed(request, marker=None, sort_dir='desc',
else:
request_size = limit
_normalize_list_input(filters, **kwargs)
kwargs = {'filters': filters or {}}
if marker:
kwargs['marker'] = marker
kwargs['sort_key'] = sort_key
@ -183,13 +319,25 @@ def image_list_detailed(request, marker=None, sort_dir='desc',
else:
images = list(images_iter)
return images, has_more_data, has_prev_data
# TODO(jpichon): Do it better
wrapped_images = []
for image in images:
wrapped_images.append(Image(image))
return wrapped_images, has_more_data, has_prev_data
def image_update(request, image_id, **kwargs):
image_data = kwargs.get('data', None)
try:
return glanceclient(request).images.update(image_id, **kwargs)
# Horizon doesn't support purging image properties. Make sure we don't
# unintentionally remove properties when using v1. We don't need a
# similar setting for v2 because you have to specify which properties
# to remove, and the default is nothing gets removed.
if VERSIONS.active < 2:
kwargs['purge_props'] = False
return Image(glanceclient(request).images.update(
image_id, **kwargs))
finally:
if image_data:
try:
@ -215,14 +363,15 @@ def get_image_upload_mode():
return mode
class ExternallyUploadedImage(base.APIResourceWrapper):
class ExternallyUploadedImage(Image):
def __init__(self, apiresource, request):
self._attrs = apiresource._info.keys()
super(ExternallyUploadedImage, self).__init__(apiresource=apiresource)
super(ExternallyUploadedImage, self).__init__(apiresource)
image_endpoint = base.url_for(request, 'image')
# FIXME(tsufiev): Horizon doesn't work with Glance V2 API yet,
# remove hardcoded /v1 as soon as it supports both
self._url = "%s/v1/images/%s" % (image_endpoint, self.id)
if VERSIONS.active >= 2:
upload_template = "%s/v2/images/%s/file"
else:
upload_template = "%s/v1/images/%s"
self._url = upload_template % (image_endpoint, self.id)
self._token_id = request.user.token.id
def to_dict(self):
@ -233,6 +382,14 @@ class ExternallyUploadedImage(base.APIResourceWrapper):
})
return base_dict
@property
def upload_url(self):
return self._url
@property
def token_id(self):
return self._token_id
def image_create(request, **kwargs):
"""Create image.
@ -251,8 +408,13 @@ def image_create(request, **kwargs):
some time and is handed off to a separate thread.
"""
data = kwargs.pop('data', None)
location = None
if VERSIONS.active >= 2:
location = kwargs.pop('location', None)
image = glanceclient(request).images.create(**kwargs)
if location is not None:
glanceclient(request).images.add_location(image.id, location, {})
if data:
if isinstance(data, six.string_types):
@ -268,12 +430,16 @@ def image_create(request, **kwargs):
data = SimpleUploadedFile(data.name,
data.read(),
data.content_type)
thread.start_new_thread(image_update,
(request, image.id),
{'data': data,
'purge_props': False})
if VERSIONS.active < 2:
thread.start_new_thread(image_update,
(request, image.id),
{'data': data})
else:
def upload():
return glanceclient(request).images.upload(image.id, data)
thread.start_new_thread(upload, ())
return image
return Image(image)
def image_update_properties(request, image_id, remove_props=None, **kwargs):

View File

@ -40,7 +40,10 @@ class Settings(generic.View):
"""
url_regex = r'settings/$'
SPECIALS = {
'HORIZON_IMAGES_UPLOAD_MODE': api.glance.get_image_upload_mode()
'HORIZON_IMAGES_UPLOAD_MODE': api.glance.get_image_upload_mode(),
'HORIZON_ACTIVE_IMAGE_VERSION': api.glance.VERSIONS.active,
'IMAGES_ALLOW_LOCATION': getattr(settings, 'IMAGES_ALLOW_LOCATION',
False)
}
@rest_utils.ajax()

View File

@ -52,7 +52,8 @@ class Image(generic.View):
http://localhost/api/glance/images/cc758c90-3d98-4ea1-af44-aab405c9c915
"""
return api.glance.image_get(request, image_id).to_dict()
image = api.glance.image_get(request, image_id)
return image.to_dict(show_ext_attrs=True)
@rest_utils.ajax(data_required=True)
def patch(self, request, image_id):
@ -80,7 +81,6 @@ class Image(generic.View):
"""
meta = create_image_metadata(request.DATA)
meta['purge_props'] = False
api.glance.image_update(request, image_id, **meta)
@ -322,8 +322,8 @@ def create_image_metadata(data):
'min_ram': data.get('min_ram', 0),
'name': data.get('name'),
'disk_format': data.get('disk_format'),
'container_format': data.get('container_format'),
'properties': {}}
'container_format': data.get('container_format')}
properties = {}
# 'architecture' will be directly mapped
# into the .properties by the handle_unknown_properties function.
@ -331,12 +331,17 @@ def create_image_metadata(data):
# compatibility.
props = data.get('properties')
if props and props.get('description'):
meta['properties']['description'] = props.get('description')
properties['description'] = props.get('description')
if data.get('kernel'):
meta['properties']['kernel_id'] = data.get('kernel')
properties['kernel_id'] = data.get('kernel')
if data.get('ramdisk'):
meta['properties']['ramdisk_id'] = data.get('ramdisk')
handle_unknown_properties(data, meta)
properties['ramdisk_id'] = data.get('ramdisk')
handle_unknown_properties(data, properties)
if api.glance.VERSIONS.active >= 2:
meta.update(properties)
else:
meta['properties'] = properties
handle_visibility(data.get('visibility'), meta)
except KeyError as e:
@ -345,7 +350,7 @@ def create_image_metadata(data):
return meta
def handle_unknown_properties(data, meta):
def handle_unknown_properties(data, properties):
# The Glance API takes in both known and unknown fields. Unknown fields
# are assumed as metadata. To achieve this and continue to use the
# existing horizon api wrapper, we need this function. This way, the
@ -359,19 +364,18 @@ def handle_unknown_properties(data, meta):
'deleted_at', 'is_public', 'virtual_size',
'status', 'size', 'owner', 'id', 'updated_at']
other_props = {k: v for (k, v) in data.items() if k not in known_props}
meta['properties'].update(other_props)
properties.update(other_props)
def handle_visibility(visibility, meta):
# The following expects a 'visibility' parameter to be passed via
# the AJAX call, then translates this to a Glance API v1 is_public
# parameter. In the future, if the 'visibility' param is exposed on the
# glance API, you can check for version, e.g.:
# if float(api.glance.get_version()) < 2.0:
mapping_to_v1 = {'public': True, 'private': False, 'shared': False}
# note: presence of 'visibility' previously checked for in general call
try:
meta['is_public'] = mapping_to_v1[visibility]
is_public = mapping_to_v1[visibility]
if api.glance.VERSIONS.active >= 2:
meta['visibility'] = visibility
else:
meta['is_public'] = is_public
except KeyError as e:
raise rest_utils.AjaxError(400,
'invalid visibility option: %s' % e.args[0])

View File

@ -65,25 +65,34 @@ def create_image_metadata(data):
else:
container_format = 'bare'
# The Create form uses 'is_public' but the Update form uses 'public'. Just
# being tolerant here so we don't break anything else.
meta = {'is_public': data.get('is_public', data.get('public', False)),
'protected': data['protected'],
meta = {'protected': data['protected'],
'disk_format': disk_format,
'container_format': container_format,
'min_disk': (data['minimum_disk'] or 0),
'min_ram': (data['minimum_ram'] or 0),
'name': data['name'],
'properties': {}}
'name': data['name']}
if 'description' in data:
meta['properties']['description'] = data['description']
is_public = data.get('is_public', data.get('public', False))
properties = {}
# NOTE(tsufiev): in V2 the way how empty non-base attributes (AKA metadata)
# are handled has changed: in V2 empty metadata is kept in image
# properties, while in V1 they were omitted. Skip empty description (which
# is metadata) to keep the same behavior between V1 and V2
if data.get('description'):
properties['description'] = data['description']
if data.get('kernel'):
meta['properties']['kernel_id'] = data['kernel']
properties['kernel_id'] = data['kernel']
if data.get('ramdisk'):
meta['properties']['ramdisk_id'] = data['ramdisk']
properties['ramdisk_id'] = data['ramdisk']
if data.get('architecture'):
meta['properties']['architecture'] = data['architecture']
properties['architecture'] = data['architecture']
if api.glance.VERSIONS.active < 2:
meta.update({'is_public': is_public, 'properties': properties})
else:
meta['visibility'] = 'public' if is_public else 'private'
meta.update(properties)
return meta
@ -195,6 +204,24 @@ class CreateImageForm(CreateParent):
self._hide_file_source_type()
if not policy.check((("image", "set_image_location"),), request):
self._hide_url_source_type()
# GlanceV2 feature removals
if api.glance.VERSIONS.active >= 2:
# NOTE: GlanceV2 doesn't support copy-from feature, sorry!
self._hide_is_copying()
if not getattr(settings, 'IMAGES_ALLOW_LOCATION', False):
self._hide_url_source_type()
if (api.glance.get_image_upload_mode() == 'off' or not
policy.check((("image", "upload_image"),), request)):
# Neither setting a location nor uploading image data is
# allowed, so throw an error.
msg = _('The current Horizon settings indicate no valid '
'image creation methods are available. Providing '
'an image location and/or uploading from the '
'local file system must be allowed to support '
'image creation.')
messages.error(request, msg)
raise ValidationError(msg)
if not policy.check((("image", "publicize_image"),), request):
self._hide_is_public()
@ -252,6 +279,10 @@ class CreateImageForm(CreateParent):
self.fields['is_public'].widget = HiddenInput()
self.fields['is_public'].initial = False
def _hide_is_copying(self):
self.fields['is_copying'].widget = HiddenInput()
self.fields['is_copying'].initial = False
def clean(self):
data = super(CreateImageForm, self).clean()
@ -278,7 +309,7 @@ class CreateImageForm(CreateParent):
policy.check((("image", "upload_image"),), request) and
data.get('image_file', None)):
meta['data'] = data['image_file']
elif data['is_copying']:
elif data.get('is_copying'):
meta['copy_from'] = data['image_url']
else:
meta['location'] = data['image_url']
@ -369,9 +400,6 @@ class UpdateImageForm(forms.SelfHandlingForm):
image_id = data['image_id']
error_updating = _('Unable to update image "%s".')
meta = create_image_metadata(data)
# Ensure we do not delete properties that have already been
# set on an image.
meta['purge_props'] = False
try:
image = api.glance.image_update(request, image_id, **meta)

View File

@ -206,8 +206,14 @@ class OwnerFilter(tables.FixedFilterAction):
new_dict = button_dict.copy()
new_dict['value'] = new_dict['tenant']
buttons.append(new_dict)
buttons.append(make_dict(_('Shared with Project'), 'shared',
'fa-share-square-o'))
# FIXME(bpokorny): Remove this check once admins can list images with
# GlanceV2 without getting all images in the whole cloud.
if api.glance.VERSIONS.active >= 2:
buttons.append(make_dict(_('Non-Public from Other Projects'),
'other', 'fa-group'))
else:
buttons.append(make_dict(_('Shared with Project'), 'shared',
'fa-share-square-o'))
buttons.append(make_dict(_('Public'), 'public', 'fa-group'))
return buttons
@ -223,14 +229,15 @@ class OwnerFilter(tables.FixedFilterAction):
def get_image_categories(im, user_tenant_id):
categories = []
if api.glance.is_image_public(im):
if im.is_public:
categories.append('public')
if im.owner == user_tenant_id:
categories.append('project')
elif im.owner in filter_tenant_ids():
categories.append(im.owner)
elif not api.glance.is_image_public(im):
elif not im.is_public:
categories.append('shared')
categories.append('other')
return categories

View File

@ -38,7 +38,7 @@ from openstack_dashboard.dashboards.project.images.images import tables
IMAGES_INDEX_URL = reverse('horizon:project:images:index')
class CreateImageFormTests(test.TestCase):
class CreateImageFormTests(test.ResetImageAPIVersionMixin, test.TestCase):
@test.create_stubs({api.glance: ('image_list_detailed',)})
def test_no_location_or_file(self):
filters = {'disk_format': 'aki'}
@ -64,6 +64,7 @@ class CreateImageFormTests(test.TestCase):
self.assertFalse(form.is_valid())
@override_settings(HORIZON_IMAGES_ALLOW_UPLOAD=False)
@override_settings(IMAGES_ALLOW_LOCATION=True)
@test.create_stubs({api.glance: ('image_list_detailed',)})
def test_image_upload_disabled(self):
filters = {'disk_format': 'aki'}
@ -81,7 +82,8 @@ class CreateImageFormTests(test.TestCase):
source_type_dict = dict(form.fields['source_type'].choices)
self.assertNotIn('file', source_type_dict)
def test_create_image_metadata_docker(self):
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_create_image_metadata_docker_v1(self):
form_data = {
'name': u'Docker image',
'description': u'Docker image test',
@ -106,8 +108,29 @@ class CreateImageFormTests(test.TestCase):
self.assertEqual(meta['properties']['architecture'],
form_data['architecture'])
def test_create_image_metadata_docker_v2(self):
form_data = {
'name': u'Docker image',
'description': u'Docker image test',
'source_type': u'url',
'image_url': u'/',
'disk_format': u'docker',
'architecture': u'x86-64',
'minimum_disk': 15,
'minimum_ram': 512,
'is_public': False,
'protected': False,
'is_copying': False
}
meta = forms.create_image_metadata(form_data)
self.assertEqual(meta['disk_format'], 'raw')
self.assertEqual(meta['container_format'], 'docker')
self.assertNotIn('properties', meta)
self.assertEqual(meta['description'], form_data['description'])
self.assertEqual(meta['architecture'], form_data['architecture'])
class UpdateImageFormTests(test.TestCase):
class UpdateImageFormTests(test.ResetImageAPIVersionMixin, test.TestCase):
def test_is_format_field_editable(self):
form = forms.UpdateImageForm({})
disk_format = form.fields['disk_format']
@ -127,8 +150,9 @@ class UpdateImageFormTests(test.TestCase):
self.assertEqual(res.context['image'].disk_format,
image.disk_format)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
@test.create_stubs({api.glance: ('image_update', 'image_get')})
def test_image_update_post(self):
def test_image_update_post_v1(self):
image = self.images.first()
data = {
'name': u'Ubuntu 11.10',
@ -156,10 +180,49 @@ class UpdateImageFormTests(test.TestCase):
name=data['name'],
min_ram=data['minimum_ram'],
min_disk=data['minimum_disk'],
properties={'description': data['description'],
'architecture':
data['architecture']},
purge_props=False).AndReturn(image)
properties={
'description': data['description'],
'architecture':
data['architecture']}).AndReturn(image)
self.mox.ReplayAll()
url = reverse('horizon:project:images:images:update',
args=[image.id])
res = self.client.post(url, data)
self.assertNoFormErrors(res)
self.assertEqual(res.status_code, 302)
@test.create_stubs({api.glance: ('image_update', 'image_get')})
def test_image_update_post_v2(self):
image = self.images.first()
data = {
'name': u'Ubuntu 11.10',
'image_id': str(image.id),
'description': u'Login with admin/admin',
'source_type': u'url',
'image_url': u'http://cloud-images.ubuntu.com/releases/'
u'oneiric/release/ubuntu-11.10-server-cloudimg'
u'-amd64-disk1.img',
'disk_format': u'qcow2',
'architecture': u'x86-64',
'minimum_disk': 15,
'minimum_ram': 512,
'is_public': False,
'protected': False,
'method': 'UpdateImageForm'}
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
.AndReturn(image)
api.glance.image_update(IsA(http.HttpRequest),
image.id,
visibility='private',
protected=data['protected'],
disk_format=data['disk_format'],
container_format="bare",
name=data['name'],
min_ram=data['minimum_ram'],
min_disk=data['minimum_disk'],
description=data['description'],
architecture=data['architecture']).\
AndReturn(image)
self.mox.ReplayAll()
url = reverse('horizon:project:images:images:update',
args=[image.id])
@ -168,7 +231,7 @@ class UpdateImageFormTests(test.TestCase):
self.assertEqual(res.status_code, 302)
class ImageViewTests(test.TestCase):
class ImageViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
@test.create_stubs({api.glance: ('image_list_detailed',)})
def test_image_create_get(self):
filters = {'disk_format': 'aki'}
@ -186,8 +249,9 @@ class ImageViewTests(test.TestCase):
self.assertTemplateUsed(res,
'project/images/images/create.html')
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
@test.create_stubs({api.glance: ('image_create',)})
def test_image_create_post_copy_from(self):
def test_image_create_post_copy_from_v1(self):
data = {
'source_type': u'url',
'image_url': u'http://cloud-images.ubuntu.com/releases/'
@ -198,8 +262,9 @@ class ImageViewTests(test.TestCase):
api_data = {'copy_from': data['image_url']}
self._test_image_create(data, api_data)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
@test.create_stubs({api.glance: ('image_create',)})
def test_image_create_post_location(self):
def test_image_create_post_location_v1(self):
data = {
'source_type': u'url',
'image_url': u'http://cloud-images.ubuntu.com/releases/'
@ -210,8 +275,21 @@ class ImageViewTests(test.TestCase):
api_data = {'location': data['image_url']}
self._test_image_create(data, api_data)
@override_settings(IMAGES_ALLOW_LOCATION=True)
@test.create_stubs({api.glance: ('image_create',)})
def test_image_create_post_upload(self):
def test_image_create_post_location_v2(self):
data = {
'source_type': u'url',
'image_url': u'http://cloud-images.ubuntu.com/releases/'
u'oneiric/release/ubuntu-11.10-server-cloudimg'
u'-amd64-disk1.img'}
api_data = {'location': data['image_url']}
self._test_image_create(data, api_data)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
@test.create_stubs({api.glance: ('image_create',)})
def test_image_create_post_upload_v1(self):
temp_file = tempfile.NamedTemporaryFile()
temp_file.write(b'123')
temp_file.flush()
@ -224,7 +302,38 @@ class ImageViewTests(test.TestCase):
self._test_image_create(data, api_data)
@test.create_stubs({api.glance: ('image_create',)})
def test_image_create_post_with_kernel_ramdisk(self):
def test_image_create_post_upload_v2(self):
temp_file = tempfile.NamedTemporaryFile()
temp_file.write(b'123')
temp_file.flush()
temp_file.seek(0)
data = {'source_type': u'file',
'image_file': temp_file}
api_data = {'data': IsA(InMemoryUploadedFile)}
self._test_image_create(data, api_data)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
@test.create_stubs({api.glance: ('image_create',)})
def test_image_create_post_with_kernel_ramdisk_v1(self):
temp_file = tempfile.NamedTemporaryFile()
temp_file.write(b'123')
temp_file.flush()
temp_file.seek(0)
data = {
'source_type': u'file',
'image_file': temp_file,
'kernel_id': '007e7d55-fe1e-4c5c-bf08-44b4a496482e',
'ramdisk_id': '007e7d55-fe1e-4c5c-bf08-44b4a496482a'
}
api_data = {'data': IsA(InMemoryUploadedFile)}
self._test_image_create(data, api_data)
@test.create_stubs({api.glance: ('image_create',)})
def test_image_create_post_with_kernel_ramdisk_v2(self):
temp_file = tempfile.NamedTemporaryFile()
temp_file.write(b'123')
temp_file.flush()
@ -256,14 +365,22 @@ class ImageViewTests(test.TestCase):
api_data = {'container_format': 'bare',
'disk_format': data['disk_format'],
'is_public': True,
'protected': False,
'min_disk': data['minimum_disk'],
'min_ram': data['minimum_ram'],
'properties': {
'description': data['description'],
'architecture': data['architecture']},
'name': data['name']}
if api.glance.VERSIONS.active < 2:
api_data.update({'is_public': True,
'properties': {
'description': data['description'],
'architecture': data['architecture']}
})
else:
api_data.update({'visibility': 'public',
'description': data['description'],
'architecture': data['architecture']
})
api_data.update(extra_api_data)
filters = {'disk_format': 'aki'}
@ -286,12 +403,9 @@ class ImageViewTests(test.TestCase):
self.assertNoFormErrors(res)
self.assertEqual(res.status_code, 302)
@test.create_stubs({api.glance: ('image_get',)})
def test_image_detail_get(self):
image = self.images.first()
def _test_image_detail_get(self, image):
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
.AndReturn(self.images.first())
.AndReturn(image)
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:project:images:images:detail',
@ -302,10 +416,20 @@ class ImageViewTests(test.TestCase):
self.assertEqual(res.context['image'].name, image.name)
self.assertEqual(res.context['image'].protected, image.protected)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
@test.create_stubs({api.glance: ('image_get',)})
def test_image_detail_custom_props_get(self):
image = self.images.list()[8]
def test_image_detail_get_v1(self):
image = self.images.first()
self._test_image_detail_get(image)
@test.create_stubs({api.glance: ('image_get',)})
def test_image_detail_get_v2(self):
image = self.imagesV2.first()
self._test_image_detail_get(image)
def _test_image_detail_custom_props_get(self, image):
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
.AndReturn(image)
self.mox.ReplayAll()
@ -320,8 +444,8 @@ class ImageViewTests(test.TestCase):
self.assertNotIn(('description'), image_keys)
# Test custom properties are sorted
self.assertEqual(image_props[0], ('bar', 'bar', 'bar val'))
self.assertEqual(image_props[1], ('foo', 'foo', 'foo val'))
self.assertLess(image_props.index(('bar', 'bar', 'bar val')),
image_props.index(('foo', 'foo', 'foo val')))
# Test all custom properties appear in template
self.assertContains(res, '<dt title="bar">bar</dt>')
@ -329,10 +453,20 @@ class ImageViewTests(test.TestCase):
self.assertContains(res, '<dt title="foo">foo</dt>')
self.assertContains(res, '<dd>foo val</dd>')
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
@test.create_stubs({api.glance: ('image_get',)})
def test_protected_image_detail_get(self):
image = self.images.list()[2]
def test_image_detail_custom_props_get_v1(self):
image = self.images.list()[8]
self._test_image_detail_custom_props_get(image)
@test.create_stubs({api.glance: ('image_get',)})
def test_image_detail_custom_props_get_v2(self):
image = self.imagesV2.list()[2]
self._test_image_detail_custom_props_get(image)
def _test_protected_image_detail_get(self, image):
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
.AndReturn(image)
self.mox.ReplayAll()
@ -344,6 +478,19 @@ class ImageViewTests(test.TestCase):
'horizon/common/_detail.html')
self.assertEqual(res.context['image'].protected, image.protected)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
@test.create_stubs({api.glance: ('image_get',)})
def test_protected_image_detail_get_v1(self):
image = self.images.list()[2]
self._test_protected_image_detail_get(image)
@test.create_stubs({api.glance: ('image_get',)})
def test_protected_image_detail_get_v2(self):
image = self.imagesV2.list()[1]
self._test_protected_image_detail_get(image)
@test.create_stubs({api.glance: ('image_get',)})
def test_image_detail_get_with_exception(self):
image = self.images.first()
@ -359,9 +506,8 @@ class ImageViewTests(test.TestCase):
@test.create_stubs({api.glance: ('image_get',)})
def test_image_update_get(self):
image = self.images.first()
image.disk_format = "ami"
image.is_public = True
image = self.images.filter(is_public=True)[0]
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
.AndReturn(image)
self.mox.ReplayAll()

View File

@ -19,6 +19,7 @@
"""
Views for managing images.
"""
from django.conf import settings
from django.core.urlresolvers import reverse
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
@ -71,6 +72,9 @@ class CreateView(forms.ModalFormView):
context = super(CreateView, self).get_context_data(**kwargs)
upload_mode = api.glance.get_image_upload_mode()
context['image_upload_enabled'] = upload_mode != 'off'
context['images_allow_location'] = getattr(settings,
'IMAGES_ALLOW_LOCATION',
False)
return context

View File

@ -8,18 +8,22 @@
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>
{% if image_upload_enabled %}
{% if image_upload_enabled and images_allow_location %}
{% trans "Images can be provided via an HTTP/HTTPS URL or be uploaded from your local file system." %}
{% elif image_upload_enabled %}
{% trans "Currently only images uploaded from your local file system are supported." %}
{% else %}
{% trans "Currently only images available via an HTTP/HTTPS URL are supported. The image location must be accessible to the Image Service." %}
{% endif %}
</p>
<p>
<strong>{% trans "Please note: " %}</strong>
{% if image_upload_enabled %}
{% trans "If you select an image via an HTTP/HTTPS URL, the Image Location field MUST be a valid and direct URL to the image binary; it must also be accessible to the Image Service. URLs that redirect or serve error pages will result in unusable images." %}
{% else %}
{% trans "The Image Location field MUST be a valid and direct URL to the image binary. URLs that redirect or serve error pages will result in unusable images." %}
{% if images_allow_location %}
<strong>{% trans "Please note: " %}</strong>
{% if image_upload_enabled %}
{% trans "If you select an image via an HTTP/HTTPS URL, the Image Location field MUST be a valid and direct URL to the image binary; it must also be accessible to the Image Service. URLs that redirect or serve error pages will result in unusable images." %}
{% else %}
{% trans "The Image Location field MUST be a valid and direct URL to the image binary. URLs that redirect or serve error pages will result in unusable images." %}
{% endif %}
{% endif %}
</p>
{% endblock %}

View File

@ -28,6 +28,7 @@ from django.core.urlresolvers import reverse
from django.forms import widgets
from django import http
import django.test
from django.test.utils import override_settings
from django.utils.http import urlencode
from mox3.mox import IgnoreArg # noqa
from mox3.mox import IsA # noqa
@ -54,7 +55,14 @@ VOLUME_SEARCH_OPTS = dict(status=AVAILABLE, bootable=True)
SNAPSHOT_SEARCH_OPTS = dict(status=AVAILABLE)
class InstanceTests(helpers.TestCase):
class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
def setUp(self):
super(InstanceTests, self).setUp()
if api.glance.VERSIONS.active < 2:
self.versioned_images = self.images
else:
self.versioned_images = self.imagesV2
@helpers.create_stubs({
api.nova: (
'flavor_list',
@ -1511,7 +1519,7 @@ class InstanceTests(helpers.TestCase):
config_drive=True,
config_drive_default=False,
test_with_profile=False):
image = self.images.first()
image = self.versioned_images.first()
api.nova.extension_supported('BlockDeviceMappingV2Boot',
IsA(http.HttpRequest)) \
@ -1525,7 +1533,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -1674,6 +1682,10 @@ class InstanceTests(helpers.TestCase):
self.assertEqual(step.action.initial['config_drive'],
config_drive_default)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_instance_get_glance_v1(self):
self.test_launch_instance_get()
@django.test.utils.override_settings(
OPENSTACK_HYPERVISOR_FEATURES={'can_set_password': False})
def test_launch_instance_get_without_password(self):
@ -1777,7 +1789,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -1853,6 +1865,10 @@ class InstanceTests(helpers.TestCase):
for volume in bootable_volumes:
self.assertIn(volume, volume_sources_ids)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_instance_get_bootable_volumes_glance_v1(self):
self.test_launch_instance_get_bootable_volumes()
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
def test_launch_instance_get_bootable_volumes_with_profile(self):
@ -1879,7 +1895,7 @@ class InstanceTests(helpers.TestCase):
test_with_profile=False,
test_with_multi_nics=False):
flavor = self.flavors.first()
image = self.images.first()
image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
sec_group = self.security_groups.first()
@ -1903,7 +1919,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -2024,6 +2040,10 @@ class InstanceTests(helpers.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_instance_post_glance_v1(self):
self.test_launch_instance_post()
def test_launch_instance_post_no_disk_config_supported(self):
self.test_launch_instance_post(disk_config=False)
@ -2046,7 +2066,7 @@ class InstanceTests(helpers.TestCase):
test_with_multi_nics=False,
):
flavor = self.flavors.first()
image = self.images.first()
image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
sec_group = self.security_groups.first()
@ -2068,7 +2088,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'is_public': True,
'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -2181,6 +2201,10 @@ class InstanceTests(helpers.TestCase):
def test_launch_instance_post_with_profile_and_port_error(self):
self._test_launch_instance_post_with_profile_and_port_error()
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_instance_post_with_profile_and_port_error_glance_v1(self):
self.test_launch_instance_post_with_profile_and_port_error()
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
@helpers.create_stubs({api.glance: ('image_list_detailed',),
@ -2263,7 +2287,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -2367,6 +2391,10 @@ class InstanceTests(helpers.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_instance_post_boot_from_volume_glance_v1(self):
self.test_launch_instance_post_boot_from_volume()
def test_launch_instance_post_boot_from_volume_with_bdmv2(self):
self.test_launch_instance_post_boot_from_volume(test_with_bdmv2=True)
@ -2422,7 +2450,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -2527,6 +2555,10 @@ class InstanceTests(helpers.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_lnch_inst_post_no_images_avail_boot_from_volume_glance_v1(self):
self.test_launch_instance_post_no_images_available_boot_from_volume()
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
def test_lnch_inst_post_no_images_avail_boot_from_vol_with_profile(self):
@ -2712,7 +2744,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -2817,6 +2849,10 @@ class InstanceTests(helpers.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_instance_post_boot_from_snapshot_glance_v1(self):
self.test_launch_instance_post_boot_from_snapshot()
def test_launch_instance_post_boot_from_snapshot_with_bdmv2(self):
self.test_launch_instance_post_boot_from_snapshot(test_with_bdmv2=True)
@ -2945,7 +2981,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -3000,6 +3036,10 @@ class InstanceTests(helpers.TestCase):
self.assertTemplateUsed(res, views.WorkflowView.template_name)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_flavorlist_error_glance_v1(self):
self.test_launch_flavorlist_error()
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
def test_launch_flavorlist_error_with_profile(self):
@ -3024,7 +3064,7 @@ class InstanceTests(helpers.TestCase):
def test_launch_form_keystone_exception(self,
test_with_profile=False):
flavor = self.flavors.first()
image = self.images.first()
image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
sec_group = self.security_groups.first()
@ -3055,7 +3095,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -3151,6 +3191,10 @@ class InstanceTests(helpers.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_form_keystone_exception_with_profile_glance_v1(self):
self.test_launch_form_keystone_exception()
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
def test_launch_form_keystone_exception_with_profile(self):
@ -3172,7 +3216,7 @@ class InstanceTests(helpers.TestCase):
def test_launch_form_instance_count_error(self,
test_with_profile=False):
flavor = self.flavors.first()
image = self.images.first()
image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
volume = self.volumes.first()
@ -3197,7 +3241,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -3273,6 +3317,10 @@ class InstanceTests(helpers.TestCase):
self.assertContains(res, "greater than or equal to 1")
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_form_instance_count_error_glance_v1(self):
self.test_launch_form_instance_count_error()
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'profile_list',
@ -3290,7 +3338,7 @@ class InstanceTests(helpers.TestCase):
def _test_launch_form_count_error(self, resource,
avail, test_with_profile=False):
flavor = self.flavors.first()
image = self.images.first()
image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
volume = self.volumes.first()
@ -3320,7 +3368,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -3407,7 +3455,11 @@ class InstanceTests(helpers.TestCase):
"512, Requested: 1024)" % {'avail': avail})
self.assertContains(res, msg)
def test_launch_form_cores_count_error(self):
def test_launch_form_cores_count_error_glance_v2(self):
self._test_launch_form_count_error('cores', 1, test_with_profile=False)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_form_cores_count_error_glance_v1(self):
self._test_launch_form_count_error('cores', 1, test_with_profile=False)
def test_launch_form_ram_count_error(self):
@ -3461,7 +3513,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -3547,18 +3599,22 @@ class InstanceTests(helpers.TestCase):
test_with_profile=False,
):
flavor = self.flavors.first()
image = self.images.first()
image = self.versioned_images.first()
image.min_ram = flavor.ram
image.min_disk = flavor.disk + 1
self._test_launch_form_instance_requirement_error(image, flavor,
test_with_profile)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_form_instance_requirement_error_disk_glance_v1(self):
self.test_launch_form_instance_requirement_error_disk()
def test_launch_form_instance_requirement_error_ram(
self,
test_with_profile=False,
):
flavor = self.flavors.first()
image = self.images.first()
image = self.versioned_images.first()
image.min_ram = flavor.ram + 1
image.min_disk = flavor.disk
self._test_launch_form_instance_requirement_error(image, flavor,
@ -3593,7 +3649,7 @@ class InstanceTests(helpers.TestCase):
widget_class,
widget_attrs):
flavor = self.flavors.first()
image = self.images.first()
image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
volume = self.volumes.first()
@ -3617,7 +3673,7 @@ class InstanceTests(helpers.TestCase):
IsA(http.HttpRequest),
filters={'is_public': True,
'status': 'active'}).AndReturn(
[self.images.list(), False, False])
[self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -3692,8 +3748,8 @@ class InstanceTests(helpers.TestCase):
for widget_part in widget_content.split():
self.assertContains(res, widget_part)
@django.test.utils.override_settings(
OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': True})
@override_settings(
OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': True},)
def test_launch_form_instance_device_name_showed(self):
self._test_launch_form_instance_show_device_name(
u'vda', widgets.TextInput, {
@ -3701,6 +3757,17 @@ class InstanceTests(helpers.TestCase):
'attrs': {'id': 'id_device_name'}}
)
@override_settings(
OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': True},
OPENSTACK_API_VERSIONS={'image': 1}
)
def test_launch_form_instance_device_name_showed_glance_v1(self):
self._test_launch_form_instance_show_device_name(
u'vda', widgets.TextInput, {
'name': 'device_name', 'value': 'vda',
'attrs': {'id': 'id_device_name'}}
)
@django.test.utils.override_settings(
OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': False})
def test_launch_form_instance_device_name_hidden(self):
@ -3754,7 +3821,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -3834,22 +3901,26 @@ class InstanceTests(helpers.TestCase):
def test_launch_form_instance_volume_size_error(self,
test_with_profile=False):
image = self.images.get(name='protected_images')
image = self.versioned_images.get(name='protected_images')
volume_size = image.min_disk // 2
msg = ("The Volume size is too small for the &#39;%s&#39; image" %
image.name)
self._test_launch_form_instance_volume_size(image, volume_size, msg,
test_with_profile)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_form_instance_volume_size_error_glance_v1(self):
self.test_launch_form_instance_volume_size_error()
def test_launch_form_instance_non_int_volume_size(self,
test_with_profile=False):
image = self.images.get(name='protected_images')
image = self.versioned_images.get(name='protected_images')
msg = "Enter a whole number."
self._test_launch_form_instance_volume_size(image, 1.5, msg,
test_with_profile)
def test_launch_form_instance_volume_exceed_quota(self):
image = self.images.get(name='protected_images')
image = self.versioned_images.get(name='protected_images')
msg = "Requested volume exceeds quota: Available: 0, Requested: 1"
self._test_launch_form_instance_volume_size(image, image.min_disk,
msg, False, 0)
@ -3975,7 +4046,7 @@ class InstanceTests(helpers.TestCase):
quotas: ('tenant_quota_usages',)})
def test_launch_with_empty_device_name_allowed(self):
flavor = self.flavors.get(name='m1.massive')
image = self.images.first()
image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
sec_group = self.security_groups.first()
@ -4007,7 +4078,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -4094,6 +4165,10 @@ class InstanceTests(helpers.TestCase):
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_launch_with_empty_device_name_allowed_glance_v1(self):
self.test_launch_with_empty_device_name_allowed()
@helpers.create_stubs({
api.nova: ('flavor_list', 'server_list', 'tenant_absolute_limits',
'extension_supported',),
@ -4157,7 +4232,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -4218,6 +4293,10 @@ class InstanceTests(helpers.TestCase):
html=True,
msg_prefix="The default key pair was not selected.")
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_select_default_keypair_if_only_one_glance_v1(self):
self.test_select_default_keypair_if_only_one()
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
def test_select_default_keypair_if_only_one_with_profile(self):
@ -4945,7 +5024,7 @@ class InstanceAjaxTests(helpers.TestCase):
self.assertContains(res, "Not available")
class ConsoleManagerTests(helpers.TestCase):
class ConsoleManagerTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
def setup_consoles(self):
# Need to refresh with mocks or will fail since mox do not detect
@ -5266,9 +5345,8 @@ class ConsoleManagerTests(helpers.TestCase):
cinder: ('volume_list',
'volume_snapshot_list',),
quotas: ('tenant_quota_usages',)})
def test_port_cleanup_called_on_failed_vm_launch(self):
def _test_port_cleanup_called_on_failed_vm_launch(self, image, images):
flavor = self.flavors.first()
image = self.images.first()
keypair = self.keypairs.first()
server = self.servers.first()
sec_group = self.security_groups.first()
@ -5298,7 +5376,7 @@ class ConsoleManagerTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
.AndReturn([self.images.list(), False, False])
.AndReturn([images, False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@ -5390,3 +5468,14 @@ class ConsoleManagerTests(helpers.TestCase):
res = self.client.post(url, form_data)
self.assertRedirectsNoFollow(res, INDEX_URL)
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_port_cleanup_called_on_failed_vm_launch_v1(self):
image = self.images.first()
images = self.images.list()
self._test_port_cleanup_called_on_failed_vm_launch(image, images)
def test_port_cleanup_called_on_failed_vm_launch_v2(self):
image = self.imagesV2.first()
images = self.imagesV2.list()
self._test_port_cleanup_called_on_failed_vm_launch(image, images)

View File

@ -40,7 +40,7 @@ VOLUME_VOLUMES_TAB_URL = urlunquote(reverse(
SEARCH_OPTS = dict(status=api.cinder.VOLUME_STATE_AVAILABLE)
class VolumeViewTests(test.TestCase):
class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
@test.create_stubs({cinder: ('volume_create',
'volume_snapshot_list',
'volume_type_list',
@ -752,12 +752,17 @@ class VolumeViewTests(test.TestCase):
image.min_disk = 30
self._test_create_volume_from_image_under_image_min_disk_size(image)
def test_create_volume_from_image_under_image_property_min_disk_size(self):
@override_settings(OPENSTACK_API_VERSIONS={'image': 1})
def test_create_volume_from_image_under_image_prop_min_disk_size_v1(self):
image = self.images.get(name="protected_images")
image.min_disk = 0
image.properties['min_disk'] = 30
self._test_create_volume_from_image_under_image_min_disk_size(image)
def test_create_volume_from_image_under_image_prop_min_disk_size_v2(self):
image = self.imagesV2.get(name="protected_images")
self._test_create_volume_from_image_under_image_min_disk_size(image)
@test.create_stubs({cinder: ('volume_snapshot_list',
'volume_type_list',
'volume_type_default',

View File

@ -57,6 +57,7 @@ WEBROOT = '/'
#OPENSTACK_API_VERSIONS = {
# "data-processing": 1.1,
# "identity": 3,
# "image": 2,
# "volume": 2,
# "compute": 2,
#}
@ -373,6 +374,11 @@ IMAGE_RESERVED_CUSTOM_PROPERTIES = []
# image form. See documentation for deployment considerations.
#HORIZON_IMAGES_UPLOAD_MODE = 'legacy'
# Allow a location to be set when creating or updating Glance images.
# If using Glance V2, this value should be False unless the Glance
# configuration and policies allow setting locations.
#IMAGES_ALLOW_LOCATION = False
# OPENSTACK_ENDPOINT_TYPE specifies the endpoint type to use for the endpoints
# in the Keystone service catalog. Use this setting when Horizon is running
# external to the OpenStack environment. The default is 'publicURL'.

View File

@ -44,7 +44,11 @@
ctrl.image = image.data;
ctrl.image.properties = Object.keys(ctrl.image.properties).map(function mapProps(prop) {
return {name: prop, value: ctrl.image.properties[prop]};
var propValue = ctrl.image.properties[prop];
if ($.isArray(propValue) && propValue.length === 0) {
propValue = '';
}
return {name: prop, value: propValue};
});
userSession.get().then(setProject);

View File

@ -51,12 +51,13 @@
ctrl.imageFormats = imageFormats;
ctrl.diskFormats = [];
ctrl.prepareUpload = prepareUpload;
ctrl.apiVersion = 0;
ctrl.image = {
source_type: 'url',
source_type: '',
image_url: '',
data: {},
is_copying: true,
is_copying: false,
protected: false,
min_disk: 0,
min_ram: 0,
@ -77,9 +78,7 @@
{ label: gettext('No'), value: false }
];
ctrl.imageSourceOptions = [
{ label: gettext('URL'), value: 'url' }
];
ctrl.imageSourceOptions = [];
ctrl.imageVisibilityOptions = [
{ label: gettext('Public'), value: 'public'},
@ -113,6 +112,7 @@
}
function getConfiguredFormatsAndModes(response) {
ctrl.apiVersion = response.HORIZON_ACTIVE_IMAGE_VERSION;
var settingsFormats = response.OPENSTACK_IMAGE_FORMATS;
var uploadMode = response.HORIZON_IMAGES_UPLOAD_MODE;
var dupe = angular.copy(imageFormats);
@ -122,9 +122,15 @@
}
});
if (uploadMode !== 'off') {
ctrl.imageSourceOptions.splice(0, 0, {
label: gettext('File'), value: 'file-' + uploadMode
var uploadValue = 'file-' + uploadMode;
ctrl.imageSourceOptions.push({
label: gettext('File'), value: uploadValue
});
ctrl.image.source_type = uploadValue;
}
if (ctrl.apiVersion < 2 || response.IMAGES_ALLOW_LOCATION) {
ctrl.imageSourceOptions.push({ label: gettext('URL'), value: 'url' });
ctrl.image.source_type = 'url';
}
ctrl.imageFormats = dupe;
}

View File

@ -186,18 +186,31 @@
var ctrl = createController();
settingsCall.resolve({
OPENSTACK_IMAGE_FORMATS: [],
HORIZON_IMAGES_UPLOAD_MODE: 'off'
HORIZON_IMAGES_UPLOAD_MODE: 'off',
IMAGES_ALLOW_LOCATION: true
});
$timeout.flush();
expect(ctrl.imageSourceOptions).toEqual([urlSourceOption]);
});
it('set to "off" and location disallowed disables all source options', function() {
var ctrl = createController();
settingsCall.resolve({
OPENSTACK_IMAGE_FORMATS: [],
HORIZON_IMAGES_UPLOAD_MODE: 'off',
IMAGES_ALLOW_LOCATION: false
});
$timeout.flush();
expect(ctrl.imageSourceOptions).toEqual([]);
});
it('set to a non-"off" value enables local file upload', function() {
var ctrl = createController();
var fileSourceOption = { label: gettext('File'), value: 'file-sample' };
settingsCall.resolve({
OPENSTACK_IMAGE_FORMATS: [],
HORIZON_IMAGES_UPLOAD_MODE: 'sample'
HORIZON_IMAGES_UPLOAD_MODE: 'sample',
IMAGES_ALLOW_LOCATION: true
});
$timeout.flush();
expect(ctrl.imageSourceOptions).toEqual([fileSourceOption, urlSourceOption]);

View File

@ -57,7 +57,7 @@
<label class="control-label required">
<translate>Source Type</translate>
</label>
<div class="form-field">
<div class="form-field" ng-if="ctrl.image.source_type !== ''">
<div class="btn-group">
<label class="btn btn-default btn-toggle"
ng-repeat="option in ctrl.imageSourceOptions"
@ -112,7 +112,8 @@
</p>
</div>
</div>
<div class="col-xs-6 col-sm-6" ng-if="ctrl.image.source_type === 'url'">
<div class="col-xs-6 col-sm-6"
ng-if="ctrl.image.source_type === 'url' && ctrl.apiVersion < 2">
<div class="form-group">
<label class="control-label required">
<translate>Copy Data</translate>
@ -128,6 +129,17 @@
</div>
</div>
</div>
<div class="row form-group" ng-if="ctrl.image.source_type === ''">
<div class="col-xs-9 col-sm-9">
<p class="help-block alert alert-danger">
<translate>The current Horizon settings indicate no valid
image creation methods are available. Providing
an image location and/or uploading from the
local file system must be allowed to support
image creation.</translate>
</p>
</div>
</div>
<div class="row form-group">
<div class="col-xs-6 col-sm-6">
<div class="form-group required">

View File

@ -14,11 +14,16 @@
# limitations under the License.
import mock
from openstack_dashboard import api
from openstack_dashboard.api.rest import glance
from openstack_dashboard.test import helpers as test
class ImagesRestTestCase(test.TestCase):
class ImagesRestTestCase(test.ResetImageAPIVersionMixin, test.TestCase):
def setUp(self):
super(ImagesRestTestCase, self).setUp()
api.glance.VERSIONS.clear_active_cache()
#
# Version
#
@ -70,7 +75,7 @@ class ImagesRestTestCase(test.TestCase):
gc.image_delete.assert_called_once_with(request, "1")
@mock.patch.object(glance.api, 'glance')
def test_image_edit(self, gc):
def test_image_edit_v1(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "aki", "container_format": "aki",
"visibility": "public", "protected": false,
@ -79,6 +84,7 @@ class ImagesRestTestCase(test.TestCase):
"description": "description", "kernel": "kernel",
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
''')
gc.VERSIONS.active = 1
metadata = {'name': 'Test',
'disk_format': 'aki',
@ -87,13 +93,41 @@ class ImagesRestTestCase(test.TestCase):
'protected': False,
'min_disk': 10,
'min_ram': 5,
'properties': {
'description': 'description',
'architecture': 'testArch',
'ramdisk_id': 10,
'kernel_id': 'kernel',
},
'purge_props': False}
'properties': {'description': 'description',
'architecture': 'testArch',
'ramdisk_id': 10,
'kernel_id': 'kernel'}
}
response = glance.Image().patch(request, "1")
self.assertStatusCode(response, 204)
self.assertEqual(response.content.decode('utf-8'), '')
gc.image_update.assert_called_once_with(request, '1', **metadata)
@mock.patch.object(glance.api, 'glance')
def test_image_edit_v2(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "aki", "container_format": "aki",
"visibility": "public", "protected": false,
"image_url": "test.com",
"source_type": "url", "architecture": "testArch",
"description": "description", "kernel": "kernel",
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
''')
gc.VERSIONS.active = 2
metadata = {'name': 'Test',
'disk_format': 'aki',
'container_format': 'aki',
'visibility': 'public',
'protected': False,
'min_disk': 10,
'min_ram': 5,
'description': 'description',
'architecture': 'testArch',
'ramdisk_id': 10,
'kernel_id': 'kernel'
}
response = glance.Image().patch(request, "1")
self.assertStatusCode(response, 204)
@ -126,7 +160,7 @@ class ImagesRestTestCase(test.TestCase):
**kwargs)
@mock.patch.object(glance.api, 'glance')
def test_image_create_basic(self, gc):
def test_image_create_v1_basic(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "aki", "import_data": false,
"visibility": "public", "container_format": "aki",
@ -136,6 +170,7 @@ class ImagesRestTestCase(test.TestCase):
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
''')
new = gc.image_create.return_value
gc.VERSIONS.active = 1
new.to_dict.return_value = {'name': 'testimage'}
new.name = 'testimage'
@ -162,7 +197,43 @@ class ImagesRestTestCase(test.TestCase):
gc.image_create.assert_called_once_with(request, **metadata)
@mock.patch.object(glance.api, 'glance')
def test_image_create_shared(self, gc):
def test_image_create_v2_basic(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "aki", "import_data": false,
"visibility": "public", "container_format": "aki",
"protected": false, "image_url": "test.com",
"source_type": "url", "architecture": "testArch",
"description": "description", "kernel": "kernel",
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
''')
gc.VERSIONS.active = 2
new = gc.image_create.return_value
new.to_dict.return_value = {'name': 'testimage'}
new.name = 'testimage'
metadata = {'name': 'Test',
'disk_format': 'aki',
'container_format': 'aki',
'visibility': 'public',
'protected': False,
'min_disk': 10,
'min_ram': 5,
'location': 'test.com',
'description': 'description',
'architecture': 'testArch',
'ramdisk_id': 10,
'kernel_id': 'kernel',
}
response = glance.Images().put(request)
self.assertStatusCode(response, 201)
self.assertEqual(response.content.decode('utf-8'),
'{"name": "testimage"}')
self.assertEqual(response['location'], '/api/glance/images/testimage')
gc.image_create.assert_called_once_with(request, **metadata)
@mock.patch.object(glance.api, 'glance')
def test_image_create_v1_shared(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "aki", "import_data": false,
"visibility": "shared", "container_format": "aki",
@ -171,6 +242,7 @@ class ImagesRestTestCase(test.TestCase):
"description": "description", "kernel": "kernel",
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
''')
gc.VERSIONS.active = 1
new = gc.image_create.return_value
new.to_dict.return_value = {'name': 'testimage'}
new.name = 'testimage'
@ -198,7 +270,43 @@ class ImagesRestTestCase(test.TestCase):
gc.image_create.assert_called_once_with(request, **metadata)
@mock.patch.object(glance.api, 'glance')
def test_image_create_private(self, gc):
def test_image_create_v2_shared(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "aki", "import_data": false,
"visibility": "shared", "container_format": "aki",
"protected": false, "image_url": "test.com",
"source_type": "url", "architecture": "testArch",
"description": "description", "kernel": "kernel",
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
''')
gc.VERSIONS.active = 2
new = gc.image_create.return_value
new.to_dict.return_value = {'name': 'testimage'}
new.name = 'testimage'
metadata = {'name': 'Test',
'disk_format': 'aki',
'container_format': 'aki',
'visibility': 'shared',
'protected': False,
'min_disk': 10,
'min_ram': 5,
'location': 'test.com',
'description': 'description',
'architecture': 'testArch',
'ramdisk_id': 10,
'kernel_id': 'kernel',
}
response = glance.Images().put(request)
self.assertStatusCode(response, 201)
self.assertEqual(response.content.decode('utf-8'),
'{"name": "testimage"}')
self.assertEqual(response['location'], '/api/glance/images/testimage')
gc.image_create.assert_called_once_with(request, **metadata)
@mock.patch.object(glance.api, 'glance')
def test_image_create_v1_private(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "aki", "import_data": false,
"visibility": "private", "container_format": "aki",
@ -207,6 +315,7 @@ class ImagesRestTestCase(test.TestCase):
"description": "description", "kernel": "kernel",
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
''')
gc.VERSIONS.active = 1
new = gc.image_create.return_value
new.to_dict.return_value = {'name': 'testimage'}
new.name = 'testimage'
@ -234,7 +343,43 @@ class ImagesRestTestCase(test.TestCase):
gc.image_create.assert_called_once_with(request, **metadata)
@mock.patch.object(glance.api, 'glance')
def test_image_create_bad_visibility(self, gc):
def test_image_create_v2_private(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "aki", "import_data": false,
"visibility": "private", "container_format": "aki",
"protected": false, "image_url": "test.com",
"source_type": "url", "architecture": "testArch",
"description": "description", "kernel": "kernel",
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
''')
gc.VERSIONS.active = 2
new = gc.image_create.return_value
new.to_dict.return_value = {'name': 'testimage'}
new.name = 'testimage'
metadata = {'name': 'Test',
'disk_format': 'aki',
'container_format': 'aki',
'visibility': 'private',
'protected': False,
'min_disk': 10,
'min_ram': 5,
'location': 'test.com',
'description': 'description',
'architecture': 'testArch',
'ramdisk_id': 10,
'kernel_id': 'kernel',
}
response = glance.Images().put(request)
self.assertStatusCode(response, 201)
self.assertEqual(response.content.decode('utf-8'),
'{"name": "testimage"}')
self.assertEqual(response['location'], '/api/glance/images/testimage')
gc.image_create.assert_called_once_with(request, **metadata)
@mock.patch.object(glance.api, 'glance')
def test_image_create_v1_bad_visibility(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "aki", "import_data": false,
"visibility": "verybad", "container_format": "aki",
@ -243,6 +388,7 @@ class ImagesRestTestCase(test.TestCase):
"description": "description", "kernel": "kernel",
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
''')
gc.VERSIONS.active = 1
response = glance.Images().put(request)
self.assertStatusCode(response, 400)
@ -250,12 +396,30 @@ class ImagesRestTestCase(test.TestCase):
'"invalid visibility option: verybad"')
@mock.patch.object(glance.api, 'glance')
def test_image_create_required(self, gc):
def test_image_create_v2_bad_visibility(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "aki", "import_data": false,
"visibility": "verybad", "container_format": "aki",
"protected": false, "image_url": "test.com",
"source_type": "url", "architecture": "testArch",
"description": "description", "kernel": "kernel",
"min_disk": 10, "min_ram": 5, "ramdisk": 10 }
''')
gc.VERSIONS.active = 2
response = glance.Images().put(request)
self.assertStatusCode(response, 400)
self.assertEqual(response.content.decode('utf-8'),
'"invalid visibility option: verybad"')
@mock.patch.object(glance.api, 'glance')
def test_image_create_v1_required(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "raw", "import_data": true,
"container_format": "docker",
"visibility": "public", "protected": false,
"source_type": "url", "image_url": "test.com" }''')
gc.VERSIONS.active = 1
new = gc.image_create.return_value
new.to_dict.return_value = {'name': 'testimage'}
new.name = 'testimage'
@ -276,13 +440,40 @@ class ImagesRestTestCase(test.TestCase):
gc.image_create.assert_called_once_with(request, **metadata)
@mock.patch.object(glance.api, 'glance')
def test_image_create_additional_props(self, gc):
def test_image_create_v2_required(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "raw", "import_data": true,
"container_format": "docker",
"visibility": "public", "protected": false,
"source_type": "url", "image_url": "test.com" }''')
gc.VERSIONS.active = 2
new = gc.image_create.return_value
new.to_dict.return_value = {'name': 'testimage'}
new.name = 'testimage'
metadata = {'name': 'Test',
'disk_format': 'raw',
'container_format': 'docker',
'copy_from': 'test.com',
'visibility': 'public',
'protected': False,
'min_disk': 0,
'min_ram': 0
}
response = glance.Images().put(request)
self.assertStatusCode(response, 201)
self.assertEqual(response['location'], '/api/glance/images/testimage')
gc.image_create.assert_called_once_with(request, **metadata)
@mock.patch.object(glance.api, 'glance')
def test_image_create_v1_additional_props(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "raw", "import_data": true,
"container_format": "docker",
"visibility": "public", "protected": false,
"arbitrary": "property", "another": "prop",
"source_type": "url", "image_url": "test.com" }''')
gc.VERSIONS.active = 1
new = gc.image_create.return_value
new.to_dict.return_value = {'name': 'testimage'}
new.name = 'testimage'
@ -302,6 +493,35 @@ class ImagesRestTestCase(test.TestCase):
self.assertEqual(response['location'], '/api/glance/images/testimage')
gc.image_create.assert_called_once_with(request, **metadata)
@mock.patch.object(glance.api, 'glance')
def test_image_create_v2_additional_props(self, gc):
request = self.mock_rest_request(body='''{"name": "Test",
"disk_format": "raw", "import_data": true,
"container_format": "docker",
"visibility": "public", "protected": false,
"arbitrary": "property", "another": "prop",
"source_type": "url", "image_url": "test.com" }''')
gc.VERSIONS.active = 2
new = gc.image_create.return_value
new.to_dict.return_value = {'name': 'testimage'}
new.name = 'testimage'
metadata = {'name': 'Test',
'disk_format': 'raw',
'container_format': 'docker',
'copy_from': 'test.com',
'visibility': 'public',
'protected': False,
'min_disk': 0,
'min_ram': 0,
'arbitrary': 'property',
'another': 'prop'
}
response = glance.Images().put(request)
self.assertStatusCode(response, 201)
self.assertEqual(response['location'], '/api/glance/images/testimage')
gc.image_create.assert_called_once_with(request, **metadata)
@mock.patch.object(glance.api, 'glance')
def test_namespace_get_list(self, gc):
request = self.mock_rest_request(**{'GET': {}})

View File

@ -25,10 +25,15 @@ from openstack_dashboard.test import helpers as test
class GlanceApiTests(test.APITestCase):
def setUp(self):
super(GlanceApiTests, self).setUp()
api.glance.VERSIONS.clear_active_cache()
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_image_list_detailed_no_pagination(self):
# Verify that all images are returned even with a small page size
api_images = self.images.list()
api_images = self.images_api.list()
expected_images = self.images.list() # Wrapped Images
filters = {}
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
@ -44,36 +49,38 @@ class GlanceApiTests(test.APITestCase):
images, has_more, has_prev = api.glance.image_list_detailed(
self.request)
self.assertItemsEqual(images, api_images)
self.assertListEqual(images, expected_images)
self.assertFalse(has_more)
self.assertFalse(has_prev)
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_image_list_detailed_sort_options(self):
# Verify that sort_dir and sort_key work
api_images = self.images.list()
filters = {}
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
sort_dir = 'asc'
sort_key = 'min_disk'
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_image_list_detailed_sort_options(self):
# Verify that sort_dir and sort_key work
api_images = self.images_api.list()
expected_images = self.images.list() # Wrapped Images
filters = {}
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
sort_dir = 'asc'
sort_key = 'min_disk'
glanceclient = self.stub_glanceclient()
glanceclient.images = self.mox.CreateMockAnything()
glanceclient.images.list(page_size=limit,
limit=limit,
filters=filters,
sort_dir=sort_dir,
sort_key=sort_key) \
.AndReturn(iter(api_images))
self.mox.ReplayAll()
glanceclient = self.stub_glanceclient()
glanceclient.images = self.mox.CreateMockAnything()
glanceclient.images.list(page_size=limit,
limit=limit,
filters=filters,
sort_dir=sort_dir,
sort_key=sort_key) \
.AndReturn(iter(api_images))
self.mox.ReplayAll()
images, has_more, has_prev = api.glance.image_list_detailed(
self.request,
sort_dir=sort_dir,
sort_key=sort_key)
self.assertItemsEqual(images, api_images)
self.assertFalse(has_more)
self.assertFalse(has_prev)
images, has_more, has_prev = api.glance.image_list_detailed(
self.request,
sort_dir=sort_dir,
sort_key=sort_key)
self.assertListEqual(images, expected_images)
self.assertFalse(has_more)
self.assertFalse(has_prev)
@override_settings(API_RESULT_PAGE_SIZE=2)
def test_image_list_detailed_pagination_more_page_size(self):
@ -83,7 +90,8 @@ class GlanceApiTests(test.APITestCase):
page_size = settings.API_RESULT_PAGE_SIZE
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
api_images = self.images.list()
api_images = self.images_api.list()
expected_images = self.images.list() # Wrapped Images
images_iter = iter(api_images)
glanceclient = self.stub_glanceclient()
@ -101,8 +109,8 @@ class GlanceApiTests(test.APITestCase):
marker=None,
filters=filters,
paginate=True)
expected_images = api_images[:page_size]
self.assertItemsEqual(images, expected_images)
expected_images = expected_images[:page_size]
self.assertListEqual(images, expected_images)
self.assertTrue(has_more)
self.assertFalse(has_prev)
# Ensure that only the needed number of images are consumed
@ -118,7 +126,8 @@ class GlanceApiTests(test.APITestCase):
page_size = settings.API_RESULT_PAGE_SIZE
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
api_images = self.images.list()
api_images = self.images_api.list()
expected_images = self.images.list() # Wrapped Images
images_iter = iter(api_images)
glanceclient = self.stub_glanceclient()
@ -135,8 +144,8 @@ class GlanceApiTests(test.APITestCase):
self.request,
filters=filters,
paginate=True)
expected_images = api_images[:page_size]
self.assertItemsEqual(images, expected_images)
expected_images = expected_images[:page_size]
self.assertListEqual(images, expected_images)
self.assertFalse(has_more)
self.assertFalse(has_prev)
@ -148,7 +157,8 @@ class GlanceApiTests(test.APITestCase):
page_size = settings.API_RESULT_PAGE_SIZE
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
api_images = self.images.list()
api_images = self.images_api.list()
expected_images = self.images.list() # Wrapped Images
images_iter = iter(api_images)
glanceclient = self.stub_glanceclient()
@ -164,8 +174,8 @@ class GlanceApiTests(test.APITestCase):
self.request,
filters=filters,
paginate=True)
expected_images = api_images[:page_size]
self.assertItemsEqual(images, expected_images)
expected_images = expected_images[:page_size]
self.assertListEqual(images, expected_images)
self.assertFalse(has_more)
self.assertFalse(has_prev)
self.assertEqual(len(expected_images), len(images))
@ -178,7 +188,8 @@ class GlanceApiTests(test.APITestCase):
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
marker = 'nonsense'
api_images = self.images.list()[page_size:]
api_images = self.images_api.list()[page_size:]
expected_images = self.images.list()[page_size:] # Wrapped Images
images_iter = iter(api_images)
glanceclient = self.stub_glanceclient()
@ -198,8 +209,8 @@ class GlanceApiTests(test.APITestCase):
marker=marker,
filters=filters,
paginate=True)
expected_images = api_images[:page_size]
self.assertItemsEqual(images, expected_images)
expected_images = expected_images[:page_size]
self.assertListEqual(images, expected_images)
self.assertTrue(has_more)
self.assertTrue(has_prev)
self.assertEqual(len(list(images_iter)),
@ -213,7 +224,8 @@ class GlanceApiTests(test.APITestCase):
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
marker = 'nonsense'
api_images = self.images.list()[page_size:]
api_images = self.images_api.list()[page_size:]
expected_images = self.images.list()[page_size:] # Wrapped Images
images_iter = iter(api_images)
glanceclient = self.stub_glanceclient()
@ -234,8 +246,8 @@ class GlanceApiTests(test.APITestCase):
filters=filters,
sort_dir='asc',
paginate=True)
expected_images = api_images[:page_size]
self.assertItemsEqual(images, expected_images)
expected_images = expected_images[:page_size]
self.assertListEqual(images, expected_images)
self.assertTrue(has_more)
self.assertTrue(has_prev)
self.assertEqual(len(list(images_iter)),
@ -313,11 +325,15 @@ class GlanceApiTests(test.APITestCase):
res_types = api.glance.metadefs_resource_types_list(self.request)
self.assertItemsEqual(res_types, [])
def test_image_create_external_upload(self):
def _test_image_create_external_upload(self, api_version=2):
expected_image = self.images.first()
service = base.get_service_from_catalog(self.service_catalog, 'image')
base_url = base.get_url_for_service(service, 'RegionOne', 'publicURL')
file_upload_url = '%s/v1/images/%s' % (base_url, expected_image.id)
if api_version == 1:
url_template = '%s/v1/images/%s'
else:
url_template = '%s/v2/images/%s/file'
upload_url = url_template % (base_url, expected_image.id)
glanceclient = self.stub_glanceclient()
glanceclient.images = self.mox.CreateMockAnything()
@ -325,7 +341,12 @@ class GlanceApiTests(test.APITestCase):
self.mox.ReplayAll()
actual_image = api.glance.image_create(self.request, data='sample.iso')
actual_image_dict = actual_image.to_dict()
self.assertEqual(file_upload_url, actual_image_dict['upload_url'])
self.assertEqual(self.request.user.token.id,
actual_image_dict['token_id'])
self.assertEqual(upload_url, actual_image.upload_url)
self.assertEqual(self.request.user.token.id, actual_image.token_id)
@override_settings(OPENSTACK_API_VERSIONS={"image": 1})
def test_image_create_v1_external_upload(self):
self._test_image_create_external_upload(api_version=1)
def test_image_create_v2_external_upload(self):
self._test_image_create_external_upload()

View File

@ -514,6 +514,17 @@ class APITestCase(TestCase):
return self.ceilometerclient
# Need this to test both Glance API V1 and V2 versions
class ResetImageAPIVersionMixin(object):
def setUp(self):
super(ResetImageAPIVersionMixin, self).setUp()
api.glance.VERSIONS.clear_active_cache()
def tearDown(self):
api.glance.VERSIONS.clear_active_cache()
super(ResetImageAPIVersionMixin, self).tearDown()
@unittest.skipUnless(os.environ.get('WITH_SELENIUM', False),
"The WITH_SELENIUM env variable is not set.")
class SeleniumTestCase(horizon_helpers.SeleniumTestCase):

View File

@ -21,7 +21,10 @@ from openstack_dashboard.test.integration_tests.pages.project.compute.\
volumes.volumespage import VolumesPage
DEFAULT_IMAGE_SOURCE = 'url'
# TODO(bpokorny): Set the default source back to 'url' once Glance removes
# the show_multiple_locations option, and if the default devstack policies
# allow setting locations.
DEFAULT_IMAGE_SOURCE = 'file'
DEFAULT_IMAGE_FORMAT = 'qcow2'
DEFAULT_ACCESSIBILITY = False
DEFAULT_PROTECTION = False
@ -34,10 +37,9 @@ class ImagesTable(tables.TableRegion):
name = "images"
CREATE_IMAGE_FORM_FIELDS = (
"name", "description", "source_type", "image_url",
"image_file", "kernel", "ramdisk",
"disk_format", "architecture", "minimum_disk",
"minimum_ram", "is_public", "protected"
"name", "description", "image_file", "kernel", "ramdisk",
"disk_format", "architecture", "minimum_disk", "minimum_ram",
"is_public", "protected"
)
CREATE_VOLUME_FROM_IMAGE_FORM_FIELDS = (
@ -129,7 +131,9 @@ class ImagesPage(basepage.BaseNavigationPage):
create_image_form.name.text = name
if description is not None:
create_image_form.description.text = description
create_image_form.source_type.value = image_source_type
# TODO(bpokorny): Add this back once the show_multiple_locations
# option is removed from Glance
# create_image_form.source_type.value = image_source_type
if image_source_type == 'url':
if location is None:
create_image_form.image_url.text = \

View File

@ -18,6 +18,10 @@ from openstack_dashboard.test.integration_tests.regions import messages
@decorators.config_option_required('image.panel_type', 'legacy',
message="Angular Panels not tested")
class TestImagesLegacy(helpers.TestCase):
def __init__(self, *args, **kwargs):
super(TestImagesLegacy, self).__init__(*args, **kwargs)
self.IMAGE_NAME = helpers.gen_random_resource_name("image")
@property
def images_page(self):
return self.home_pg.go_to_compute_imagespage()
@ -46,16 +50,14 @@ class TestImagesAngular(helpers.TestCase):
class TestImagesBasic(TestImagesLegacy):
"""Login as demo user"""
IMAGE_NAME = helpers.gen_random_resource_name("image")
def image_create(self, local_file=None):
def image_create(self, local_file=None, **kwargs):
images_page = self.images_page
if local_file:
images_page.create_image(self.IMAGE_NAME,
image_source_type='file',
image_file=local_file)
image_file=local_file,
**kwargs)
else:
images_page.create_image(self.IMAGE_NAME)
images_page.create_image(self.IMAGE_NAME, **kwargs)
self.assertTrue(images_page.find_message_and_dismiss(messages.INFO))
self.assertFalse(images_page.find_message_and_dismiss(messages.ERROR))
self.assertTrue(images_page.is_image_present(self.IMAGE_NAME))
@ -69,6 +71,7 @@ class TestImagesBasic(TestImagesLegacy):
self.assertFalse(images_page.find_message_and_dismiss(messages.ERROR))
self.assertFalse(images_page.is_image_present(self.IMAGE_NAME))
@decorators.skip_because(bugs=['1595335'])
def test_image_create_delete(self):
"""tests the image creation and deletion functionalities:
* creates a new image from horizon.conf http_image
@ -160,7 +163,11 @@ class TestImagesBasic(TestImagesLegacy):
'metadata2': helpers.gen_random_resource_name("value")}
with helpers.gen_temporary_file() as file_name:
images_page = self.image_create(local_file=file_name)
# TODO(tsufiev): had to add non-empty description to an image,
# because description is now considered a metadata and we want
# the metadata in a newly created image to be valid
images_page = self.image_create(local_file=file_name,
description='test description')
images_page.add_custom_metadata(self.IMAGE_NAME, new_metadata)
results = images_page.check_image_details(self.IMAGE_NAME,
new_metadata)
@ -254,8 +261,6 @@ class TestImagesBasic(TestImagesLegacy):
class TestImagesAdvanced(TestImagesLegacy):
"""Login as demo user"""
IMAGE_NAME = helpers.gen_random_resource_name("image")
def test_create_volume_from_image(self):
"""This test case checks create volume from image functionality:
Steps:
@ -316,8 +321,6 @@ class TestImagesAdvanced(TestImagesLegacy):
class TestImagesAdmin(helpers.AdminTestCase, TestImagesLegacy):
"""Login as admin user"""
IMAGE_NAME = helpers.gen_random_resource_name("image")
@property
def images_page(self):
return self.home_pg.go_to_system_imagespage()

View File

@ -137,7 +137,8 @@ AVAILABLE_REGIONS = [
]
OPENSTACK_API_VERSIONS = {
"identity": 3
"identity": 3,
"image": 2
}
OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0"

View File

@ -14,6 +14,7 @@
from glanceclient.v1 import images
from openstack_dashboard import api
from openstack_dashboard.test.test_data import utils
@ -31,10 +32,26 @@ class Namespace(dict):
return self.__dict__
class APIResourceV2(dict):
_base_props = [
'id', 'name', 'status', 'visibility', 'protected', 'checksum', 'owner',
'size', 'virtual_size', 'container_format', 'disk_format',
'created_at', 'updated_at', 'tags', 'direct_url', 'min_ram',
'min_disk', 'self', 'file', 'schema', 'locations']
def __getattr__(self, item):
if item == 'schema':
return {'properties': {k: '' for k in self._base_props}}
else:
return self.get(item)
def data(TEST):
TEST.images = utils.TestDataContainer()
TEST.images_api = utils.TestDataContainer()
TEST.snapshots = utils.TestDataContainer()
TEST.metadata_defs = utils.TestDataContainer()
TEST.imagesV2 = utils.TestDataContainer()
# Snapshots
snapshot_dict = {'name': u'snapshot',
@ -62,11 +79,11 @@ def data(TEST):
'is_public': False,
'protected': False}
snapshot = images.Image(images.ImageManager(None), snapshot_dict)
TEST.snapshots.add(snapshot)
TEST.snapshots.add(api.glance.Image(snapshot))
snapshot = images.Image(images.ImageManager(None), snapshot_dict_no_owner)
TEST.snapshots.add(snapshot)
TEST.snapshots.add(api.glance.Image(snapshot))
snapshot = images.Image(images.ImageManager(None), snapshot_dict_queued)
TEST.snapshots.add(snapshot)
TEST.snapshots.add(api.glance.Image(snapshot))
# Images
image_dict = {'id': '007e7d55-fe1e-4c5c-bf08-44b4a4964822',
@ -210,11 +227,96 @@ def data(TEST):
'protected': False}
no_name_image = images.Image(images.ImageManager(None), image_dict)
TEST.images.add(public_image, private_image, protected_image,
public_image2, private_image2, private_image3,
shared_image1, official_image1, multi_prop_image)
TEST.images_api.add(public_image, private_image, protected_image,
public_image2, private_image2, private_image3,
shared_image1, official_image1, multi_prop_image)
TEST.empty_name_image = no_name_image
TEST.images.add(api.glance.Image(public_image),
api.glance.Image(private_image),
api.glance.Image(protected_image),
api.glance.Image(public_image2),
api.glance.Image(private_image2),
api.glance.Image(private_image3),
api.glance.Image(shared_image1),
api.glance.Image(official_image1),
api.glance.Image(multi_prop_image))
TEST.empty_name_image = api.glance.Image(no_name_image)
image_v2_dicts = [{
'checksum': 'eb9139e4942121f22bbc2afc0400b2a4',
'container_format': 'novaImage',
'created_at': '2014-02-14T20:56:53',
'direct_url': 'swift+config://ref1/glance/'
'da8500d5-8b80-4b9c-8410-cc57fb8fb9d5',
'disk_format': u'qcow2',
'file': '/v2/images/'
'da8500d5-8b80-4b9c-8410-cc57fb8fb9d5/file',
'id': '007e7d55-fe1e-4c5c-bf08-44b4a4964822',
'kernel_id': 'f6ebd5f0-b110-4406-8c1e-67b28d4e85e7',
'locations': [
{'metadata': {},
'url': 'swift+config://ref1/glance/'
'da8500d5-8b80-4b9c-8410-cc57fb8fb9d5'}],
'min_ram': 0,
'name': 'public_image',
'image_type': u'image',
'min_disk': 0,
'owner': TEST.tenant.id,
'protected': False,
'ramdisk_id': '868efefc-4f2d-4ed8-82b1-7e35576a7a47',
'size': 20 * 1024 ** 3,
'status': 'active',
'tags': ['active_image'],
'updated_at': '2015-08-31T19:37:45Z',
'virtual_size': None,
'visibility': 'public'
}, {
'checksum': None,
'container_format': 'novaImage',
'created_at': '2014-03-16T06:22:14',
'disk_format': None,
'image_type': u'image',
'file': '/v2/images/885d1cb0-9f5c-4677-9d03-175be7f9f984/file',
'id': 'd6936c86-7fec-474a-85c5-5e467b371c3c',
'locations': [],
'min_disk': 30,
'min_ram': 0,
'name': 'protected_images',
'owner': TEST.tenant.id,
'protected': True,
'size': 2 * 1024 ** 3,
'status': "active",
'tags': ['empty_image'],
'updated_at': '2015-09-01T22:37:32Z',
'virtual_size': None,
'visibility': 'public'
}, {
'checksum': 'e533283e6aac072533d1d091a7d2e413',
'container_format': 'novaImage',
'created_at': '2015-09-02T00:31:16Z',
'disk_format': 'qcow2',
'file': '/v2/images/10ca6b6b-48f4-43ac-8159-aa9e9353f5e4/file',
'id': 'a67e7d45-fe1e-4c5c-bf08-44b4a4964822',
'image_type': 'an image type',
'min_disk': 0,
'min_ram': 0,
'name': 'multi_prop_image',
'owner': TEST.tenant.id,
'protected': False,
'size': 20 * 1024 ** 3,
'status': 'active',
'tags': ['custom_property_image'],
'updated_at': '2015-09-02T00:31:17Z',
'virtual_size': None,
'visibility': 'public',
'description': u'a multi prop image',
'foo': u'foo val',
'bar': u'bar val'
}]
for fixture in image_v2_dicts:
apiresource = APIResourceV2(fixture)
TEST.imagesV2.add(api.glance.Image(apiresource))
metadef_dict = {
'namespace': 'namespace_1',

View File

@ -0,0 +1,16 @@
---
features:
- Adds complete support for Glance v2 so that Horizon no longer depends on
having a Glance v1 endpoint in the Keystone catalog. Also provides
code compatibility between Glance v1 and v2.
- Adds a new config value called IMAGES_ALLOW_LOCATION, which allows users
to set locations when creating or updating images. Depending on the Glance
version, the ability to set locations is controlled by policies and/or
configuration values.
issues:
- If you set 'images_panel' to False for the ANGULAR_FEATURES option (which
is not the default) and configure Horizon to use Glance v2, Ramdisk ID and
Kernel ID don't show properly on the "Edit Image" screen.
other:
- Glance v2 doesn't support the copy-from feature, so this feature is
disabled in Horizon when using Glance v2.