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 c785ed0088..d6158d52b9 100644 --- a/openstack_dashboard/dashboards/admin/volumes/volumes/forms.py +++ b/openstack_dashboard/dashboards/admin/volumes/volumes/forms.py @@ -201,6 +201,13 @@ class MigrateVolume(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'] @@ -212,8 +219,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 7eab545702..cfed8534d8 100644 --- a/openstack_dashboard/templates/horizon/_scripts.html +++ b/openstack_dashboard/templates/horizon/_scripts.html @@ -52,6 +52,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