Embed support for external data sinks into api.glance
In case 'data' image attribute is a base string (instead of in-memory or on-disk file), api.glance sends back an image wrapper with a redirect url and a token to its caller, so the caller could upload the file to that url directly. Provide a unit test for api.glance behavior when an external upload location is used. That also requires to fix glance stub endpoint data in keystone_data.py since it didn't reflect the reality. Also document the new HORIZON_IMAGES_UPLOAD_MODE setting that will govern direct images upload and the define approach to deprecating the old HORIZON_IMAGES_ALLOW_UPLOAD setting. The old setting is deprecated as of Newton release and planned to be removed in P. 'Removing' means that it will no longer be used / referenced at all in code, not the actual presence in settings.py (it is removed from settings.py in this commit). What really matters is if the customized value of HORIZON_IMAGES_ALLOW_UPLOAD in local_settings.py will be still considered during the deprecation period. Help text in Django Create Image form in case if local file upload was enabled was wrong, fixed that. Related-Bug: #1403129 Partially implements blueprint: horizon-glance-large-image-upload Change-Id: I24ff55e0135514fae89c20175cf9c764e871969b
This commit is contained in:
@@ -866,23 +866,66 @@ appear on image detail pages.
|
|||||||
|
|
||||||
|
|
||||||
``HORIZON_IMAGES_ALLOW_UPLOAD``
|
``HORIZON_IMAGES_ALLOW_UPLOAD``
|
||||||
--------------------------------
|
-------------------------------
|
||||||
|
|
||||||
.. versionadded:: 2013.1(Grizzly)
|
.. versionadded:: 2013.1(Grizzly)
|
||||||
|
|
||||||
Default: ``True``
|
Default: ``True``
|
||||||
|
|
||||||
|
(Deprecated)
|
||||||
|
|
||||||
If set to ``False``, this setting disables *local* uploads to prevent filling
|
If set to ``False``, this setting disables *local* uploads to prevent filling
|
||||||
up the disk on the dashboard server since uploads to the Glance image store
|
up the disk on the dashboard server since uploads to the Glance image store
|
||||||
service tend to be particularly large - in the order of hundreds of megabytes
|
service tend to be particularly large - in the order of hundreds of megabytes
|
||||||
to multiple gigabytes.
|
to multiple gigabytes.
|
||||||
|
|
||||||
|
The setting is marked as deprecated and will be removed in P or later release.
|
||||||
|
It is superseded by the setting HORIZON_IMAGES_UPLOAD_MODE. Until the removal
|
||||||
|
the ``False`` value of HORIZON_IMAGES_ALLOW_UPLOAD overrides the value of
|
||||||
|
HORIZON_IMAGES_UPLOAD_MODE.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
This will not disable image creation altogether, as this setting does not
|
This will not disable image creation altogether, as this setting does not
|
||||||
affect images created by specifying an image location (URL) as the image source.
|
affect images created by specifying an image location (URL) as the image source.
|
||||||
|
|
||||||
|
|
||||||
|
``HORIZON_IMAGES_UPLOAD_MODE``
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
.. versionadded:: 10.0.0(Newton)
|
||||||
|
|
||||||
|
Default: ``"legacy"``
|
||||||
|
|
||||||
|
Valid values are ``"direct"``, ``"legacy"`` (default) and ``"off"``. ``"off"``
|
||||||
|
disables the ability to upload images via Horizon. It is equivalent to setting
|
||||||
|
``False`` on the deprecated setting ``HORIZON_IMAGES_ALLOW_UPLOAD``. ``legacy``
|
||||||
|
enables local file upload by piping the image file through the Horizon's
|
||||||
|
web-server. It is equivalent to setting ``True`` on the deprecated setting
|
||||||
|
``HORIZON_IMAGES_ALLOW_UPLOAD``. ``direct`` sends the image file directly from
|
||||||
|
the web browser to Glance. This bypasses Horizon web-server which both reduces
|
||||||
|
network hops and prevents filling up Horizon web-server's filesystem. ``direct``
|
||||||
|
is the preferred mode, but due to the following requirements it is not the default.
|
||||||
|
The ``direct`` setting requires a modern web browser, network access from the
|
||||||
|
browser to the public Glance endpoint, and CORS support to be enabled on the
|
||||||
|
Glance API service. Without CORS support, the browser will forbid the PUT request
|
||||||
|
to a location different than the Horizon server. To enable CORS support for Glance
|
||||||
|
API service, you will need to edit [cors] section of glance-api.conf file (see
|
||||||
|
`here`_ how to do it). Set `allowed_origin` to the full hostname of Horizon
|
||||||
|
web-server (e.g. http://<HOST_IP>/dashboard) and restart glance-api process.
|
||||||
|
|
||||||
|
.. _here: http://docs.openstack.org/developer/oslo.middleware/cors.html#configuration-for-oslo-config
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
To maintain the compatibility with the deprecated HORIZON_IMAGES_ALLOW_UPLOAD
|
||||||
|
setting, neither ``"direct"``, nor ``"legacy"`` modes will have an effect if
|
||||||
|
HORIZON_IMAGES_ALLOW_UPLOAD is set to ``False`` - as if HORIZON_IMAGES_UPLOAD_MODE
|
||||||
|
was set to ``"off"`` itself. When HORIZON_IMAGES_ALLOW_UPLOAD is set to ``True``,
|
||||||
|
all three modes are considered, as if HORIZON_IMAGES_ALLOW_UPLOAD setting
|
||||||
|
was removed.
|
||||||
|
|
||||||
|
|
||||||
``OPENSTACK_KEYSTONE_BACKEND``
|
``OPENSTACK_KEYSTONE_BACKEND``
|
||||||
------------------------------
|
------------------------------
|
||||||
|
|
||||||
|
@@ -24,14 +24,13 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||||
from django.core.files.uploadedfile import TemporaryUploadedFile
|
from django.core.files.uploadedfile import TemporaryUploadedFile
|
||||||
|
|
||||||
|
|
||||||
import glanceclient as glance_client
|
import glanceclient as glance_client
|
||||||
|
import six
|
||||||
from six.moves import _thread as thread
|
from six.moves import _thread as thread
|
||||||
|
|
||||||
from horizon.utils import functions as utils
|
from horizon.utils import functions as utils
|
||||||
@@ -205,6 +204,36 @@ def image_update(request, image_id, **kwargs):
|
|||||||
LOG.warning(msg)
|
LOG.warning(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_upload_mode():
|
||||||
|
if getattr(settings, 'HORIZON_IMAGES_ALLOW_UPLOAD', None) is False:
|
||||||
|
return 'off'
|
||||||
|
mode = getattr(settings, 'HORIZON_IMAGES_UPLOAD_MODE', 'legacy')
|
||||||
|
if mode not in ('off', 'legacy', 'direct'):
|
||||||
|
LOG.warning('HORIZON_IMAGES_UPLOAD_MODE has an unrecognized value of '
|
||||||
|
'"%s", reverting to default "legacy" value' % mode)
|
||||||
|
mode = 'legacy'
|
||||||
|
return mode
|
||||||
|
|
||||||
|
|
||||||
|
class ExternallyUploadedImage(base.APIResourceWrapper):
|
||||||
|
def __init__(self, apiresource, request):
|
||||||
|
self._attrs = apiresource._info.keys()
|
||||||
|
super(ExternallyUploadedImage, self).__init__(apiresource=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)
|
||||||
|
self._token_id = request.user.token.id
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
base_dict = super(ExternallyUploadedImage, self).to_dict()
|
||||||
|
base_dict.update({
|
||||||
|
'upload_url': self._url,
|
||||||
|
'token_id': self._token_id
|
||||||
|
})
|
||||||
|
return base_dict
|
||||||
|
|
||||||
|
|
||||||
def image_create(request, **kwargs):
|
def image_create(request, **kwargs):
|
||||||
"""Create image.
|
"""Create image.
|
||||||
|
|
||||||
@@ -226,10 +255,14 @@ def image_create(request, **kwargs):
|
|||||||
image = glanceclient(request).images.create(**kwargs)
|
image = glanceclient(request).images.create(**kwargs)
|
||||||
|
|
||||||
if data:
|
if data:
|
||||||
if isinstance(data, TemporaryUploadedFile):
|
if isinstance(data, six.string_types):
|
||||||
|
# The image data is meant to be uploaded externally, return a
|
||||||
|
# special wrapper to bypass the web server in a subsequent upload
|
||||||
|
return ExternallyUploadedImage(image, request)
|
||||||
|
elif isinstance(data, TemporaryUploadedFile):
|
||||||
# Hack to fool Django, so we can keep file open in the new thread.
|
# Hack to fool Django, so we can keep file open in the new thread.
|
||||||
data.file.close_called = True
|
data.file.close_called = True
|
||||||
if isinstance(data, InMemoryUploadedFile):
|
elif isinstance(data, InMemoryUploadedFile):
|
||||||
# Clone a new file for InMemeoryUploadedFile.
|
# Clone a new file for InMemeoryUploadedFile.
|
||||||
# Because the old one will be closed by Django.
|
# Because the old one will be closed by Django.
|
||||||
data = SimpleUploadedFile(data.name,
|
data = SimpleUploadedFile(data.name,
|
||||||
|
@@ -180,7 +180,7 @@ class CreateImageForm(forms.SelfHandlingForm):
|
|||||||
def __init__(self, request, *args, **kwargs):
|
def __init__(self, request, *args, **kwargs):
|
||||||
super(CreateImageForm, self).__init__(request, *args, **kwargs)
|
super(CreateImageForm, self).__init__(request, *args, **kwargs)
|
||||||
|
|
||||||
if (not settings.HORIZON_IMAGES_ALLOW_UPLOAD or
|
if (api.glance.get_image_upload_mode() == 'off' or
|
||||||
not policy.check((("image", "upload_image"),), request)):
|
not policy.check((("image", "upload_image"),), request)):
|
||||||
self._hide_file_source_type()
|
self._hide_file_source_type()
|
||||||
if not policy.check((("image", "set_image_location"),), request):
|
if not policy.check((("image", "set_image_location"),), request):
|
||||||
@@ -271,7 +271,7 @@ class CreateImageForm(forms.SelfHandlingForm):
|
|||||||
meta = create_image_metadata(data)
|
meta = create_image_metadata(data)
|
||||||
|
|
||||||
# Add image source file or URL to metadata
|
# Add image source file or URL to metadata
|
||||||
if (settings.HORIZON_IMAGES_ALLOW_UPLOAD and
|
if (api.glance.get_image_upload_mode() != 'off' and
|
||||||
policy.check((("image", "upload_image"),), request) and
|
policy.check((("image", "upload_image"),), request) and
|
||||||
data.get('image_file', None)):
|
data.get('image_file', None)):
|
||||||
meta['data'] = self.files['image_file']
|
meta['data'] = self.files['image_file']
|
||||||
|
@@ -67,6 +67,12 @@ class CreateView(forms.ModalFormView):
|
|||||||
initial[name] = tmp
|
initial[name] = tmp
|
||||||
return initial
|
return initial
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super(CreateView, self).get_context_data(**kwargs)
|
||||||
|
upload_mode = api.glance.get_image_upload_mode()
|
||||||
|
context['image_upload_enabled'] = upload_mode != 'off'
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
class UpdateView(forms.ModalFormView):
|
class UpdateView(forms.ModalFormView):
|
||||||
form_class = project_forms.UpdateImageForm
|
form_class = project_forms.UpdateImageForm
|
||||||
|
@@ -8,7 +8,7 @@
|
|||||||
{% block modal-body-right %}
|
{% block modal-body-right %}
|
||||||
<h3>{% trans "Description:" %}</h3>
|
<h3>{% trans "Description:" %}</h3>
|
||||||
<p>
|
<p>
|
||||||
{% if HORIZON_IMAGES_ALLOW_UPLOAD %}
|
{% if image_upload_enabled %}
|
||||||
{% trans "Images can be provided via an HTTP/HTTPS URL or be uploaded from your local file system." %}
|
{% trans "Images can be provided via an HTTP/HTTPS URL or be uploaded from your local file system." %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "Currently only images available via an HTTP/HTTPS URL are supported. The image location must be accessible to the Image Service." %}
|
{% trans "Currently only images available via an HTTP/HTTPS URL are supported. The image location must be accessible to the Image Service." %}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>{% trans "Please note: " %}</strong>
|
<strong>{% trans "Please note: " %}</strong>
|
||||||
{% if HORIZON_IMAGES_ALLOW_UPLOAD %}
|
{% 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." %}
|
{% 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 %}
|
{% 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." %}
|
{% 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." %}
|
||||||
|
@@ -358,6 +358,12 @@ IMAGE_CUSTOM_PROPERTY_TITLES = {
|
|||||||
# table.
|
# table.
|
||||||
IMAGE_RESERVED_CUSTOM_PROPERTIES = []
|
IMAGE_RESERVED_CUSTOM_PROPERTIES = []
|
||||||
|
|
||||||
|
# Set to 'legacy' or 'direct' to allow users to upload images to glance via
|
||||||
|
# Horizon server. When enabled, a file form field will appear on the create
|
||||||
|
# image form. If set to 'off', there will be no file form field on the create
|
||||||
|
# image form. See documentation for deployment considerations.
|
||||||
|
#HORIZON_IMAGES_UPLOAD_MODE = 'legacy'
|
||||||
|
|
||||||
# OPENSTACK_ENDPOINT_TYPE specifies the endpoint type to use for the endpoints
|
# OPENSTACK_ENDPOINT_TYPE specifies the endpoint type to use for the endpoints
|
||||||
# in the Keystone service catalog. Use this setting when Horizon is running
|
# in the Keystone service catalog. Use this setting when Horizon is running
|
||||||
# external to the OpenStack environment. The default is 'publicURL'.
|
# external to the OpenStack environment. The default is 'publicURL'.
|
||||||
|
@@ -82,11 +82,6 @@ HORIZON_CONFIG = {
|
|||||||
'integration_tests_support': INTEGRATION_TESTS_SUPPORT
|
'integration_tests_support': INTEGRATION_TESTS_SUPPORT
|
||||||
}
|
}
|
||||||
|
|
||||||
# Set to True to allow users to upload images to glance via Horizon server.
|
|
||||||
# When enabled, a file form field will appear on the create image form.
|
|
||||||
# See documentation for deployment considerations.
|
|
||||||
HORIZON_IMAGES_ALLOW_UPLOAD = True
|
|
||||||
|
|
||||||
# The OPENSTACK_IMAGE_BACKEND settings can be used to customize features
|
# The OPENSTACK_IMAGE_BACKEND settings can be used to customize features
|
||||||
# in the OpenStack Dashboard related to the Image service, such as the list
|
# in the OpenStack Dashboard related to the Image service, such as the list
|
||||||
# of supported image formats.
|
# of supported image formats.
|
||||||
@@ -434,3 +429,16 @@ HORIZON_COMPRESS_OFFLINE_CONTEXT_BASE = {
|
|||||||
|
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
# Here comes the Django settings deprecation section. Being at the very end
|
||||||
|
# of settings.py allows it to catch the settings defined in local_settings.py
|
||||||
|
# or inside one of local_settings.d/ snippets.
|
||||||
|
if 'HORIZON_IMAGES_ALLOW_UPLOAD' in globals():
|
||||||
|
message = 'The setting HORIZON_IMAGES_ALLOW_UPLOAD is deprecated in ' \
|
||||||
|
'Newton and will be removed in P release. Use the setting ' \
|
||||||
|
'HORIZON_IMAGES_UPLOAD_MODE instead.'
|
||||||
|
if not HORIZON_IMAGES_ALLOW_UPLOAD:
|
||||||
|
message += ' Keep in mind that HORIZON_IMAGES_ALLOW_UPLOAD set to ' \
|
||||||
|
'False overrides the value of HORIZON_IMAGES_UPLOAD_MODE.'
|
||||||
|
logging.warning(message)
|
||||||
|
@@ -190,10 +190,10 @@ class ApiHelperTests(test.TestCase):
|
|||||||
|
|
||||||
def test_url_for(self):
|
def test_url_for(self):
|
||||||
url = api_base.url_for(self.request, 'image')
|
url = api_base.url_for(self.request, 'image')
|
||||||
self.assertEqual('http://public.glance.example.com:9292/v1', url)
|
self.assertEqual('http://public.glance.example.com:9292', url)
|
||||||
|
|
||||||
url = api_base.url_for(self.request, 'image', endpoint_type='adminURL')
|
url = api_base.url_for(self.request, 'image', endpoint_type='adminURL')
|
||||||
self.assertEqual('http://admin.glance.example.com:9292/v1', url)
|
self.assertEqual('http://admin.glance.example.com:9292', url)
|
||||||
|
|
||||||
url = api_base.url_for(self.request, 'compute')
|
url = api_base.url_for(self.request, 'compute')
|
||||||
self.assertEqual('http://public.nova.example.com:8774/v2', url)
|
self.assertEqual('http://public.nova.example.com:8774/v2', url)
|
||||||
|
@@ -20,6 +20,7 @@ from django.conf import settings
|
|||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
|
from openstack_dashboard.api import base
|
||||||
from openstack_dashboard.test import helpers as test
|
from openstack_dashboard.test import helpers as test
|
||||||
|
|
||||||
|
|
||||||
@@ -311,3 +312,20 @@ class GlanceApiTests(test.APITestCase):
|
|||||||
|
|
||||||
res_types = api.glance.metadefs_resource_types_list(self.request)
|
res_types = api.glance.metadefs_resource_types_list(self.request)
|
||||||
self.assertItemsEqual(res_types, [])
|
self.assertItemsEqual(res_types, [])
|
||||||
|
|
||||||
|
def test_image_create_external_upload(self):
|
||||||
|
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)
|
||||||
|
|
||||||
|
glanceclient = self.stub_glanceclient()
|
||||||
|
glanceclient.images = self.mox.CreateMockAnything()
|
||||||
|
glanceclient.images.create().AndReturn(expected_image)
|
||||||
|
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'])
|
||||||
|
@@ -118,10 +118,11 @@ HORIZON_CONFIG['swift_panel'] = 'legacy'
|
|||||||
find_static_files(HORIZON_CONFIG, AVAILABLE_THEMES,
|
find_static_files(HORIZON_CONFIG, AVAILABLE_THEMES,
|
||||||
THEME_COLLECTION_DIR, ROOT_PATH)
|
THEME_COLLECTION_DIR, ROOT_PATH)
|
||||||
|
|
||||||
# Set to True to allow users to upload images to glance via Horizon server.
|
# Set to 'legacy' or 'direct' to allow users to upload images to glance via
|
||||||
# When enabled, a file form field will appear on the create image form.
|
# Horizon server. When enabled, a file form field will appear on the create
|
||||||
# See documentation for deployment considerations.
|
# image form. If set to 'off', there will be no file form field on the create
|
||||||
HORIZON_IMAGES_ALLOW_UPLOAD = True
|
# image form. See documentation for deployment considerations.
|
||||||
|
HORIZON_IMAGES_UPLOAD_MODE = 'legacy'
|
||||||
|
|
||||||
AVAILABLE_REGIONS = [
|
AVAILABLE_REGIONS = [
|
||||||
('http://localhost:5000/v2.0', 'local'),
|
('http://localhost:5000/v2.0', 'local'),
|
||||||
|
@@ -68,9 +68,9 @@ SERVICE_CATALOG = [
|
|||||||
"endpoints_links": [],
|
"endpoints_links": [],
|
||||||
"endpoints": [
|
"endpoints": [
|
||||||
{"region": "RegionOne",
|
{"region": "RegionOne",
|
||||||
"adminURL": "http://admin.glance.example.com:9292/v1",
|
"adminURL": "http://admin.glance.example.com:9292",
|
||||||
"internalURL": "http://int.glance.example.com:9292/v1",
|
"internalURL": "http://int.glance.example.com:9292",
|
||||||
"publicURL": "http://public.glance.example.com:9292/v1"}]},
|
"publicURL": "http://public.glance.example.com:9292"}]},
|
||||||
{"type": "identity",
|
{"type": "identity",
|
||||||
"name": "keystone",
|
"name": "keystone",
|
||||||
"endpoints_links": [],
|
"endpoints_links": [],
|
||||||
|
Reference in New Issue
Block a user