diff --git a/openstack_dashboard/dashboards/admin/volume_groups/__init__.py b/openstack_dashboard/dashboards/admin/volume_groups/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/openstack_dashboard/dashboards/admin/volume_groups/panel.py b/openstack_dashboard/dashboards/admin/volume_groups/panel.py
new file mode 100644
index 0000000000..444dc53713
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/volume_groups/panel.py
@@ -0,0 +1,20 @@
+# Copyright 2017 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 panel as volume_groups_panel
+
+
+class VolumeGroups(volume_groups_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
new file mode 100644
index 0000000000..42412f07ac
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/volume_groups/tables.py
@@ -0,0 +1,31 @@
+# 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.volume_groups \
+ import tables as volume_groups_tables
+
+
+class GroupsTable(volume_groups_tables.GroupsTable):
+ # TODO(vishalmanchanda): Add Project Info.column in table
+ name = tables.WrappingColumn("name_or_id",
+ verbose_name=_("Name"),
+ link="horizon:admin:volume_groups:detail")
+
+ class Meta(object):
+ name = "volume_groups"
+ verbose_name = _("Volume Groups")
+ table_actions = (
+ volume_groups_tables.GroupsFilterAction,
+ )
diff --git a/openstack_dashboard/dashboards/admin/volume_groups/tabs.py b/openstack_dashboard/dashboards/admin/volume_groups/tabs.py
new file mode 100644
index 0000000000..60be65e51a
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/volume_groups/tabs.py
@@ -0,0 +1,27 @@
+# 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.volume_groups \
+ import tabs as volume_groups_tabs
+
+
+class OverviewTab(volume_groups_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):
+ tabs = (OverviewTab,)
diff --git a/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/_detail_overview.html b/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/_detail_overview.html
new file mode 100644
index 0000000000..50d2b886a3
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/volume_groups/templates/volume_groups/_detail_overview.html
@@ -0,0 +1,42 @@
+{% load i18n sizeformat parse_date %}
+
+
+
+ - {% trans "Name" %}
+ - {{ group.name|default:_("-") }}
+ - {% trans "ID" %}
+ - {{ group.id }}
+ - {% trans "Description" %}
+ - {{ group.description|default:_("-") }}
+ - {% trans "Status" %}
+ - {{ group.status|capfirst }}
+ - {% trans "Availability Zone" %}
+ - {{ group.availability_zone }}
+ - {% trans "Group Type" %}
+ - {{ group.group_type }}
+ - {% trans "Created" %}
+ - {{ group.created_at|parse_isotime }}
+ - {% trans "Replication Status" %}
+ - {{ group.replication_status }}
+
+
+
{% trans "Volume Types" %}
+
+
+ {% for vol_type_name in group.volume_type_names %}
+ - {{ vol_type_name }}
+ {% endfor %}
+
+
+
{% trans "Volumes" %}
+
+
+ {% for vol in group.volume_names %}
+ - {{ vol.name }}
+ {% empty %}
+ -
+ {% trans "No assigned volumes" %}
+
+ {% endfor %}
+
+
diff --git a/openstack_dashboard/dashboards/admin/volume_groups/tests.py b/openstack_dashboard/dashboards/admin/volume_groups/tests.py
new file mode 100644
index 0000000000..a7b6bc6db5
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/volume_groups/tests.py
@@ -0,0 +1,85 @@
+# 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 import api
+from openstack_dashboard.api import cinder
+from openstack_dashboard.test import helpers as test
+
+
+INDEX_URL = reverse('horizon:admin:volume_groups:index')
+INDEX_TEMPLATE = 'horizon/common/_data_table_view.html'
+
+
+class AdminVolumeGroupTests(test.BaseAdminViewTests):
+ @test.create_mocks({api.cinder: ['group_list_with_vol_type_names',
+ 'group_snapshot_list']})
+ def test_index(self):
+ group = self.cinder_groups.list()
+ vg_snapshot = self.cinder_group_snapshots.list()
+ self.mock_group_list_with_vol_type_names.return_value = group
+ self.mock_group_snapshot_list.return_value = vg_snapshot
+
+ res = self.client.get(INDEX_URL)
+ self.assertTemplateUsed(res, INDEX_TEMPLATE)
+ self.assertIn('volume_groups_table', res.context)
+ volume_groups_table = res.context['volume_groups_table']
+ volume_groups = volume_groups_table.data
+ self.assertEqual(len(volume_groups), 1)
+
+ self.mock_group_list_with_vol_type_names.assert_called_once_with(
+ test.IsHttpRequest(), {'all_tenants': 1})
+ self.mock_group_snapshot_list.assert_called_once_with(
+ test.IsHttpRequest())
+
+ @test.create_mocks({cinder: ['group_get_with_vol_type_names',
+ 'volume_list',
+ 'group_snapshot_list']})
+ def test_detail_view(self):
+ group = self.cinder_groups.first()
+ volumes = self.cinder_volumes.list()
+ vg_snapshot = self.cinder_group_snapshots.list()
+
+ self.mock_group_get_with_vol_type_names.return_value = group
+ self.mock_volume_list.return_value = volumes
+ self.mock_group_snapshot_list.return_value = vg_snapshot
+
+ url = reverse('horizon:admin:volume_groups:detail',
+ args=[group.id])
+ res = self.client.get(url)
+ self.assertNoFormErrors(res)
+ self.assertEqual(res.status_code, 200)
+ self.assertTrue(group.has_snapshots)
+
+ self.mock_group_get_with_vol_type_names.assert_called_once_with(
+ test.IsHttpRequest(), group.id)
+ search_opts = {'group_id': group.id}
+ self.mock_volume_list.assert_called_once_with(
+ test.IsHttpRequest(), search_opts=search_opts)
+ self.mock_group_snapshot_list.assert_called_once_with(
+ test.IsHttpRequest(), search_opts=search_opts)
+
+ @test.create_mocks({cinder: ['group_get']})
+ def test_detail_view_with_exception(self):
+ group = self.cinder_groups.first()
+
+ self.mock_group_get.side_effect = self.exceptions.cinder
+
+ url = reverse('horizon:admin:volume_groups:detail',
+ args=[group.id])
+ res = self.client.get(url)
+ self.assertNoFormErrors(res)
+ self.assertRedirectsNoFollow(res, INDEX_URL)
+
+ self.mock_group_get.assert_called_once_with(
+ test.IsHttpRequest(), group.id)
diff --git a/openstack_dashboard/dashboards/admin/volume_groups/urls.py b/openstack_dashboard/dashboards/admin/volume_groups/urls.py
new file mode 100644
index 0000000000..7a6680927a
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/volume_groups/urls.py
@@ -0,0 +1,23 @@
+# 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.volume_groups import views
+
+
+urlpatterns = [
+ url(r'^$', views.IndexView.as_view(), name='index'),
+ url(r'^(?P[^/]+)$',
+ views.DetailView.as_view(),
+ name='detail'),
+]
diff --git a/openstack_dashboard/dashboards/admin/volume_groups/views.py b/openstack_dashboard/dashboards/admin/volume_groups/views.py
new file mode 100644
index 0000000000..15cebd1547
--- /dev/null
+++ b/openstack_dashboard/dashboards/admin/volume_groups/views.py
@@ -0,0 +1,61 @@
+# 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.volume_groups \
+ import tables as volume_group_tables
+from openstack_dashboard.dashboards.admin.volume_groups \
+ import tabs as volume_group_tabs
+from openstack_dashboard.dashboards.project.volume_groups \
+ import views as volume_groups_views
+
+
+class IndexView(tables.DataTableView):
+ table_class = volume_group_tables.GroupsTable
+ page_title = _("Groups")
+
+ def get_data(self):
+ try:
+ groups = api.cinder.group_list_with_vol_type_names(
+ self.request, {'all_tenants': 1})
+ except Exception:
+ groups = []
+ exceptions.handle(self.request,
+ _("Unable to retrieve volume groups."))
+ if not groups:
+ return groups
+ group_snapshots = api.cinder.group_snapshot_list(self.request)
+ snapshot_groups = {gs.group_id for gs in group_snapshots}
+ for g in groups:
+ g.has_snapshots = g.id in snapshot_groups
+ return groups
+
+
+class DetailView(volume_groups_views.DetailView):
+ tab_group_class = volume_group_tabs.GroupsDetailTabs
+
+ def get_context_data(self, **kwargs):
+ context = super(DetailView, self).get_context_data(**kwargs)
+ table = volume_group_tables.GroupsTable(self.request)
+ context["actions"] = table.render_row_actions(context["group"])
+ return context
+
+ @staticmethod
+ def get_redirect_url():
+ return reverse('horizon:admin:volume_groups:index')
diff --git a/openstack_dashboard/enabled/_2250_admin_volume_groups.py b/openstack_dashboard/enabled/_2250_admin_volume_groups.py
new file mode 100644
index 0000000000..66f437782a
--- /dev/null
+++ b/openstack_dashboard/enabled/_2250_admin_volume_groups.py
@@ -0,0 +1,10 @@
+# The slug of the panel to be added to HORIZON_CONFIG. Required.
+PANEL = 'volume_groups'
+# 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.volume_groups.panel.'
+ 'VolumeGroups')