summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2012-06-06 19:51:40 +0000
committerGerrit Code Review <review@openstack.org>2012-06-06 19:51:40 +0000
commitf6802a9058e74d7a3f4249c9ac5314705ae2a6f6 (patch)
tree4251b0500a524d25fff67ac1d408a4c364646565
parentab71a133d9d37359805f9dd841ecee776358ebf8 (diff)
parentca795fe604ebf33c7845f81c6019ae7302498059 (diff)
downloadtuskar-ui-f6802a9058e74d7a3f4249c9ac5314705ae2a6f6.tar.gz
Merge "Glance remote image creation."
-rw-r--r--horizon/api/glance.py17
-rw-r--r--horizon/dashboards/nova/images_and_snapshots/images/forms.py74
-rw-r--r--horizon/dashboards/nova/images_and_snapshots/images/tables.py47
-rw-r--r--horizon/dashboards/nova/images_and_snapshots/images/tests.py45
-rw-r--r--horizon/dashboards/nova/images_and_snapshots/images/urls.py3
-rw-r--r--horizon/dashboards/nova/images_and_snapshots/images/views.py7
-rw-r--r--horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_create.html33
-rw-r--r--horizon/dashboards/nova/templates/nova/images_and_snapshots/images/create.html11
8 files changed, 215 insertions, 22 deletions
diff --git a/horizon/api/glance.py b/horizon/api/glance.py
index 8151f3d8..675de176 100644
--- a/horizon/api/glance.py
+++ b/horizon/api/glance.py
@@ -21,6 +21,7 @@
from __future__ import absolute_import
import logging
+import thread
import urlparse
from django.conf import settings
@@ -69,6 +70,22 @@ def image_update(request, image_id, **kwargs):
return glanceclient(request).images.update(image_id, **kwargs)
+def image_create(request, **kwargs):
+ copy_from = None
+
+ if kwargs.get('copy_from'):
+ copy_from = kwargs.pop('copy_from')
+
+ image = glanceclient(request).images.create(**kwargs)
+
+ if copy_from:
+ thread.start_new_thread(image_update,
+ (request, image.id),
+ {'copy_from': copy_from})
+
+ return image
+
+
def snapshot_list_detailed(request, marker=None, extra_filters=None):
filters = {'property-image_type': 'snapshot'}
filters.update(extra_filters or {})
diff --git a/horizon/dashboards/nova/images_and_snapshots/images/forms.py b/horizon/dashboards/nova/images_and_snapshots/images/forms.py
index d47e7f4d..afe361ac 100644
--- a/horizon/dashboards/nova/images_and_snapshots/images/forms.py
+++ b/horizon/dashboards/nova/images_and_snapshots/images/forms.py
@@ -36,6 +36,69 @@ from horizon import forms
LOG = logging.getLogger(__name__)
+class CreateImageForm(forms.SelfHandlingForm):
+ completion_view = 'horizon:nova:images_and_snapshots:index'
+
+ name = forms.CharField(max_length="255", label=_("Name"), required=True)
+ copy_from = forms.CharField(max_length="255",
+ label=_("Image Location"),
+ help_text=_("An external (HTTP) URL where"
+ " the image should be loaded from."),
+ required=True)
+ disk_format = forms.ChoiceField(label=_('Format'),
+ required=True,
+ choices=[('', ''),
+ ('aki',
+ 'Amazon Kernel Image (AKI)'),
+ ('ami',
+ 'Amazon Machine Image (AMI)'),
+ ('ari',
+ 'Amazon Ramdisk Image (ARI)'),
+ ('iso',
+ 'Optical Disk Image (ISO)'),
+ ('qcow2',
+ 'QEMU Emulator (QCOW2)'),
+ ('raw', 'Raw'),
+ ('vdi', 'VDI'),
+ ('vhd', 'VHD'),
+ ('vmdk', 'VMDK')],
+ widget=forms.Select(attrs={'class':
+ 'switchable'}))
+ minimum_disk = forms.IntegerField(label=_("Minimum Disk (GB)"),
+ help_text=_('The minimum disk size'
+ ' required to boot the'
+ ' image. If unspecified, this'
+ ' value defaults to 0'
+ ' (no minimum).'),
+ required=False)
+ minimum_ram = forms.IntegerField(label=_("Minimum Ram (MB)"),
+ help_text=_('The minimum disk size'
+ ' required to boot the'
+ ' image. If unspecified, this'
+ ' value defaults to 0 (no'
+ ' minimum).'),
+ required=False)
+ is_public = forms.BooleanField(label=_("Public"), required=False)
+
+ def handle(self, request, data):
+ meta = {'is_public': data['is_public'],
+ 'disk_format': data['disk_format'],
+ 'container_format': 'bare', # Not used in Glance ATM.
+ 'copy_from': data['copy_from'],
+ 'min_disk': (data['minimum_disk'] or 0),
+ 'min_ram': (data['minimum_ram'] or 0),
+ 'name': data['name']}
+
+ try:
+ api.glance.image_create(request, **meta)
+ messages.success(request,
+ _('Your image %s has been queued for creation.' %
+ data['name']))
+ except:
+ exceptions.handle(request, _('Unable to create new image.'))
+ return shortcuts.redirect(self.get_success_url())
+
+
class UpdateImageForm(forms.SelfHandlingForm):
completion_view = 'horizon:nova:images_and_snapshots:index'
@@ -55,16 +118,11 @@ class UpdateImageForm(forms.SelfHandlingForm):
widget=forms.TextInput(
attrs={'readonly': 'readonly'}
))
- container_format = forms.CharField(label=_("Container Format"),
- widget=forms.TextInput(
- attrs={'readonly': 'readonly'}
- ))
- disk_format = forms.CharField(label=_("Disk Format"),
+ disk_format = forms.CharField(label=_("Format"),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}
))
- public = forms.BooleanField(label=_("Public"),
- required=False)
+ public = forms.BooleanField(label=_("Public"), required=False)
def handle(self, request, data):
# TODO add public flag to image meta properties
@@ -73,7 +131,7 @@ class UpdateImageForm(forms.SelfHandlingForm):
meta = {'is_public': data['public'],
'disk_format': data['disk_format'],
- 'container_format': data['container_format'],
+ 'container_format': 'bare',
'name': data['name'],
'properties': {}}
if data['kernel']:
diff --git a/horizon/dashboards/nova/images_and_snapshots/images/tables.py b/horizon/dashboards/nova/images_and_snapshots/images/tables.py
index 546cd488..573edfad 100644
--- a/horizon/dashboards/nova/images_and_snapshots/images/tables.py
+++ b/horizon/dashboards/nova/images_and_snapshots/images/tables.py
@@ -55,6 +55,13 @@ class DeleteImage(tables.DeleteAction):
api.image_delete(request, obj_id)
+class CreateImage(tables.LinkAction):
+ name = "create"
+ verbose_name = _("Create Image")
+ url = "horizon:nova:images_and_snapshots:images:create"
+ classes = ("ajax-modal", "btn-create")
+
+
class EditImage(tables.LinkAction):
name = "edit"
verbose_name = _("Edit")
@@ -73,33 +80,53 @@ def get_image_type(image):
return getattr(image.properties, "image_type", "Image")
-def get_container_format(image):
- container_format = getattr(image, "container_format", "")
+def get_format(image):
+ format = getattr(image, "disk_format", "")
# The "container_format" attribute can actually be set to None,
# which will raise an error if you call upper() on it.
- if container_format is not None:
- return container_format.upper()
+ if format is not None:
+ return format.upper()
+
+
+class UpdateRow(tables.Row):
+ ajax = True
+
+ def get_data(self, request, image_id):
+ image = api.image_get(request, image_id)
+ return image
class ImagesTable(tables.DataTable):
+ STATUS_CHOICES = (
+ ("active", True),
+ ("saving", None),
+ ("queued", None),
+ ("pending_delete", None),
+ ("killed", False),
+ ("deleted", False),
+ )
name = tables.Column("name", link="horizon:nova:images_and_snapshots:" \
"images:detail",
verbose_name=_("Image Name"))
image_type = tables.Column(get_image_type,
verbose_name=_("Type"),
filters=(filters.title,))
- status = tables.Column("status", filters=(filters.title,),
- verbose_name=_("Status"))
+ status = tables.Column("status",
+ filters=(filters.title,),
+ verbose_name=_("Status"),
+ status=True,
+ status_choices=STATUS_CHOICES)
public = tables.Column("is_public",
verbose_name=_("Public"),
empty_value=False,
filters=(filters.yesno, filters.capfirst))
- container_format = tables.Column(get_container_format,
- verbose_name=_("Container Format"))
+ disk_format = tables.Column(get_format, verbose_name=_("Format"))
class Meta:
name = "images"
+ row_class = UpdateRow
+ status_columns = ["status"]
verbose_name = _("Images")
- table_actions = (DeleteImage,)
- row_actions = (LaunchImage, EditImage, DeleteImage)
+ table_actions = (CreateImage, DeleteImage,)
+ row_actions = (LaunchImage, EditImage, DeleteImage,)
pagination_param = "image_marker"
diff --git a/horizon/dashboards/nova/images_and_snapshots/images/tests.py b/horizon/dashboards/nova/images_and_snapshots/images/tests.py
index c9aa1056..b93a7da1 100644
--- a/horizon/dashboards/nova/images_and_snapshots/images/tests.py
+++ b/horizon/dashboards/nova/images_and_snapshots/images/tests.py
@@ -24,16 +24,54 @@ from django.core.urlresolvers import reverse
from horizon import api
from horizon import test
-from mox import IsA
+from mox import IgnoreArg, IsA
IMAGES_INDEX_URL = reverse('horizon:nova:images_and_snapshots:index')
class ImageViewTests(test.TestCase):
+ def test_image_create_get(self):
+ url = reverse('horizon:nova:images_and_snapshots:images:create')
+ res = self.client.get(url)
+ self.assertTemplateUsed(res,
+ 'nova/images_and_snapshots/images/create.html')
+
+ @test.create_stubs({api.glance: ('image_create',)})
+ def test_image_create_post(self):
+ data = {
+ 'name': u'Ubuntu 11.10',
+ 'copy_from': u'http://cloud-images.ubuntu.com/releases/'
+ u'oneiric/release/ubuntu-11.10-server-cloudimg'
+ u'-amd64-disk1.img',
+ '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",
+ copy_from=data['copy_from'],
+ disk_format=data['disk_format'],
+ is_public=True,
+ min_disk=data['minimum_disk'],
+ min_ram=data['minimum_ram'],
+ name=data['name']). \
+ AndReturn(self.images.first())
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:nova: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()
- self.mox.StubOutWithMock(api.glance, 'image_get')
+
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
.AndReturn(self.images.first())
self.mox.ReplayAll()
@@ -45,9 +83,10 @@ class ImageViewTests(test.TestCase):
'nova/images_and_snapshots/images/detail.html')
self.assertEqual(res.context['image'].name, image.name)
+ @test.create_stubs({api.glance: ('image_get',)})
def test_image_detail_get_with_exception(self):
image = self.images.first()
- self.mox.StubOutWithMock(api.glance, 'image_get')
+
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
.AndRaise(self.exceptions.glance)
self.mox.ReplayAll()
diff --git a/horizon/dashboards/nova/images_and_snapshots/images/urls.py b/horizon/dashboards/nova/images_and_snapshots/images/urls.py
index dc26e1e0..a4e7ad18 100644
--- a/horizon/dashboards/nova/images_and_snapshots/images/urls.py
+++ b/horizon/dashboards/nova/images_and_snapshots/images/urls.py
@@ -20,12 +20,13 @@
from django.conf.urls.defaults import patterns, url
-from .views import UpdateView, DetailView
+from .views import UpdateView, DetailView, CreateView
VIEWS_MOD = 'horizon.dashboards.nova.images_and_snapshots.images.views'
urlpatterns = patterns(VIEWS_MOD,
+ url(r'^create/$', CreateView.as_view(), name='create'),
url(r'^(?P<image_id>[^/]+)/update/$', UpdateView.as_view(), name='update'),
url(r'^(?P<image_id>[^/]+)/$', DetailView.as_view(), name='detail'),
)
diff --git a/horizon/dashboards/nova/images_and_snapshots/images/views.py b/horizon/dashboards/nova/images_and_snapshots/images/views.py
index 1e4deee5..947bd8a7 100644
--- a/horizon/dashboards/nova/images_and_snapshots/images/views.py
+++ b/horizon/dashboards/nova/images_and_snapshots/images/views.py
@@ -32,12 +32,19 @@ from horizon import exceptions
from horizon import forms
from horizon import tabs
from .forms import UpdateImageForm
+from .forms import CreateImageForm
from .tabs import ImageDetailTabs
LOG = logging.getLogger(__name__)
+class CreateView(forms.ModalFormView):
+ form_class = CreateImageForm
+ template_name = 'nova/images_and_snapshots/images/create.html'
+ context_object_name = 'image'
+
+
class UpdateView(forms.ModalFormView):
form_class = UpdateImageForm
template_name = 'nova/images_and_snapshots/images/update.html'
diff --git a/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_create.html b/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_create.html
new file mode 100644
index 00000000..0df41f8e
--- /dev/null
+++ b/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/_create.html
@@ -0,0 +1,33 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+
+{% block form_id %}create_image_form{% endblock %}
+{% block form_action %}{% url horizon:nova:images_and_snapshots:images:create %}{% endblock %}
+
+{% block modal-header %}{% trans "Create An Image" %}{% endblock %}
+
+{% block modal-body %}
+<div class="left">
+ <fieldset>
+ {% include "horizon/common/_form_fields.html" %}
+ </fieldset>
+</div>
+<div class="right">
+ <h3>{% trans "Description:" %}</h3>
+ <p>
+ {% trans "Specify an image to upload to the Image Service." %}
+ </p>
+ <p>
+ {% trans "Currently only images available via an HTTP URL are supported. The image location must be accessible to the Image Service. Compressed image binaries are supported (.zip and .tar.gz.)" %}
+ </p>
+ <p>
+ <strong>{% trans "Please note: " %}</strong>
+ {% trans "The Image Location field MUST be a valid and direct URL to the image binary. URLs that redirect or serve error pages will results in unusable images." %}
+ </p>
+</div>
+{% endblock %}
+
+{% block modal-footer %}
+ <input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Image" %}" />
+ <a href="{% url horizon:nova:images_and_snapshots:index %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
+{% endblock %}
diff --git a/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/create.html b/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/create.html
new file mode 100644
index 00000000..b9fa856f
--- /dev/null
+++ b/horizon/dashboards/nova/templates/nova/images_and_snapshots/images/create.html
@@ -0,0 +1,11 @@
+{% extends 'nova/base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Create An Image" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Create An Image") %}
+{% endblock page_header %}
+
+{% block dash_main %}
+ {% include 'nova/images_and_snapshots/images/_create.html' %}
+{% endblock %}