From 057d891f31f3901f7f933d12401aebc53759aff0 Mon Sep 17 00:00:00 2001 From: Tihomir Trifonov Date: Thu, 25 Oct 2012 14:48:27 +0300 Subject: [PATCH] Added support for volume types As cinder already supports volume types, it's time to be added in Horizon. The types are added in admin panel. A volume can either have a *valid* type, or None. There are two minor issues: 1. When a type is deleted, if there is a volume with this type, the type is returned as the id, instead of the name. Which is good as consistency, but maybe a type should not be deleted if used by a volume? 2. If no vol type is passed(None) value, the type is being assigned as 'None', and returned as a string('None') 3. In the create volume type form, the 'Description' is empty at the moment, I couldn't find any help info to add for volume types... Implements blueprint volume-types Tested with n-cinder and n-vol services. Since Folsom both support volume-types. I'm not sure if it is possible a grizzly Dashboard to work with essex nova, which could be a problem. Dashboard doesn't seem to provide permissions based on tables, so if it needs to check if a service is available to show/hide volume types, this will need some more effors to implement permissions on table/action level. Or maybe this needs to be added as a standalone panel? Patch set 2: implemented suggestions, added one more thing: When creating a volume from a snapshot, set the volume type initial as the type of the original volume from which the snapshot is being created. Patch Set 3: Updated the description of volume types, based on the notes from: https://etherpad.openstack.org/grizzly-cinder-volumetypes https://etherpad.openstack.org/cinder-usecases Any feedback on the description is welcome. Change-Id: Ib0c136c5c8cd9fbd34ce1dd346260f404c96f667 --- openstack_dashboard/api/cinder.py | 18 +- .../dashboards/admin/volumes/forms.py | 44 +++++ .../dashboards/admin/volumes/tables.py | 33 ++++ .../volumes/_create_volume_type.html | 29 ++++ .../templates/volumes/create_volume_type.html | 11 ++ .../volumes/templates/volumes/index.html | 10 +- .../dashboards/admin/volumes/tests.py | 54 +++++- .../dashboards/admin/volumes/urls.py | 3 +- .../dashboards/admin/volumes/views.py | 36 +++- .../volume_snapshots/tests.py | 14 +- .../dashboards/project/volumes/forms.py | 43 +++-- .../dashboards/project/volumes/tables.py | 12 +- .../dashboards/project/volumes/tests.py | 157 +++++++++++------- .../dashboards/project/volumes/views.py | 18 +- .../test/test_data/nova_data.py | 15 +- 15 files changed, 389 insertions(+), 108 deletions(-) create mode 100644 openstack_dashboard/dashboards/admin/volumes/forms.py create mode 100644 openstack_dashboard/dashboards/admin/volumes/templates/volumes/_create_volume_type.html create mode 100644 openstack_dashboard/dashboards/admin/volumes/templates/volumes/create_volume_type.html diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py index d0beae649c..d083f00b79 100644 --- a/openstack_dashboard/api/cinder.py +++ b/openstack_dashboard/api/cinder.py @@ -77,9 +77,11 @@ def volume_get(request, volume_id): return volume_data -def volume_create(request, size, name, description, snapshot_id=None): +def volume_create(request, size, name, description, volume_type, + snapshot_id=None): return cinderclient(request).volumes.create(size, display_name=name, - display_description=description, snapshot_id=snapshot_id) + display_description=description, volume_type=volume_type, + snapshot_id=snapshot_id) def volume_delete(request, volume_id): @@ -113,3 +115,15 @@ def tenant_quota_update(request, tenant_id, **kwargs): def default_quota_get(request, tenant_id): return QuotaSet(cinderclient(request).quotas.defaults(tenant_id)) + + +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_delete(request, volume_type_id): + return cinderclient(request).volume_types.delete(volume_type_id) diff --git a/openstack_dashboard/dashboards/admin/volumes/forms.py b/openstack_dashboard/dashboards/admin/volumes/forms.py new file mode 100644 index 0000000000..9d0886fc3c --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/forms.py @@ -0,0 +1,44 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2012 United States Government as represented by the +# Administrator of the National Aeronautics and Space Administration. +# All Rights Reserved. +# +# Copyright 2012 Nebula, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import messages + +from openstack_dashboard.api import cinder + + +class CreateVolumeType(forms.SelfHandlingForm): + name = forms.CharField(max_length="255", label=_("Name")) + + def handle(self, request, data): + try: + # Remove any new lines in the public key + volume_type = cinder.volume_type_create(request, + data['name']) + messages.success(request, _('Successfully created volume type: %s') + % data['name']) + return volume_type + except: + exceptions.handle(request, + _('Unable to create volume type.')) + return False diff --git a/openstack_dashboard/dashboards/admin/volumes/tables.py b/openstack_dashboard/dashboards/admin/volumes/tables.py index 1337436957..def5bb2f02 100644 --- a/openstack_dashboard/dashboards/admin/volumes/tables.py +++ b/openstack_dashboard/dashboards/admin/volumes/tables.py @@ -1,10 +1,26 @@ from django.utils.translation import ugettext_lazy as _ from horizon import tables +from openstack_dashboard.api import cinder from openstack_dashboard.dashboards.project.volumes.tables import (UpdateRow, VolumesTable as _VolumesTable, DeleteVolume) +class CreateVolumeType(tables.LinkAction): + name = "create" + verbose_name = _("Create Volume Type") + url = "horizon:admin:volumes:create_type" + classes = ("ajax-modal", "btn-create") + + +class DeleteVolumeType(tables.DeleteAction): + data_type_singular = _("Volume Type") + data_type_plural = _("Volume Types") + + def delete(self, request, obj_id): + cinder.volume_type_delete(request, obj_id) + + class VolumesTable(_VolumesTable): name = tables.Column("display_name", verbose_name=_("Name"), @@ -17,3 +33,20 @@ class VolumesTable(_VolumesTable): row_class = UpdateRow table_actions = (DeleteVolume,) row_actions = (DeleteVolume,) + + +class VolumeTypesTable(tables.DataTable): + name = tables.Column("name", + verbose_name=_("Name")) + + def get_object_display(self, vol_type): + return vol_type.name + + def get_object_id(self, vol_type): + return str(vol_type.id) + + class Meta: + name = "volume_types" + verbose_name = _("Volume Types") + table_actions = (CreateVolumeType, DeleteVolumeType,) + row_actions = (DeleteVolumeType,) diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/_create_volume_type.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/_create_volume_type.html new file mode 100644 index 0000000000..9a51878e49 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/_create_volume_type.html @@ -0,0 +1,29 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} + +{% block form_id %}{% endblock %} +{% block form_action %}{% url horizon:admin:volumes:create_type %}{% endblock %} + +{% block modal_id %}create_volume_type_modal{% endblock %} +{% block modal-header %}{% trans "Create Volume Type" %}{% endblock %} + +{% block modal-body %} +
+
+ {% include "horizon/common/_form_fields.html" %} +
+
+
+

{% trans "Description" %}:

+

{% blocktrans %} + The volume type defines the characteristics of a volume. + It usually maps to a set of capabilities of the storage back-end driver to be used for this volume. + Examples: "Performance", "SSD", "Backup", etc. + {% endblocktrans %}

+
+{% endblock %} + +{% block modal-footer %} + + {% trans "Cancel" %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/create_volume_type.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/create_volume_type.html new file mode 100644 index 0000000000..d60b695d55 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/create_volume_type.html @@ -0,0 +1,11 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Create Volume Type" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Create a Volume Type") %} +{% endblock page_header %} + +{% block main %} + {% include 'admin/volumes/_create_volume_type.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/index.html b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/index.html index 4079378aad..531571dec1 100644 --- a/openstack_dashboard/dashboards/admin/volumes/templates/volumes/index.html +++ b/openstack_dashboard/dashboards/admin/volumes/templates/volumes/index.html @@ -7,7 +7,11 @@ {% endblock page_header %} {% block main %} - {{ table.render }} +
+ {{ volumes_table.render }} +
+ +
+ {{ volume_types_table.render }} +
{% endblock %} - - diff --git a/openstack_dashboard/dashboards/admin/volumes/tests.py b/openstack_dashboard/dashboards/admin/volumes/tests.py index fd0dafa589..f9ae005ca1 100644 --- a/openstack_dashboard/dashboards/admin/volumes/tests.py +++ b/openstack_dashboard/dashboards/admin/volumes/tests.py @@ -19,15 +19,21 @@ from django.core.urlresolvers import reverse from mox import IsA from openstack_dashboard import api +from openstack_dashboard.api import cinder from openstack_dashboard.test import helpers as test class VolumeTests(test.BaseAdminViewTests): - @test.create_stubs({api: ('server_list', 'volume_list',)}) + @test.create_stubs({api.nova: ('server_list',), + cinder: ('volume_list', + 'volume_type_list',)}) def test_index(self): - api.volume_list(IsA(http.HttpRequest), search_opts={ - 'all_tenants': 1}).AndReturn(self.volumes.list()) - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) + cinder.volume_list(IsA(http.HttpRequest), search_opts={ + 'all_tenants': 1}).AndReturn(self.volumes.list()) + api.nova.server_list(IsA(http.HttpRequest)).\ + AndReturn(self.servers.list()) + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) self.mox.ReplayAll() @@ -37,3 +43,43 @@ class VolumeTests(test.BaseAdminViewTests): volumes = res.context['volumes_table'].data self.assertItemsEqual(volumes, self.volumes.list()) + + @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()) + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:admin:volumes:create_type'), + formData) + + redirect = reverse('horizon:admin:volumes:index') + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, redirect) + + @test.create_stubs({api.nova: ('server_list',), + cinder: ('volume_list', + 'volume_type_list', + 'volume_type_delete',)}) + def test_delete_volume_type(self): + volume_type = self.volume_types.first() + formData = {'action': 'volume_types__delete__%s' % volume_type.id} + + cinder.volume_list(IsA(http.HttpRequest), search_opts={ + 'all_tenants': 1}).AndReturn(self.volumes.list()) + api.nova.server_list(IsA(http.HttpRequest)).\ + AndReturn(self.servers.list()) + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) + cinder.volume_type_delete(IsA(http.HttpRequest), + str(volume_type.id)) + self.mox.ReplayAll() + + res = self.client.post(reverse('horizon:admin:volumes:index'), + formData) + + redirect = reverse('horizon:admin:volumes:index') + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, redirect) diff --git a/openstack_dashboard/dashboards/admin/volumes/urls.py b/openstack_dashboard/dashboards/admin/volumes/urls.py index f42c753f09..5c0280cb5d 100644 --- a/openstack_dashboard/dashboards/admin/volumes/urls.py +++ b/openstack_dashboard/dashboards/admin/volumes/urls.py @@ -1,8 +1,9 @@ from django.conf.urls.defaults import patterns, url -from .views import IndexView, DetailView +from .views import IndexView, DetailView, CreateVolumeTypeView urlpatterns = patterns('', url(r'^$', IndexView.as_view(), name='index'), + url(r'^create_type$', CreateVolumeTypeView.as_view(), name='create_type'), url(r'^(?P[^/]+)/$', DetailView.as_view(), name='detail'), ) diff --git a/openstack_dashboard/dashboards/admin/volumes/views.py b/openstack_dashboard/dashboards/admin/volumes/views.py index 5682aa8a9e..68e844d8ef 100644 --- a/openstack_dashboard/dashboards/admin/volumes/views.py +++ b/openstack_dashboard/dashboards/admin/volumes/views.py @@ -18,22 +18,48 @@ Admin views for managing volumes. """ +from django.utils.translation import ugettext_lazy as _ +from django.core.urlresolvers import reverse from openstack_dashboard.dashboards.project.volumes.views import \ - IndexView as _IndexView, DetailView as _DetailView -from .tables import VolumesTable + VolumeTableMixIn, DetailView as _DetailView +from openstack_dashboard.api import cinder + +from .tables import VolumesTable, VolumeTypesTable +from .forms import CreateVolumeType +from horizon import exceptions +from horizon import forms +from horizon import tables -class IndexView(_IndexView): - table_class = VolumesTable +class IndexView(tables.MultiTableView, VolumeTableMixIn): + table_classes = (VolumesTable, VolumeTypesTable) template_name = "admin/volumes/index.html" - def get_data(self): + def get_volumes_data(self): volumes = self._get_volumes(search_opts={'all_tenants': 1}) instances = self._get_instances() self._set_id_if_nameless(volumes, instances) self._set_attachments_string(volumes, instances) return volumes + def get_volume_types_data(self): + try: + volume_types = cinder.volume_type_list(self.request) + except: + volume_types = [] + exceptions.handle(self.request, + _("Unable to retrieve volume types")) + return volume_types + class DetailView(_DetailView): template_name = "admin/volumes/detail.html" + + +class CreateVolumeTypeView(forms.ModalFormView): + form_class = CreateVolumeType + template_name = 'admin/volumes/create_volume_type.html' + success_url = 'horizon:admin:volumes:index' + + def get_success_url(self): + return reverse(self.success_url) diff --git a/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tests.py b/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tests.py index 1cdba5b29a..51a331d252 100644 --- a/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tests.py +++ b/openstack_dashboard/dashboards/project/images_and_snapshots/volume_snapshots/tests.py @@ -22,7 +22,7 @@ from django import http from django.core.urlresolvers import reverse from mox import IsA -from openstack_dashboard import api +from openstack_dashboard.api import cinder from openstack_dashboard.test import helpers as test @@ -38,16 +38,16 @@ class VolumeSnapshotsViewTests(test.TestCase): self.assertTemplateUsed(res, 'project/volumes/create_snapshot.html') + @test.create_stubs({cinder: ('volume_snapshot_create',)}) def test_create_snapshot_post(self): volume = self.volumes.first() snapshot = self.volume_snapshots.first() - self.mox.StubOutWithMock(api, 'volume_snapshot_create') - api.volume_snapshot_create(IsA(http.HttpRequest), - volume.id, - snapshot.display_name, - snapshot.display_description) \ - .AndReturn(snapshot) + cinder.volume_snapshot_create(IsA(http.HttpRequest), + volume.id, + snapshot.display_name, + snapshot.display_description) \ + .AndReturn(snapshot) self.mox.ReplayAll() formData = {'method': 'CreateSnapshotForm', diff --git a/openstack_dashboard/dashboards/project/volumes/forms.py b/openstack_dashboard/dashboards/project/volumes/forms.py index c9d0ffd36a..3a121fd2d9 100644 --- a/openstack_dashboard/dashboards/project/volumes/forms.py +++ b/openstack_dashboard/dashboards/project/volumes/forms.py @@ -28,6 +28,8 @@ class CreateForm(forms.SelfHandlingForm): name = forms.CharField(max_length="255", label=_("Volume Name")) description = forms.CharField(widget=forms.Textarea, label=_("Description"), required=False) + type = forms.ChoiceField(label=_("Type"), + required=False) size = forms.IntegerField(min_value=1, label=_("Size (GB)")) snapshot_source = forms.ChoiceField(label=_("Use snapshot as a source"), widget=SelectWidget( @@ -40,6 +42,10 @@ 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 = [("", "")] + \ + [(type.name, type.name) + for type in volume_types] if ("snapshot_id" in request.GET): try: snapshot = self.get_snapshot(request, @@ -48,6 +54,13 @@ class CreateForm(forms.SelfHandlingForm): self.fields['size'].initial = snapshot.size self.fields['snapshot_source'].choices = ((snapshot.id, snapshot),) + try: + # Set the volume type from the original volume + orig_volume = cinder.volume_get(request, + snapshot.volume_id) + self.fields['type'].initial = orig_volume.volume_type + except: + pass self.fields['size'].help_text = _('Volume size must be equal ' 'to or greater than the snapshot size (%sGB)' % snapshot.size) @@ -56,7 +69,7 @@ class CreateForm(forms.SelfHandlingForm): _('Unable to load the specified snapshot.')) else: try: - snapshots = api.volume_snapshot_list(request) + snapshots = cinder.volume_snapshot_list(request) if snapshots: choices = [('', _("Choose a snapshot"))] + \ [(s.id, s) for s in snapshots] @@ -102,19 +115,22 @@ class CreateForm(forms.SelfHandlingForm): ' volumes.') raise ValidationError(error_message) - volume = api.volume_create(request, - data['size'], - data['name'], - data['description'], - snapshot_id=snapshot_id) + volume = cinder.volume_create(request, + data['size'], + data['name'], + data['description'], + data['type'], + snapshot_id=snapshot_id) message = 'Creating volume "%s"' % data['name'] messages.info(request, message) return volume except ValidationError, e: - return self.api_error(e.messages[0]) + self.api_error(e.messages[0]) + return False except: exceptions.handle(request, ignore=True) - return self.api_error(_("Unable to create volume.")) + self.api_error(_("Unable to create volume.")) + return False @memoized def get_snapshot(self, request, id): @@ -176,7 +192,8 @@ class AttachForm(forms.SelfHandlingForm): data['volume_id'], data['instance'], data.get('device', '')) - vol_name = api.volume_get(request, data['volume_id']).display_name + vol_name = cinder.volume_get(request, + data['volume_id']).display_name message = _('Attaching volume %(vol)s to instance ' '%(inst)s on %(dev)s.') % {"vol": vol_name, @@ -206,10 +223,10 @@ class CreateSnapshotForm(forms.SelfHandlingForm): def handle(self, request, data): try: - snapshot = api.volume_snapshot_create(request, - data['volume_id'], - data['name'], - data['description']) + snapshot = cinder.volume_snapshot_create(request, + data['volume_id'], + data['name'], + data['description']) message = _('Creating volume snapshot "%s"') % data['name'] messages.info(request, message) diff --git a/openstack_dashboard/dashboards/project/volumes/tables.py b/openstack_dashboard/dashboards/project/volumes/tables.py index 43349907a1..b2ddbbbe5f 100644 --- a/openstack_dashboard/dashboards/project/volumes/tables.py +++ b/openstack_dashboard/dashboards/project/volumes/tables.py @@ -26,6 +26,7 @@ from horizon import exceptions from horizon import tables from openstack_dashboard import api +from openstack_dashboard.api import cinder LOG = logging.getLogger(__name__) @@ -41,7 +42,7 @@ class DeleteVolume(tables.DeleteAction): obj = self.table.get_object_by_id(obj_id) name = self.table.get_object_display(obj) try: - api.volume_delete(request, obj_id) + cinder.volume_delete(request, obj_id) except: msg = _('Unable to delete volume "%s". One or more snapshots ' 'depend on it.') @@ -85,7 +86,7 @@ class UpdateRow(tables.Row): ajax = True def get_data(self, request, volume_id): - volume = api.volume_get(request, volume_id) + volume = cinder.volume_get(request, volume_id) return volume @@ -133,6 +134,10 @@ class AttachmentColumn(tables.Column): return safestring.mark_safe(", ".join(attachments)) +def get_volume_type(volume): + return volume.volume_type if volume.volume_type != "None" else None + + class VolumesTableBase(tables.DataTable): STATUS_CHOICES = ( ("in-use", True), @@ -163,6 +168,9 @@ class VolumesTable(VolumesTableBase): name = tables.Column("display_name", verbose_name=_("Name"), link="horizon:project:volumes:detail") + volume_type = tables.Column(get_volume_type, + verbose_name=_("Type"), + empty_value="-") attachments = AttachmentColumn("attachments", verbose_name=_("Attached To")) diff --git a/openstack_dashboard/dashboards/project/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/tests.py index f1959e8c3c..41b2de3008 100644 --- a/openstack_dashboard/dashboards/project/volumes/tests.py +++ b/openstack_dashboard/dashboards/project/volumes/tests.py @@ -32,25 +32,32 @@ from openstack_dashboard.usage import quotas class VolumeViewTests(test.TestCase): - @test.create_stubs({api: ('volume_create', - 'volume_snapshot_list'), + @test.create_stubs({cinder: ('volume_create', + 'volume_snapshot_list', + 'volume_type_list',), quotas: ('tenant_quota_usages',)}) def test_create_volume(self): volume = self.volumes.first() + volume_type = self.volume_types.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, 'snapshot_source': ''} + 'type': volume_type.name, + 'size': 50, + 'snapshot_source': ''} + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) quotas.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'], - snapshot_id=None).AndReturn(volume) + cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_snapshots.list()) + cinder.volume_create(IsA(http.HttpRequest), + formData['size'], + formData['name'], + formData['description'], + formData['type'], + snapshot_id=None).AndReturn(volume) self.mox.ReplayAll() @@ -60,9 +67,11 @@ class VolumeViewTests(test.TestCase): redirect_url = reverse('horizon:project:volumes:index') self.assertRedirectsNoFollow(res, redirect_url) - @test.create_stubs({api: ('volume_create', - 'volume_snapshot_list'), - cinder: ('volume_snapshot_get',), + @test.create_stubs({cinder: ('volume_create', + 'volume_snapshot_list', + 'volume_snapshot_get', + 'volume_get', + 'volume_type_list',), quotas: ('tenant_quota_usages',)}) def test_create_volume_from_snapshot(self): volume = self.volumes.first() @@ -71,30 +80,40 @@ class VolumeViewTests(test.TestCase): 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} + 'size': 50, + 'type': '', + 'snapshot_source': snapshot.id} # first call- with url param + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) cinder.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) + cinder.volume_get(IsA(http.HttpRequest), snapshot.volume_id).\ + AndReturn(self.volumes.first()) + cinder.volume_create(IsA(http.HttpRequest), + formData['size'], + formData['name'], + formData['description'], + '', + snapshot_id=snapshot.id).\ + AndReturn(volume) # second call- with dropdown + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) - api.volume_snapshot_list(IsA(http.HttpRequest)).\ + cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ AndReturn(self.volume_snapshots.list()) cinder.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) + cinder.volume_create(IsA(http.HttpRequest), + formData['size'], + formData['name'], + formData['description'], + '', + snapshot_id=snapshot.id).\ + AndReturn(volume) self.mox.ReplayAll() @@ -114,7 +133,9 @@ class VolumeViewTests(test.TestCase): redirect_url = reverse('horizon:project:volumes:index') self.assertRedirectsNoFollow(res, redirect_url) - @test.create_stubs({cinder: ('volume_snapshot_get',), + @test.create_stubs({cinder: ('volume_snapshot_get', + 'volume_type_list', + 'volume_get',), quotas: ('tenant_quota_usages',)}) def test_create_volume_from_snapshot_invalid_size(self): usage = {'gigabytes': {'available': 250}, 'volumes': {'available': 6}} @@ -124,9 +145,13 @@ class VolumeViewTests(test.TestCase): 'method': u'CreateForm', 'size': 20, 'snapshot_source': snapshot.id} + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) cinder.volume_snapshot_get(IsA(http.HttpRequest), str(snapshot.id)).AndReturn(snapshot) + cinder.volume_get(IsA(http.HttpRequest), snapshot.volume_id).\ + AndReturn(self.volumes.first()) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) self.mox.ReplayAll() @@ -140,7 +165,7 @@ class VolumeViewTests(test.TestCase): "The volume size cannot be less than the " "snapshot size (40GB)") - @test.create_stubs({api: ('volume_snapshot_list',), + @test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list',), quotas: ('tenant_quota_usages',)}) def test_create_volume_gb_used_over_alloted_quota(self): usage = {'gigabytes': {'available': 100, 'used': 20}} @@ -149,9 +174,11 @@ class VolumeViewTests(test.TestCase): 'method': u'CreateForm', 'size': 5000} + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) - api.volume_snapshot_list(IsA(http.HttpRequest)).\ - AndReturn(self.volume_snapshots.list()) + cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_snapshots.list()) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) self.mox.ReplayAll() @@ -163,7 +190,7 @@ class VolumeViewTests(test.TestCase): ' have 100GB of your quota available.'] self.assertEqual(res.context['form'].errors['__all__'], expected_error) - @test.create_stubs({api: ('volume_snapshot_list',), + @test.create_stubs({cinder: ('volume_snapshot_list', 'volume_type_list',), quotas: ('tenant_quota_usages',)}) def test_create_volume_number_over_alloted_quota(self): usage = {'gigabytes': {'available': 100, 'used': 20}, @@ -173,9 +200,11 @@ class VolumeViewTests(test.TestCase): 'method': u'CreateForm', 'size': 10} + cinder.volume_type_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_types.list()) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) - api.volume_snapshot_list(IsA(http.HttpRequest)).\ - AndReturn(self.volume_snapshots.list()) + cinder.volume_snapshot_list(IsA(http.HttpRequest)).\ + AndReturn(self.volume_snapshots.list()) quotas.tenant_quota_usages(IsA(http.HttpRequest)).AndReturn(usage) self.mox.ReplayAll() @@ -187,21 +216,23 @@ class VolumeViewTests(test.TestCase): ' volumes.'] self.assertEqual(res.context['form'].errors['__all__'], expected_error) - @test.create_stubs({api: ('volume_list', - 'volume_delete', - 'server_list')}) + @test.create_stubs({cinder: ('volume_list', + 'volume_delete',), + api.nova: ('server_list',)}) def test_delete_volume(self): volume = self.volumes.first() formData = {'action': 'volumes__delete__%s' % volume.id} - api.volume_list(IsA(http.HttpRequest), search_opts=None).\ - AndReturn(self.volumes.list()) - api.volume_delete(IsA(http.HttpRequest), volume.id) - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) - api.volume_list(IsA(http.HttpRequest), search_opts=None).\ - AndReturn(self.volumes.list()) - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) + cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\ + AndReturn(self.volumes.list()) + cinder.volume_delete(IsA(http.HttpRequest), volume.id) + api.nova.server_list(IsA(http.HttpRequest)).\ + AndReturn(self.servers.list()) + cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\ + AndReturn(self.volumes.list()) + api.nova.server_list(IsA(http.HttpRequest)).\ + AndReturn(self.servers.list()) self.mox.ReplayAll() @@ -209,9 +240,9 @@ class VolumeViewTests(test.TestCase): res = self.client.post(url, formData, follow=True) self.assertMessageCount(res, count=0) - @test.create_stubs({api: ('volume_list', - 'volume_delete', - 'server_list')}) + @test.create_stubs({cinder: ('volume_list', + 'volume_delete',), + api.nova: ('server_list',)}) def test_delete_volume_error_existing_snapshot(self): volume = self.volumes.first() formData = {'action': @@ -219,14 +250,16 @@ class VolumeViewTests(test.TestCase): exc = self.exceptions.cinder.__class__(400, "error: dependent snapshots") - api.volume_list(IsA(http.HttpRequest), search_opts=None).\ - AndReturn(self.volumes.list()) - api.volume_delete(IsA(http.HttpRequest), volume.id). \ - AndRaise(exc) - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) - api.volume_list(IsA(http.HttpRequest), search_opts=None).\ - AndReturn(self.volumes.list()) - api.server_list(IsA(http.HttpRequest)).AndReturn(self.servers.list()) + cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\ + AndReturn(self.volumes.list()) + cinder.volume_delete(IsA(http.HttpRequest), volume.id).\ + AndRaise(exc) + api.nova.server_list(IsA(http.HttpRequest)).\ + AndReturn(self.servers.list()) + cinder.volume_list(IsA(http.HttpRequest), search_opts=None).\ + AndReturn(self.volumes.list()) + api.nova.server_list(IsA(http.HttpRequest)).\ + AndReturn(self.servers.list()) self.mox.ReplayAll() @@ -238,12 +271,12 @@ class VolumeViewTests(test.TestCase): u'One or more snapshots depend on it.' % volume.display_name) - @test.create_stubs({api: ('volume_get',), api.nova: ('server_list',)}) + @test.create_stubs({cinder: ('volume_get',), api.nova: ('server_list',)}) def test_edit_attachments(self): volume = self.volumes.first() servers = self.servers.list() - api.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume) + cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume) api.nova.server_list(IsA(http.HttpRequest)).AndReturn(servers) self.mox.ReplayAll() @@ -258,7 +291,7 @@ class VolumeViewTests(test.TestCase): self.assertTrue(isinstance(form.fields['device'].widget, widgets.TextInput)) - @test.create_stubs({api: ('volume_get',), api.nova: ('server_list',)}) + @test.create_stubs({cinder: ('volume_get',), api.nova: ('server_list',)}) def test_edit_attachments_cannot_set_mount_point(self): PREV = settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] = False @@ -266,7 +299,7 @@ class VolumeViewTests(test.TestCase): volume = self.volumes.first() servers = self.servers.list() - api.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume) + cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume) api.nova.server_list(IsA(http.HttpRequest)).AndReturn(servers) self.mox.ReplayAll() @@ -278,14 +311,14 @@ class VolumeViewTests(test.TestCase): widgets.HiddenInput)) settings.OPENSTACK_HYPERVISOR_FEATURES['can_set_mount_point'] = PREV - @test.create_stubs({api: ('volume_get',), + @test.create_stubs({cinder: ('volume_get',), api.nova: ('server_get', 'server_list',)}) def test_edit_attachments_attached_volume(self): server = self.servers.first() volume = self.volumes.list()[0] - api.volume_get(IsA(http.HttpRequest), volume.id) \ - .AndReturn(volume) + cinder.volume_get(IsA(http.HttpRequest), volume.id) \ + .AndReturn(volume) api.nova.server_list(IsA(http.HttpRequest)) \ .AndReturn(self.servers.list()) diff --git a/openstack_dashboard/dashboards/project/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/views.py index a9516cc06f..1c4cb239f8 100644 --- a/openstack_dashboard/dashboards/project/volumes/views.py +++ b/openstack_dashboard/dashboards/project/volumes/views.py @@ -30,6 +30,7 @@ from horizon import tables from horizon import tabs from openstack_dashboard import api +from openstack_dashboard.api import cinder from openstack_dashboard.usage import quotas from .forms import CreateForm, AttachForm, CreateSnapshotForm from .tables import AttachmentsTable, VolumesTable @@ -39,22 +40,18 @@ from .tabs import VolumeDetailTabs LOG = logging.getLogger(__name__) -class IndexView(tables.DataTableView): - table_class = VolumesTable - template_name = 'project/volumes/index.html' - +class VolumeTableMixIn(object): def _get_volumes(self, search_opts=None): try: - return api.volume_list(self.request, search_opts=search_opts) + return cinder.volume_list(self.request, search_opts=search_opts) except: exceptions.handle(self.request, _('Unable to retrieve volume list.')) def _get_instances(self): try: - return api.server_list(self.request) + return api.nova.server_list(self.request) except: - instance_list = [] exceptions.handle(self.request, _("Unable to retrieve volume/instance " "attachment information")) @@ -73,6 +70,11 @@ class IndexView(tables.DataTableView): server_id = att.get('server_id', None) att['instance'] = instances.get(server_id, None) + +class IndexView(tables.DataTableView, VolumeTableMixIn): + table_class = VolumesTable + template_name = 'project/volumes/index.html' + def get_data(self): volumes = self._get_volumes() instances = self._get_instances() @@ -124,7 +126,7 @@ class EditAttachmentsView(tables.DataTableView, forms.ModalFormView): if not hasattr(self, "_object"): volume_id = self.kwargs['volume_id'] try: - self._object = api.volume_get(self.request, volume_id) + self._object = cinder.volume_get(self.request, volume_id) except: self._object = None exceptions.handle(self.request, diff --git a/openstack_dashboard/test/test_data/nova_data.py b/openstack_dashboard/test/test_data/nova_data.py index 8b8f128429..dbe46f3500 100644 --- a/openstack_dashboard/test/test_data/nova_data.py +++ b/openstack_dashboard/test/test_data/nova_data.py @@ -14,7 +14,8 @@ import json -from novaclient.v1_1 import (flavors, keypairs, servers, volumes, quotas, +from novaclient.v1_1 import (flavors, keypairs, servers, volumes, + volume_types, quotas, floating_ips, usage, certs, volume_snapshots as vol_snaps, security_group_rules as rules, @@ -145,6 +146,7 @@ def data(TEST): TEST.usages = TestDataContainer() TEST.certs = TestDataContainer() TEST.volume_snapshots = TestDataContainer() + TEST.volume_types = TestDataContainer() # Volumes volume = volumes.Volume(volumes.VolumeManager(None), @@ -154,6 +156,7 @@ def data(TEST): size=40, display_name='Volume name', created_at='2012-04-01 10:30:00', + volume_type=None, attachments=[])) nameless_volume = volumes.Volume(volumes.VolumeManager(None), dict(id="3b189ac8-9166-ac7f-90c9-16c8bf9e01ac", @@ -164,6 +167,7 @@ def data(TEST): display_description='', device="/dev/hda", created_at='2010-11-21 18:34:25', + volume_type='vol_type_1', attachments=[{"id": "1", "server_id": '1', "device": "/dev/hda"}])) attached_volume = volumes.Volume(volumes.VolumeManager(None), @@ -175,12 +179,21 @@ def data(TEST): display_description='', device="/dev/hdk", created_at='2011-05-01 11:54:33', + volume_type='vol_type_2', attachments=[{"id": "2", "server_id": '1', "device": "/dev/hdk"}])) TEST.volumes.add(volume) TEST.volumes.add(nameless_volume) TEST.volumes.add(attached_volume) + vol_type1 = volume_types.VolumeType(volume_types.VolumeTypeManager(None), + {'id': 1, + 'name': 'vol_type_1'}) + vol_type2 = volume_types.VolumeType(volume_types.VolumeTypeManager(None), + {'id': 2, + 'name': 'vol_type_2'}) + TEST.volume_types.add(vol_type1, vol_type2) + # Flavors flavor_1 = flavors.Flavor(flavors.FlavorManager(None), {'id': "1",