Glance remote image creation.
Implements Blueprint image-copy-from Change-Id: I373301b5020d49c78fdba2bcf10c5cc4e05686ef
This commit is contained in:
parent
24355b7d3c
commit
ca795fe604
@ -21,6 +21,7 @@
|
|||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import thread
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -69,6 +70,22 @@ def image_update(request, image_id, **kwargs):
|
|||||||
return glanceclient(request).images.update(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):
|
def snapshot_list_detailed(request, marker=None, extra_filters=None):
|
||||||
filters = {'property-image_type': 'snapshot'}
|
filters = {'property-image_type': 'snapshot'}
|
||||||
filters.update(extra_filters or {})
|
filters.update(extra_filters or {})
|
||||||
|
@ -36,6 +36,69 @@ from horizon import forms
|
|||||||
LOG = logging.getLogger(__name__)
|
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):
|
class UpdateImageForm(forms.SelfHandlingForm):
|
||||||
completion_view = 'horizon:nova:images_and_snapshots:index'
|
completion_view = 'horizon:nova:images_and_snapshots:index'
|
||||||
|
|
||||||
@ -55,16 +118,11 @@ class UpdateImageForm(forms.SelfHandlingForm):
|
|||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
attrs={'readonly': 'readonly'}
|
attrs={'readonly': 'readonly'}
|
||||||
))
|
))
|
||||||
container_format = forms.CharField(label=_("Container Format"),
|
disk_format = forms.CharField(label=_("Format"),
|
||||||
widget=forms.TextInput(
|
widget=forms.TextInput(
|
||||||
attrs={'readonly': 'readonly'}
|
attrs={'readonly': 'readonly'}
|
||||||
))
|
))
|
||||||
disk_format = forms.CharField(label=_("Disk Format"),
|
public = forms.BooleanField(label=_("Public"), required=False)
|
||||||
widget=forms.TextInput(
|
|
||||||
attrs={'readonly': 'readonly'}
|
|
||||||
))
|
|
||||||
public = forms.BooleanField(label=_("Public"),
|
|
||||||
required=False)
|
|
||||||
|
|
||||||
def handle(self, request, data):
|
def handle(self, request, data):
|
||||||
# TODO add public flag to image meta properties
|
# TODO add public flag to image meta properties
|
||||||
@ -73,7 +131,7 @@ class UpdateImageForm(forms.SelfHandlingForm):
|
|||||||
|
|
||||||
meta = {'is_public': data['public'],
|
meta = {'is_public': data['public'],
|
||||||
'disk_format': data['disk_format'],
|
'disk_format': data['disk_format'],
|
||||||
'container_format': data['container_format'],
|
'container_format': 'bare',
|
||||||
'name': data['name'],
|
'name': data['name'],
|
||||||
'properties': {}}
|
'properties': {}}
|
||||||
if data['kernel']:
|
if data['kernel']:
|
||||||
|
@ -55,6 +55,13 @@ class DeleteImage(tables.DeleteAction):
|
|||||||
api.image_delete(request, obj_id)
|
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):
|
class EditImage(tables.LinkAction):
|
||||||
name = "edit"
|
name = "edit"
|
||||||
verbose_name = _("Edit")
|
verbose_name = _("Edit")
|
||||||
@ -73,33 +80,53 @@ def get_image_type(image):
|
|||||||
return getattr(image.properties, "image_type", "Image")
|
return getattr(image.properties, "image_type", "Image")
|
||||||
|
|
||||||
|
|
||||||
def get_container_format(image):
|
def get_format(image):
|
||||||
container_format = getattr(image, "container_format", "")
|
format = getattr(image, "disk_format", "")
|
||||||
# The "container_format" attribute can actually be set to None,
|
# The "container_format" attribute can actually be set to None,
|
||||||
# which will raise an error if you call upper() on it.
|
# which will raise an error if you call upper() on it.
|
||||||
if container_format is not None:
|
if format is not None:
|
||||||
return container_format.upper()
|
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):
|
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:" \
|
name = tables.Column("name", link="horizon:nova:images_and_snapshots:" \
|
||||||
"images:detail",
|
"images:detail",
|
||||||
verbose_name=_("Image Name"))
|
verbose_name=_("Image Name"))
|
||||||
image_type = tables.Column(get_image_type,
|
image_type = tables.Column(get_image_type,
|
||||||
verbose_name=_("Type"),
|
verbose_name=_("Type"),
|
||||||
filters=(filters.title,))
|
filters=(filters.title,))
|
||||||
status = tables.Column("status", filters=(filters.title,),
|
status = tables.Column("status",
|
||||||
verbose_name=_("Status"))
|
filters=(filters.title,),
|
||||||
|
verbose_name=_("Status"),
|
||||||
|
status=True,
|
||||||
|
status_choices=STATUS_CHOICES)
|
||||||
public = tables.Column("is_public",
|
public = tables.Column("is_public",
|
||||||
verbose_name=_("Public"),
|
verbose_name=_("Public"),
|
||||||
empty_value=False,
|
empty_value=False,
|
||||||
filters=(filters.yesno, filters.capfirst))
|
filters=(filters.yesno, filters.capfirst))
|
||||||
container_format = tables.Column(get_container_format,
|
disk_format = tables.Column(get_format, verbose_name=_("Format"))
|
||||||
verbose_name=_("Container Format"))
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
name = "images"
|
name = "images"
|
||||||
|
row_class = UpdateRow
|
||||||
|
status_columns = ["status"]
|
||||||
verbose_name = _("Images")
|
verbose_name = _("Images")
|
||||||
table_actions = (DeleteImage,)
|
table_actions = (CreateImage, DeleteImage,)
|
||||||
row_actions = (LaunchImage, EditImage, DeleteImage)
|
row_actions = (LaunchImage, EditImage, DeleteImage,)
|
||||||
pagination_param = "image_marker"
|
pagination_param = "image_marker"
|
||||||
|
@ -24,16 +24,54 @@ from django.core.urlresolvers import reverse
|
|||||||
from horizon import api
|
from horizon import api
|
||||||
from horizon import test
|
from horizon import test
|
||||||
|
|
||||||
from mox import IsA
|
from mox import IgnoreArg, IsA
|
||||||
|
|
||||||
|
|
||||||
IMAGES_INDEX_URL = reverse('horizon:nova:images_and_snapshots:index')
|
IMAGES_INDEX_URL = reverse('horizon:nova:images_and_snapshots:index')
|
||||||
|
|
||||||
|
|
||||||
class ImageViewTests(test.TestCase):
|
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):
|
def test_image_detail_get(self):
|
||||||
image = self.images.first()
|
image = self.images.first()
|
||||||
self.mox.StubOutWithMock(api.glance, 'image_get')
|
|
||||||
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
|
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
|
||||||
.AndReturn(self.images.first())
|
.AndReturn(self.images.first())
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
@ -45,9 +83,10 @@ class ImageViewTests(test.TestCase):
|
|||||||
'nova/images_and_snapshots/images/detail.html')
|
'nova/images_and_snapshots/images/detail.html')
|
||||||
self.assertEqual(res.context['image'].name, image.name)
|
self.assertEqual(res.context['image'].name, image.name)
|
||||||
|
|
||||||
|
@test.create_stubs({api.glance: ('image_get',)})
|
||||||
def test_image_detail_get_with_exception(self):
|
def test_image_detail_get_with_exception(self):
|
||||||
image = self.images.first()
|
image = self.images.first()
|
||||||
self.mox.StubOutWithMock(api.glance, 'image_get')
|
|
||||||
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
|
api.glance.image_get(IsA(http.HttpRequest), str(image.id)) \
|
||||||
.AndRaise(self.exceptions.glance)
|
.AndRaise(self.exceptions.glance)
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
@ -20,12 +20,13 @@
|
|||||||
|
|
||||||
from django.conf.urls.defaults import patterns, url
|
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'
|
VIEWS_MOD = 'horizon.dashboards.nova.images_and_snapshots.images.views'
|
||||||
|
|
||||||
|
|
||||||
urlpatterns = patterns(VIEWS_MOD,
|
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>[^/]+)/update/$', UpdateView.as_view(), name='update'),
|
||||||
url(r'^(?P<image_id>[^/]+)/$', DetailView.as_view(), name='detail'),
|
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 forms
|
||||||
from horizon import tabs
|
from horizon import tabs
|
||||||
from .forms import UpdateImageForm
|
from .forms import UpdateImageForm
|
||||||
|
from .forms import CreateImageForm
|
||||||
from .tabs import ImageDetailTabs
|
from .tabs import ImageDetailTabs
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
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):
|
class UpdateView(forms.ModalFormView):
|
||||||
form_class = UpdateImageForm
|
form_class = UpdateImageForm
|
||||||
template_name = 'nova/images_and_snapshots/images/update.html'
|
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…
Reference in New Issue
Block a user