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
|
.. _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
|
Session Storage
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
{% block form_id %}create_image_form{% endblock %}
|
{% block form_id %}create_image_form{% endblock %}
|
||||||
{% block form_action %}{% url horizon:admin:images:create %}{% 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 %}
|
{% block modal-header %}{% trans "Create An Image" %}{% endblock %}
|
||||||
|
|
||||||
|
@ -24,6 +24,9 @@ Views for managing images.
|
|||||||
|
|
||||||
import logging
|
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 django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
@ -42,7 +45,10 @@ class CreateImageForm(forms.SelfHandlingForm):
|
|||||||
label=_("Image Location"),
|
label=_("Image Location"),
|
||||||
help_text=_("An external (HTTP) URL to load "
|
help_text=_("An external (HTTP) URL to load "
|
||||||
"the image from."),
|
"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'),
|
disk_format = forms.ChoiceField(label=_('Format'),
|
||||||
required=True,
|
required=True,
|
||||||
choices=[('', ''),
|
choices=[('', ''),
|
||||||
@ -81,6 +87,22 @@ class CreateImageForm(forms.SelfHandlingForm):
|
|||||||
required=False)
|
required=False)
|
||||||
is_public = forms.BooleanField(label=_("Public"), 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):
|
def handle(self, request, data):
|
||||||
# Glance does not really do anything with container_format at the
|
# 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
|
# 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'],
|
meta = {'is_public': data['is_public'],
|
||||||
'disk_format': data['disk_format'],
|
'disk_format': data['disk_format'],
|
||||||
'container_format': container_format,
|
'container_format': container_format,
|
||||||
'copy_from': data['copy_from'],
|
|
||||||
'min_disk': (data['minimum_disk'] or 0),
|
'min_disk': (data['minimum_disk'] or 0),
|
||||||
'min_ram': (data['minimum_ram'] or 0),
|
'min_ram': (data['minimum_ram'] or 0),
|
||||||
'name': data['name']}
|
'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:
|
try:
|
||||||
image = api.glance.image_create(request, **meta)
|
image = api.glance.image_create(request, **meta)
|
||||||
messages.success(request,
|
messages.success(request,
|
||||||
|
@ -18,9 +18,13 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from django import http
|
from django import http
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.files.uploadedfile import InMemoryUploadedFile
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
|
from django.forms.widgets import HiddenInput
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
|
|
||||||
from mox import IsA
|
from mox import IsA
|
||||||
@ -30,11 +34,39 @@ from openstack_dashboard import api
|
|||||||
from openstack_dashboard.test import helpers as test
|
from openstack_dashboard.test import helpers as test
|
||||||
|
|
||||||
from . import tables
|
from . import tables
|
||||||
|
from .forms import CreateImageForm
|
||||||
|
|
||||||
|
|
||||||
IMAGES_INDEX_URL = reverse('horizon:project:images_and_snapshots:index')
|
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):
|
class ImageViewTests(test.TestCase):
|
||||||
def test_image_create_get(self):
|
def test_image_create_get(self):
|
||||||
url = reverse('horizon:project:images_and_snapshots:images:create')
|
url = reverse('horizon:project:images_and_snapshots:images:create')
|
||||||
@ -43,7 +75,7 @@ class ImageViewTests(test.TestCase):
|
|||||||
'project/images_and_snapshots/images/create.html')
|
'project/images_and_snapshots/images/create.html')
|
||||||
|
|
||||||
@test.create_stubs({api.glance: ('image_create',)})
|
@test.create_stubs({api.glance: ('image_create',)})
|
||||||
def test_image_create_post(self):
|
def test_image_create_post_copy_from(self):
|
||||||
data = {
|
data = {
|
||||||
'name': u'Ubuntu 11.10',
|
'name': u'Ubuntu 11.10',
|
||||||
'copy_from': u'http://cloud-images.ubuntu.com/releases/'
|
'copy_from': u'http://cloud-images.ubuntu.com/releases/'
|
||||||
@ -72,6 +104,38 @@ class ImageViewTests(test.TestCase):
|
|||||||
self.assertNoFormErrors(res)
|
self.assertNoFormErrors(res)
|
||||||
self.assertEqual(res.status_code, 302)
|
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',)})
|
@test.create_stubs({api.glance: ('image_get',)})
|
||||||
def test_image_detail_get(self):
|
def test_image_detail_get(self):
|
||||||
image = self.images.first()
|
image = self.images.first()
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
{% block form_id %}create_image_form{% endblock %}
|
{% block form_id %}create_image_form{% endblock %}
|
||||||
{% block form_action %}{% url horizon:project:images_and_snapshots:images:create %}{% 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 %}
|
{% block modal-header %}{% trans "Create An Image" %}{% endblock %}
|
||||||
|
|
||||||
|
@ -65,6 +65,11 @@ HORIZON_CONFIG = {
|
|||||||
'unauthorized': exceptions.UNAUTHORIZED},
|
'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 = (
|
MIDDLEWARE_CLASSES = (
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
'django.middleware.csrf.CsrfViewMiddleware',
|
'django.middleware.csrf.CsrfViewMiddleware',
|
||||||
|
@ -55,6 +55,11 @@ HORIZON_CONFIG = {
|
|||||||
'unauthorized': UNAUTHORIZED},
|
'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 = [
|
AVAILABLE_REGIONS = [
|
||||||
('http://localhost:5000/v2.0', 'local'),
|
('http://localhost:5000/v2.0', 'local'),
|
||||||
('http://remote:5000/v2.0', 'remote'),
|
('http://remote:5000/v2.0', 'remote'),
|
||||||
|
Loading…
Reference in New Issue
Block a user