Volume Types tab with QOS Specs

This feature adds a new "Volume Types" tab to the Admin-Volumes panel, and
places QOS Specs on the panel. Future check-ins will add further
QOS Spec functionality, such as adding and deleting, etc.

Partially Implements: blueprint cinder-qos-specs
Closes-Bug: 1284506
Change-Id: I497f3bd00bb13356f5d0734f465cbf6feb02f781
This commit is contained in:
Rich Hagarty 2014-08-01 08:45:20 -07:00 committed by lin-hua-cheng
parent c268f36350
commit cae34cf1a2
30 changed files with 383 additions and 152 deletions

View File

@ -122,6 +122,14 @@ class VolTypeExtraSpec(object):
self.value = val
class QosSpec(object):
def __init__(self, spec_id, key, val):
self.spec_id = spec_id
self.id = key
self.key = key
self.value = val
def cinderclient(request):
api_version = VERSIONS.get_active_version()
@ -393,6 +401,39 @@ def volume_type_extra_delete(request, type_id, keys):
return vol_type.unset_keys([keys])
def qos_spec_list(request):
return cinderclient(request).qos_specs.list()
def qos_spec_get(request, qos_spec_id):
return cinderclient(request).qos_specs.get(qos_spec_id)
def qos_spec_delete(request, qos_spec_id):
return cinderclient(request).qos_specs.delete(qos_spec_id, force=True)
def qos_spec_create(request, name, qos_spec_id):
return cinderclient(request).qos_specs.create(name, qos_spec_id)
def qos_spec_get_keys(request, qos_spec_id, raw=False):
spec = cinderclient(request).qos_specs.get(qos_spec_id)
qos_specs = spec.specs
if raw:
return spec
return [QosSpec(qos_spec_id, key, value) for
key, value in qos_specs.items()]
def qos_spec_set_keys(request, qos_spec_id, specs):
return cinderclient(request).qos_specs.set_keys(qos_spec_id, specs)
def qos_spec_unset_keys(request, qos_spec_id, specs):
return cinderclient(request).qos_specs.unset_keys(qos_spec_id, specs)
@memoized
def tenant_absolute_limits(request):
limits = cinderclient(request).limits.get().absolute

View File

@ -22,6 +22,10 @@ from openstack_dashboard.api import keystone
from openstack_dashboard.dashboards.admin.volumes.snapshots \
import tables as snapshots_tables
from openstack_dashboard.dashboards.admin.volumes.volume_types.qos_specs \
import tables as qos_tables
from openstack_dashboard.dashboards.admin.volumes.volume_types \
import tables as volume_types_tables
from openstack_dashboard.dashboards.admin.volumes.volumes \
import tables as volumes_tables
from openstack_dashboard.dashboards.project.volumes \
@ -29,8 +33,7 @@ from openstack_dashboard.dashboards.project.volumes \
class VolumeTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn):
table_classes = (volumes_tables.VolumesTable,
volumes_tables.VolumeTypesTable)
table_classes = (volumes_tables.VolumesTable,)
name = _("Volumes")
slug = "volumes_tab"
template_name = "admin/volumes/volumes/volumes_tables.html"
@ -57,6 +60,15 @@ class VolumeTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn):
return volumes
class VolumeTypesTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn):
table_classes = (volume_types_tables.VolumeTypesTable,
qos_tables.QosSpecsTable)
name = _("Volume Types")
slug = "volume_types_tab"
template_name = "admin/volumes/volume_types/volume_types_tables.html"
preload = False
def get_volume_types_data(self):
try:
volume_types = cinder.volume_type_list(self.request)
@ -66,6 +78,15 @@ class VolumeTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn):
_("Unable to retrieve volume types"))
return volume_types
def get_qos_specs_data(self):
try:
qos_specs = cinder.qos_spec_list(self.request)
except Exception:
qos_specs = []
exceptions.handle(self.request,
_("Unable to retrieve QOS specs"))
return qos_specs
class SnapshotTab(tabs.TableTab):
table_classes = (snapshots_tables.VolumeSnapshotsTable,)
@ -114,5 +135,5 @@ class SnapshotTab(tabs.TableTab):
class VolumesGroupTabs(tabs.TabGroup):
slug = "volumes_group_tabs"
tabs = (VolumeTab, SnapshotTab,)
tabs = (VolumeTab, VolumeTypesTab, SnapshotTab)
sticky = True

View File

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:admin:volumes:volumes:create_type' %}{% endblock %}
{% block form_action %}{% url 'horizon:admin:volumes:volume_types:create_type' %}{% endblock %}
{% block modal_id %}create_volume_type_modal{% endblock %}
{% block modal-header %}{% trans "Create Volume Type" %}{% endblock %}
@ -31,5 +31,5 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create Volume Type" %}" />
<a href="{% url 'horizon:admin:volumes:volumes_tab' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url 'horizon:admin:volumes:volume_types_tab' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -7,5 +7,5 @@
{% endblock page_header %}
{% block main %}
{% include 'admin/volumes/volumes/_create_volume_type.html' %}
{% include 'admin/volumes/volume_types/_create_volume_type.html' %}
{% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}extra_spec_create_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:volumes:volumes:extras:create' vol_type.id %}{% endblock %}
{% block form_action %}{% url 'horizon:admin:volumes:volume_types:extras:create' vol_type.id %}{% endblock %}
{% block modal_id %}extra_spec_create_modal{% endblock %}
@ -23,6 +23,6 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Create" %}" />
<a href="{% url 'horizon:admin:volumes:volumes:extras:index' vol_type.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url 'horizon:admin:volumes:volume_types:extras:index' vol_type.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -3,7 +3,7 @@
{% load url from future %}
{% block form_id %}extra_spec_edit_form{% endblock %}
{% block form_action %}{% url 'horizon:admin:volumes:volumes:extras:edit' vol_type.id key %}{% endblock %}
{% block form_action %}{% url 'horizon:admin:volumes:volume_types:extras:edit' vol_type.id key %}{% endblock %}
{% block modal_id %}extra_spec_edit_modal{% endblock %}
@ -23,6 +23,6 @@
{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Save" %}" />
<a href="{% url 'horizon:admin:volumes:volumes:extras:index' vol_type.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
<a href="{% url 'horizon:admin:volumes:volume_types:extras:index' vol_type.id %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}

View File

@ -8,5 +8,5 @@
{% endblock page_header %}
{% block main %}
{% include "admin/volumes/volumes/extras/_create.html" %}
{% include "admin/volumes/volume_types/extras/_create.html" %}
{% endblock %}

View File

@ -8,5 +8,5 @@
{% endblock page_header %}
{% block main %}
{% include "admin/volumes/volumes/extras/_edit.html" %}
{% include "admin/volumes/volume_types/extras/_edit.html" %}
{% endblock %}

View File

@ -8,5 +8,5 @@
{% endblock page_header %}
{% block main %}
{% include "admin/volumes/volumes/extras/_index.html" %}
{% include "admin/volumes/volume_types/extras/_index.html" %}
{% endblock %}

View File

@ -0,0 +1,9 @@
{% block main %}
<div id="volumes-type">
{{ volume_types_table.render }}
</div>
<div id="qos-specs">
{{ qos_specs_table.render }}
</div>
{% endblock %}

View File

@ -2,8 +2,4 @@
<div id="volumes">
{{ volumes_table.render }}
</div>
<div id="volumes-type">
{{ volume_types_table.render }}
</div>
{% endblock %}

View File

@ -24,75 +24,25 @@ from openstack_dashboard.test import helpers as test
class VolumeTests(test.BaseAdminViewTests):
@test.create_stubs({api.nova: ('server_list',),
cinder: ('volume_list',
'volume_type_list',),
cinder: ('volume_list',),
keystone: ('tenant_list',)})
def test_index(self):
cinder.volume_list(IsA(http.HttpRequest), search_opts={
'all_tenants': True}).AndReturn(self.cinder_volumes.list())
'all_tenants': True}).\
AndReturn(self.cinder_volumes.list())
api.nova.server_list(IsA(http.HttpRequest), search_opts={
'all_tenants': True}) \
.AndReturn([self.servers.list(), False])
cinder.volume_type_list(IsA(http.HttpRequest)).\
AndReturn(self.volume_types.list())
keystone.tenant_list(IsA(http.HttpRequest)). \
AndReturn([self.tenants.list(), False])
'all_tenants': True}) \
.AndReturn([self.servers.list(), False])
keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn([self.tenants.list(), False])
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:admin:volumes:index'))
self.assertTemplateUsed(res, 'admin/volumes/index.html')
volumes = res.context['volumes_table'].data
self.assertItemsEqual(volumes, self.cinder_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:volumes:create_type'),
formData)
redirect = reverse('horizon:admin:volumes:volumes_tab')
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, redirect)
@test.create_stubs({api.nova: ('server_list',),
cinder: ('volume_list',
'volume_type_list',
'volume_type_delete',),
keystone: ('tenant_list',)})
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': True}).AndReturn(self.cinder_volumes.list())
api.nova.server_list(IsA(http.HttpRequest), search_opts={
'all_tenants': True}) \
.AndReturn([self.servers.list(), False])
cinder.volume_type_list(IsA(http.HttpRequest)).\
AndReturn(self.volume_types.list())
cinder.volume_type_delete(IsA(http.HttpRequest),
str(volume_type.id))
keystone.tenant_list(IsA(http.HttpRequest)) \
.AndReturn([self.tenants.list(), False])
self.mox.ReplayAll()
res = self.client.post(
reverse('horizon:admin:volumes:volumes_tab'),
formData)
redirect = reverse('horizon:admin:volumes:volumes_tab')
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, redirect)
@test.create_stubs({cinder: ('volume_reset_state',
'volume_get')})
def test_update_volume_status(self):
@ -111,11 +61,30 @@ class VolumeTests(test.BaseAdminViewTests):
formData)
self.assertNoFormErrors(res)
@test.create_stubs({api.nova: ('server_list',),
cinder: ('volume_list',
@test.create_stubs({cinder: ('volume_type_list',
'qos_spec_list',)})
def test_volume_types_tab(self):
cinder.volume_type_list(IsA(http.HttpRequest)).\
AndReturn(self.volume_types.list())
cinder.qos_spec_list(IsA(http.HttpRequest)).\
AndReturn(self.cinder_qos_specs.list())
self.mox.ReplayAll()
res = self.client.get(reverse(
'horizon:admin:volumes:volume_types_tab'))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res,
'admin/volumes/volume_types/volume_types_tables.html')
volume_types = res.context['volume_types_table'].data
self.assertItemsEqual(volume_types, self.volume_types.list())
qos_specs = res.context['qos_specs_table'].data
self.assertItemsEqual(qos_specs, self.cinder_qos_specs.list())
@test.create_stubs({cinder: ('volume_list',
'volume_snapshot_list',),
keystone: ('tenant_list',)})
def test_snapshot_tab(self):
def test_snapshots_tab(self):
cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts={
'all_tenants': True}). \
AndReturn(self.cinder_volume_snapshots.list())
@ -124,8 +93,11 @@ class VolumeTests(test.BaseAdminViewTests):
AndReturn(self.cinder_volumes.list())
keystone.tenant_list(IsA(http.HttpRequest)). \
AndReturn([self.tenants.list(), False])
self.mox.ReplayAll()
self.mox.ReplayAll()
res = self.client.get(reverse('horizon:admin:volumes:snapshots_tab'))
self.assertEqual(res.status_code, 200)
self.assertTemplateUsed(res, 'horizon/common/_detail_table.html')
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, self.cinder_volume_snapshots.list())

View File

@ -17,6 +17,8 @@ from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.admin.volumes.snapshots \
import urls as snapshot_urls
from openstack_dashboard.dashboards.admin.volumes import views
from openstack_dashboard.dashboards.admin.volumes.volume_types \
import urls as volume_types_urls
from openstack_dashboard.dashboards.admin.volumes.volumes \
import urls as volumes_urls
@ -26,6 +28,9 @@ urlpatterns = patterns('',
views.IndexView.as_view(), name='snapshots_tab'),
url(r'^\?tab=volumes_group_tabs__volumes_tab$',
views.IndexView.as_view(), name='volumes_tab'),
url(r'^\?tab=volumes_group_tabs__volume_types_tab$',
views.IndexView.as_view(), name='volume_types_tab'),
url(r'', include(volumes_urls, namespace='volumes')),
url(r'snapshots/', include(snapshot_urls, namespace='snapshots')),
url(r'', include(volume_types_urls, namespace='volume_types')),
)

View File

@ -31,7 +31,7 @@ class ExtraSpecDelete(tables.DeleteAction):
class ExtraSpecCreate(tables.LinkAction):
name = "create"
verbose_name = _("Create")
url = "horizon:admin:volumes:volumes:extras:create"
url = "horizon:admin:volumes:volume_types:extras:create"
classes = ("ajax-modal")
icon = "plus"
@ -42,7 +42,7 @@ class ExtraSpecCreate(tables.LinkAction):
class ExtraSpecEdit(tables.LinkAction):
name = "edit"
verbose_name = _("Edit")
url = "horizon:admin:volumes:volumes:extras:edit"
url = "horizon:admin:volumes:volume_types:extras:edit"
classes = ("btn-edit", "ajax-modal")
def get_link_url(self, extra_spec):

View File

@ -32,12 +32,12 @@ class VolTypeExtrasTests(test.BaseAdminViewTests):
api.cinder.volume_type_extra_get(IsA(http.HttpRequest),
vol_type.id).AndReturn(extras)
self.mox.ReplayAll()
url = reverse('horizon:admin:volumes:volumes:extras:index',
url = reverse('horizon:admin:volumes:volume_types:extras:index',
args=[vol_type.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp,
"admin/volumes/volumes/extras/index.html")
"admin/volumes/volume_types/extras/index.html")
@test.create_stubs({api.cinder: ('volume_type_extra_get',
'volume_type_get'), })
@ -50,7 +50,7 @@ class VolTypeExtrasTests(test.BaseAdminViewTests):
vol_type.id) \
.AndRaise(self.exceptions.cinder)
self.mox.ReplayAll()
url = reverse('horizon:admin:volumes:volumes:extras:index',
url = reverse('horizon:admin:volumes:volume_types:extras:index',
args=[vol_type.id])
resp = self.client.get(url)
self.assertEqual(len(resp.context['extras_table'].data), 0)
@ -59,10 +59,12 @@ class VolTypeExtrasTests(test.BaseAdminViewTests):
@test.create_stubs({api.cinder: ('volume_type_extra_set', ), })
def test_extra_create_post(self):
vol_type = self.cinder_volume_types.first()
create_url = reverse('horizon:admin:volumes:volumes:extras:create',
args=[vol_type.id])
index_url = reverse('horizon:admin:volumes:volumes:extras:index',
args=[vol_type.id])
create_url = reverse(
'horizon:admin:volumes:volume_types:extras:create',
args=[vol_type.id])
index_url = reverse(
'horizon:admin:volumes:volume_types:extras:index',
args=[vol_type.id])
data = {'key': u'k1',
'value': u'v1'}
@ -80,8 +82,9 @@ class VolTypeExtrasTests(test.BaseAdminViewTests):
@test.create_stubs({api.cinder: ('volume_type_get', ), })
def test_extra_create_get(self):
vol_type = self.cinder_volume_types.first()
create_url = reverse('horizon:admin:volumes:volumes:extras:create',
args=[vol_type.id])
create_url = reverse(
'horizon:admin:volumes:volume_types:extras:create',
args=[vol_type.id])
api.cinder.volume_type_get(IsA(http.HttpRequest),
vol_type.id).AndReturn(vol_type)
@ -89,17 +92,17 @@ class VolTypeExtrasTests(test.BaseAdminViewTests):
resp = self.client.get(create_url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp,
'admin/volumes/volumes/extras/create.html')
self.assertTemplateUsed(
resp, 'admin/volumes/volume_types/extras/create.html')
@test.create_stubs({api.cinder: ('volume_type_extra_get',
'volume_type_extra_set',), })
def test_extra_edit(self):
vol_type = self.cinder_volume_types.first()
key = 'foo'
edit_url = reverse('horizon:admin:volumes:volumes:extras:edit',
args=[vol_type.id, key])
index_url = reverse('horizon:admin:volumes:volumes:extras:index',
edit_url = reverse('horizon:admin:volumes:volume_types:extras:edit',
args=[vol_type.id, key])
index_url = reverse('horizon:admin:volumes:volume_types:extras:index',
args=[vol_type.id])
data = {'value': u'v1'}
@ -124,7 +127,7 @@ class VolTypeExtrasTests(test.BaseAdminViewTests):
vol_type = self.cinder_volume_types.first()
extras = [api.cinder.VolTypeExtraSpec(vol_type.id, 'k1', 'v1')]
formData = {'action': 'extras__delete__k1'}
index_url = reverse('horizon:admin:volumes:volumes:extras:index',
index_url = reverse('horizon:admin:volumes:volume_types:extras:index',
args=[vol_type.id])
api.cinder.volume_type_extra_get(IsA(http.HttpRequest),

View File

@ -13,7 +13,7 @@
from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.admin.volumes.volumes.extras \
from openstack_dashboard.dashboards.admin.volumes.volume_types.extras \
import views
urlpatterns = patterns('',

View File

@ -19,9 +19,9 @@ from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.volumes.volumes.extras \
from openstack_dashboard.dashboards.admin.volumes.volume_types.extras \
import forms as project_forms
from openstack_dashboard.dashboards.admin.volumes.volumes.extras \
from openstack_dashboard.dashboards.admin.volumes.volume_types.extras \
import tables as project_tables
@ -41,7 +41,7 @@ class ExtraSpecMixin(object):
class IndexView(ExtraSpecMixin, forms.ModalFormMixin, tables.DataTableView):
table_class = project_tables.ExtraSpecsTable
template_name = 'admin/volumes/volumes/extras/index.html'
template_name = 'admin/volumes/volume_types/extras/index.html'
def get_data(self):
try:
@ -58,7 +58,7 @@ class IndexView(ExtraSpecMixin, forms.ModalFormMixin, tables.DataTableView):
class CreateView(ExtraSpecMixin, forms.ModalFormView):
form_class = project_forms.CreateExtraSpec
template_name = 'admin/volumes/volumes/extras/create.html'
template_name = 'admin/volumes/volume_types/extras/create.html'
def get_initial(self):
return {'type_id': self.kwargs['type_id']}
@ -69,8 +69,8 @@ class CreateView(ExtraSpecMixin, forms.ModalFormView):
class EditView(ExtraSpecMixin, forms.ModalFormView):
form_class = project_forms.EditExtraSpec
template_name = 'admin/volumes/volumes/extras/edit.html'
success_url = 'horizon:admin:volumes:volumes:extras:index'
template_name = 'admin/volumes/volume_types/extras/edit.html'
success_url = 'horizon:admin:volumes:volume_types:extras:index'
def get_success_url(self):
return reverse(self.success_url,

View File

@ -0,0 +1,35 @@
# 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.template import defaultfilters as filters
from django.utils.translation import ugettext_lazy as _
from horizon import tables
def render_spec_keys(qos_spec):
qos_spec_keys = ["%s=%s" % (key, value)
for key, value in qos_spec.specs.items()]
return qos_spec_keys
class QosSpecsTable(tables.DataTable):
name = tables.Column('name', verbose_name=_('Name'))
consumer = tables.Column('consumer', verbose_name=_('Consumer'))
specs = tables.Column(render_spec_keys,
verbose_name=_('Specs'),
wrap_list=True,
filters=(filters.unordered_list,))
class Meta:
name = "qos_specs"
verbose_name = _("QOS Specs")

View File

@ -0,0 +1,59 @@
# 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 tables
from openstack_dashboard.api import cinder
class CreateVolumeType(tables.LinkAction):
name = "create"
verbose_name = _("Create Volume Type")
url = "horizon:admin:volumes:volume_types:create_type"
classes = ("ajax-modal",)
icon = "plus"
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",)
policy_rules = (("volume", "volume_extension:types_manage"),)
class DeleteVolumeType(tables.DeleteAction):
data_type_singular = _("Volume Type")
data_type_plural = _("Volume Types")
policy_rules = (("volume", "volume_extension:types_manage"),)
def delete(self, request, obj_id):
cinder.volume_type_delete(request, obj_id)
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 = (ViewVolumeTypeExtras, DeleteVolumeType,)

View File

@ -0,0 +1,64 @@
# 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.core.urlresolvers import reverse
from django import http
from mox import IsA # noqa
from openstack_dashboard import api
from openstack_dashboard.api import cinder
from openstack_dashboard.api import keystone
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())
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)
self.assertRedirectsNoFollow(res, redirect)
@test.create_stubs({api.nova: ('server_list',),
cinder: ('volume_list',
'volume_type_list',
'qos_spec_list',
'volume_type_delete',),
keystone: ('tenant_list',)})
def test_delete_volume_type(self):
volume_type = self.volume_types.first()
formData = {'action': 'volume_types__delete__%s' % volume_type.id}
cinder.volume_type_list(IsA(http.HttpRequest)).\
AndReturn(self.volume_types.list())
cinder.qos_spec_list(IsA(http.HttpRequest)).\
AndReturn(self.cinder_qos_specs.list())
cinder.volume_type_delete(IsA(http.HttpRequest),
str(volume_type.id))
self.mox.ReplayAll()
res = self.client.post(
reverse('horizon:admin:volumes:volumes_tab'),
formData)
redirect = reverse('horizon:admin:volumes:volumes_tab')
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, redirect)

View File

@ -0,0 +1,29 @@
# 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.conf.urls import include # noqa
from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.admin.volumes.volume_types.extras \
import urls as extras_urls
from openstack_dashboard.dashboards.admin.volumes.volume_types \
import views
VIEWS_MOD = ('openstack_dashboard.dashboards.admin.volumes.volume_types.views')
urlpatterns = patterns('VIEWS_MOD',
url(r'^create_type$', views.CreateVolumeTypeView.as_view(),
name='create_type'),
url(r'^(?P<type_id>[^/]+)/extras/',
include(extras_urls, namespace='extras')),
)

View File

@ -0,0 +1,31 @@
# 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.
"""
Admin views for managing volumes.
"""
from django.core.urlresolvers import reverse
from horizon import forms
from openstack_dashboard.dashboards.admin.volumes.volumes \
import forms as volumes_forms
class CreateVolumeTypeView(forms.ModalFormView):
form_class = volumes_forms.CreateVolumeType
template_name = 'admin/volumes/volume_types/create_volume_type.html'
success_url = 'horizon:admin:volumes:volume_types_tab'
def get_success_url(self):
return reverse(self.success_url)

View File

@ -13,37 +13,10 @@
from django.utils.translation import ugettext_lazy as _
from horizon import tables
from openstack_dashboard.api import cinder
from openstack_dashboard.dashboards.project.volumes \
.volumes import tables as volumes_tables
class CreateVolumeType(tables.LinkAction):
name = "create"
verbose_name = _("Create Volume Type")
url = "horizon:admin:volumes:volumes:create_type"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("volume", "volume_extension:types_manage"),)
class ViewVolumeTypeExtras(tables.LinkAction):
name = "extras"
verbose_name = _("View Extra Specs")
url = "horizon:admin:volumes:volumes:extras:index"
classes = ("btn-edit",)
policy_rules = (("volume", "volume_extension:types_manage"),)
class DeleteVolumeType(tables.DeleteAction):
data_type_singular = _("Volume Type")
data_type_plural = _("Volume Types")
policy_rules = (("volume", "volume_extension:types_manage"),)
def delete(self, request, obj_id):
cinder.volume_type_delete(request, obj_id)
class VolumesFilterAction(tables.FilterAction):
def filter(self, table, volumes, filter_string):
@ -79,20 +52,3 @@ class VolumesTable(volumes_tables.VolumesTable):
row_actions = (volumes_tables.DeleteVolume, UpdateVolumeStatusAction)
columns = ('tenant', 'host', 'name', 'size', 'status', 'volume_type',
'attachments', 'bootable', 'encryption',)
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 = (ViewVolumeTypeExtras, DeleteVolumeType,)

View File

@ -14,20 +14,14 @@ from django.conf.urls import include # noqa
from django.conf.urls import patterns # noqa
from django.conf.urls import url # noqa
from openstack_dashboard.dashboards.admin.volumes.volumes.extras \
import urls as extras_urls
from openstack_dashboard.dashboards.admin.volumes.volumes \
import views
VIEWS_MOD = ('openstack_dashboard.dashboards.admin.volumes.volumes.views')
urlpatterns = patterns(VIEWS_MOD,
url(r'^create_type$', views.CreateVolumeTypeView.as_view(),
name='create_type'),
url(r'^(?P<volume_id>[^/]+)/$', views.DetailView.as_view(),
name='detail'),
url(r'^(?P<volume_id>[^/]+)/update_status$',
views.UpdateStatusView.as_view(), name='update_status'),
url(r'^(?P<type_id>[^/]+)/extras/',
include(extras_urls, namespace='extras')),
)

View File

@ -18,6 +18,7 @@ from cinderclient.v1 import services
from cinderclient.v1 import volume_snapshots as vol_snaps
from cinderclient.v1 import volume_types
from cinderclient.v1 import volumes
from cinderclient.v2 import qos_specs
from cinderclient.v2 import volume_backups as vol_backups
from cinderclient.v2 import volume_snapshots as vol_snaps_v2
from cinderclient.v2 import volumes as volumes_v2
@ -33,6 +34,7 @@ def data(TEST):
TEST.cinder_volumes = utils.TestDataContainer()
TEST.cinder_volume_backups = utils.TestDataContainer()
TEST.cinder_volume_types = utils.TestDataContainer()
TEST.cinder_qos_specs = utils.TestDataContainer()
TEST.cinder_volume_snapshots = utils.TestDataContainer()
TEST.cinder_quotas = utils.TestDataContainer()
TEST.cinder_quota_usages = utils.TestDataContainer()
@ -223,3 +225,17 @@ def data(TEST):
"maxTotalVolumeGigabytes": 1000,
"maxTotalVolumes": 10}}
TEST.cinder_limits = limits
# QOS Specs
qos_spec1 = qos_specs.QoSSpecs(qos_specs.QoSSpecsManager(None),
{"id": "418db45d-6992-4674-b226-80aacad2073c",
"name": "high_iops",
"consumer": "back-end",
"specs": {"minIOPS": "1000", "maxIOPS": '100000'}})
qos_spec2 = qos_specs.QoSSpecs(qos_specs.QoSSpecsManager(None),
{"id": "6ed7035f-992e-4075-8ed6-6eff19b3192d",
"name": "high_bws",
"consumer": "back-end",
"specs": {"maxBWS": '5000'}})
TEST.cinder_qos_specs.add(qos_spec1, qos_spec2)