Merge "Added action for creating a volume from snapshot"
This commit is contained in:
@@ -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):
|
||||
|
||||
@@ -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",)
|
||||
|
||||
@@ -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', [])
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
|
||||
Reference in New Issue
Block a user