Merge "Add volume group-type list/show support for admin panel"

This commit is contained in:
Zuul 2019-02-09 08:19:36 +00:00 committed by Gerrit Code Review
commit 91d45cf269
13 changed files with 567 additions and 2 deletions

View File

@ -1139,9 +1139,13 @@ def group_type_create(request, name, description=None, is_public=None):
@profiler.trace
def group_type_update(request, group_type_id, data):
def group_type_update(request, group_type_id, name=None, description=None,
is_public=None):
client = _cinderclient_with_generic_groups(request)
return GroupType(client.group_types.update(group_type_id, **data))
return GroupType(client.group_types.update(group_type_id,
name,
description,
is_public))
@profiler.trace

View File

@ -0,0 +1,107 @@
# 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.forms import ValidationError
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.api import cinder
class CreateGroupType(forms.SelfHandlingForm):
name = forms.CharField(max_length=255, label=_("Name"))
group_type_description = forms.CharField(
max_length=255,
widget=forms.Textarea(attrs={'rows': 4}),
label=_("Description"),
required=False)
is_public = forms.BooleanField(
label=_("Public"),
initial=True,
required=False,
help_text=_("By default, group type is created as public. To "
"create a private group type, uncheck this field."))
def clean_name(self):
cleaned_name = self.cleaned_data['name']
if cleaned_name.isspace():
raise ValidationError(_('Group type name can not be empty.'))
return cleaned_name
def handle(self, request, data):
try:
group_type = cinder.group_type_create(
request,
data['name'],
data['group_type_description'],
data['is_public'])
messages.success(request, _('Successfully created group type: %s')
% data['name'])
return group_type
except Exception as e:
if getattr(e, 'code', None) == 409:
msg = _('Group type name "%s" already '
'exists.') % data['name']
self._errors['name'] = self.error_class([msg])
else:
redirect = reverse("horizon:admin:group_types:index")
exceptions.handle(request,
_('Unable to create group type.'),
redirect=redirect)
class EditGroupType(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)
is_public = forms.BooleanField(label=_("Public"), required=False,
help_text=_(
"To make group type private, uncheck "
"this field."))
def clean_name(self):
cleaned_name = self.cleaned_data['name']
if cleaned_name.isspace():
msg = _('New name cannot be empty.')
self._errors['name'] = self.error_class([msg])
return cleaned_name
def handle(self, request, data):
group_type_id = self.initial['id']
try:
cinder.group_type_update(request,
group_type_id,
data['name'],
data['description'],
data['is_public'])
message = _('Successfully updated group type.')
messages.success(request, message)
return True
except Exception as ex:
redirect = reverse("horizon:admin:group_types:index")
if ex.code == 409:
error_message = _('New name conflicts with another '
'group type.')
else:
error_message = _('Unable to update group type.')
exceptions.handle(request, error_message,
redirect=redirect)

View File

@ -0,0 +1,24 @@
# 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 _
import horizon
class GroupTypes(horizon.Panel):
name = _("Group Types")
slug = 'group_types'
permissions = (
('openstack.services.volume', 'openstack.services.volumev3'),
)
policy_rules = (("volume", "group:group_types_manage"),)

View File

@ -0,0 +1,124 @@
# 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.urls import reverse
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
class CreateGroupType(tables.LinkAction):
name = "create"
verbose_name = _("Create Group Type")
url = "horizon:admin:group_types:create_type"
classes = ("ajax-modal",)
icon = "plus"
policy_rules = (("volume", "group:group_types_manage"),)
class EditGroupType(tables.LinkAction):
name = "edit"
verbose_name = _("Edit Group Type")
url = "horizon:admin:group_types:update_type"
classes = ("ajax-modal",)
icon = "pencil"
policy_rules = (("volume", "group:group_types_manage"),)
class GroupTypesFilterAction(tables.FilterAction):
def filter(self, table, group_types, filter_string):
"""Naive case-insensitive search."""
query = filter_string.lower()
return [group_type for group_type in group_types
if query in group_type.name.lower()]
class DeleteGroupType(tables.DeleteAction):
@staticmethod
def action_present(count):
return ungettext_lazy(
u"Delete Group Type",
u"Delete Group Types",
count
)
@staticmethod
def action_past(count):
return ungettext_lazy(
u"Deleted Group Type",
u"Deleted Group Types",
count
)
policy_rules = (("volume", "group:group_types_manage"),)
def delete(self, request, obj_id):
try:
cinder.group_type_delete(request, obj_id)
except exceptions.BadRequest as e:
redirect_url = reverse("horizon:admin:group_types:index")
exceptions.handle(request, e, redirect=redirect_url)
class UpdateRow(tables.Row):
ajax = True
def get_data(self, request, group_type_id):
try:
group_type = \
cinder.group_type_get(request, group_type_id)
except Exception:
exceptions.handle(request,
_('Unable to retrieve group type.'))
return group_type
class GroupTypesTable(tables.DataTable):
name = tables.WrappingColumn("name", verbose_name=_("Name"),
form_field=forms.CharField(max_length=64))
description = tables.Column(lambda obj: getattr(obj, 'description', None),
verbose_name=_('Description'),
form_field=forms.CharField(
widget=forms.Textarea(attrs={'rows': 4}),
required=False))
public = tables.Column("is_public",
verbose_name=_("Public"),
filters=(filters.yesno, filters.capfirst),
form_field=forms.BooleanField(
label=_('Public'), required=False))
def get_object_display(self, group_type):
return group_type.name
def get_object_id(self, group_type):
return str(group_type.id)
class Meta(object):
name = "group_types"
verbose_name = _("Group Types")
table_actions = (
GroupTypesFilterAction,
CreateGroupType,
DeleteGroupType,
)
row_actions = (
EditGroupType,
DeleteGroupType
)
row_class = UpdateRow

View File

@ -0,0 +1,16 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block modal-body-right %}
<h3>{% trans "Description:" %}</h3>
<p>{% blocktrans trimmed %}
Group type is a type or label that can be selected at group creation
time in OpenStack. 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. This is equivalent to the
<tt>cinder type-create</tt> command. Once the group type gets created,
click the "Extra Specs" button to set up extra specs key-value
pair(s) for that group type.
{% endblocktrans %}
</p>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block form_id %}{% endblock %}
{% block form_action %}{% url 'horizon:admin:group_types:update_type' group_type.id %}{% endblock %}
{% block modal_id %}update_group_type_modal{% endblock %}
{% block modal-header %}{% trans "Edit Group Type" %}{% endblock %}
{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
<div class="right">
<h3>{% trans "Description:" %}</h3>
<p>{% trans "Modify group type name, description, and public status." %}</p>
</div>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Create Group Type" %}{% endblock %}
{% block main %}
{% include 'admin/group_types/_create_group_type.html' %}
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Edit Group Type" %}{% endblock %}
{% block page_header %}
{% include "horizon/common/_page_header.html" with title=_("Edit Group Type") %}
{% endblock page_header %}
{% block main %}
{% include 'admin/group_types/_update_group_type.html' %}
{% endblock %}

View File

@ -0,0 +1,114 @@
# 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 horizon import exceptions
from openstack_dashboard import api
from openstack_dashboard.api import cinder
from openstack_dashboard.test import helpers as test
INDEX_URL = reverse('horizon:admin:group_types:index')
class GroupTypeTests(test.BaseAdminViewTests):
@test.create_mocks({cinder: ('group_type_create',)})
def test_create_group_type(self):
group_type = self.cinder_group_types.first()
formData = {'name': 'group type 1',
'group_type_description': 'test desc',
'is_public': True}
self.mock_group_type_create.return_value = group_type
url = reverse('horizon:admin:group_types:create_type')
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_group_type_create.assert_called_once_with(
test.IsHttpRequest(),
formData['name'],
formData['group_type_description'],
formData['is_public'])
@test.create_mocks({api.cinder: ('group_type_get',
'group_type_update')})
def _test_update_group_type(self, is_public):
group_type = self.cinder_group_types.first()
formData = {'name': group_type.name,
'description': 'test desc updated',
'is_public': is_public}
self.mock_group_type_get.return_value = group_type
self.mock_group_type_update.return_value = group_type
url = reverse('horizon:admin:group_types:update_type',
args=[group_type.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_group_type_get.assert_called_once_with(
test.IsHttpRequest(), group_type.id)
self.mock_group_type_update.assert_called_once_with(
test.IsHttpRequest(),
group_type.id,
formData['name'],
formData['description'],
formData['is_public'])
def test_update_group_type_public_true(self):
self._test_update_group_type(True)
def test_update_group_type_public_false(self):
self._test_update_group_type(False)
@test.create_mocks({api.cinder: ('group_type_list',
'group_type_delete',)})
def test_delete_group_type(self):
group_type = self.cinder_group_types.first()
formData = {'action': 'group_types__delete__%s' % group_type.id}
self.mock_group_type_list.return_value = \
self.cinder_group_types.list()
self.mock_group_type_delete.return_value = None
res = self.client.post(INDEX_URL, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_group_type_list.\
assert_called_once_with(test.IsHttpRequest())
self.mock_group_type_delete.assert_called_once_with(
test.IsHttpRequest(), group_type.id)
@test.create_mocks({api.cinder: ('group_type_list',
'group_type_delete',)})
def test_delete_group_type_exception(self):
group_type = self.cinder_group_types.first()
formData = {'action': 'group_types__delete__%s' % group_type.id}
self.mock_group_type_list.return_value = \
self.cinder_group_types.list()
self.mock_group_type_delete.side_effect = exceptions.BadRequest()
res = self.client.post(INDEX_URL, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_group_type_list.\
assert_called_once_with(test.IsHttpRequest())
self.mock_group_type_delete.assert_called_once_with(
test.IsHttpRequest(), group_type.id)

View File

@ -0,0 +1,26 @@
# 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 \
import views
urlpatterns = [
url(r'^$', views.GroupTypesView.as_view(), name='index'),
url(r'^create_type$', views.CreateGroupTypeView.as_view(),
name='create_type'),
url(r'^(?P<type_id>[^/]+)/update_type/$',
views.EditGroupTypeView.as_view(),
name='update_type'),
]

View File

@ -0,0 +1,102 @@
# 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 horizon.utils import memoized
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.group_types \
import forms as group_types_forms
from openstack_dashboard.dashboards.admin.group_types \
import tables as group_types_tables
INDEX_URL = 'horizon:admin:group_types:index'
class GroupTypesView(tables.DataTableView):
table_class = group_types_tables.GroupTypesTable
page_title = _("Group Types")
def get_data(self):
try:
group_types = api.cinder.group_type_list(self.request)
except Exception:
group_types = []
exceptions.handle(self.request,
_("Unable to retrieve group types."))
return group_types
class CreateGroupTypeView(forms.ModalFormView):
form_class = group_types_forms.CreateGroupType
modal_id = "create_group_type_modal"
template_name = 'admin/group_types/create_group_type.html'
submit_label = _("Create Group Type")
submit_url = reverse_lazy("horizon:admin:group_types:create_type")
success_url = reverse_lazy('horizon:admin:group_types:index')
page_title = _("Create a Group Type")
class EditGroupTypeView(forms.ModalFormView):
form_class = group_types_forms.EditGroupType
template_name = 'admin/group_types/update_group_type.html'
success_url = reverse_lazy('horizon:admin:group_types:index')
cancel_url = reverse_lazy('horizon:admin:group_types:index')
submit_label = _('Edit')
@memoized.memoized_method
def get_data(self):
try:
group_type_id = self.kwargs['type_id']
group_type = api.cinder.group_type_get(self.request,
group_type_id)
except Exception:
error_message = _(
'Unable to retrieve group type for: "%s"') \
% group_type_id
exceptions.handle(self.request,
error_message,
redirect=self.success_url)
return group_type
def get_context_data(self, **kwargs):
context = super(EditGroupTypeView, self).get_context_data(**kwargs)
context['group_type'] = self.get_data()
return context
def get_initial(self):
group_type = self.get_data()
return {'id': self.kwargs['type_id'],
'name': group_type.name,
'is_public': getattr(group_type, 'is_public', True),
'description': getattr(group_type, 'description', "")}
def _get_group_type_name(request, kwargs):
try:
group_type_list = api.cinder.group_type_list(request)
for group_type in group_type_list:
if group_type.id == kwargs['group_type_id']:
return group_type.name
except Exception:
msg = _('Unable to retrieve group type name.')
url = reverse('INDEX_URL')
exceptions.handle(request, msg, redirect=url)

View File

@ -0,0 +1,10 @@
# The slug of the panel to be added to HORIZON_CONFIG. Required.
PANEL = 'group_types'
# The slug of the dashboard the PANEL associated with. Required.
PANEL_DASHBOARD = 'admin'
# The slug of the panel group the PANEL is associated with.
PANEL_GROUP = 'volume'
# Python panel class of the PANEL to be added.
ADD_PANEL = \
'openstack_dashboard.dashboards.admin.group_types.panel.GroupTypes'