Merge "Support for Glance v2"
This commit is contained in:
commit
5d5bc86c97
@ -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``
|
||||
------------------------------
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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])
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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 %}
|
||||
|
@ -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 '%s' 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)
|
||||
|
@ -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',
|
||||
|
@ -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'.
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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]);
|
||||
|
@ -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">
|
||||
|
@ -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': {}})
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
@ -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 = \
|
||||
|
@ -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()
|
||||
|
@ -137,7 +137,8 @@ AVAILABLE_REGIONS = [
|
||||
]
|
||||
|
||||
OPENSTACK_API_VERSIONS = {
|
||||
"identity": 3
|
||||
"identity": 3,
|
||||
"image": 2
|
||||
}
|
||||
|
||||
OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0"
|
||||
|
@ -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',
|
||||
|
16
releasenotes/notes/glance-v2-ba86ba34611f95ce.yaml
Normal file
16
releasenotes/notes/glance-v2-ba86ba34611f95ce.yaml
Normal 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.
|
Loading…
Reference in New Issue
Block a user