diff --git a/openstack_dashboard/dashboards/admin/volume_groups/forms.py b/openstack_dashboard/dashboards/admin/volume_groups/forms.py new file mode 100644 index 0000000000..e883e3b4c9 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volume_groups/forms.py @@ -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' diff --git a/openstack_dashboard/dashboards/admin/volume_groups/panel.py b/openstack_dashboard/dashboards/admin/volume_groups/panel.py index 444dc53713..f13ecd4c69 100644 --- a/openstack_dashboard/dashboards/admin/volume_groups/panel.py +++ b/openstack_dashboard/dashboards/admin/volume_groups/panel.py @@ -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"),) diff --git a/openstack_dashboard/dashboards/admin/volume_groups/tables.py b/openstack_dashboard/dashboards/admin/volume_groups/tables.py index 42412f07ac..bab271b85a 100644 --- a/openstack_dashboard/dashboards/admin/volume_groups/tables.py +++ b/openstack_dashboard/dashboards/admin/volume_groups/tables.py @@ -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",) diff --git a/openstack_dashboard/dashboards/admin/volume_groups/tabs.py b/openstack_dashboard/dashboards/admin/volume_groups/tabs.py index bf6274fe99..517790caef 100644 --- a/openstack_dashboard/dashboards/admin/volume_groups/tabs.py +++ b/openstack_dashboard/dashboards/admin/volume_groups/tabs.py @@ -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,) diff --git a/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/_delete.html b/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/_delete.html new file mode 100644 index 0000000000..79f26df15f --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/_delete.html @@ -0,0 +1,9 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block title %}{{ page_title }}{% endblock %} + +{% block modal-body-right %} +

{% trans "Volume groups can not be deleted if they contain volumes." %}

+

{% trans "Check the "Delete Volumes" box to also delete any volumes associated with this group." %}

+

{% trans "Note that a volume can not be deleted if it is "attached" or has any dependent snapshots." %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/_remove_vols.html b/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/_remove_vols.html new file mode 100644 index 0000000000..48c5b29ac5 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/_remove_vols.html @@ -0,0 +1,7 @@ +{% extends "horizon/common/_modal_form.html" %} +{% load i18n %} +{% block title %}{{ page_title }}{% endblock %} + +{% block modal-body %} +

{% trans "This action will unassign all volumes that are currently contained in this group." %}

+{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/delete.html b/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/delete.html new file mode 100644 index 0000000000..4b1c9574ea --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/delete.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{{ page_title }}{% endblock %} + +{% block main %} + {% include 'admin/volumes/volume_groups/_delete.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/remove_vols.html b/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/remove_vols.html new file mode 100644 index 0000000000..87b8bfb8ac --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/remove_vols.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{{ page_title }}{% endblock %} + +{% block main %} + {% include 'admin/volume_groups/_remove_vols.html' %} +{% endblock %} diff --git a/openstack_dashboard/dashboards/admin/volume_groups/tests.py b/openstack_dashboard/dashboards/admin/volume_groups/tests.py index a7b6bc6db5..951b21fb39 100644 --- a/openstack_dashboard/dashboards/admin/volume_groups/tests.py +++ b/openstack_dashboard/dashboards/admin/volume_groups/tests.py @@ -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']}) diff --git a/openstack_dashboard/dashboards/admin/volume_groups/urls.py b/openstack_dashboard/dashboards/admin/volume_groups/urls.py index 7a6680927a..d34156626d 100644 --- a/openstack_dashboard/dashboards/admin/volume_groups/urls.py +++ b/openstack_dashboard/dashboards/admin/volume_groups/urls.py @@ -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[^/]+)$', views.DetailView.as_view(), name='detail'), + url(r'^(?P[^/]+)/remove_volumese/$', + views.RemoveVolumesView.as_view(), + name='remove_volumes'), + url(r'^(?P[^/]+)/delete/$', + views.DeleteView.as_view(), + name='delete'), + url(r'^(?P[^/]+)/manage/$', + views.ManageView.as_view(), + name='manage'), ] diff --git a/openstack_dashboard/dashboards/admin/volume_groups/views.py b/openstack_dashboard/dashboards/admin/volume_groups/views.py index 15cebd1547..d2bfa53709 100644 --- a/openstack_dashboard/dashboards/admin/volume_groups/views.py +++ b/openstack_dashboard/dashboards/admin/volume_groups/views.py @@ -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 diff --git a/openstack_dashboard/dashboards/admin/volume_groups/workflows.py b/openstack_dashboard/dashboards/admin/volume_groups/workflows.py new file mode 100644 index 0000000000..97322cc634 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/volume_groups/workflows.py @@ -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 diff --git a/openstack_dashboard/dashboards/project/volume_groups/forms.py b/openstack_dashboard/dashboards/project/volume_groups/forms.py index d19a99331c..3567e7ca3f 100644 --- a/openstack_dashboard/dashboards/project/volume_groups/forms.py +++ b/openstack_dashboard/dashboards/project/volume_groups/forms.py @@ -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) diff --git a/releasenotes/notes/generic-volume-groups-c0bf175f5d7d3a37.yaml b/releasenotes/notes/generic-volume-groups-c0bf175f5d7d3a37.yaml new file mode 100644 index 0000000000..653db0ac1c --- /dev/null +++ b/releasenotes/notes/generic-volume-groups-c0bf175f5d7d3a37.yaml @@ -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.