Merge "Added action for creating a volume from snapshot"

This commit is contained in:
Jenkins
2012-07-24 18:34:30 +00:00
committed by Gerrit Code Review
8 changed files with 240 additions and 23 deletions

View File

@@ -518,9 +518,9 @@ def volume_instance_list(request, instance_id):
return volumes
def volume_create(request, size, name, description):
def volume_create(request, size, name, description, snapshot_id=None):
return cinderclient(request).volumes.create(size, display_name=name,
display_description=description)
display_description=description, snapshot_id=snapshot_id)
def volume_delete(request, volume_id):

View File

@@ -16,6 +16,8 @@
import logging
from django.core.urlresolvers import reverse
from django.utils.http import urlencode
from django.utils.translation import ugettext_lazy as _
from horizon import api
@@ -34,6 +36,21 @@ class DeleteVolumeSnapshot(tables.DeleteAction):
api.volume_snapshot_delete(request, obj_id)
class CreateVolumeFromSnapshot(tables.LinkAction):
name = "create_from_snapshot"
verbose_name = _("Create Volume")
url = "horizon:nova:volumes:create"
classes = ("ajax-modal", "btn-camera")
def get_link_url(self, datum):
base_url = reverse(self.url)
params = urlencode({"snapshot_id": self.table.get_object_id(datum)})
return "?".join([base_url, params])
def allowed(self, request, volume=None):
return volume.status == "available" if volume else False
class UpdateRow(tables.Row):
ajax = True
@@ -51,6 +68,6 @@ class VolumeSnapshotsTable(volume_tables.VolumesTableBase):
name = "volume_snapshots"
verbose_name = _("Volume Snapshots")
table_actions = (DeleteVolumeSnapshot,)
row_actions = (DeleteVolumeSnapshot,)
row_actions = (CreateVolumeFromSnapshot, DeleteVolumeSnapshot)
row_class = UpdateRow
status_columns = ("status",)

View File

@@ -15,15 +15,54 @@ from horizon import api
from horizon import forms
from horizon import exceptions
from horizon import messages
from horizon.utils.fields import SelectWidget
from horizon.utils.memoized import memoized
from ..instances.tables import ACTIVE_STATES
class CreateForm(forms.SelfHandlingForm):
name = forms.CharField(max_length="255", label="Volume Name")
name = forms.CharField(max_length="255", label=_("Volume Name"))
description = forms.CharField(widget=forms.Textarea,
label=_("Description"), required=False)
size = forms.IntegerField(min_value=1, label="Size (GB)")
size = forms.IntegerField(min_value=1, label=_("Size (GB)"))
snapshot_source = forms.ChoiceField(label=_("Use snapshot as a source"),
widget=SelectWidget(
attrs={'class': 'snapshot-selector'},
data_attrs=('size', 'display_name'),
transform=lambda x:
("%s (%sGB)" % (x.display_name,
x.size))),
required=False)
def __init__(self, request, *args, **kwargs):
super(CreateForm, self).__init__(request, *args, **kwargs)
if ("snapshot_id" in request.GET):
try:
snapshot = self.get_snapshot(request,
request.GET["snapshot_id"])
self.fields['name'].initial = snapshot.display_name
self.fields['size'].initial = snapshot.size
self.fields['snapshot_source'].choices = ((snapshot.id,
snapshot),)
self.fields['size'].help_text = _('Volume size must be equal '
'to or greater than the snapshot size (%sGB)'
% snapshot.size)
except:
exceptions.handle(request,
_('Unable to load the specified snapshot.'))
else:
try:
snapshots = api.volume_snapshot_list(request)
if snapshots:
choices = [('', _("Choose a snapshot"))] + \
[(s.id, s) for s in snapshots]
self.fields['snapshot_source'].choices = choices
else:
del self.fields['snapshot_source']
except:
exceptions.handle(request, _("Unable to retrieve "
"volume snapshots."))
def handle(self, request, data):
try:
@@ -33,8 +72,20 @@ class CreateForm(forms.SelfHandlingForm):
# send it off to Nova to try and create.
usages = api.tenant_quota_usages(request)
if type(data['size']) is str:
data['size'] = int(data['size'])
snapshot_id = None
if (data.get("snapshot_source", None)):
# Create from Snapshot
snapshot = self.get_snapshot(request,
data["snapshot_source"])
snapshot_id = snapshot.id
if (data['size'] < snapshot.size):
error_message = _('The volume size cannot be less than '
'the snapshot size (%sGB)' %
snapshot.size)
raise ValidationError(error_message)
else:
if type(data['size']) is str:
data['size'] = int(data['size'])
if usages['gigabytes']['available'] < data['size']:
error_message = _('A volume of %(req)iGB cannot be created as '
@@ -51,7 +102,8 @@ class CreateForm(forms.SelfHandlingForm):
volume = api.volume_create(request,
data['size'],
data['name'],
data['description'])
data['description'],
snapshot_id=snapshot_id)
message = 'Creating volume "%s"' % data['name']
messages.info(request, message)
return volume
@@ -61,12 +113,16 @@ class CreateForm(forms.SelfHandlingForm):
exceptions.handle(request, ignore=True)
return self.api_error(_("Unable to create volume."))
@memoized
def get_snapshot(self, request, id):
return api.nova.volume_snapshot_get(request, id)
class AttachForm(forms.SelfHandlingForm):
instance = forms.ChoiceField(label="Attach to Instance",
instance = forms.ChoiceField(label=_("Attach to Instance"),
help_text=_("Select an instance to "
"attach to."))
device = forms.CharField(label="Device Name", initial="/dev/vdc")
device = forms.CharField(label=_("Device Name"), initial="/dev/vdc")
def __init__(self, *args, **kwargs):
super(AttachForm, self).__init__(*args, **kwargs)
@@ -126,8 +182,8 @@ class CreateSnapshotForm(forms.SelfHandlingForm):
description = forms.CharField(widget=forms.Textarea,
label=_("Description"), required=False)
def __init__(self, *args, **kwargs):
super(CreateSnapshotForm, self).__init__(*args, **kwargs)
def __init__(self, request, *args, **kwargs):
super(CreateSnapshotForm, self).__init__(request, *args, **kwargs)
# populate volume_id
volume_id = kwargs.get('initial', {}).get('volume_id', [])

View File

@@ -2,7 +2,7 @@
{% load i18n horizon humanize %}
{% block form_id %}{% endblock %}
{% block form_action %}{% url horizon:nova:volumes:create %}{% endblock %}
{% block form_action %}{% url horizon:nova:volumes:create %}?{{ request.GET.urlencode }}{% endblock %}
{% block modal_id %}create_volume_modal{% endblock %}
{% block modal-header %}{% trans "Create Volume" %}{% endblock %}

View File

@@ -27,20 +27,24 @@ from horizon import test
class VolumeViewTests(test.TestCase):
@test.create_stubs({api: ('tenant_quota_usages', 'volume_create',)})
@test.create_stubs({api: ('tenant_quota_usages', 'volume_create',
'volume_snapshot_list')})
def test_create_volume(self):
volume = self.volumes.first()
usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}}
formData = {'name': u'A Volume I Am Making',
'description': u'This is a volume I am making for a test.',
'method': u'CreateForm',
'size': 50}
'size': 50, 'snapshot_source': ''}
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
api.volume_snapshot_list(IsA(http.HttpRequest)).\
AndReturn(self.volume_snapshots.list())
api.volume_create(IsA(http.HttpRequest),
formData['size'],
formData['name'],
formData['description']).AndReturn(volume)
formData['description'],
snapshot_id=None).AndReturn(volume)
self.mox.ReplayAll()
@@ -50,7 +54,86 @@ class VolumeViewTests(test.TestCase):
redirect_url = reverse('horizon:nova:volumes:index')
self.assertRedirectsNoFollow(res, redirect_url)
@test.create_stubs({api: ('tenant_quota_usages',)})
@test.create_stubs({api: ('tenant_quota_usages', 'volume_create',
'volume_snapshot_list'),
api.nova: ('volume_snapshot_get',)})
def test_create_volume_from_snapshot(self):
volume = self.volumes.first()
usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}}
snapshot = self.volume_snapshots.first()
formData = {'name': u'A Volume I Am Making',
'description': u'This is a volume I am making for a test.',
'method': u'CreateForm',
'size': 50, 'snapshot_source': snapshot.id}
# first call- with url param
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
api.nova.volume_snapshot_get(IsA(http.HttpRequest),
str(snapshot.id)).AndReturn(snapshot)
api.volume_create(IsA(http.HttpRequest),
formData['size'],
formData['name'],
formData['description'],
snapshot_id=snapshot.id).\
AndReturn(volume)
# second call- with dropdown
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
api.volume_snapshot_list(IsA(http.HttpRequest)).\
AndReturn(self.volume_snapshots.list())
api.nova.volume_snapshot_get(IsA(http.HttpRequest),
str(snapshot.id)).AndReturn(snapshot)
api.volume_create(IsA(http.HttpRequest),
formData['size'],
formData['name'],
formData['description'],
snapshot_id=snapshot.id).\
AndReturn(volume)
self.mox.ReplayAll()
# get snapshot from url
url = reverse('horizon:nova:volumes:create')
res = self.client.post("?".join([url,
"snapshot_id=" + str(snapshot.id)]),
formData)
redirect_url = reverse('horizon:nova:volumes:index')
self.assertRedirectsNoFollow(res, redirect_url)
# get snapshot from dropdown list
url = reverse('horizon:nova:volumes:create')
res = self.client.post(url, formData)
redirect_url = reverse('horizon:nova:volumes:index')
self.assertRedirectsNoFollow(res, redirect_url)
@test.create_stubs({api: ('tenant_quota_usages',),
api.nova: ('volume_snapshot_get',)})
def test_create_volume_from_snapshot_invalid_size(self):
usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}}
snapshot = self.volume_snapshots.first()
formData = {'name': u'A Volume I Am Making',
'description': u'This is a volume I am making for a test.',
'method': u'CreateForm',
'size': 20, 'snapshot_source': snapshot.id}
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
api.nova.volume_snapshot_get(IsA(http.HttpRequest),
str(snapshot.id)).AndReturn(snapshot)
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
self.mox.ReplayAll()
url = reverse('horizon:nova:volumes:create')
res = self.client.post("?".join([url,
"snapshot_id=" + str(snapshot.id)]),
formData, follow=True)
self.assertEqual(res.redirect_chain, [])
self.assertFormError(res, 'form', None,
"The volume size cannot be less than the "
"snapshot size (40GB)")
@test.create_stubs({api: ('tenant_quota_usages', 'volume_snapshot_list')})
def test_create_volume_gb_used_over_alloted_quota(self):
usage = {'gigabytes': {'available': 100, 'used': 20}}
formData = {'name': u'This Volume Is Huge!',
@@ -59,6 +142,8 @@ class VolumeViewTests(test.TestCase):
'size': 5000}
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
api.volume_snapshot_list(IsA(http.HttpRequest)).\
AndReturn(self.volume_snapshots.list())
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
self.mox.ReplayAll()
@@ -70,7 +155,7 @@ class VolumeViewTests(test.TestCase):
' have 100GB of your quota available.']
self.assertEqual(res.context['form'].errors['__all__'], expected_error)
@test.create_stubs({api: ('tenant_quota_usages',)})
@test.create_stubs({api: ('tenant_quota_usages', 'volume_snapshot_list')})
def test_create_volume_number_over_alloted_quota(self):
usage = {'gigabytes': {'available': 100, 'used': 20},
'volumes': {'available': 0}}
@@ -80,6 +165,8 @@ class VolumeViewTests(test.TestCase):
'size': 10}
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
api.volume_snapshot_list(IsA(http.HttpRequest)).\
AndReturn(self.volume_snapshots.list())
api.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage)
self.mox.ReplayAll()

View File

@@ -96,7 +96,9 @@ class CreateSnapshotView(forms.ModalFormView):
success_url = reverse_lazy("horizon:nova:images_and_snapshots:index")
def get_context_data(self, **kwargs):
return {'volume_id': self.kwargs['volume_id']}
context = super(CreateSnapshotView, self).get_context_data(**kwargs)
context['volume_id'] = self.kwargs['volume_id']
return context
def get_initial(self):
return {'volume_id': self.kwargs["volume_id"]}

View File

@@ -1,7 +1,7 @@
/* Namespace for core functionality related to Forms. */
horizon.forms = {
handle_source_group: function() {
$(document).on("change", "#id_source_group", function (evt) {
$("div.table_wrapper, #modal_wrapper").on("change", "#id_source_group", function (evt) {
var $sourceGroup = $('#id_source_group'),
$cidrContainer = $('#id_cidr').closest(".control-group");
if($sourceGroup.val() === "") {
@@ -10,6 +10,16 @@ horizon.forms = {
$cidrContainer.addClass("hide");
}
});
},
handle_snapshot_source: function() {
$("div.table_wrapper, #modal_wrapper").on("change", "select#id_snapshot_source", function(evt) {
var $option = $(this).find("option:selected");
var $form = $(this).closest('form');
var $volName = $form.find('input#id_name');
$volName.val($option.data("display_name"));
var $volSize = $form.find('input#id_size');
$volSize.val($option.data("size"));
});
}
};
@@ -48,6 +58,7 @@ horizon.addInitFunction(function () {
horizon.modals.addModalInitFunction(horizon.forms.bind_add_item_handlers);
horizon.forms.handle_source_group();
horizon.forms.handle_snapshot_source();
// Bind event handlers to confirm dangerous actions.
$("body").on("click", "form button.btn-danger", function (evt) {
@@ -62,8 +73,8 @@ horizon.addInitFunction(function () {
var type = $(this).val();
$(this).closest('fieldset').find('input[type=text]').each(function(index, obj){
var label_val = "";
if ($(obj).attr("data-" + type)){
label_val = $(obj).attr("data-" + type);
if ($(obj).data(type)){
label_val = $(obj).data(type);
} else if ($(obj).attr("data")){
label_val = $(obj).attr("data");
} else

View File

@@ -1,8 +1,11 @@
import re
import netaddr
from django.core.exceptions import ValidationError
from django.forms import forms
from django.forms import forms, widgets
from django.utils.translation import ugettext as _
from django.utils.encoding import force_unicode
from django.utils.html import escape, conditional_escape
from django.utils.functional import Promise
ip_allowed_symbols_re = re.compile(r'^[a-fA-F0-9:/\.]+$')
IPv4 = 1
@@ -82,3 +85,44 @@ class IPField(forms.Field):
def clean(self, value):
super(IPField, self).clean(value)
return str(getattr(self, "ip", ""))
class SelectWidget(widgets.Select):
"""
Customizable select widget, that allows to render
data-xxx attributes from choices.
.. attribute:: data_attrs
Specifies object properties to serialize as
data-xxx attribute. If passed ('id', ),
this will be rendered as:
<option data-id="123">option_value</option>
where 123 is the value of choice_value.id
.. attribute:: transform
A callable used to render the display value
from the option object.
"""
def __init__(self, attrs=None, choices=(), data_attrs=(), transform=None):
self.data_attrs = data_attrs
self.transform = transform
super(SelectWidget, self).__init__(attrs, choices)
def render_option(self, selected_choices, option_value, option_label):
option_value = force_unicode(option_value)
other_html = (option_value in selected_choices) and \
u' selected="selected"' or ''
if not isinstance(option_label, (basestring, Promise)):
for data_attr in self.data_attrs:
data_value = conditional_escape(
force_unicode(getattr(option_label,
data_attr, "")))
other_html += ' data-%s="%s"' % (data_attr, data_value)
if self.transform:
option_label = self.transform(option_label)
return u'<option value="%s"%s>%s</option>' % (
escape(option_value), other_html,
conditional_escape(force_unicode(option_label)))