Add volume-group table actions for admin panel

This commit allows admin to perform the following
table action :
1. manage volumes (Add/remove volumes from group),
2. remove all volumes from group
3. delete group

Partially-Implements blueprint cinder-generic-volume-groups

Change-Id: I7fcc0dfa8195ecb7914a883fec85c573caa4ae86
This commit is contained in:
manchandavishal 2019-01-23 04:50:11 +00:00
parent 47b23498d6
commit 6084b329f5
14 changed files with 286 additions and 18 deletions

View File

@ -0,0 +1,24 @@
# Copyright 2019 NEC Corporation
#
# 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 openstack_dashboard.dashboards.project.volume_groups \
import forms as project_forms
class RemoveVolsForm(project_forms.RemoveVolsForm):
failure_url = 'horizon:admin:volume_groups:index'
class DeleteForm(project_forms.DeleteForm):
failure_url = 'horizon:admin:volume_groups:index'

View File

@ -1,4 +1,4 @@
# Copyright 2017 NEC Corporation
# Copyright 2019 NEC Corporation
#
# 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
@ -13,8 +13,8 @@
# under the License.
from openstack_dashboard.dashboards.project.volume_groups \
import panel as volume_groups_panel
import panel as project_panel
class VolumeGroups(volume_groups_panel.VolumeGroups):
class VolumeGroups(project_panel.VolumeGroups):
policy_rules = (("volume", "context_is_admin"),)

View File

@ -1,3 +1,5 @@
# Copyright 2019 NEC Corporation
#
# 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
@ -14,10 +16,22 @@ from django.utils.translation import ugettext_lazy as _
from horizon import tables
from openstack_dashboard.dashboards.project.volume_groups \
import tables as volume_groups_tables
import tables as project_tables
class GroupsTable(volume_groups_tables.GroupsTable):
class DeleteGroup(project_tables.DeleteGroup):
url = "horizon:admin:volume_groups:delete"
class RemoveAllVolumes(project_tables.RemoveAllVolumes):
url = "horizon:admin:volume_groups:remove_volumes"
class ManageVolumes(project_tables.ManageVolumes):
url = "horizon:admin:volume_groups:manage"
class GroupsTable(project_tables.GroupsTable):
# TODO(vishalmanchanda): Add Project Info.column in table
name = tables.WrappingColumn("name_or_id",
verbose_name=_("Name"),
@ -27,5 +41,12 @@ class GroupsTable(volume_groups_tables.GroupsTable):
name = "volume_groups"
verbose_name = _("Volume Groups")
table_actions = (
volume_groups_tables.GroupsFilterAction,
project_tables.GroupsFilterAction,
)
row_actions = (
ManageVolumes,
RemoveAllVolumes,
DeleteGroup,
)
row_class = project_tables.UpdateRow
status_columns = ("status",)

View File

@ -1,3 +1,5 @@
# Copyright 2019 NEC Corporation
#
# 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
@ -13,15 +15,15 @@
from django.urls import reverse
from openstack_dashboard.dashboards.project.volume_groups \
import tabs as volume_groups_tabs
import tabs as project_tabs
class OverviewTab(volume_groups_tabs.OverviewTab):
class OverviewTab(project_tabs.OverviewTab):
template_name = ("admin/volume_groups/_detail_overview.html")
def get_redirect_url(self):
return reverse('horizon:admin:volume_groups:index')
class GroupsDetailTabs(volume_groups_tabs.GroupsDetailTabs):
class GroupsDetailTabs(project_tabs.GroupsDetailTabs):
tabs = (OverviewTab,)

View File

@ -0,0 +1,9 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block modal-body-right %}
<p>{% trans "Volume groups can not be deleted if they contain volumes." %}</p>
<p>{% trans "Check the &quot;Delete Volumes&quot; box to also delete any volumes associated with this group." %}</p>
<p>{% trans "Note that a volume can not be deleted if it is &quot;attached&quot; or has any dependent snapshots." %}</p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block modal-body %}
<p>{% trans "This action will unassign all volumes that are currently contained in this group." %}</p>
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% include 'admin/volumes/volume_groups/_delete.html' %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{{ page_title }}{% endblock %}
{% block main %}
{% include 'admin/volume_groups/_remove_vols.html' %}
{% endblock %}

View File

@ -1,3 +1,5 @@
# Copyright 2019 NEC Corporation
#
# 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
@ -12,6 +14,8 @@
from django.urls import reverse
import mock
from openstack_dashboard import api
from openstack_dashboard.api import cinder
from openstack_dashboard.test import helpers as test
@ -42,6 +46,126 @@ class AdminVolumeGroupTests(test.BaseAdminViewTests):
self.mock_group_snapshot_list.assert_called_once_with(
test.IsHttpRequest())
@test.create_mocks({cinder: ['group_get', 'group_delete']})
def test_delete_group(self):
group = self.cinder_groups.first()
self.mock_group_get.return_value = group
self.mock_group_delete.return_value = None
url = reverse('horizon:admin:volume_groups:delete',
args=[group.id])
res = self.client.post(url)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_group_get.assert_called_once_with(test.IsHttpRequest(),
group.id)
self.mock_group_delete.assert_called_once_with(test.IsHttpRequest(),
group.id,
delete_volumes=False)
@test.create_mocks({cinder: ['group_get', 'group_delete']})
def test_delete_group_delete_volumes_flag(self):
group = self.cinder_consistencygroups.first()
formData = {'delete_volumes': True}
self.mock_group_get.return_value = group
self.mock_group_delete.return_value = None
url = reverse('horizon:admin:volume_groups:delete',
args=[group.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_group_get.assert_called_once_with(test.IsHttpRequest(),
group.id)
self.mock_group_delete.assert_called_once_with(test.IsHttpRequest(),
group.id,
delete_volumes=True)
@test.create_mocks({cinder: ['group_get', 'group_delete']})
def test_delete_group_exception(self):
group = self.cinder_groups.first()
formData = {'delete_volumes': False}
self.mock_group_get.return_value = group
self.mock_group_delete.side_effect = self.exceptions.cinder
url = reverse('horizon:admin:volume_groups:delete',
args=[group.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.mock_group_get.assert_called_once_with(test.IsHttpRequest(),
group.id)
self.mock_group_delete.assert_called_once_with(test.IsHttpRequest(),
group.id,
delete_volumes=False)
def test_update_group_add_vol(self):
self._test_update_group_add_remove_vol(add=True)
def test_update_group_remove_vol(self):
self._test_update_group_add_remove_vol(add=False)
@test.create_mocks({cinder: ['volume_list',
'volume_type_list',
'group_get',
'group_update']})
def _test_update_group_add_remove_vol(self, add=True):
group = self.cinder_groups.first()
volume_types = self.cinder_volume_types.list()
volumes = (self.cinder_volumes.list() +
self.cinder_group_volumes.list())
group_voltype_names = [t.name for t in volume_types
if t.id in group.volume_types]
compat_volumes = [v for v in volumes
if v.volume_type in group_voltype_names]
compat_volume_ids = [v.id for v in compat_volumes]
assigned_volume_ids = [v.id for v in compat_volumes
if getattr(v, 'group_id', None)]
add_volume_ids = [v.id for v in compat_volumes
if v.id not in assigned_volume_ids]
new_volums = compat_volume_ids if add else []
formData = {
'default_add_volumes_to_group_role': 'member',
'add_volumes_to_group_role_member': new_volums,
}
self.mock_volume_list.return_value = volumes
self.mock_volume_type_list.return_value = volume_types
self.mock_group_get.return_value = group
self.mock_group_update.return_value = group
url = reverse('horizon:admin:volume_groups:manage',
args=[group.id])
res = self.client.post(url, formData)
self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assert_mock_multiple_calls_with_same_arguments(
self.mock_volume_list, 2,
mock.call(test.IsHttpRequest()))
self.mock_volume_type_list.assert_called_once_with(
test.IsHttpRequest())
self.mock_group_get.assert_called_once_with(
test.IsHttpRequest(), group.id)
if add:
self.mock_group_update.assert_called_once_with(
test.IsHttpRequest(), group.id,
add_volumes=add_volume_ids,
remove_volumes=[])
else:
self.mock_group_update.assert_called_once_with(
test.IsHttpRequest(), group.id,
add_volumes=[],
remove_volumes=assigned_volume_ids)
@test.create_mocks({cinder: ['group_get_with_vol_type_names',
'volume_list',
'group_snapshot_list']})

View File

@ -1,3 +1,5 @@
# Copyright 2019 NEC Corporation
#
# 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
@ -20,4 +22,13 @@ urlpatterns = [
url(r'^(?P<group_id>[^/]+)$',
views.DetailView.as_view(),
name='detail'),
url(r'^(?P<group_id>[^/]+)/remove_volumese/$',
views.RemoveVolumesView.as_view(),
name='remove_volumes'),
url(r'^(?P<group_id>[^/]+)/delete/$',
views.DeleteView.as_view(),
name='delete'),
url(r'^(?P<group_id>[^/]+)/manage/$',
views.ManageView.as_view(),
name='manage'),
]

View File

@ -1,3 +1,5 @@
# Copyright 2019 NEC Corporation
#
# 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
@ -11,6 +13,7 @@
# 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
@ -19,15 +22,19 @@ from horizon import tables
from openstack_dashboard import api
from openstack_dashboard.dashboards.admin.volume_groups \
import tables as volume_group_tables
import forms as admin_forms
from openstack_dashboard.dashboards.admin.volume_groups \
import tabs as volume_group_tabs
import tables as admin_tables
from openstack_dashboard.dashboards.admin.volume_groups \
import tabs as admin_tabs
from openstack_dashboard.dashboards.admin.volume_groups \
import workflows as admin_workflows
from openstack_dashboard.dashboards.project.volume_groups \
import views as volume_groups_views
import views as project_views
class IndexView(tables.DataTableView):
table_class = volume_group_tables.GroupsTable
table_class = admin_tables.GroupsTable
page_title = _("Groups")
def get_data(self):
@ -47,12 +54,30 @@ class IndexView(tables.DataTableView):
return groups
class DetailView(volume_groups_views.DetailView):
tab_group_class = volume_group_tabs.GroupsDetailTabs
class RemoveVolumesView(project_views.RemoveVolumesView):
template_name = 'admin/volume_groups/remove_vols.html'
form_class = admin_forms.RemoveVolsForm
success_url = reverse_lazy('horizon:admin:volume_groups:index')
submit_url = "horizon:admin:volume_groups:remove_volumes"
class DeleteView(project_views.DeleteView):
template_name = 'admin/volume_groups/delete.html'
form_class = admin_forms.DeleteForm
success_url = reverse_lazy('horizon:admin:volume_groups:index')
submit_url = "horizon:admin:volume_groups:delete"
class ManageView(project_views.ManageView):
workflow_class = admin_workflows.UpdateGroupWorkflow
class DetailView(project_views.DetailView):
tab_group_class = admin_tabs.GroupsDetailTabs
def get_context_data(self, **kwargs):
context = super(DetailView, self).get_context_data(**kwargs)
table = volume_group_tables.GroupsTable(self.request)
table = admin_tables.GroupsTable(self.request)
context["actions"] = table.render_row_actions(context["group"])
return context

View File

@ -0,0 +1,22 @@
# Copyright 2019 NEC Corporation
#
# 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 openstack_dashboard.dashboards.project.volume_groups \
import workflows as project_workflows
INDEX_URL = "horizon:admin:volume_groups:index"
class UpdateGroupWorkflow(project_workflows.UpdateGroupWorkflow):
success_url = INDEX_URL

View File

@ -58,6 +58,8 @@ class UpdateForm(forms.SelfHandlingForm):
class RemoveVolsForm(forms.SelfHandlingForm):
failure_url = 'horizon:project:volume_groups:index'
def handle(self, request, data):
group_id = self.initial['group_id']
name = self.initial['name']
@ -79,7 +81,7 @@ class RemoveVolsForm(forms.SelfHandlingForm):
return True
except Exception:
redirect = reverse("horizon:project:volume_groups:index")
redirect = reverse(self.failure_url)
exceptions.handle(request,
_('Errors occurred in removing volumes '
'from group.'),
@ -89,6 +91,7 @@ class RemoveVolsForm(forms.SelfHandlingForm):
class DeleteForm(forms.SelfHandlingForm):
delete_volumes = forms.BooleanField(label=_("Delete Volumes"),
required=False)
failure_url = 'horizon:project:volume_groups:index'
def handle(self, request, data):
group_id = self.initial['group_id']
@ -103,7 +106,7 @@ class DeleteForm(forms.SelfHandlingForm):
return True
except Exception:
redirect = reverse("horizon:project:volume_groups:index")
redirect = reverse(self.failure_url)
exceptions.handle(request, _('Errors occurred in deleting group.'),
redirect=redirect)

View File

@ -0,0 +1,6 @@
---
features:
- |
[:blueprint:`cinder-generic-volume-groups`]
Cinder generic groups is now supported for admin panel.
Admin is now able to view all groups for differenet users.