From 84074ce0208baa039860c0636585e7f2b9024a0c Mon Sep 17 00:00:00 2001 From: Gloria Gu Date: Tue, 11 Nov 2014 11:50:44 -0800 Subject: [PATCH] Added volume type description for volume type Added the following features for admin volumes types 1. "Create Volume Type" dialog should include a description field. 2. The volume types table should include description column. 3. The volume types table name and description columns are in-line editable. 4. The 'Edit Volume Type' action is added for the volume type. User should be able to update volume type name and description. Added the following features in project volumes 1. "Create Volume" dialog will have description for the selected volume type when volume type select is available. 2. "No Volume type" will have some description as well. Implements: blueprint volume-type-description Change-Id: I7c8548756bcd3566873876bbc59f9b9c21d6846b --- horizon/static/horizon/js/horizon.volumes.js | 75 +++++++++++++++++++ openstack_dashboard/api/cinder.py | 32 +++++++- .../volume_types/_update_volume_type.html | 21 ++++++ .../volume_types/update_volume_type.html | 11 +++ .../admin/volumes/volume_types/forms.py | 40 +++++++++- .../admin/volumes/volume_types/tables.py | 74 +++++++++++++++++- .../admin/volumes/volume_types/tests.py | 40 ++++++++-- .../admin/volumes/volume_types/urls.py | 3 + .../admin/volumes/volume_types/views.py | 39 ++++++++++ .../dashboards/admin/volumes/volumes/forms.py | 13 +++- .../templates/volumes/volumes/_limits.html | 20 ++++- .../project/volumes/volumes/forms.py | 5 +- .../project/volumes/volumes/tests.py | 20 +++++ .../project/volumes/volumes/views.py | 43 +++++++++++ .../templates/horizon/_scripts.html | 1 + .../test/api_tests/cinder_tests.py | 32 ++++++++ .../test/test_data/cinder_data.py | 4 +- 17 files changed, 454 insertions(+), 19 deletions(-) create mode 100644 horizon/static/horizon/js/horizon.volumes.js create mode 100644 openstack_dashboard/dashboards/admin/volumes/templates/volumes/volume_types/_update_volume_type.html create mode 100644 openstack_dashboard/dashboards/admin/volumes/templates/volumes/volume_types/update_volume_type.html diff --git a/horizon/static/horizon/js/horizon.volumes.js b/horizon/static/horizon/js/horizon.volumes.js new file mode 100644 index 0000000000..09e6ac1255 --- /dev/null +++ b/horizon/static/horizon/js/horizon.volumes.js @@ -0,0 +1,75 @@ + +horizon.Volumes = { + selected_volume_type: null, + volume_types: [], + + initWithTypes: function(volume_types) { + this.volume_types = volume_types; + + this._attachInputHandlers(); + + this.getSelectedType(); + this.showTypeDescription(); + }, + + /* + *Returns the type object for the selected type in the form. + */ + getSelectedType: function() { + + this.selected_volume_type = $.grep(this.volume_types, function(type) { + var selected_name = $("#id_type").children(":selected").val(); + return type.name === selected_name; + })[0]; + + return this.selected_volume_type; + }, + + showTypeDescription: function() { + this.getSelectedType(); + + if (this.selected_volume_type) { + var description = this.selected_volume_type.description; + var name = this.selected_volume_type.name; + if (name === 'no_type') { + $("#id_show_volume_type_name").html(""); + } else { + $("#id_show_volume_type_name").html(name); + } + if (description) { + $("#id_show_volume_type_desc").html(description); + } else { + $("#id_show_volume_type_desc").html( + gettext('No description available.')); + } + } + }, + + toggleTypeDescription: function() { + var selected_volume_source = + $("#id_volume_source_type").children(":selected").val(); + if(selected_volume_source === 'volume_source' || + selected_volume_source === 'snapshot_source') { + $("#id_show_volume_type_desc_div").hide(); + } + else { + $("#id_show_volume_type_desc_div").show(); + } + }, + + _attachInputHandlers: function() { + var scope = this; + + var eventCallback_type = function() { + scope.showTypeDescription(); + }; + + $('#id_type').on('change', eventCallback_type); + + var eventCallback_volume_source_type = function() { + scope.toggleTypeDescription(); + }; + + $('#id_volume_source_type').on('change', eventCallback_volume_source_type); + } +}; diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py index f4506e1b52..78fbf5c0e0 100644 --- a/openstack_dashboard/api/cinder.py +++ b/openstack_dashboard/api/cinder.py @@ -448,6 +448,24 @@ def volume_type_list_with_qos_associations(request): return vol_types +def volume_type_get_with_qos_association(request, volume_type_id): + vol_type = volume_type_get(request, volume_type_id) + vol_type.associated_qos_spec = "" + + # get all currently defined qos specs + qos_specs = qos_spec_list(request) + for qos_spec in qos_specs: + # get all volume types this qos spec is associated with + assoc_vol_types = qos_spec_get_associations(request, qos_spec.id) + for assoc_vol_type in assoc_vol_types: + if vol_type.id == assoc_vol_type.id: + # update volume type to hold this association info + vol_type.associated_qos_spec = qos_spec.name + return vol_type + + return vol_type + + def default_quota_update(request, **kwargs): cinderclient(request).quota_classes.update(DEFAULT_QUOTA_NAME, **kwargs) @@ -456,8 +474,18 @@ def volume_type_list(request): return cinderclient(request).volume_types.list() -def volume_type_create(request, name): - return cinderclient(request).volume_types.create(name) +def volume_type_create(request, name, description=None): + return cinderclient(request).volume_types.create(name, description) + + +def volume_type_update(request, volume_type_id, name=None, description=None): + return cinderclient(request).volume_types.update(volume_type_id, + name, + description) + + +def volume_type_default(request): + return cinderclient(request).volume_types.default() def volume_type_delete(request, volume_type_id): diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volume_types/_update_volume_type.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volume_types/_update_volume_type.html new file mode 100644 index 0000000000..66e3428c43 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volume_types/_update_volume_type.html @@ -0,0 +1,21 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% load url from future %} + +{% block form_id %}{% endblock %} +{% block form_action %}{% url 'horizon:admin:volumes:volume_types:update_type' volume_type.id %}{% endblock %} + +{% block modal_id %}update_volume_type_modal{% endblock %} +{% block modal-header %}{% trans "Edit Volume Type" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description:" %}

+

{% trans "Modify volume type name and description." %}

+
+{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volume_types/update_volume_type.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volume_types/update_volume_type.html new file mode 100644 index 0000000000..ee7e4156c1 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/volume_types/update_volume_type.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Edit Volume Type" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Edit Volume Type") %} +{% endblock page_header %} + +{% block main %} + {% include 'admin/volumes/volume_types/_update_volume_type.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volumes/volume_types/forms.py b/openstack_dashboard/dashboards/admin/volumes/volume_types/forms.py index d5d8a9c966..d8668df32b 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volume_types/forms.py +++ b/openstack_dashboard/dashboards/admin/volumes/volume_types/forms.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. - from django.core.urlresolvers import reverse + from django.utils.translation import ugettext_lazy as _ from horizon import exceptions @@ -166,3 +166,41 @@ class EditQosSpecConsumer(forms.SelfHandlingForm): redirect = reverse("horizon:admin:volumes:index") exceptions.handle(request, _('Error editing QoS Spec consumer.'), redirect=redirect) + + +class EditVolumeType(forms.SelfHandlingForm): + name = forms.CharField(max_length=255, + label=_("Name")) + description = forms.CharField(max_length=255, + widget=forms.Textarea(attrs={'rows': 4}), + label=_("Description"), + required=False) + + def clean_name(self): + cleaned_name = self.cleaned_data['name'] + if len(cleaned_name.strip()) == 0: + msg = _('New name cannot be empty.') + self._errors['name'] = self.error_class([msg]) + + return cleaned_name + + def handle(self, request, data): + volume_type_id = self.initial['id'] + try: + cinder.volume_type_update(request, + volume_type_id, + data['name'], + data['description']) + message = _('Successfully updated volume type.') + messages.success(request, message) + return True + except Exception as ex: + redirect = reverse("horizon:admin:volumes:index") + if ex.code == 409: + error_message = _('New name conflicts with another ' + 'volume type.') + else: + error_message = _('Unable to update volume type.') + + exceptions.handle(request, error_message, + redirect=redirect) diff --git a/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py b/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py index eca43a5bdb..12b2c83a5b 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py +++ b/openstack_dashboard/dashboards/admin/volumes/volume_types/tables.py @@ -15,9 +15,11 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext_lazy from horizon import exceptions +from horizon import forms from horizon import tables from openstack_dashboard.api import cinder +from openstack_dashboard import policy class CreateVolumeType(tables.LinkAction): @@ -29,11 +31,21 @@ class CreateVolumeType(tables.LinkAction): policy_rules = (("volume", "volume_extension:types_manage"),) +class EditVolumeType(tables.LinkAction): + name = "edit" + verbose_name = _("Edit Volume Type") + url = "horizon:admin:volumes:volume_types:update_type" + classes = ("ajax-modal",) + icon = "pencil" + policy_rules = (("volume", "volume_extension:types_manage"),) + + class ViewVolumeTypeExtras(tables.LinkAction): name = "extras" verbose_name = _("View Extra Specs") url = "horizon:admin:volumes:volume_types:extras:index" - classes = ("btn-edit",) + classes = ("ajax-modal",) + icon = "pencil" policy_rules = (("volume", "volume_extension:types_manage"),) @@ -142,8 +154,64 @@ class VolumeTypesFilterAction(tables.FilterAction): if query in volume_type.name.lower()] +class UpdateRow(tables.Row): + ajax = True + + def get_data(self, request, volume_type_id): + try: + volume_type = \ + cinder.volume_type_get_with_qos_association(request, + volume_type_id) + except Exception: + exceptions.handle(request, + _('Unable to retrieve volume type qos.')) + return volume_type + + +class UpdateCell(tables.UpdateAction): + def allowed(self, request, volume_type, cell): + return policy.check( + ("volume_extension", "volume_extension:types_manage"), request) + + def update_cell(self, request, data, volume_type_id, + cell_name, new_cell_value): + # inline update volume type name and/or description + try: + vol_type_obj = data + # updating changed value by new value + setattr(vol_type_obj, cell_name, new_cell_value) + name_value = getattr(vol_type_obj, 'name', None) + desc_value = getattr(vol_type_obj, 'description', None) + + cinder.volume_type_update( + request, + volume_type_id, + name=name_value, + description=desc_value) + except Exception as ex: + if ex.code and ex.code == 409: + error_message = _('New name conflicts with another ' + 'volume type.') + else: + error_message = _('Unable to update the volume type.') + exceptions.handle(request, error_message) + return False + + return True + + class VolumeTypesTable(tables.DataTable): - name = tables.Column("name", verbose_name=_("Name")) + name = tables.Column("name", verbose_name=_("Name"), + form_field=forms.CharField( + max_length=64, required=True), + update_action=UpdateCell) + description = tables.Column(lambda obj: getattr(obj, 'description', None), + verbose_name=_('Description'), + form_field=forms.CharField( + widget=forms.Textarea(attrs={'rows': 4}), + required=False), + update_action=UpdateCell) + assoc_qos_spec = tables.Column("associated_qos_spec", verbose_name=_("Associated QoS Spec")) encryption = tables.Column(get_volume_type_encryption, @@ -166,8 +234,10 @@ class VolumeTypesTable(tables.DataTable): row_actions = (CreateVolumeTypeEncryption, ViewVolumeTypeExtras, ManageQosSpecAssociation, + EditVolumeType, DeleteVolumeTypeEncryption, DeleteVolumeType,) + row_class = UpdateRow # QOS Specs section of panel diff --git a/openstack_dashboard/dashboards/admin/volumes/volume_types/tests.py b/openstack_dashboard/dashboards/admin/volumes/volume_types/tests.py index df122acbdc..68ee75784d 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volume_types/tests.py +++ b/openstack_dashboard/dashboards/admin/volumes/volume_types/tests.py @@ -23,18 +23,42 @@ from openstack_dashboard.test import helpers as test class VolumeTypeTests(test.BaseAdminViewTests): @test.create_stubs({cinder: ('volume_type_create',)}) def test_create_volume_type(self): - formData = {'name': 'volume type 1'} - cinder.volume_type_create(IsA(http.HttpRequest), - formData['name']).\ - AndReturn(self.volume_types.first()) + formData = {'name': 'volume type 1', + 'vol_type_description': 'test desc'} + cinder.volume_type_create( + IsA(http.HttpRequest), + formData['name'], + formData['vol_type_description']).AndReturn( + self.volume_types.first()) self.mox.ReplayAll() res = self.client.post( reverse('horizon:admin:volumes:volume_types:create_type'), formData) - - redirect = reverse('horizon:admin:volumes:volume_types_tab') self.assertNoFormErrors(res) + redirect = reverse('horizon:admin:volumes:volume_types_tab') + self.assertRedirectsNoFollow(res, redirect) + + @test.create_stubs({cinder: ('volume_type_get', + 'volume_type_update')}) + def test_update_volume_type(self): + volume_type = self.cinder_volume_types.first() + formData = {'name': volume_type.name, + 'description': 'test desc updated'} + volume_type = cinder.volume_type_get( + IsA(http.HttpRequest), volume_type.id).AndReturn(volume_type) + cinder.volume_type_update( + IsA(http.HttpRequest), + volume_type.id, + formData['name'], + formData['description']).AndReturn(volume_type) + self.mox.ReplayAll() + + url = reverse('horizon:admin:volumes:volume_types:update_type', + args=[volume_type.id]) + res = self.client.post(url, formData) + self.assertNoFormErrors(res) + redirect = reverse('horizon:admin:volumes:volume_types_tab') self.assertRedirectsNoFollow(res, redirect) @test.create_stubs({api.nova: ('server_list',), @@ -45,7 +69,7 @@ class VolumeTypeTests(test.BaseAdminViewTests): 'volume_encryption_type_list'), keystone: ('tenant_list',)}) def test_delete_volume_type(self): - volume_type = self.volume_types.first() + volume_type = self.cinder_volume_types.first() formData = {'action': 'volume_types__delete__%s' % volume_type.id} encryption_list = (self.cinder_volume_encryption_types.list()[0], self.cinder_volume_encryption_types.list()[1]) @@ -58,7 +82,7 @@ class VolumeTypeTests(test.BaseAdminViewTests): cinder.volume_encryption_type_list(IsA(http.HttpRequest))\ .AndReturn(encryption_list) cinder.volume_type_delete(IsA(http.HttpRequest), - str(volume_type.id)) + volume_type.id) self.mox.ReplayAll() res = self.client.post( diff --git a/openstack_dashboard/dashboards/admin/volumes/volume_types/urls.py b/openstack_dashboard/dashboards/admin/volumes/volume_types/urls.py index e717b00016..65dff3e30b 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volume_types/urls.py +++ b/openstack_dashboard/dashboards/admin/volumes/volume_types/urls.py @@ -27,6 +27,9 @@ urlpatterns = patterns( 'VIEWS_MOD', url(r'^create_type$', views.CreateVolumeTypeView.as_view(), name='create_type'), + url(r'^(?P[^/]+)/update_type/$', + views.EditVolumeTypeView.as_view(), + name='update_type'), url(r'^create_qos_spec$', views.CreateQosSpecView.as_view(), name='create_qos_spec'), url(r'^(?P[^/]+)/manage_qos_spec_association/$', diff --git a/openstack_dashboard/dashboards/admin/volumes/volume_types/views.py b/openstack_dashboard/dashboards/admin/volumes/volume_types/views.py index b760b3bad3..ca1d1fe275 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volume_types/views.py +++ b/openstack_dashboard/dashboards/admin/volumes/volume_types/views.py @@ -116,6 +116,45 @@ class CreateVolumeTypeEncryptionView(forms.ModalFormView): 'volume_type_id': self.kwargs['volume_type_id']} +class EditVolumeTypeView(forms.ModalFormView): + form_class = volume_types_forms.EditVolumeType + template_name = 'admin/volumes/volume_types/update_volume_type.html' + success_url = 'horizon:admin:volumes:volume_types_tab' + cancel_url = 'horizon:admin:volumes:volume_types_tab' + submit_label = _('Edit') + + def get_success_url(self): + return reverse(self.success_url) + + @memoized.memoized_method + def get_data(self): + try: + volume_type_id = self.kwargs['type_id'] + volume_type = api.cinder.volume_type_get(self.request, + volume_type_id) + except Exception: + error_message = _( + 'Unable to retrieve volume type for: "%s"') \ + % volume_type_id + exceptions.handle(self.request, + error_message, + redirect=self.success_url) + + return volume_type + + def get_context_data(self, **kwargs): + context = super(EditVolumeTypeView, self).get_context_data(**kwargs) + context['volume_type'] = self.get_data() + + return context + + def get_initial(self): + volume_type = self.get_data() + return {'id': self.kwargs['type_id'], + 'name': volume_type.name, + 'description': getattr(volume_type, 'description', "")} + + class CreateQosSpecView(forms.ModalFormView): form_class = volumes_forms.CreateQosSpec modal_header = _("Create QoS Spec") diff --git a/openstack_dashboard/dashboards/admin/volumes/volumes/forms.py b/openstack_dashboard/dashboards/admin/volumes/volumes/forms.py index 343142bb59..6c99887fa8 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volumes/forms.py +++ b/openstack_dashboard/dashboards/admin/volumes/volumes/forms.py @@ -152,6 +152,13 @@ class UnmanageVolume(forms.SelfHandlingForm): class CreateVolumeType(forms.SelfHandlingForm): name = forms.CharField(max_length=255, label=_("Name")) + vol_type_description = forms.CharField( + max_length=255, + widget=forms.Textarea( + attrs={'class': 'modal-body-fixed-width', + 'rows': 4}), + label=_("Description"), + required=False) def clean_name(self): cleaned_name = self.cleaned_data['name'] @@ -163,8 +170,10 @@ class CreateVolumeType(forms.SelfHandlingForm): def handle(self, request, data): try: # Remove any new lines in the public key - volume_type = cinder.volume_type_create(request, - data['name']) + volume_type = cinder.volume_type_create( + request, + data['name'], + data['vol_type_description']) messages.success(request, _('Successfully created volume type: %s') % data['name']) return volume_type diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_limits.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_limits.html index 27ad123065..587888a2b0 100644 --- a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_limits.html +++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_limits.html @@ -2,7 +2,16 @@

{% trans "Description:" %}

-

{% block title %}{% trans "Volumes are block devices that can be attached to instances." %}{% endblock %}

+

{% blocktrans %} + Volumes are block devices that can be attached to instances. + {% endblocktrans %} +

+ +
+

{% trans "Volume Type Description:" %}

+

+

+

{% block head %}{% trans "Volume Limits" %}{% endblock %}

@@ -22,7 +31,6 @@
- diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/forms.py b/openstack_dashboard/dashboards/project/volumes/volumes/forms.py index 3c07cf84e7..7a39a5f41d 100644 --- a/openstack_dashboard/dashboards/project/volumes/volumes/forms.py +++ b/openstack_dashboard/dashboards/project/volumes/volumes/forms.py @@ -265,7 +265,7 @@ class CreateForm(forms.SelfHandlingForm): def __init__(self, request, *args, **kwargs): super(CreateForm, self).__init__(request, *args, **kwargs) volume_types = cinder.volume_type_list(request) - self.fields['type'].choices = [("", _("No volume type"))] + \ + self.fields['type'].choices = [("no_type", _("No volume type"))] + \ [(type.name, type.name) for type in volume_types] @@ -379,6 +379,9 @@ class CreateForm(forms.SelfHandlingForm): metadata = {} + if data['type'] == 'no_type': + data['type'] = '' + volume = cinder.volume_create(request, data['size'], data['name'], diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py index ae8d0d82f1..7b63ebaf42 100644 --- a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py +++ b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py @@ -103,6 +103,7 @@ class VolumeViewTests(test.TestCase): url = reverse('horizon:project:volumes:volumes:create') res = self.client.post(url, formData) + self.assertNoFormErrors(res) redirect_url = VOLUME_VOLUMES_TAB_URL self.assertRedirectsNoFollow(res, redirect_url) @@ -436,6 +437,7 @@ class VolumeViewTests(test.TestCase): @test.create_stubs({cinder: ('volume_snapshot_get', 'volume_type_list', + 'volume_type_default', 'volume_get'), api.glance: ('image_list_detailed',), quotas: ('tenant_limit_usages',)}) @@ -452,6 +454,10 @@ class VolumeViewTests(test.TestCase): cinder.volume_type_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_types.list()) + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) + cinder.volume_type_default(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.first()) quotas.tenant_limit_usages(IsA(http.HttpRequest)).\ AndReturn(usage_limit) cinder.volume_snapshot_get(IsA(http.HttpRequest), @@ -600,6 +606,7 @@ class VolumeViewTests(test.TestCase): self.assertRedirectsNoFollow(res, redirect_url) @test.create_stubs({cinder: ('volume_type_list', + 'volume_type_default', 'availability_zone_list', 'extension_supported'), api.glance: ('image_get', @@ -618,6 +625,10 @@ class VolumeViewTests(test.TestCase): cinder.volume_type_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_types.list()) + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) + cinder.volume_type_default(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.first()) quotas.tenant_limit_usages(IsA(http.HttpRequest)).\ AndReturn(usage_limit) api.glance.image_get(IsA(http.HttpRequest), @@ -664,6 +675,8 @@ class VolumeViewTests(test.TestCase): 'method': u'CreateForm', 'size': 5, 'image_source': image.id} + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) cinder.volume_type_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_types.list()) quotas.tenant_limit_usages(IsA(http.HttpRequest)).\ @@ -701,6 +714,7 @@ class VolumeViewTests(test.TestCase): @test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list', + 'volume_type_default', 'volume_list', 'availability_zone_list', 'extension_supported'), @@ -718,6 +732,10 @@ class VolumeViewTests(test.TestCase): cinder.volume_type_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_types.list()) + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) + cinder.volume_type_default(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.first()) quotas.tenant_limit_usages(IsA(http.HttpRequest)).\ AndReturn(usage_limit) cinder.volume_snapshot_list(IsA(http.HttpRequest), @@ -768,6 +786,8 @@ class VolumeViewTests(test.TestCase): 'method': u'CreateForm', 'size': 10} + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) cinder.volume_type_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_types.list()) quotas.tenant_limit_usages(IsA(http.HttpRequest)).\ diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/volumes/views.py index 1a0947ac1c..257eb255f0 100644 --- a/openstack_dashboard/dashboards/project/volumes/volumes/views.py +++ b/openstack_dashboard/dashboards/project/volumes/volumes/views.py @@ -16,8 +16,11 @@ Views for managing volumes. """ +import json + from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse_lazy +from django.utils import encoding from django.utils.translation import ugettext_lazy as _ from django.views import generic @@ -29,6 +32,7 @@ from horizon.utils import memoized from openstack_dashboard import api from openstack_dashboard.api import cinder +from openstack_dashboard import exceptions as dashboard_exception from openstack_dashboard.usage import quotas from openstack_dashboard.dashboards.project.volumes \ @@ -97,10 +101,49 @@ class CreateView(forms.ModalFormView): context = super(CreateView, self).get_context_data(**kwargs) try: context['usages'] = quotas.tenant_limit_usages(self.request) + context['volume_types'] = self._get_volume_types() except Exception: exceptions.handle(self.request) return context + def _get_volume_types(self): + try: + volume_types = cinder.volume_type_list(self.request) + except Exception: + exceptions.handle(self.request, + _('Unable to retrieve volume type list.')) + + # check if we have default volume type so we can present the + # description of no volume type differently + default_type = None + try: + default_type = cinder.volume_type_default(self.request) + except dashboard_exception.NOT_FOUND: + pass + + if default_type is not None: + d_name = getattr(default_type, "name", "") + message =\ + _("If \"No volume type\" is selected, the default " + "volume type \"%(name)s\" will be set for the " + "created volume.") + params = {'name': d_name} + no_type_description = encoding.force_text(message % params) + else: + message = \ + _("If \"No volume type\" is selected, the volume will be " + "created without a volume type.") + + no_type_description = encoding.force_text(message) + + type_descriptions = [{'name': 'no_type', + 'description': no_type_description}] + \ + [{'name': type.name, + 'description': getattr(type, "description", "")} + for type in volume_types] + + return json.dumps(type_descriptions) + class ExtendView(forms.ModalFormView): form_class = project_forms.ExtendForm diff --git a/openstack_dashboard/templates/horizon/_scripts.html b/openstack_dashboard/templates/horizon/_scripts.html index 1ac8a485e7..69100499be 100644 --- a/openstack_dashboard/templates/horizon/_scripts.html +++ b/openstack_dashboard/templates/horizon/_scripts.html @@ -53,6 +53,7 @@ + {% for file in HORIZON_CONFIG.js_files %} diff --git a/openstack_dashboard/test/api_tests/cinder_tests.py b/openstack_dashboard/test/api_tests/cinder_tests.py index 35ce79ea4f..85202bcfe8 100644 --- a/openstack_dashboard/test/api_tests/cinder_tests.py +++ b/openstack_dashboard/test/api_tests/cinder_tests.py @@ -94,6 +94,28 @@ class CinderApiTests(test.APITestCase): associate_spec = assoc_vol_types[0].associated_qos_spec self.assertTrue(associate_spec, qos_specs_only_one[0].name) + def test_volume_type_get_with_qos_association(self): + volume_type = self.cinder_volume_types.first() + qos_specs_full = self.cinder_qos_specs.list() + qos_specs_only_one = [qos_specs_full[0]] + associations = self.cinder_qos_spec_associations.list() + + cinderclient = self.stub_cinderclient() + cinderclient.volume_types = self.mox.CreateMockAnything() + cinderclient.volume_types.get(volume_type.id).AndReturn(volume_type) + cinderclient.qos_specs = self.mox.CreateMockAnything() + cinderclient.qos_specs.list().AndReturn(qos_specs_only_one) + cinderclient.qos_specs.get_associations = self.mox.CreateMockAnything() + cinderclient.qos_specs.get_associations(qos_specs_only_one[0].id).\ + AndReturn(associations) + self.mox.ReplayAll() + + assoc_vol_type = \ + api.cinder.volume_type_get_with_qos_association(self.request, + volume_type.id) + associate_spec = assoc_vol_type.associated_qos_spec + self.assertTrue(associate_spec, qos_specs_only_one[0].name) + def test_absolute_limits_with_negative_values(self): values = {"maxTotalVolumes": -1, "totalVolumesUsed": -1} expected_results = {"maxTotalVolumes": float("inf"), @@ -126,6 +148,16 @@ class CinderApiTests(test.APITestCase): # No assertions are necessary. Verification is handled by mox. api.cinder.pool_list(self.request, detailed=True) + def test_volume_type_default(self): + volume_type = self.cinder_volume_types.first() + cinderclient = self.stub_cinderclient() + cinderclient.volume_types = self.mox.CreateMockAnything() + cinderclient.volume_types.default().AndReturn(volume_type) + self.mox.ReplayAll() + + default_volume_type = api.cinder.volume_type_default(self.request) + self.assertEqual(default_volume_type, volume_type) + class CinderApiVersionTests(test.TestCase): diff --git a/openstack_dashboard/test/test_data/cinder_data.py b/openstack_dashboard/test/test_data/cinder_data.py index 2f014c4ae9..360ba84b21 100644 --- a/openstack_dashboard/test/test_data/cinder_data.py +++ b/openstack_dashboard/test/test_data/cinder_data.py @@ -144,10 +144,12 @@ def data(TEST): vol_type1 = volume_types.VolumeType(volume_types.VolumeTypeManager(None), {'id': u'1', 'name': u'vol_type_1', + 'description': 'type 1 description', 'extra_specs': {'foo': 'bar'}}) vol_type2 = volume_types.VolumeType(volume_types.VolumeTypeManager(None), {'id': u'2', - 'name': u'vol_type_2'}) + 'name': u'vol_type_2', + 'description': 'type 2 description'}) TEST.cinder_volume_types.add(vol_type1, vol_type2) # Volumes - Cinder v2