diff --git a/openstack_dashboard/dashboards/admin/vg_snapshots/__init__.py b/openstack_dashboard/dashboards/admin/vg_snapshots/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstack_dashboard/dashboards/admin/vg_snapshots/panel.py b/openstack_dashboard/dashboards/admin/vg_snapshots/panel.py new file mode 100644 index 0000000000..821f57f8f3 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/vg_snapshots/panel.py @@ -0,0 +1,20 @@ +# 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.vg_snapshots \ + import panel as project_panel + + +class GroupSnapshots(project_panel.GroupSnapshots): + policy_rules = (("volume", "context_is_admin"),) diff --git a/openstack_dashboard/dashboards/admin/vg_snapshots/tables.py b/openstack_dashboard/dashboards/admin/vg_snapshots/tables.py new file mode 100644 index 0000000000..16684e362f --- /dev/null +++ b/openstack_dashboard/dashboards/admin/vg_snapshots/tables.py @@ -0,0 +1,43 @@ +# 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 django.utils.translation import ugettext_lazy as _ + +from horizon import tables + +from openstack_dashboard.dashboards.project.vg_snapshots \ + import tables as project_tables + + +class GroupSnapshotsTable(project_tables.GroupSnapshotsTable): + # TODO(vishalmanchanda): Add Project Info.column in table + name = tables.Column("name_or_id", + verbose_name=_("Name"), + link="horizon:admin:vg_snapshots:detail") + group = project_tables.GroupNameColumn( + "name", verbose_name=_("Group"), + link="horizon:admin:volume_groups:detail") + + class Meta(object): + name = "volume_vg_snapshots" + verbose_name = _("Group Snapshots") + table_actions = ( + project_tables.GroupSnapshotsFilterAction, + project_tables.DeleteGroupSnapshot, + ) + row_actions = ( + project_tables.DeleteGroupSnapshot, + ) + row_class = project_tables.UpdateRow + status_columns = ("status",) diff --git a/openstack_dashboard/dashboards/admin/vg_snapshots/tabs.py b/openstack_dashboard/dashboards/admin/vg_snapshots/tabs.py new file mode 100644 index 0000000000..2fec23639f --- /dev/null +++ b/openstack_dashboard/dashboards/admin/vg_snapshots/tabs.py @@ -0,0 +1,29 @@ +# 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 django.urls import reverse + +from openstack_dashboard.dashboards.project.vg_snapshots \ + import tabs as project_tabs + + +class OverviewTab(project_tabs.OverviewTab): + template_name = "admin/vg_snapshots/_detail_overview.html" + + def get_redirect_url(self): + return reverse('horizon:admin:vg_snapshots:index') + + +class DetailTabs(project_tabs.DetailTabs): + tabs = (OverviewTab,) diff --git a/openstack_dashboard/dashboards/admin/vg_snapshots/templates/vg_snapshots/_detail_overview.html b/openstack_dashboard/dashboards/admin/vg_snapshots/templates/vg_snapshots/_detail_overview.html new file mode 100644 index 0000000000..c390392102 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/vg_snapshots/templates/vg_snapshots/_detail_overview.html @@ -0,0 +1,50 @@ +{% load i18n sizeformat parse_date %} + +
+
+
{% trans "Name" %}
+
{{ vg_snapshot.name }}
+
{% trans "ID" %}
+
{{ vg_snapshot.id }}
+ {% if vg_snapshot.description %} +
{% trans "Description" %}
+
{{ vg_snapshot.description }}
+ {% endif %} +
{% trans "Status" %}
+
{{ vg_snapshot.status|capfirst }}
+
{% trans "Group" %}
+
+ + {% if vg_snapshot.vg_name %} + {{ vg_snapshot.vg_name }} + {% else %} + {{ vg_snapshot.group_id }} + {% endif %} + +
+
{% trans "Group Type" %}
+
{{ vg_snapshot.group_type_id }}
+
{% trans "Created" %}
+
{{ vg_snapshot.created_at|parse_isotime }}
+
+ +

{% trans "Snapshot Volume Types" %}

+
+
+ {% for vol_type_names in vg_snapshot.volume_type_names %} +
{{ vol_type_names }}
+ {% endfor %} +
+ +

{% trans "Snapshot Volumes" %}

+
+
+ {% for vol_names in vg_snapshot.volume_names %} +
{{ vol_names }}
+ {% empty %} +
+ {% trans "No assigned volumes" %} +
+ {% endfor %} +
+
diff --git a/openstack_dashboard/dashboards/admin/vg_snapshots/tests.py b/openstack_dashboard/dashboards/admin/vg_snapshots/tests.py new file mode 100644 index 0000000000..058248bcc7 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/vg_snapshots/tests.py @@ -0,0 +1,149 @@ +# 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 django.urls import reverse +import mock + +from openstack_dashboard import api + +from openstack_dashboard.test import helpers as test + + +INDEX_URL = reverse('horizon:admin:vg_snapshots:index') +INDEX_TEMPLATE = 'horizon/common/_data_table_view.html' + + +class AdminGroupSnapshotTests(test.BaseAdminViewTests): + @test.create_mocks({ + api.cinder: ['group_list', + 'group_snapshot_list']}) + def test_index(self): + vg_snapshots = self.cinder_group_snapshots.list() + groups = self.cinder_groups.list() + self.mock_group_snapshot_list.return_value = vg_snapshots + self.mock_group_list.return_value = groups + + res = self.client.get(INDEX_URL) + self.assertTemplateUsed(res, INDEX_TEMPLATE) + self.assertIn('volume_vg_snapshots_table', res.context) + volume_vg_snapshots_table = res.context['volume_vg_snapshots_table'] + volume_vg_snapshots = volume_vg_snapshots_table.data + self.assertEqual(len(volume_vg_snapshots), 1) + + self.mock_group_snapshot_list.assert_called_once_with( + test.IsHttpRequest(), {'all_tenants': 1}) + self.mock_group_list.assert_called_once_with( + test.IsHttpRequest(), {'all_tenants': 1}) + + @test.create_mocks({ + api.cinder: ['group_list', + 'group_snapshot_delete', + 'group_snapshot_list']}) + def test_delete_group_snapshot(self): + vg_snapshots = self.cinder_group_snapshots.list() + vg_snapshot = self.cinder_group_snapshots.first() + self.mock_group_snapshot_list.return_value = vg_snapshots + self.mock_group_snapshot_delete.return_value = None + self.mock_group_list.return_value = self.cinder_groups.list() + + form_data = {'action': 'volume_vg_snapshots__delete_vg_snapshot__%s' + % vg_snapshot.id} + res = self.client.post(INDEX_URL, form_data, follow=True) + self.assertEqual(res.status_code, 200) + self.assertIn("Scheduled deletion of Snapshot: %s" % vg_snapshot.name, + [m.message for m in res.context['messages']]) + + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_group_snapshot_list, 2, + mock.call(test.IsHttpRequest(), {'all_tenants': 1})) + self.mock_group_snapshot_delete.assert_called_once_with( + test.IsHttpRequest(), vg_snapshot.id) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_group_list, 2, + mock.call(test.IsHttpRequest(), {'all_tenants': 1})) + + @test.create_mocks({ + api.cinder: ['group_list', + 'group_snapshot_delete', + 'group_snapshot_list']}) + def test_delete_group_snapshot_exception(self): + vg_snapshots = self.cinder_group_snapshots.list() + vg_snapshot = self.cinder_group_snapshots.first() + self.mock_group_snapshot_list.return_value = vg_snapshots + self.mock_group_snapshot_delete.side_effect = self.exceptions.cinder + self.mock_group_list.return_value = self.cinder_groups.list() + + form_data = {'action': 'volume_vg_snapshots__delete_vg_snapshot__%s' + % vg_snapshot.id} + res = self.client.post(INDEX_URL, form_data, follow=True) + self.assertEqual(res.status_code, 200) + self.assertIn("Unable to delete snapshot: %s" % vg_snapshot.name, + [m.message for m in res.context['messages']]) + + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_group_snapshot_list, 2, + mock.call(test.IsHttpRequest(), {'all_tenants': 1})) + self.mock_group_snapshot_delete.assert_called_once_with( + test.IsHttpRequest(), vg_snapshot.id) + self.assert_mock_multiple_calls_with_same_arguments( + self.mock_group_list, 2, + mock.call(test.IsHttpRequest(), {'all_tenants': 1})) + + @test.create_mocks({ + api.cinder: ['group_snapshot_get', + 'group_get', + 'volume_type_get', + 'volume_list']}) + def test_detail_view(self): + vg_snapshot = self.cinder_group_snapshots.first() + group = self.cinder_groups.first() + volume_type = self.cinder_volume_types.first() + volumes = self.cinder_volumes.list() + + self.mock_group_snapshot_get.return_value = vg_snapshot + self.mock_group_get.return_value = group + self.mock_volume_type_get.return_value = volume_type + self.mock_volume_list.return_value = volumes + + url = reverse( + 'horizon:admin:vg_snapshots:detail', + args=[vg_snapshot.id]) + res = self.client.get(url) + self.assertNoFormErrors(res) + self.assertEqual(res.status_code, 200) + self.mock_group_snapshot_get.assert_called_once_with( + test.IsHttpRequest(), vg_snapshot.id) + self.mock_group_get.assert_called_once_with( + test.IsHttpRequest(), group.id) + self.mock_volume_type_get.assert_called_once_with( + test.IsHttpRequest(), volume_type.id) + search_opts = {'group_id': group.id} + self.mock_volume_list.assert_called_once_with( + test.IsHttpRequest(), search_opts=search_opts) + + @test.create_mocks({api.cinder: ['group_snapshot_get']}) + def test_detail_view_with_exception(self): + vg_snapshot = self.cinder_group_snapshots.first() + + self.mock_group_snapshot_get.side_effect = self.exceptions.cinder + + url = reverse( + 'horizon:admin:vg_snapshots:detail', + args=[vg_snapshot.id]) + res = self.client.get(url) + self.assertNoFormErrors(res) + self.assertRedirectsNoFollow(res, INDEX_URL) + + self.mock_group_snapshot_get.assert_called_once_with( + test.IsHttpRequest(), vg_snapshot.id) diff --git a/openstack_dashboard/dashboards/admin/vg_snapshots/urls.py b/openstack_dashboard/dashboards/admin/vg_snapshots/urls.py new file mode 100644 index 0000000000..c8225143e1 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/vg_snapshots/urls.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 django.conf.urls import url + +from openstack_dashboard.dashboards.admin.vg_snapshots import views + +urlpatterns = [ + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^(?P[^/]+)/detail/$', + views.DetailView.as_view(), + name='detail'), +] diff --git a/openstack_dashboard/dashboards/admin/vg_snapshots/views.py b/openstack_dashboard/dashboards/admin/vg_snapshots/views.py new file mode 100644 index 0000000000..e5ff355103 --- /dev/null +++ b/openstack_dashboard/dashboards/admin/vg_snapshots/views.py @@ -0,0 +1,68 @@ +# 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 django.urls import reverse +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tables + +from openstack_dashboard import api +from openstack_dashboard.dashboards.admin.vg_snapshots \ + import tables as admin_tables +from openstack_dashboard.dashboards.admin.vg_snapshots \ + import tabs as admin_tabs +from openstack_dashboard.dashboards.project.vg_snapshots \ + import views as project_views + +INDEX_URL = "horizon:admin:vg_snapshots:index" + + +class IndexView(tables.DataTableView): + table_class = admin_tables.GroupSnapshotsTable + page_title = _("Group Snapshots") + + def get_data(self): + try: + vg_snapshots = api.cinder.group_snapshot_list( + self.request, {'all_tenants': 1}) + except Exception: + vg_snapshots = [] + exceptions.handle(self.request, _("Unable to retrieve " + "volume group snapshots.")) + try: + groups = dict((g.id, g) for g + in api.cinder.group_list(self.request, + {'all_tenants': 1})) + except Exception: + groups = {} + exceptions.handle(self.request, + _("Unable to retrieve volume groups.")) + for vg_snapshot in vg_snapshots: + vg_snapshot.group = groups.get(vg_snapshot.group_id) + return vg_snapshots + + +class DetailView(project_views.DetailView): + tab_group_class = admin_tabs.DetailTabs + + def get_context_data(self, **kwargs): + context = super(DetailView, self).get_context_data(**kwargs) + table = admin_tables.GroupSnapshotsTable(self.request) + context["actions"] = table.render_row_actions(context["vg_snapshot"]) + return context + + @staticmethod + def get_redirect_url(): + return reverse(INDEX_URL) diff --git a/openstack_dashboard/enabled/_2260_admin_vg_snapshots.py b/openstack_dashboard/enabled/_2260_admin_vg_snapshots.py new file mode 100644 index 0000000000..0cd1211a74 --- /dev/null +++ b/openstack_dashboard/enabled/_2260_admin_vg_snapshots.py @@ -0,0 +1,10 @@ +# The slug of the panel to be added to HORIZON_CONFIG. Required. +PANEL = 'vg_snapshots' +# 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.vg_snapshots.panel.' + 'GroupSnapshots')