summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2016-09-16 02:13:50 +0000
committerGerrit Code Review <review@openstack.org>2016-09-16 02:13:50 +0000
commit5d5bc86c97b728d5763ea503811c3ebb25516497 (patch)
tree15c38719fcf13ebd9651efe4167124316ef30136
parentc8ba33d38e0b331f15e918088d27c84432ece5f9 (diff)
parentcf0aac9400ec05473c2d19b524e9833e1b75ea9e (diff)
downloadhorizon-5d5bc86c97b728d5763ea503811c3ebb25516497.tar.gz
Merge "Support for Glance v2"
-rwxr-xr-xdoc/source/topics/settings.rst16
-rw-r--r--openstack_dashboard/api/glance.py222
-rw-r--r--openstack_dashboard/api/rest/config.py5
-rw-r--r--openstack_dashboard/api/rest/glance.py36
-rw-r--r--openstack_dashboard/dashboards/project/images/images/forms.py60
-rw-r--r--openstack_dashboard/dashboards/project/images/images/tables.py15
-rw-r--r--openstack_dashboard/dashboards/project/images/images/tests.py208
-rw-r--r--openstack_dashboard/dashboards/project/images/images/views.py4
-rw-r--r--openstack_dashboard/dashboards/project/images/templates/images/images/_create.html16
-rw-r--r--openstack_dashboard/dashboards/project/instances/tests.py163
-rw-r--r--openstack_dashboard/dashboards/project/volumes/volumes/tests.py9
-rw-r--r--openstack_dashboard/local/local_settings.py.example6
-rw-r--r--openstack_dashboard/static/app/core/images/details/overview.controller.js6
-rw-r--r--openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.js20
-rw-r--r--openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.spec.js17
-rw-r--r--openstack_dashboard/static/app/core/images/steps/create-image/create-image.html16
-rw-r--r--openstack_dashboard/test/api_tests/glance_rest_tests.py250
-rw-r--r--openstack_dashboard/test/api_tests/glance_tests.py119
-rw-r--r--openstack_dashboard/test/helpers.py11
-rw-r--r--openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py16
-rw-r--r--openstack_dashboard/test/integration_tests/tests/test_images.py25
-rw-r--r--openstack_dashboard/test/settings.py3
-rw-r--r--openstack_dashboard/test/test_data/glance_data.py116
-rw-r--r--releasenotes/notes/glance-v2-ba86ba34611f95ce.yaml16
24 files changed, 1133 insertions, 242 deletions
diff --git a/doc/source/topics/settings.rst b/doc/source/topics/settings.rst
index 1c2c38094..9098bf971 100755
--- a/doc/source/topics/settings.rst
+++ b/doc/source/topics/settings.rst
@@ -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``
------------------------------
diff --git a/openstack_dashboard/api/glance.py b/openstack_dashboard/api/glance.py
index 494725d0b..6627124b3 100644
--- a/openstack_dashboard/api/glance.py
+++ b/openstack_dashboard/api/glance.py
@@ -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):
diff --git a/openstack_dashboard/api/rest/config.py b/openstack_dashboard/api/rest/config.py
index f655add70..27bcf8875 100644
--- a/openstack_dashboard/api/rest/config.py
+++ b/openstack_dashboard/api/rest/config.py
@@ -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()
diff --git a/openstack_dashboard/api/rest/glance.py b/openstack_dashboard/api/rest/glance.py
index 935aab523..830b12be0 100644
--- a/openstack_dashboard/api/rest/glance.py
+++ b/openstack_dashboard/api/rest/glance.py
@@ -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])
diff --git a/openstack_dashboard/dashboards/project/images/images/forms.py b/openstack_dashboard/dashboards/project/images/images/forms.py
index 0105a116f..1c3a44999 100644
--- a/openstack_dashboard/dashboards/project/images/images/forms.py
+++ b/openstack_dashboard/dashboards/project/images/images/forms.py
@@ -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': {}}
-
- if 'description' in data:
- meta['properties']['description'] = data['description']
+ 'name': data['name']}
+
+ 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)
diff --git a/openstack_dashboard/dashboards/project/images/images/tables.py b/openstack_dashboard/dashboards/project/images/images/tables.py
index df18973fd..e5e10b4fd 100644
--- a/openstack_dashboard/dashboards/project/images/images/tables.py
+++ b/openstack_dashboard/dashboards/project/images/images/tables.py
@@ -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
diff --git a/openstack_dashboard/dashboards/project/images/images/tests.py b/openstack_dashboard/dashboards/project/images/images/tests.py
index ef1f336d5..df0e21699 100644
--- a/openstack_dashboard/dashboards/project/images/images/tests.py
+++ b/openstack_dashboard/dashboards/project/images/images/tests.py
@@ -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,10 @@ 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])
@@ -167,8 +191,47 @@ class UpdateImageFormTests(test.TestCase):
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])
+ res = self.client.post(url, data)
+ self.assertNoFormErrors(res)
+ 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,34 @@ 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_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()
+ 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)
+
@test.create_stubs({api.glance: ('image_create',)})
- def test_image_create_post_upload(self):
+ def test_image_create_post_upload_v2(self):
temp_file = tempfile.NamedTemporaryFile()
temp_file.write(b'123')
temp_file.flush()
@@ -223,8 +314,26 @@ class ImageViewTests(test.TestCase):
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(self):
+ 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()
diff --git a/openstack_dashboard/dashboards/project/images/images/views.py b/openstack_dashboard/dashboards/project/images/images/views.py
index 853a6ab7d..1fafc3871 100644
--- a/openstack_dashboard/dashboards/project/images/images/views.py
+++ b/openstack_dashboard/dashboards/project/images/images/views.py
@@ -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
diff --git a/openstack_dashboard/dashboards/project/images/templates/images/images/_create.html b/openstack_dashboard/dashboards/project/images/templates/images/images/_create.html
index e3c471a93..d9c425404 100644
--- a/openstack_dashboard/dashboards/project/images/templates/images/images/_create.html
+++ b/openstack_dashboard/dashboards/project/images/templates/images/images/_create.html
@@ -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 %}
diff --git a/openstack_dashboard/dashboards/project/instances/tests.py b/openstack_dashboard/dashboards/project/instances/tests.py
index 6283f9059..8794499a5 100644
--- a/openstack_dashboard/dashboards/project/instances/tests.py
+++ b/openstack_dashboard/dashboards/project/instances/tests.py
@@ -28,6 +28,7 @@ from django.core.urlresolvers import reverse
from django.forms import widgets
from django import http
import django.test
+from django.test.utils import override_settings
from django.utils.http import urlencode
from mox3.mox import IgnoreArg # noqa
from mox3.mox import IsA # noqa
@@ -54,7 +55,14 @@ VOLUME_SEARCH_OPTS = dict(status=AVAILABLE, bootable=True)
SNAPSHOT_SEARCH_OPTS = dict(status=AVAILABLE)
-class InstanceTests(helpers.TestCase):
+class InstanceTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
+ def setUp(self):
+ super(InstanceTests, self).setUp()
+ if api.glance.VERSIONS.active < 2:
+ self.versioned_images = self.images
+ else:
+ self.versioned_images = self.imagesV2
+
@helpers.create_stubs({
api.nova: (
'flavor_list',
@@ -1511,7 +1519,7 @@ class InstanceTests(helpers.TestCase):
config_drive=True,
config_drive_default=False,
test_with_profile=False):
- image = self.images.first()
+ image = self.versioned_images.first()
api.nova.extension_supported('BlockDeviceMappingV2Boot',
IsA(http.HttpRequest)) \
@@ -1525,7 +1533,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -1674,6 +1682,10 @@ class InstanceTests(helpers.TestCase):
self.assertEqual(step.action.initial['config_drive'],
config_drive_default)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_instance_get_glance_v1(self):
+ self.test_launch_instance_get()
+
@django.test.utils.override_settings(
OPENSTACK_HYPERVISOR_FEATURES={'can_set_password': False})
def test_launch_instance_get_without_password(self):
@@ -1777,7 +1789,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -1853,6 +1865,10 @@ class InstanceTests(helpers.TestCase):
for volume in bootable_volumes:
self.assertIn(volume, volume_sources_ids)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_instance_get_bootable_volumes_glance_v1(self):
+ self.test_launch_instance_get_bootable_volumes()
+
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
def test_launch_instance_get_bootable_volumes_with_profile(self):
@@ -1879,7 +1895,7 @@ class InstanceTests(helpers.TestCase):
test_with_profile=False,
test_with_multi_nics=False):
flavor = self.flavors.first()
- image = self.images.first()
+ image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
sec_group = self.security_groups.first()
@@ -1903,7 +1919,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -2024,6 +2040,10 @@ class InstanceTests(helpers.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_instance_post_glance_v1(self):
+ self.test_launch_instance_post()
+
def test_launch_instance_post_no_disk_config_supported(self):
self.test_launch_instance_post(disk_config=False)
@@ -2046,7 +2066,7 @@ class InstanceTests(helpers.TestCase):
test_with_multi_nics=False,
):
flavor = self.flavors.first()
- image = self.images.first()
+ image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
sec_group = self.security_groups.first()
@@ -2068,7 +2088,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(IsA(http.HttpRequest),
filters={'is_public': True,
'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -2181,6 +2201,10 @@ class InstanceTests(helpers.TestCase):
def test_launch_instance_post_with_profile_and_port_error(self):
self._test_launch_instance_post_with_profile_and_port_error()
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_instance_post_with_profile_and_port_error_glance_v1(self):
+ self.test_launch_instance_post_with_profile_and_port_error()
+
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
@helpers.create_stubs({api.glance: ('image_list_detailed',),
@@ -2263,7 +2287,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -2367,6 +2391,10 @@ class InstanceTests(helpers.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_instance_post_boot_from_volume_glance_v1(self):
+ self.test_launch_instance_post_boot_from_volume()
+
def test_launch_instance_post_boot_from_volume_with_bdmv2(self):
self.test_launch_instance_post_boot_from_volume(test_with_bdmv2=True)
@@ -2422,7 +2450,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -2527,6 +2555,10 @@ class InstanceTests(helpers.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_lnch_inst_post_no_images_avail_boot_from_volume_glance_v1(self):
+ self.test_launch_instance_post_no_images_available_boot_from_volume()
+
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
def test_lnch_inst_post_no_images_avail_boot_from_vol_with_profile(self):
@@ -2712,7 +2744,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -2817,6 +2849,10 @@ class InstanceTests(helpers.TestCase):
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_instance_post_boot_from_snapshot_glance_v1(self):
+ self.test_launch_instance_post_boot_from_snapshot()
+
def test_launch_instance_post_boot_from_snapshot_with_bdmv2(self):
self.test_launch_instance_post_boot_from_snapshot(test_with_bdmv2=True)
@@ -2945,7 +2981,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -3000,6 +3036,10 @@ class InstanceTests(helpers.TestCase):
self.assertTemplateUsed(res, views.WorkflowView.template_name)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_flavorlist_error_glance_v1(self):
+ self.test_launch_flavorlist_error()
+
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
def test_launch_flavorlist_error_with_profile(self):
@@ -3024,7 +3064,7 @@ class InstanceTests(helpers.TestCase):
def test_launch_form_keystone_exception(self,
test_with_profile=False):
flavor = self.flavors.first()
- image = self.images.first()
+ image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
sec_group = self.security_groups.first()
@@ -3055,7 +3095,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -3151,6 +3191,10 @@ class InstanceTests(helpers.TestCase):
self.assertRedirectsNoFollow(res, INDEX_URL)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_form_keystone_exception_with_profile_glance_v1(self):
+ self.test_launch_form_keystone_exception()
+
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
def test_launch_form_keystone_exception_with_profile(self):
@@ -3172,7 +3216,7 @@ class InstanceTests(helpers.TestCase):
def test_launch_form_instance_count_error(self,
test_with_profile=False):
flavor = self.flavors.first()
- image = self.images.first()
+ image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
volume = self.volumes.first()
@@ -3197,7 +3241,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -3273,6 +3317,10 @@ class InstanceTests(helpers.TestCase):
self.assertContains(res, "greater than or equal to 1")
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_form_instance_count_error_glance_v1(self):
+ self.test_launch_form_instance_count_error()
+
@helpers.create_stubs({api.glance: ('image_list_detailed',),
api.neutron: ('network_list',
'profile_list',
@@ -3290,7 +3338,7 @@ class InstanceTests(helpers.TestCase):
def _test_launch_form_count_error(self, resource,
avail, test_with_profile=False):
flavor = self.flavors.first()
- image = self.images.first()
+ image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
volume = self.volumes.first()
@@ -3320,7 +3368,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -3407,7 +3455,11 @@ class InstanceTests(helpers.TestCase):
"512, Requested: 1024)" % {'avail': avail})
self.assertContains(res, msg)
- def test_launch_form_cores_count_error(self):
+ def test_launch_form_cores_count_error_glance_v2(self):
+ self._test_launch_form_count_error('cores', 1, test_with_profile=False)
+
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_form_cores_count_error_glance_v1(self):
self._test_launch_form_count_error('cores', 1, test_with_profile=False)
def test_launch_form_ram_count_error(self):
@@ -3461,7 +3513,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -3547,18 +3599,22 @@ class InstanceTests(helpers.TestCase):
test_with_profile=False,
):
flavor = self.flavors.first()
- image = self.images.first()
+ image = self.versioned_images.first()
image.min_ram = flavor.ram
image.min_disk = flavor.disk + 1
self._test_launch_form_instance_requirement_error(image, flavor,
test_with_profile)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_form_instance_requirement_error_disk_glance_v1(self):
+ self.test_launch_form_instance_requirement_error_disk()
+
def test_launch_form_instance_requirement_error_ram(
self,
test_with_profile=False,
):
flavor = self.flavors.first()
- image = self.images.first()
+ image = self.versioned_images.first()
image.min_ram = flavor.ram + 1
image.min_disk = flavor.disk
self._test_launch_form_instance_requirement_error(image, flavor,
@@ -3593,7 +3649,7 @@ class InstanceTests(helpers.TestCase):
widget_class,
widget_attrs):
flavor = self.flavors.first()
- image = self.images.first()
+ image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
volume = self.volumes.first()
@@ -3617,7 +3673,7 @@ class InstanceTests(helpers.TestCase):
IsA(http.HttpRequest),
filters={'is_public': True,
'status': 'active'}).AndReturn(
- [self.images.list(), False, False])
+ [self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -3692,8 +3748,8 @@ class InstanceTests(helpers.TestCase):
for widget_part in widget_content.split():
self.assertContains(res, widget_part)
- @django.test.utils.override_settings(
- OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': True})
+ @override_settings(
+ OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': True},)
def test_launch_form_instance_device_name_showed(self):
self._test_launch_form_instance_show_device_name(
u'vda', widgets.TextInput, {
@@ -3701,6 +3757,17 @@ class InstanceTests(helpers.TestCase):
'attrs': {'id': 'id_device_name'}}
)
+ @override_settings(
+ OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': True},
+ OPENSTACK_API_VERSIONS={'image': 1}
+ )
+ def test_launch_form_instance_device_name_showed_glance_v1(self):
+ self._test_launch_form_instance_show_device_name(
+ u'vda', widgets.TextInput, {
+ 'name': 'device_name', 'value': 'vda',
+ 'attrs': {'id': 'id_device_name'}}
+ )
+
@django.test.utils.override_settings(
OPENSTACK_HYPERVISOR_FEATURES={'can_set_mount_point': False})
def test_launch_form_instance_device_name_hidden(self):
@@ -3754,7 +3821,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -3834,22 +3901,26 @@ class InstanceTests(helpers.TestCase):
def test_launch_form_instance_volume_size_error(self,
test_with_profile=False):
- image = self.images.get(name='protected_images')
+ image = self.versioned_images.get(name='protected_images')
volume_size = image.min_disk // 2
msg = ("The Volume size is too small for the &#39;%s&#39; image" %
image.name)
self._test_launch_form_instance_volume_size(image, volume_size, msg,
test_with_profile)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_form_instance_volume_size_error_glance_v1(self):
+ self.test_launch_form_instance_volume_size_error()
+
def test_launch_form_instance_non_int_volume_size(self,
test_with_profile=False):
- image = self.images.get(name='protected_images')
+ image = self.versioned_images.get(name='protected_images')
msg = "Enter a whole number."
self._test_launch_form_instance_volume_size(image, 1.5, msg,
test_with_profile)
def test_launch_form_instance_volume_exceed_quota(self):
- image = self.images.get(name='protected_images')
+ image = self.versioned_images.get(name='protected_images')
msg = "Requested volume exceeds quota: Available: 0, Requested: 1"
self._test_launch_form_instance_volume_size(image, image.min_disk,
msg, False, 0)
@@ -3975,7 +4046,7 @@ class InstanceTests(helpers.TestCase):
quotas: ('tenant_quota_usages',)})
def test_launch_with_empty_device_name_allowed(self):
flavor = self.flavors.get(name='m1.massive')
- image = self.images.first()
+ image = self.versioned_images.first()
keypair = self.keypairs.first()
server = self.servers.first()
sec_group = self.security_groups.first()
@@ -4007,7 +4078,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -4094,6 +4165,10 @@ class InstanceTests(helpers.TestCase):
res = self.client.post(url, form_data)
self.assertNoFormErrors(res)
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_launch_with_empty_device_name_allowed_glance_v1(self):
+ self.test_launch_with_empty_device_name_allowed()
+
@helpers.create_stubs({
api.nova: ('flavor_list', 'server_list', 'tenant_absolute_limits',
'extension_supported',),
@@ -4157,7 +4232,7 @@ class InstanceTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([self.versioned_images.list(), False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -4218,6 +4293,10 @@ class InstanceTests(helpers.TestCase):
html=True,
msg_prefix="The default key pair was not selected.")
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_select_default_keypair_if_only_one_glance_v1(self):
+ self.test_select_default_keypair_if_only_one()
+
@helpers.update_settings(
OPENSTACK_NEUTRON_NETWORK={'profile_support': 'cisco'})
def test_select_default_keypair_if_only_one_with_profile(self):
@@ -4945,7 +5024,7 @@ class InstanceAjaxTests(helpers.TestCase):
self.assertContains(res, "Not available")
-class ConsoleManagerTests(helpers.TestCase):
+class ConsoleManagerTests(helpers.ResetImageAPIVersionMixin, helpers.TestCase):
def setup_consoles(self):
# Need to refresh with mocks or will fail since mox do not detect
@@ -5266,9 +5345,8 @@ class ConsoleManagerTests(helpers.TestCase):
cinder: ('volume_list',
'volume_snapshot_list',),
quotas: ('tenant_quota_usages',)})
- def test_port_cleanup_called_on_failed_vm_launch(self):
+ def _test_port_cleanup_called_on_failed_vm_launch(self, image, images):
flavor = self.flavors.first()
- image = self.images.first()
keypair = self.keypairs.first()
server = self.servers.first()
sec_group = self.security_groups.first()
@@ -5298,7 +5376,7 @@ class ConsoleManagerTests(helpers.TestCase):
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'is_public': True, 'status': 'active'}) \
- .AndReturn([self.images.list(), False, False])
+ .AndReturn([images, False, False])
api.glance.image_list_detailed(
IsA(http.HttpRequest),
filters={'property-owner_id': self.tenant.id,
@@ -5390,3 +5468,14 @@ class ConsoleManagerTests(helpers.TestCase):
res = self.client.post(url, form_data)
self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ @override_settings(OPENSTACK_API_VERSIONS={'image': 1})
+ def test_port_cleanup_called_on_failed_vm_launch_v1(self):
+ image = self.images.first()
+ images = self.images.list()
+ self._test_port_cleanup_called_on_failed_vm_launch(image, images)
+
+ def test_port_cleanup_called_on_failed_vm_launch_v2(self):
+ image = self.imagesV2.first()
+ images = self.imagesV2.list()
+ self._test_port_cleanup_called_on_failed_vm_launch(image, images)
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
index 778e88ba0..41b2ac060 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
@@ -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',
diff --git a/openstack_dashboard/local/local_settings.py.example b/openstack_dashboard/local/local_settings.py.example
index 9d0e29f67..8044ddbfd 100644
--- a/openstack_dashboard/local/local_settings.py.example
+++ b/openstack_dashboard/local/local_settings.py.example
@@ -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'.
diff --git a/openstack_dashboard/static/app/core/images/details/overview.controller.js b/openstack_dashboard/static/app/core/images/details/overview.controller.js
index 73691f033..810a8a171 100644
--- a/openstack_dashboard/static/app/core/images/details/overview.controller.js
+++ b/openstack_dashboard/static/app/core/images/details/overview.controller.js
@@ -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);
diff --git a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.js b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.js
index 7a0e0f328..bcf73e52f 100644
--- a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.js
+++ b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.js
@@ -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;
}
diff --git a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.spec.js b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.spec.js
index 49419ce3e..aec0706f8 100644
--- a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.spec.js
+++ b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.controller.spec.js
@@ -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]);
diff --git a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html
index dc18aabf5..b150844bf 100644
--- a/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html
+++ b/openstack_dashboard/static/app/core/images/steps/create-image/create-image.html
@@ -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">
diff --git a/openstack_dashboard/test/api_tests/glance_rest_tests.py b/openstack_dashboard/test/api_tests/glance_rest_tests.py
index 58ae091c0..41410b141 100644
--- a/openstack_dashboard/test/api_tests/glance_rest_tests.py
+++ b/openstack_dashboard/test/api_tests/glance_rest_tests.py
@@ -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,60 @@ 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",
+ "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 = 1
+
+ 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_v2_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 +405,7 @@ class ImagesRestTestCase(test.TestCase):
"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)
@@ -250,12 +413,13 @@ 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_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'
@@ -303,6 +494,35 @@ class ImagesRestTestCase(test.TestCase):
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': {}})
gc.metadefs_namespace_full_list.return_value = (
diff --git a/openstack_dashboard/test/api_tests/glance_tests.py b/openstack_dashboard/test/api_tests/glance_tests.py
index 6d0feef46..f86b662fa 100644
--- a/openstack_dashboard/test/api_tests/glance_tests.py
+++ b/openstack_dashboard/test/api_tests/glance_tests.py
@@ -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'
-
- 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)
+ @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()
+
+ 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()
diff --git a/openstack_dashboard/test/helpers.py b/openstack_dashboard/test/helpers.py
index 42d3158ed..58ff91788 100644
--- a/openstack_dashboard/test/helpers.py
+++ b/openstack_dashboard/test/helpers.py
@@ -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):
diff --git a/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py b/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py
index 2bb7b8384..88448ba71 100644
--- a/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py
+++ b/openstack_dashboard/test/integration_tests/pages/project/compute/imagespage.py
@@ -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 = \
diff --git a/openstack_dashboard/test/integration_tests/tests/test_images.py b/openstack_dashboard/test/integration_tests/tests/test_images.py
index 9e40ca25f..c165eab72 100644
--- a/openstack_dashboard/test/integration_tests/tests/test_images.py
+++ b/openstack_dashboard/test/integration_tests/tests/test_images.py
@@ -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()
diff --git a/openstack_dashboard/test/settings.py b/openstack_dashboard/test/settings.py
index 6d45615e6..2622781b5 100644
--- a/openstack_dashboard/test/settings.py
+++ b/openstack_dashboard/test/settings.py
@@ -137,7 +137,8 @@ AVAILABLE_REGIONS = [
]
OPENSTACK_API_VERSIONS = {
- "identity": 3
+ "identity": 3,
+ "image": 2
}
OPENSTACK_KEYSTONE_URL = "http://localhost:5000/v2.0"
diff --git a/openstack_dashboard/test/test_data/glance_data.py b/openstack_dashboard/test/test_data/glance_data.py
index 2e3277ccd..fa9c13fca 100644
--- a/openstack_dashboard/test/test_data/glance_data.py
+++ b/openstack_dashboard/test/test_data/glance_data.py
@@ -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.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)
- TEST.empty_name_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',
diff --git a/releasenotes/notes/glance-v2-ba86ba34611f95ce.yaml b/releasenotes/notes/glance-v2-ba86ba34611f95ce.yaml
new file mode 100644
index 000000000..4777a12fc
--- /dev/null
+++ b/releasenotes/notes/glance-v2-ba86ba34611f95ce.yaml
@@ -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.