Implements ability to upload local image to glance.

Change-Id: Icb53633bb1d5e1dd827f24d81ce908ed497b44f3
Implements: blueprint image-upload
This commit is contained in:
Cody A.W. Somerville 2013-01-29 04:35:18 -05:00
parent 4b1b9799c4
commit 27150a2b8a
7 changed files with 126 additions and 3 deletions

View File

@ -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
===============

View File

@ -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 %}

View File

@ -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,

View File

@ -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()

View File

@ -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 %}

View File

@ -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',

View File

@ -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'),