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:
parent
b06657b07d
commit
9497a23723
20
openstack_dashboard/dashboards/admin/vg_snapshots/panel.py
Normal file
20
openstack_dashboard/dashboards/admin/vg_snapshots/panel.py
Normal 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"),)
|
43
openstack_dashboard/dashboards/admin/vg_snapshots/tables.py
Normal file
43
openstack_dashboard/dashboards/admin/vg_snapshots/tables.py
Normal 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",)
|
29
openstack_dashboard/dashboards/admin/vg_snapshots/tabs.py
Normal file
29
openstack_dashboard/dashboards/admin/vg_snapshots/tabs.py
Normal 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,)
|
@ -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>
|
149
openstack_dashboard/dashboards/admin/vg_snapshots/tests.py
Normal file
149
openstack_dashboard/dashboards/admin/vg_snapshots/tests.py
Normal 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)
|
24
openstack_dashboard/dashboards/admin/vg_snapshots/urls.py
Normal file
24
openstack_dashboard/dashboards/admin/vg_snapshots/urls.py
Normal 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'),
|
||||
]
|
68
openstack_dashboard/dashboards/admin/vg_snapshots/views.py
Normal file
68
openstack_dashboard/dashboards/admin/vg_snapshots/views.py
Normal 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)
|
10
openstack_dashboard/enabled/_2260_admin_vg_snapshots.py
Normal file
10
openstack_dashboard/enabled/_2260_admin_vg_snapshots.py
Normal 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')
|
Loading…
Reference in New Issue
Block a user