Add volume extend functionality support

Cinder now has the ablity to extend a volume, This functionality
should be exposed through Horizon.

Change-Id: Iff26453fd0a83af97fad4adabbcd53f646a15e51
Implements: blueprint volume-extend
This commit is contained in:
Zhenguo Niu 2013-12-19 13:09:04 +08:00 committed by niu-zglinux
parent fdbf85c30a
commit 3c68470cfc
9 changed files with 206 additions and 2 deletions

View File

@ -101,6 +101,10 @@ def volume_create(request, size, name, description, volume_type,
availability_zone=availability_zone)
def volume_extend(request, volume_id, new_size):
return cinderclient(request).volumes.extend(volume_id, new_size)
def volume_delete(request, volume_id):
return cinderclient(request).volumes.delete(volume_id)

View File

@ -421,3 +421,36 @@ class UpdateForm(forms.SelfHandlingForm):
exceptions.handle(request,
_('Unable to update volume.'),
redirect=redirect)
class ExtendForm(forms.SelfHandlingForm):
name = forms.CharField(label=_("Volume Name"),
widget=forms.TextInput(
attrs={'readonly': 'readonly'}
))
new_size = forms.IntegerField(min_value=1, label=_("Size (GB)"))
def clean(self):
cleaned_data = super(ExtendForm, self).clean()
new_size = cleaned_data.get('new_size', 1)
if new_size <= self.initial['orig_size']:
raise ValidationError(
_("New size for extend must be greater than current size."))
return cleaned_data
def handle(self, request, data):
volume_id = self.initial['id']
try:
volume = cinder.volume_extend(request,
volume_id,
data['new_size'])
message = _('Successfully extended volume: "%s"') % data['name']
messages.success(request, message)
return volume
except Exception:
redirect = reverse("horizon:project:volumes:index")
exceptions.handle(request,
_('Unable to extend volume.'),
redirect=redirect)

View File

@ -32,7 +32,7 @@ from openstack_dashboard import policy
from openstack_dashboard.usage import quotas
DELETABLE_STATES = ("available", "error")
DELETABLE_STATES = ("available", "error", "error_extending")
class DeleteVolume(tables.DeleteAction):
@ -86,6 +86,23 @@ class CreateVolume(tables.LinkAction):
return True
class ExtendVolume(tables.LinkAction):
name = "extend"
verbose_name = _("Extend Volume")
url = "horizon:project:volumes:extend"
classes = ("ajax-modal", "btn-extend")
policy_rules = (("volume", "volume:extend"),)
def get_policy_target(self, request, datum=None):
project_id = None
if datum:
project_id = getattr(datum, "os-vol-tenant-attr:tenant_id", None)
return {"project_id": project_id}
def allowed(self, request, volume=None):
return volume.status in ("available", "in-use")
class EditAttachments(tables.LinkAction):
name = "attachments"
verbose_name = _("Edit Attachments")
@ -255,7 +272,7 @@ class VolumesTable(VolumesTableBase):
status_columns = ["status"]
row_class = UpdateRow
table_actions = (CreateVolume, DeleteVolume, VolumesFilterAction)
row_actions = (EditAttachments, EditVolume,
row_actions = (EditVolume, ExtendVolume, EditAttachments,
CreateSnapshot, DeleteVolume)

View File

@ -0,0 +1,26 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}
{% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:project:volumes:extend' volume.id%}{% endblock %}
{% block modal_id %}extend_volume_modal{% endblock %}
{% block modal-header %}{% trans "Extend Volume" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right quota-dynamic">
{% include "project/volumes/_extend_limits.html" with usages=usages %}
</div>
{% endblock %}
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Extend Volume" %}" />
<a href="{% url 'horizon:project:volumes:index' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% load i18n horizon humanize %}
<h3>{% trans "Description" %}:</h3>
<p>{% trans "From here you can extend the size of a volume." %}</p>
<h3>{% trans "Volume Limits" %}</h3>
<div class="quota_title clearfix">
<strong>{% trans "Total Gigabytes" %} <span>({{ usages.gigabytesUsed|intcomma }} {% trans "GB" %})</span></strong>
<p>{{ usages.maxTotalVolumeGigabytes|quota:_("GB")|intcomma }}</p>
</div>
<div id="quota_size" data-progress-indicator-for="id_new_size" data-quota-limit="{{ usages.maxTotalVolumeGigabytes }}" data-quota-used="{{ usages.gigabytesUsed }}" class="quota_bar">
</div>
<script type="text/javascript" charset="utf-8">
if(typeof horizon.Quota !== 'undefined') {
horizon.Quota.init();
} else {
addHorizonLoadEvent(function() {
horizon.Quota.init();
});
}
</script>

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Extend Volume" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Extend Volume") %}
{% endblock page_header %}
{% block main %}
{% include 'project/volumes/_extend.html' %}
{% endblock %}

View File

@ -859,3 +859,53 @@ class VolumeViewTests(test.TestCase):
args=[volume.id])
res = self.client.post(url, formData)
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
@test.create_stubs({cinder: ('volume_get',
'volume_extend')})
def test_extend_volume(self):
volume = self.volumes.first()
formData = {'name': u'A Volume I Am Making',
'orig_size': volume.size,
'new_size': 100}
cinder.volume_get(IsA(http.HttpRequest), volume.id).\
AndReturn(self.volumes.first())
cinder.volume_extend(IsA(http.HttpRequest),
volume.id,
formData['new_size']).AndReturn(volume)
self.mox.ReplayAll()
url = reverse('horizon:project:volumes:extend',
args=[volume.id])
res = self.client.post(url, formData)
redirect_url = reverse('horizon:project:volumes:index')
self.assertRedirectsNoFollow(res, redirect_url)
@test.create_stubs({cinder: ('volume_get',),
quotas: ('tenant_limit_usages',)})
def test_extend_volume_with_wrong_size(self):
volume = self.volumes.first()
usage_limit = {'maxTotalVolumeGigabytes': 100,
'gigabytesUsed': 20,
'volumesUsed': len(self.volumes.list()),
'maxTotalVolumes': 6}
formData = {'name': u'A Volume I Am Making',
'orig_size': volume.size,
'new_size': 10}
cinder.volume_get(IsA(http.HttpRequest), volume.id).\
AndReturn(self.volumes.first())
quotas.tenant_limit_usages(IsA(http.HttpRequest)).\
AndReturn(usage_limit)
self.mox.ReplayAll()
url = reverse('horizon:project:volumes:extend',
args=[volume.id])
res = self.client.post(url, formData)
self.assertFormError(res, 'form', None,
"New size for extend must be greater than "
"current size.")

View File

@ -23,6 +23,9 @@ from openstack_dashboard.dashboards.project.volumes import views
urlpatterns = patterns('openstack_dashboard.dashboards.project.volumes.views',
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<volume_id>[^/]+)/extend/$',
views.ExtendView.as_view(),
name='extend'),
url(r'^(?P<volume_id>[^/]+)/attach/$',
views.EditAttachmentsView.as_view(),
name='attach'),

View File

@ -132,6 +132,41 @@ class CreateView(forms.ModalFormView):
return context
class ExtendView(forms.ModalFormView):
form_class = project_forms.ExtendForm
template_name = 'project/volumes/extend.html'
success_url = reverse_lazy("horizon:project:volumes:index")
def get_object(self):
if not hasattr(self, "_object"):
volume_id = self.kwargs['volume_id']
try:
self._object = cinder.volume_get(self.request, volume_id)
except Exception:
self._object = None
exceptions.handle(self.request,
_('Unable to retrieve volume information.'))
return self._object
def get_context_data(self, **kwargs):
context = super(ExtendView, self).get_context_data(**kwargs)
context['volume'] = self.get_object()
try:
usages = quotas.tenant_limit_usages(self.request)
usages['gigabytesUsed'] = (usages['gigabytesUsed']
- context['volume'].size)
context['usages'] = usages
except Exception:
exceptions.handle(self.request)
return context
def get_initial(self):
volume = self.get_object()
return {'id': self.kwargs['volume_id'],
'name': volume.display_name,
'orig_size': volume.size}
class CreateSnapshotView(forms.ModalFormView):
form_class = project_forms.CreateSnapshotForm
template_name = 'project/volumes/create_snapshot.html'