Add volume group-specs-list support for admin panel

This commit allow admin to list/show cinder volume group-spec
using horizon dashboard and user can perform the following
table action :
1. group-spec-create
2. group-spec-edit
3. group-spec-delete

Partially-Implements blueprint cinder-generic-volume-groups

Change-Id: I7a24e21bbf86595bc7e1251d29caed7f7ff5dec8
This commit is contained in:
manchandavishal 2019-02-19 09:05:11 +00:00 committed by Akihiro Motoki
parent 8bc497a5e3
commit 4b523122b0
16 changed files with 566 additions and 5 deletions

View File

@ -152,6 +152,14 @@ class VolTypeExtraSpec(object):
self.value = val self.value = val
class GroupTypeSpec(object):
def __init__(self, group_type_id, key, val):
self.group_type_id = group_type_id
self.id = key
self.key = key
self.value = val
class QosSpec(object): class QosSpec(object):
def __init__(self, id, key, val): def __init__(self, id, key, val):
self.id = id self.id = id
@ -1154,16 +1162,28 @@ def group_type_delete(request, group_type_id):
client.group_types.delete(group_type_id) client.group_types.delete(group_type_id)
@profiler.trace
def group_type_spec_list(request, group_type_id, raw=False):
group_type = group_type_get(request, group_type_id)
specs = group_type._apiresource.get_keys()
if raw:
return specs
return [GroupTypeSpec(group_type_id, key, value) for
key, value in specs.items()]
@profiler.trace @profiler.trace
def group_type_spec_set(request, group_type_id, metadata): def group_type_spec_set(request, group_type_id, metadata):
client = _cinderclient_with_generic_groups(request) group_type = group_type_get(request, group_type_id)
client.group_types.set_keys(metadata) if not metadata:
return None
return group_type._apiresource.set_keys(metadata)
@profiler.trace @profiler.trace
def group_type_spec_unset(request, group_type_id, keys): def group_type_spec_unset(request, group_type_id, keys):
client = _cinderclient_with_generic_groups(request) group_type = group_type_get(request, group_type_id)
client.group_types.unset_keys(keys) return group_type._apiresource.unset_keys(keys)
@profiler.trace @profiler.trace

View File

@ -0,0 +1,81 @@
# 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.
import re
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import messages
from openstack_dashboard import api
KEY_NAME_REGEX = re.compile(r"^[a-zA-Z0-9_.:-]+$", re.UNICODE)
KEY_ERROR_MESSAGES = {
'invalid': _('Key names can only contain alphanumeric characters, '
'underscores, periods, colons and hyphens')}
class CreateSpec(forms.SelfHandlingForm):
key = forms.RegexField(max_length=255, label=_("Key"),
regex=KEY_NAME_REGEX,
error_messages=KEY_ERROR_MESSAGES)
value = forms.CharField(max_length=255, label=_("Value"))
def handle(self, request, data):
group_type_id = self.initial['group_type_id']
error_msg = _('key with name "%s" already exists.Use Edit to '
'update the value, else create key with different '
'name.') % data['key']
try:
specs_list = api.cinder.group_type_spec_list(self.request,
group_type_id)
for spec in specs_list:
if spec.key.lower() == data['key'].lower():
raise forms.ValidationError(error_msg)
api.cinder.group_type_spec_set(request,
group_type_id,
{data['key']: data['value']})
msg = _('Created group type spec "%s".') % data['key']
messages.success(request, msg)
return True
except forms.ValidationError:
messages.error(request, error_msg)
except Exception:
redirect = reverse("horizon:admin:group_types:index")
exceptions.handle(request,
_("Unable to create group type spec."),
redirect=redirect)
class EditSpec(forms.SelfHandlingForm):
value = forms.CharField(max_length=255, label=_("Value"))
def handle(self, request, data):
key = self.initial['key']
group_type_id = self.initial['group_type_id']
try:
api.cinder.group_type_spec_set(request,
group_type_id,
{key: data['value']})
msg = _('Saved group spec "%s".') % key
messages.success(request, msg)
return True
except Exception:
redirect = reverse("horizon:admin:group_types:index")
exceptions.handle(request,
_("Unable to edit group type spec."),
redirect=redirect)

View File

@ -0,0 +1,87 @@
# 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.urls import reverse
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext_lazy
from six.moves.urllib import parse
from horizon import tables
from openstack_dashboard import api
class GroupTypeSpecDelete(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Spec",
u"Delete Specs",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Delete Spec",
u"Delete Specs",
count
)
def delete(self, request, obj_id):
key = parse.unquote(obj_id)
api.cinder.group_type_spec_unset(request,
self.table.kwargs['type_id'],
[key])
def get_success_url(self, request):
return reverse('horizon:admin:group_types:index')
class GroupTypeSpecCreate(tables.LinkAction):
name = "create"
verbose_name = _("Create Spec")
url = "horizon:admin:group_types:specs:create"
classes = ("ajax-modal",)
icon = "plus"
def get_link_url(self, group_type_spec=None):
return reverse(self.url, args=[self.table.kwargs['type_id']])
class GroupTypeSpecEdit(tables.LinkAction):
name = "edit"
verbose_name = _("Edit Spec")
url = "horizon:admin:group_types:specs:edit"
classes = ("btn-edit", "ajax-modal")
def get_link_url(self, group_type_spec):
return reverse(self.url, args=[self.table.kwargs['type_id'],
group_type_spec.key])
class GroupTypeSpecsTable(tables.DataTable):
key = tables.Column('key', verbose_name=_('Key'))
value = tables.Column('value', verbose_name=_('Value'))
class Meta(object):
name = "specs"
verbose_name = _("Group Type Specs")
table_actions = (GroupTypeSpecCreate, GroupTypeSpecDelete)
row_actions = (GroupTypeSpecEdit, GroupTypeSpecDelete)
def get_object_id(self, datum):
return parse.quote(datum.key)
def get_object_display(self, datum):
return datum.key

View File

@ -0,0 +1,141 @@
# 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.urls import reverse
from openstack_dashboard import api
from openstack_dashboard.test import helpers as test
class GroupTypeSpecTests(test.BaseAdminViewTests):
@test.create_mocks({api.cinder: ('group_type_spec_list',
'group_type_get')})
def test_list_specs_when_none_exists(self):
group_type = self.cinder_group_types.first()
specs = [api.cinder.GroupTypeSpec(group_type.id, 'k1', 'v1')]
self.mock_group_type_get.return_value = group_type
self.mock_group_type_spec_list.return_value = specs
url = reverse('horizon:admin:group_types:specs:index',
args=[group_type.id])
resp = self.client.get(url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(resp,
"admin/group_types/specs/index.html")
self.mock_group_type_get.assert_called_once_with(
test.IsHttpRequest(), group_type.id)
self.mock_group_type_spec_list.assert_called_once_with(
test.IsHttpRequest(), group_type.id)
@test.create_mocks({api.cinder: ('group_type_spec_list',
'group_type_get')})
def test_specs_view_with_exception(self):
group_type = self.cinder_group_types.first()
self.mock_group_type_get.return_value = group_type
self.mock_group_type_spec_list.side_effect = self.exceptions.cinder
url = reverse('horizon:admin:group_types:specs:index',
args=[group_type.id])
resp = self.client.get(url)
self.assertEqual(len(resp.context['specs_table'].data), 0)
self.assertMessageCount(resp, error=1)
self.mock_group_type_get.assert_called_once_with(
test.IsHttpRequest(), group_type.id)
self.mock_group_type_spec_list.assert_called_once_with(
test.IsHttpRequest(), group_type.id)
@test.create_mocks({api.cinder: ('group_type_spec_list',
'group_type_spec_set', )})
def test_spec_create_post(self):
group_type = self.cinder_group_types.first()
create_url = reverse(
'horizon:admin:group_types:specs:create',
args=[group_type.id])
index_url = reverse(
'horizon:admin:group_types:index')
data = {'key': u'k1',
'value': u'v1'}
self.mock_group_type_spec_set.return_value = None
resp = self.client.post(create_url, data)
self.assertNoFormErrors(resp)
self.assertMessageCount(success=1)
self.assertRedirectsNoFollow(resp, index_url)
self.mock_group_type_spec_set.assert_called_once_with(
test.IsHttpRequest(),
group_type.id,
{data['key']: data['value']})
@test.create_mocks({api.cinder: ('group_type_get', )})
def test_spec_create_get(self):
group_type = self.cinder_group_types.first()
create_url = reverse(
'horizon:admin:group_types:specs:create',
args=[group_type.id])
self.mock_group_type_get.return_value = group_type
resp = self.client.get(create_url)
self.assertEqual(resp.status_code, 200)
self.assertTemplateUsed(
resp, 'admin/group_types/specs/create.html')
self.mock_group_type_get.assert_called_once_with(
test.IsHttpRequest(), group_type.id)
@test.create_mocks({api.cinder: ('group_type_spec_list',
'group_type_spec_set',)})
def test_spec_edit(self):
group_type = self.cinder_group_types.first()
key = 'foo'
edit_url = reverse('horizon:admin:group_types:specs:edit',
args=[group_type.id, key])
index_url = reverse('horizon:admin:group_types:index')
data = {'value': u'v1'}
specs = {key: data['value']}
self.mock_group_type_spec_list.return_value = specs
self.mock_group_type_spec_set.return_value = None
resp = self.client.post(edit_url, data)
self.assertNoFormErrors(resp)
self.assertMessageCount(success=1)
self.assertRedirectsNoFollow(resp, index_url)
self.mock_group_type_spec_list.assert_called_once_with(
test.IsHttpRequest(), group_type.id, raw=True)
self.mock_group_type_spec_set.assert_called_once_with(
test.IsHttpRequest(), group_type.id, specs)
@test.create_mocks({api.cinder: ('group_type_spec_list',
'group_type_spec_unset')})
def test_spec_delete(self):
group_type = self.cinder_group_types.first()
specs = [api.cinder.GroupTypeSpec(group_type.id, 'k1', 'v1')]
formData = {'action': 'specs__delete__k1'}
index_url = reverse('horizon:admin:group_types:specs:index',
args=[group_type.id])
self.mock_group_type_spec_list.return_value = specs
self.mock_group_type_spec_unset.return_value = group_type
res = self.client.post(index_url, formData)
redirect = reverse('horizon:admin:group_types:index')
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, redirect)
self.mock_group_type_spec_list.assert_called_once_with(
test.IsHttpRequest(), group_type.id)
self.mock_group_type_spec_unset.assert_called_once_with(
test.IsHttpRequest(), group_type.id, ['k1'])

View File

@ -0,0 +1,22 @@
# 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 url
from openstack_dashboard.dashboards.admin.group_types.specs \
import views
urlpatterns = [
url(r'^$', views.IndexView.as_view(), name='index'),
url(r'^create/$', views.CreateView.as_view(), name='create'),
url(r'^(?P<key>[^/]+)/edit/$', views.EditView.as_view(), name='edit'),
]

View File

@ -0,0 +1,125 @@
# 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.urls import reverse
from django.urls import reverse_lazy
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import forms
from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.group_types.specs \
import forms as admin_forms
from openstack_dashboard.dashboards.admin.group_types.specs \
import tables as admin_tables
class GroupTypeSpecMixin(object):
def get_context_data(self, **kwargs):
context = super(GroupTypeSpecMixin, self).get_context_data(**kwargs)
try:
context['group_type'] = api.cinder.group_type_get(
self.request, self.kwargs['type_id'])
except Exception:
exceptions.handle(self.request,
_("Unable to retrieve group type details."))
if 'key' in self.kwargs:
context['key'] = self.kwargs['key']
return context
class IndexView(GroupTypeSpecMixin, forms.ModalFormMixin, tables.DataTableView):
table_class = admin_tables.GroupTypeSpecsTable
template_name = 'admin/group_types/specs/index.html'
def get_data(self):
try:
group_type_id = self.kwargs['type_id']
specs_list = api.cinder.group_type_spec_list(self.request,
group_type_id)
specs_list.sort(key=lambda es: (es.key,))
except Exception:
specs_list = []
exceptions.handle(self.request,
_('Unable to retrieve group type spec list.'))
return specs_list
class CreateView(GroupTypeSpecMixin, forms.ModalFormView):
form_class = admin_forms.CreateSpec
form_id = "group_type_spec_create_form"
modal_header = _("Create Group Type Spec")
modal_id = "group_type_spec_create_modal"
submit_label = _("Create")
submit_url = "horizon:admin:group_types:specs:create"
template_name = 'admin/group_types/specs/create.html'
success_url = 'horizon:admin:group_types:index'
cancel_url = reverse_lazy('horizon:admin:group_types:index')
def get_initial(self):
return {'group_type_id': self.kwargs['type_id']}
def get_success_url(self):
return reverse(self.success_url)
def get_context_data(self, **kwargs):
context = super(CreateView, self).get_context_data(**kwargs)
args = (self.kwargs['type_id'],)
context['submit_url'] = reverse(self.submit_url, args=args)
return context
class EditView(GroupTypeSpecMixin, forms.ModalFormView):
form_class = admin_forms.EditSpec
form_id = "group_type_spec_edit_form"
modal_header = _('Edit Group Type Spec Value: %s')
modal_id = "group_type_spec_edit_modal"
submit_label = _("Save")
submit_url = "horizon:admin:group_types:specs:edit"
template_name = 'admin/group_types/specs/edit.html'
success_url = 'horizon:admin:group_types:index'
cancel_url = reverse_lazy('horizon:admin:group_types:index')
def get_success_url(self):
return reverse(self.success_url)
def get_initial(self):
group_type_id = self.kwargs['type_id']
key = self.kwargs['key']
try:
group_specs = api.cinder.group_type_spec_list(self.request,
group_type_id,
raw=True)
except Exception:
group_specs = {}
exceptions.handle(self.request,
_('Unable to retrieve group type spec '
'details.'))
return {'group_type_id': group_type_id,
'key': key,
'value': group_specs.get(key, '')}
def get_context_data(self, **kwargs):
context = super(EditView, self).get_context_data(**kwargs)
args = (self.kwargs['type_id'], self.kwargs['key'],)
context['submit_url'] = reverse(self.submit_url, args=args)
context['modal_header'] = self.modal_header % self.kwargs['key']
return context
def form_invalid(self, form):
context = super(EditView, self).get_context_data()
context = self._populate_context(context)
context['form'] = form
context['modal_header'] = self.modal_header % self.kwargs['key']
return self.render_to_response(context)

View File

@ -41,6 +41,15 @@ class EditGroupType(tables.LinkAction):
policy_rules = (("volume", "group:group_types_manage"),) policy_rules = (("volume", "group:group_types_manage"),)
class GroupTypeSpecs(tables.LinkAction):
name = "specs"
verbose_name = _("View Specs")
url = "horizon:admin:group_types:specs:index"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (("volume", "group:group_types_manage"),)
class GroupTypesFilterAction(tables.FilterAction): class GroupTypesFilterAction(tables.FilterAction):
def filter(self, table, group_types, filter_string): def filter(self, table, group_types, filter_string):
@ -118,6 +127,7 @@ class GroupTypesTable(tables.DataTable):
DeleteGroupType, DeleteGroupType,
) )
row_actions = ( row_actions = (
GroupTypeSpecs,
EditGroupType, EditGroupType,
DeleteGroupType DeleteGroupType
) )

View File

@ -0,0 +1,7 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% trans 'Create a new "group spec" key-value pair for a group type.' %}</p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% blocktrans with key=key %}Update the "group spec" value for "{{ key }}"{% endblocktrans %}</p>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "horizon/common/_modal.html" %}
{% load i18n %}
{% block modal_id %}group_type_specs_modal{% endblock %}
{% block modal-header %}{% trans "Group Type Specs" %}{% endblock %}
{% block modal-body %}
{{ table.render }}
{% endblock %}
{% block modal-footer %}
<a href="{% url 'horizon:admin:group_types:index' %}" class="btn btn-default cancel">{% trans "Close" %}</a>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Group Type Spec" %}{% endblock %}
{% block page_header %}
<h2>
{% blocktrans with group_type_name=group_type.name %}Group Type: {{ group_type_name }} {% endblocktrans %}
</h2>
{% endblock page_header %}
{% block main %}
{% include "admin/group_types/specs/_create.html" %}
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Edit Group Type Spec" %}{% endblock %}
{% block page_header %}
<h2>
{% blocktrans with group_type_name=group_type.name %}Group Type: {{ group_type_name }} {% endblocktrans %}
</h2>
{% endblock page_header %}
{% block main %}
{% include "admin/group_types/specs/_edit.html" %}
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Group Type Specs" %}{% endblock %}
{% block page_header %}
<h2>{% blocktrans with group_type_name=group_type.name %}Group Type: {{ group_type_name }}{% endblocktrans %}</h2>
{% endblock page_header %}
{% block main %}
{% include "admin/group_types/specs/_index.html" %}
{% endblock %}

View File

@ -10,8 +10,11 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from django.conf.urls import include
from django.conf.urls import url from django.conf.urls import url
from openstack_dashboard.dashboards.admin.group_types.specs \
import urls as specs_urls
from openstack_dashboard.dashboards.admin.group_types \ from openstack_dashboard.dashboards.admin.group_types \
import views import views
@ -23,4 +26,6 @@ urlpatterns = [
url(r'^(?P<type_id>[^/]+)/update_type/$', url(r'^(?P<type_id>[^/]+)/update_type/$',
views.EditGroupTypeView.as_view(), views.EditGroupTypeView.as_view(),
name='update_type'), name='update_type'),
url(r'^(?P<type_id>[^/]+)/specs/',
include((specs_urls, 'specs'))),
] ]

View File

@ -3,4 +3,7 @@ features:
- | - |
[:blueprint:`cinder-generic-volume-groups`] [:blueprint:`cinder-generic-volume-groups`]
Cinder generic groups is now supported for admin panel. Cinder generic groups is now supported for admin panel.
Admin is now able to view all groups for differenet users. Admin is now able to view all groups and group snapshots
for differenet users.
Also group-type and group-type-spec support added to admin panel.
Admin is able to create group-type and group-type-spec now.