Implements ability to upload local image to glance.
Change-Id: Icb53633bb1d5e1dd827f24d81ce908ed497b44f3 Implements: blueprint image-upload
This commit is contained in:
parent
4b1b9799c4
commit
27150a2b8a
@ -33,6 +33,27 @@ how to customize it, and where other components may take over:
|
||||
|
||||
.. _a known bug in python-keystoneclient: https://bugs.launchpad.net/keystone/+bug/1004114
|
||||
|
||||
File Uploads
|
||||
============
|
||||
|
||||
Horizon allows users to upload files via their web browser to other OpenStack
|
||||
services such as Glance and Swift. Files uploaded through this mechanism are
|
||||
first stored on the Horizon server before being forwarded on - files are not
|
||||
uploaded directly or streamed as Horizon receives them. As Horizon itself does
|
||||
not impose any restrictions on the size of file uploads, production deployments
|
||||
will want to consider configuring their server hosting the Horizon application
|
||||
to enforce such a limit to prevent large uploads exhausting system resources
|
||||
and disrupting services. Deployments using Apache2 can use the
|
||||
`LimitRequestBody directive`_ to achieve this.
|
||||
|
||||
Uploads to the Glance image store service tend to be particularly large - in
|
||||
the order of hundreds of megabytes to multiple gigabytes. Deployments are able
|
||||
to disable the ability to upload images through Horizon by setting
|
||||
``HORIZON_IMAGES_ALLOW_UPLOAD`` to ``False`` in your ``local_settings.py``
|
||||
file.
|
||||
|
||||
.. _LimitRequestBody directive: http://httpd.apache.org/docs/2.2/mod/core.html#limitrequestbody
|
||||
|
||||
Session Storage
|
||||
===============
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
{% block form_id %}create_image_form{% endblock %}
|
||||
{% block form_action %}{% url horizon:admin:images:create %}{% endblock %}
|
||||
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
|
||||
|
||||
{% block modal-header %}{% trans "Create An Image" %}{% endblock %}
|
||||
|
||||
|
@ -24,6 +24,9 @@ Views for managing images.
|
||||
|
||||
import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.forms import ValidationError
|
||||
from django.forms.widgets import HiddenInput
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
@ -42,7 +45,10 @@ class CreateImageForm(forms.SelfHandlingForm):
|
||||
label=_("Image Location"),
|
||||
help_text=_("An external (HTTP) URL to load "
|
||||
"the image from."),
|
||||
required=True)
|
||||
required=False)
|
||||
image_file = forms.FileField(label=_("Image File"),
|
||||
help_text=("A local image to upload."),
|
||||
required=False)
|
||||
disk_format = forms.ChoiceField(label=_('Format'),
|
||||
required=True,
|
||||
choices=[('', ''),
|
||||
@ -81,6 +87,22 @@ class CreateImageForm(forms.SelfHandlingForm):
|
||||
required=False)
|
||||
is_public = forms.BooleanField(label=_("Public"), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CreateImageForm, self).__init__(*args, **kwargs)
|
||||
if not settings.HORIZON_IMAGES_ALLOW_UPLOAD:
|
||||
self.fields['image_file'].widget = HiddenInput()
|
||||
|
||||
def clean(self):
|
||||
data = super(CreateImageForm, self).clean()
|
||||
if not data['copy_from'] and not data['image_file']:
|
||||
raise ValidationError(
|
||||
_("A image or external image location must be specified."))
|
||||
elif data['copy_from'] and data['image_file']:
|
||||
raise ValidationError(
|
||||
_("Can not specify both image and external image location."))
|
||||
else:
|
||||
return data
|
||||
|
||||
def handle(self, request, data):
|
||||
# Glance does not really do anything with container_format at the
|
||||
# moment. It requires it is set to the same disk_format for the three
|
||||
@ -95,11 +117,15 @@ class CreateImageForm(forms.SelfHandlingForm):
|
||||
meta = {'is_public': data['is_public'],
|
||||
'disk_format': data['disk_format'],
|
||||
'container_format': container_format,
|
||||
'copy_from': data['copy_from'],
|
||||
'min_disk': (data['minimum_disk'] or 0),
|
||||
'min_ram': (data['minimum_ram'] or 0),
|
||||
'name': data['name']}
|
||||
|
||||
if settings.HORIZON_IMAGES_ALLOW_UPLOAD and data['image_file']:
|
||||
meta['data'] = self.files['image_file']
|
||||
else:
|
||||
meta['copy_from'] = data['copy_from']
|
||||
|
||||
try:
|
||||
image = api.glance.image_create(request, **meta)
|
||||
messages.success(request,
|
||||
|
@ -18,9 +18,13 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import tempfile
|
||||
|
||||
from django import http
|
||||
from django.conf import settings
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.forms.widgets import HiddenInput
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from mox import IsA
|
||||
@ -30,11 +34,39 @@ from openstack_dashboard import api
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
from . import tables
|
||||
from .forms import CreateImageForm
|
||||
|
||||
|
||||
IMAGES_INDEX_URL = reverse('horizon:project:images_and_snapshots:index')
|
||||
|
||||
|
||||
class CreateImageFormTests(test.TestCase):
|
||||
def test_no_location_or_file(self):
|
||||
"""
|
||||
The form will not be valid if both copy_from and image_file are not
|
||||
provided.
|
||||
"""
|
||||
post = {
|
||||
'name': u'Ubuntu 11.10',
|
||||
'disk_format': u'qcow2',
|
||||
'minimum_disk': 15,
|
||||
'minimum_ram': 512,
|
||||
'is_public': 1}
|
||||
files = {}
|
||||
form = CreateImageForm(post, files)
|
||||
self.assertEqual(form.is_valid(), False)
|
||||
|
||||
@override_settings(HORIZON_IMAGES_ALLOW_UPLOAD=False)
|
||||
def test_image_upload_disabled(self):
|
||||
"""
|
||||
If HORIZON_IMAGES_ALLOW_UPLOAD is false, the image_file field widget
|
||||
will be a HiddenInput widget instead of a FileInput widget.
|
||||
"""
|
||||
form = CreateImageForm({})
|
||||
self.assertEqual(
|
||||
isinstance(form.fields['image_file'].widget, HiddenInput), True)
|
||||
|
||||
|
||||
class ImageViewTests(test.TestCase):
|
||||
def test_image_create_get(self):
|
||||
url = reverse('horizon:project:images_and_snapshots:images:create')
|
||||
@ -43,7 +75,7 @@ class ImageViewTests(test.TestCase):
|
||||
'project/images_and_snapshots/images/create.html')
|
||||
|
||||
@test.create_stubs({api.glance: ('image_create',)})
|
||||
def test_image_create_post(self):
|
||||
def test_image_create_post_copy_from(self):
|
||||
data = {
|
||||
'name': u'Ubuntu 11.10',
|
||||
'copy_from': u'http://cloud-images.ubuntu.com/releases/'
|
||||
@ -72,6 +104,38 @@ class ImageViewTests(test.TestCase):
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertEqual(res.status_code, 302)
|
||||
|
||||
@test.create_stubs({api.glance: ('image_create',)})
|
||||
def test_image_create_post_upload(self):
|
||||
temp_file = tempfile.TemporaryFile()
|
||||
temp_file.write('123')
|
||||
temp_file.flush()
|
||||
temp_file.seek(0)
|
||||
data = {
|
||||
'name': u'Test Image',
|
||||
'image_file': temp_file,
|
||||
'disk_format': u'qcow2',
|
||||
'minimum_disk': 15,
|
||||
'minimum_ram': 512,
|
||||
'is_public': 1,
|
||||
'method': 'CreateImageForm'}
|
||||
|
||||
api.glance.image_create(IsA(http.HttpRequest),
|
||||
container_format="bare",
|
||||
disk_format=data['disk_format'],
|
||||
is_public=True,
|
||||
min_disk=data['minimum_disk'],
|
||||
min_ram=data['minimum_ram'],
|
||||
name=data['name'],
|
||||
data=IsA(InMemoryUploadedFile)). \
|
||||
AndReturn(self.images.first())
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:images_and_snapshots:images:create')
|
||||
res = self.client.post(url, data)
|
||||
|
||||
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()
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
{% block form_id %}create_image_form{% endblock %}
|
||||
{% block form_action %}{% url horizon:project:images_and_snapshots:images:create %}{% endblock %}
|
||||
{% block form_attrs %}enctype="multipart/form-data"{% endblock %}
|
||||
|
||||
{% block modal-header %}{% trans "Create An Image" %}{% endblock %}
|
||||
|
||||
|
@ -65,6 +65,11 @@ HORIZON_CONFIG = {
|
||||
'unauthorized': exceptions.UNAUTHORIZED},
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
|
@ -55,6 +55,11 @@ HORIZON_CONFIG = {
|
||||
'unauthorized': UNAUTHORIZED},
|
||||
}
|
||||
|
||||
# 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
|
||||
|
||||
AVAILABLE_REGIONS = [
|
||||
('http://localhost:5000/v2.0', 'local'),
|
||||
('http://remote:5000/v2.0', 'remote'),
|
||||
|
Loading…
Reference in New Issue
Block a user