Glance remote image creation.
Implements Blueprint image-copy-from Change-Id: I373301b5020d49c78fdba2bcf10c5cc4e05686ef
This commit is contained in:
parent
24355b7d3c
commit
ca795fe604
horizon
api
dashboards/nova
images_and_snapshots/images
templates/nova/images_and_snapshots/images
@ -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 {})
|
||||
|
@ -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']:
|
||||
|
@ -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"
|
||||
|
@ -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()
|
||||
|
@ -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'),
|
||||
)
|
||||
|
@ -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'
|
||||
|
@ -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 %}
|
@ -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 %}
|
Loading…
x
Reference in New Issue
Block a user