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')