Merge "Glance remote image creation."

This commit is contained in:
Jenkins 2012-06-06 19:51:40 +00:00 committed by Gerrit Code Review
commit f6802a9058
8 changed files with 215 additions and 22 deletions

View File

@ -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 {})

View File

@ -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']:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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