Add volume-group snapshot for admin panel

This commit allow admin to list/show and delete volume-group snapshots
using horizon dashboard.

Partially-Implements blueprint cinder-generic-volume-groups

Change-Id: If00d156a7a56ed699425b35ac60314c3a8cd049c
This commit is contained in:
manchandavishal 2019-01-28 18:36:26 +00:00
parent b06657b07d
commit 9497a23723
9 changed files with 393 additions and 0 deletions

View File

@ -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"),)

View File

@ -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",)

View File

@ -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,)

View File

@ -0,0 +1,50 @@
{% load i18n sizeformat parse_date %}
<div class="detail">
<dl class="dl-horizontal">
<dt>{% trans "Name" %}</dt>
<dd data-display="{{ vg_snapshot.name|default:vg_snapshot.id }}">{{ vg_snapshot.name }}</dd>
<dt>{% trans "ID" %}</dt>
<dd>{{ vg_snapshot.id }}</dd>
{% if vg_snapshot.description %}
<dt>{% trans "Description" %}</dt>
<dd>{{ vg_snapshot.description }}</dd>
{% endif %}
<dt>{% trans "Status" %}</dt>
<dd>{{ vg_snapshot.status|capfirst }}</dd>
<dt>{% trans "Group" %}</dt>
<dd>
<a href="{% url 'horizon:admin:volume_groups:detail' vg_snapshot.group_id %}">
{% if vg_snapshot.vg_name %}
{{ vg_snapshot.vg_name }}
{% else %}
{{ vg_snapshot.group_id }}
{% endif %}
</a>
</dd>
<dt>{% trans "Group Type" %}</dt>
<dd>{{ vg_snapshot.group_type_id }}</dd>
<dt>{% trans "Created" %}</dt>
<dd>{{ vg_snapshot.created_at|parse_isotime }}</dd>
</dl>
<h4>{% trans "Snapshot Volume Types" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% for vol_type_names in vg_snapshot.volume_type_names %}
<dd>{{ vol_type_names }}</dd>
{% endfor %}
</dl>
<h4>{% trans "Snapshot Volumes" %}</h4>
<hr class="header_rule">
<dl class="dl-horizontal">
{% for vol_names in vg_snapshot.volume_names %}
<dd>{{ vol_names }}</dd>
{% empty %}
<dd>
<em>{% trans "No assigned volumes" %}</em>
</dd>
{% endfor %}
</dl>
</div>

View File

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

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 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<vg_snapshot_id>[^/]+)/detail/$',
views.DetailView.as_view(),
name='detail'),
]

View File

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

View File

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