Show generic group info in volume and volume snapshot pages

blueprint cinder-generic-volume-groups
Co-Authored-By: Ivan Kolodyazhny <e0ne@e0ne.info>
Change-Id: I96515087a3e3a5328cceaff4e0e9a811601c7ba0
This commit is contained in:
Akihiro Motoki 2018-01-11 05:31:39 +09:00 committed by Ivan Kolodyazhny
parent 32d463a298
commit ef4d8d69c9
15 changed files with 230 additions and 36 deletions

View File

@ -100,7 +100,7 @@ class Volume(BaseCinderAPIResourceWrapper):
class VolumeSnapshot(BaseCinderAPIResourceWrapper):
_attrs = ['id', 'name', 'description', 'size', 'status',
'created_at', 'volume_id',
'created_at', 'volume_id', 'group_snapshot_id',
'os-extended-snapshot-attributes:project_id',
'metadata']
@ -344,7 +344,8 @@ def volume_list_paged(request, search_opts=None, marker=None, paginate=False,
@profiler.trace
def volume_get(request, volume_id):
volume_data = cinderclient(request).volumes.get(volume_id)
client = _cinderclient_with_generic_groups(request)
volume_data = client.volumes.get(volume_id)
for attachment in volume_data.attachments:
if "server_id" in attachment:
@ -455,7 +456,8 @@ def volume_migrate(request, volume_id, host, force_host_copy=False,
@profiler.trace
def volume_snapshot_get(request, snapshot_id):
snapshot = cinderclient(request).volume_snapshots.get(snapshot_id)
client = _cinderclient_with_generic_groups(request)
snapshot = client.volume_snapshots.get(snapshot_id)
return VolumeSnapshot(snapshot)
@ -473,7 +475,7 @@ def volume_snapshot_list_paged(request, search_opts=None, marker=None,
has_more_data = False
has_prev_data = False
snapshots = []
c_client = cinderclient(request)
c_client = _cinderclient_with_generic_groups(request)
if c_client is None:
return snapshots, has_more_data, has_more_data

View File

@ -96,6 +96,13 @@ class DeleteVolumeSnapshot(policy.PolicyTargetMixin, tables.DeleteAction):
def delete(self, request, obj_id):
api.cinder.volume_snapshot_delete(request, obj_id)
def allowed(self, request, datum=None):
if datum:
# Can't delete snapshot if part of group snapshot
if datum.group_snapshot:
return False
return True
class EditVolumeSnapshot(policy.PolicyTargetMixin, tables.LinkAction):
name = "edit"
@ -159,6 +166,11 @@ class UpdateRow(tables.Row):
def get_data(self, request, snapshot_id):
snapshot = cinder.volume_snapshot_get(request, snapshot_id)
snapshot._volume = cinder.volume_get(request, snapshot.volume_id)
if getattr(snapshot, 'group_snapshot_id', None):
snapshot.group_snapshot = cinder.group_snapshot_get(
request, snapshot.group_snapshot_id)
else:
snapshot.group_snapshot = None
return snapshot
@ -174,6 +186,17 @@ class SnapshotVolumeNameColumn(tables.WrappingColumn):
return reverse(self.link, args=(volume_id,))
class GroupSnapshotNameColumn(tables.WrappingColumn):
def get_raw_data(self, snapshot):
group_snapshot = snapshot.group_snapshot
return group_snapshot.name_or_id if group_snapshot else _("-")
def get_link_url(self, snapshot):
group_snapshot = snapshot.group_snapshot
if group_snapshot:
return reverse(self.link, args=(group_snapshot.id,))
class VolumeSnapshotsFilterAction(tables.FilterAction):
def filter(self, table, snapshots, filter_string):
@ -184,6 +207,10 @@ class VolumeSnapshotsFilterAction(tables.FilterAction):
class VolumeDetailsSnapshotsTable(volume_tables.VolumesTableBase):
group_snapshot = GroupSnapshotNameColumn(
"name",
verbose_name=_("Group Snapshot"),
link="horizon:project:vg_snapshots:detail")
name = tables.WrappingColumn(
"name",
verbose_name=_("Name"),

View File

@ -36,7 +36,8 @@ class OverviewTab(tabs.Tab):
_('Unable to retrieve snapshot details.'),
redirect=redirect)
return {"snapshot": snapshot,
"volume": volume}
"volume": volume,
"group_snapshot": snapshot.group_snapshot}
def get_redirect_url(self):
return reverse('horizon:project:snapshots:index')

View File

@ -22,6 +22,12 @@
{% endif %}
</a>
</dd>
<dt>{% trans "Group Snapshot" %}</dt>
{% if group_snapshot %}
<dd><a href="{% url 'horizon:project:vg_snapshots:detail' snapshot.group_snapshot_id %}">{{ group_snapshot.name_or_id }}</a></dd>
{% else %}
<dd>{% trans "-" %}</dd>
{% endif %}
</dl>
<h4>{% trans "Specs" %}</h4>

View File

@ -34,15 +34,18 @@ INDEX_URL = reverse('horizon:project:snapshots:index')
class VolumeSnapshotsViewTests(test.TestCase):
@test.create_mocks({api.cinder: ('volume_snapshot_list_paged',
'volume_list'),
'volume_list',
'group_snapshot_list'),
api.base: ('is_service_enabled',)})
def _test_snapshots_index_paginated(self, marker, sort_dir, snapshots, url,
has_more, has_prev):
has_more, has_prev, with_groups=False):
self.mock_is_service_enabled.return_value = True
self.mock_volume_snapshot_list_paged.return_value = [snapshots,
has_more,
has_prev]
self.mock_volume_list.return_value = self.cinder_volumes.list()
self.mock_group_snapshot_list.return_value = \
self.cinder_volume_snapshots_with_groups.list()
res = self.client.get(urlunquote(url))
self.assertEqual(res.status_code, 200)
@ -56,17 +59,21 @@ class VolumeSnapshotsViewTests(test.TestCase):
paginate=True)
self.mock_volume_list.assert_called_once_with(test.IsHttpRequest())
if with_groups:
self.mock_group_snapshot_list.assert_called_once_with(
test.IsHttpRequest())
return res
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_snapshots_index_paginated(self):
mox_snapshots = self.cinder_volume_snapshots.list()
mock_snapshots = self.cinder_volume_snapshots.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL
next = snapshot_tables.VolumeSnapshotsTable._meta.pagination_param
# get first page
expected_snapshots = mox_snapshots[:size]
expected_snapshots = mock_snapshots[:size]
res = self._test_snapshots_index_paginated(
marker=None, sort_dir="desc", snapshots=expected_snapshots,
url=base_url, has_more=True, has_prev=False)
@ -74,7 +81,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
self.assertItemsEqual(snapshots, expected_snapshots)
# get second page
expected_snapshots = mox_snapshots[size:2 * size]
expected_snapshots = mock_snapshots[size:2 * size]
marker = expected_snapshots[0].id
url = base_url + "?%s=%s" % (next, marker)
@ -85,7 +92,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
self.assertItemsEqual(snapshots, expected_snapshots)
# get last page
expected_snapshots = mox_snapshots[-size:]
expected_snapshots = mock_snapshots[-size:]
marker = expected_snapshots[0].id
url = base_url + "?%s=%s" % (next, marker)
res = self._test_snapshots_index_paginated(
@ -94,15 +101,29 @@ class VolumeSnapshotsViewTests(test.TestCase):
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, expected_snapshots)
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_snapshots_index_with_group(self):
mock_snapshots = self.cinder_volume_snapshots_with_groups.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL
# get first page
expected_snapshots = mock_snapshots[:size]
res = self._test_snapshots_index_paginated(
marker=None, sort_dir="desc", snapshots=expected_snapshots,
url=base_url, has_more=False, has_prev=False, with_groups=True)
snapshots = res.context['volume_snapshots_table'].data
self.assertItemsEqual(snapshots, mock_snapshots)
@override_settings(API_RESULT_PAGE_SIZE=1)
def test_snapshots_index_paginated_prev_page(self):
mox_snapshots = self.cinder_volume_snapshots.list()
mock_snapshots = self.cinder_volume_snapshots.list()
size = settings.API_RESULT_PAGE_SIZE
base_url = INDEX_URL
prev = snapshot_tables.VolumeSnapshotsTable._meta.prev_pagination_param
# prev from some page
expected_snapshots = mox_snapshots[size:2 * size]
expected_snapshots = mock_snapshots[size:2 * size]
marker = expected_snapshots[0].id
url = base_url + "?%s=%s" % (prev, marker)
res = self._test_snapshots_index_paginated(
@ -112,7 +133,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
self.assertItemsEqual(snapshots, expected_snapshots)
# back to first page
expected_snapshots = mox_snapshots[:size]
expected_snapshots = mock_snapshots[:size]
marker = expected_snapshots[0].id
url = base_url + "?%s=%s" % (prev, marker)
res = self._test_snapshots_index_paginated(

View File

@ -38,6 +38,7 @@ class SnapshotsView(tables.PagedTableMixin, tables.DataTableView):
def get_data(self):
snapshots = []
volumes = {}
needs_gs = False
if cinder.is_volume_service_enabled(self.request):
try:
marker, sort_dir = self._get_marker()
@ -45,15 +46,36 @@ class SnapshotsView(tables.PagedTableMixin, tables.DataTableView):
cinder.volume_snapshot_list_paged(
self.request, paginate=True, marker=marker,
sort_dir=sort_dir)
except Exception:
exceptions.handle(self.request,
_("Unable to retrieve volume snapshots."))
try:
volumes = cinder.volume_list(self.request)
volumes = dict((v.id, v) for v in volumes)
except Exception:
exceptions.handle(self.request, _("Unable to retrieve "
"volume snapshots."))
exceptions.handle(self.request,
_("Unable to retrieve volumes."))
needs_gs = any(getattr(snapshot, 'group_snapshot_id', None)
for snapshot in snapshots)
if needs_gs:
try:
group_snapshots = cinder.group_snapshot_list(self.request)
group_snapshots = dict((gs.id, gs) for gs
in group_snapshots)
except Exception:
group_snapshots = {}
exceptions.handle(self.request,
_("Unable to retrieve group snapshots."))
for snapshot in snapshots:
volume = volumes.get(snapshot.volume_id)
setattr(snapshot, '_volume', volume)
if needs_gs:
group_snapshot = group_snapshots.get(
snapshot.group_snapshot_id)
snapshot.group_snapshot = group_snapshot
else:
snapshot.group_snapshot = None
return snapshots
@ -127,6 +149,11 @@ class DetailView(tabs.TabView):
snapshot_id)
snapshot._volume = cinder.volume_get(self.request,
snapshot.volume_id)
if getattr(snapshot, 'group_snapshot_id', None):
snapshot.group_snapshot = cinder.group_snapshot_get(
self.request, snapshot.group_snapshot_id)
else:
snapshot.group_snapshot = None
except Exception:
redirect = self.get_redirect_url()
exceptions.handle(self.request,

View File

@ -61,21 +61,21 @@ class UpdateRow(tables.Row):
vg_snapshot = cinder.group_snapshot_get(request, vg_snapshot_id)
if getattr(vg_snapshot, 'group_id', None):
try:
vg_snapshot._group = cinder.group_get(request,
vg_snapshot.group_id)
vg_snapshot.group = cinder.group_get(request,
vg_snapshot.group_id)
except Exception:
exceptions.handle(request, _("Unable to retrieve group"))
vg_snapshot._group = None
vg_snapshot.group = None
return vg_snapshot
class GroupNameColumn(tables.WrappingColumn):
def get_raw_data(self, snapshot):
group = snapshot._group
group = snapshot.group
return group.name_or_id if group else _("-")
def get_link_url(self, snapshot):
group = snapshot._group
group = snapshot.group
if group:
return reverse(self.link, args=(group.id,))

View File

@ -56,7 +56,7 @@ class IndexView(tables.DataTableView):
exceptions.handle(self.request,
_("Unable to retrieve volume groups."))
for gs in vg_snapshots:
gs._group = groups.get(gs.group_id)
gs.group = groups.get(gs.group_id)
return vg_snapshots

View File

@ -119,7 +119,9 @@ class DeleteVolume(VolumePolicyTargetMixin, tables.DeleteAction):
# Can't delete volume if part of consistency group
if getattr(volume, 'consistencygroup_id', None):
return False
# Can't delete volume if part of volume group
if volume.group:
return False
return (volume.status in DELETABLE_STATES and
not getattr(volume, 'has_snapshot', False))
return True
@ -339,6 +341,14 @@ class UpdateRow(tables.Row):
def get_data(self, request, volume_id):
volume = cinder.volume_get(request, volume_id)
if volume and getattr(volume, 'group_id', None):
try:
volume.group = cinder.group_get(request, volume.group_id)
except Exception:
exceptions.handle(request, _("Unable to retrieve group."))
volume.group = None
else:
volume.group = None
return volume
@ -393,6 +403,17 @@ class AttachmentColumn(tables.WrappingColumn):
return safestring.mark_safe(", ".join(attachments))
class GroupNameColumn(tables.WrappingColumn):
def get_raw_data(self, volume):
group = volume.group
return group.name_or_id if group else _("-")
def get_link_url(self, volume):
group = volume.group
if group:
return reverse(self.link, args=(group.id,))
def get_volume_type(volume):
return volume.volume_type if volume.volume_type != "None" else None
@ -500,6 +521,10 @@ class VolumesTable(VolumesTableBase):
name = tables.WrappingColumn("name",
verbose_name=_("Name"),
link="horizon:project:volumes:detail")
group = GroupNameColumn(
"name",
verbose_name=_("Group"),
link="horizon:project:volume_groups:detail")
volume_type = tables.Column(get_volume_type,
verbose_name=_("Type"))
attachments = AttachmentColumn("attachments",

View File

@ -14,8 +14,10 @@
from django.utils.translation import ugettext_lazy as _
from horizon import exceptions
from horizon import tabs
from openstack_dashboard.api import cinder
from openstack_dashboard.dashboards.project.snapshots import tables
@ -25,8 +27,10 @@ class OverviewTab(tabs.Tab):
template_name = ("project/volumes/_detail_overview.html")
def get_context_data(self, request):
volume = self.tab_group.kwargs['volume']
return {
'volume': self.tab_group.kwargs['volume'],
'volume': volume,
'group': volume.group,
'detail_url': {
'instance': 'horizon:project:instances:detail',
'image': 'horizon:project:images:images:detail',
@ -47,9 +51,28 @@ class SnapshotTab(tabs.TableTab):
snapshots = self.tab_group.kwargs['snapshots']
volume = self.tab_group.kwargs['volume']
if volume is not None:
for snapshot in snapshots:
snapshot._volume = volume
if volume is None:
return snapshots
needs_gs = any(getattr(snapshot, 'group_snapshot_id', None)
for snapshot in snapshots)
if needs_gs:
try:
group_snapshots_list = cinder.group_snapshot_list(self.request)
group_snapshots = dict((gs.id, gs) for gs
in group_snapshots_list)
except Exception:
group_snapshots = {}
exceptions.handle(self.request,
_("Unable to retrieve group snapshots."))
for snapshot in snapshots:
snapshot._volume = volume
if needs_gs:
gs_id = snapshot.group_snapshot_id
snapshot.group_snapshot = group_snapshots.get(gs_id)
else:
snapshot.group_snapshot = None
return snapshots

View File

@ -12,6 +12,12 @@
{% endif %}
<dt>{% trans "Status" %}</dt>
<dd>{{ volume.status_label|capfirst }}</dd>
<dt>{% trans "Group" %}</dt>
{% if group %}
<dd><a href="{% url 'horizon:project:volume_groups:detail' volume.group_id %}">{{ group.name_or_id }}</a></dd>
{% else %}
<dd>{% trans "-" %}</dd>
{% endif %}
</dl>
<h4>{% trans "Specs" %}</h4>

View File

@ -44,9 +44,10 @@ class VolumeIndexViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
'volume_backup_supported',
'volume_snapshot_list',
'volume_list_paged',
'tenant_absolute_limits'],
'tenant_absolute_limits',
'group_list'],
})
def _test_index(self, with_attachments):
def _test_index(self, with_attachments=False, with_groups=False):
vol_snaps = self.cinder_volume_snapshots.list()
volumes = self.cinder_volumes.list()
if with_attachments:
@ -56,6 +57,10 @@ class VolumeIndexViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
volume.attachments = []
self.mock_volume_backup_supported.return_value = False
if with_groups:
self.mock_group_list.return_value = self.cinder_groups.list()
volumes = self.cinder_group_volumes.list()
self.mock_volume_list_paged.return_value = [volumes, False, False]
if with_attachments:
self.mock_server_get.return_value = server
@ -73,6 +78,9 @@ class VolumeIndexViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
search_opts=None)
self.mock_volume_snapshot_list.assert_called_once()
if with_groups:
self.mock_group_list.assert_called_once_with(test.IsHttpRequest())
self.mock_volume_backup_supported.assert_called_with(
test.IsHttpRequest())
self.mock_volume_list_paged.assert_called_once_with(
@ -89,6 +97,9 @@ class VolumeIndexViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
def test_index_no_volume_attachments(self):
self._test_index(False)
def test_index_with_volume_groups(self):
self._test_index(with_groups=True)
@test.create_mocks({
api.nova: ['server_get', 'server_list'],
cinder: ['tenant_absolute_limits',

View File

@ -109,6 +109,24 @@ class VolumeTableMixIn(object):
attached_instance_ids.append(server_id)
return attached_instance_ids
def _get_groups(self, volumes):
needs_group = False
if volumes and hasattr(volumes[0], 'group_id'):
needs_group = True
if needs_group:
try:
groups_list = cinder.group_list(self.request)
groups = dict((g.id, g) for g in groups_list)
except Exception:
groups = {}
exceptions.handle(self.request,
_("Unable to retrieve volume groups"))
for volume in volumes:
if needs_group:
volume.group = groups.get(volume.group_id)
else:
volume.group = None
# set attachment string and if volume has snapshots
def _set_volume_attributes(self,
volumes,
@ -137,6 +155,7 @@ class VolumesView(tables.PagedTableMixin, VolumeTableMixIn,
volume_ids_with_snapshots = self._get_volumes_ids_with_snapshots()
self._set_volume_attributes(
volumes, instances, volume_ids_with_snapshots)
self._get_groups(volumes)
return volumes
@ -172,6 +191,10 @@ class DetailView(tabs.TabbedTableView):
for att in volume.attachments:
att['instance'] = nova.server_get(self.request,
att['server_id'])
if getattr(volume, 'group_id', None):
volume.group = cinder.group_get(self.request, volume.group_id)
else:
volume.group = None
except Exception:
redirect = self.get_redirect_url()
exceptions.handle(self.request,

View File

@ -62,6 +62,7 @@ def data(TEST):
TEST.cinder_group_types = utils.TestDataContainer()
TEST.cinder_group_snapshots = utils.TestDataContainer()
TEST.cinder_group_volumes = utils.TestDataContainer()
TEST.cinder_volume_snapshots_with_groups = utils.TestDataContainer()
# Services
service_1 = services.Service(services.ServiceManager(None), {
@ -566,3 +567,17 @@ def data(TEST):
'attachments': []})
TEST.cinder_group_volumes.add(group_volume_1)
TEST.cinder_group_volumes.add(group_volume_2)
snapshot5 = vol_snaps.Snapshot(
vol_snaps.SnapshotManager(None),
{'id': 'cd6be1eb-82ca-4587-8036-13c37c00c2b1',
'name': '',
'description': 'v2 volume snapshot with metadata description',
'size': 80,
'status': 'available',
'volume_id': '7e4efa56-9ca1-45ff-b83c-2efb2383930d',
'metadata': {'snapshot_meta_key': 'snapshot_meta_value'},
'group_snapshot_id': group_snapshot_1.id})
TEST.cinder_volume_snapshots_with_groups.add(
api.cinder.VolumeSnapshot(snapshot5))

View File

@ -296,11 +296,15 @@ class CinderApiTests(test.APIMockTestCase):
self.assertTrue(more_data)
self.assertFalse(prev_data)
@mock.patch.object(api.cinder, 'cinderclient')
def test_volume_snapshot_list(self, mock_cinderclient):
@test.create_mocks({
api.cinder: [
('_cinderclient_with_generic_groups', 'cinderclient_groups'),
]
})
def test_volume_snapshot_list(self):
search_opts = {'all_tenants': 1}
volume_snapshots = self.cinder_volume_snapshots.list()
cinderclient = mock_cinderclient.return_value
cinderclient = self.mock_cinderclient_groups.return_value
snapshots_mock = cinderclient.volume_snapshots.list
snapshots_mock.return_value = volume_snapshots
@ -308,9 +312,12 @@ class CinderApiTests(test.APIMockTestCase):
api.cinder.volume_snapshot_list(self.request, search_opts=search_opts)
snapshots_mock.assert_called_once_with(search_opts=search_opts)
@mock.patch.object(api.cinder, 'cinderclient')
def test_volume_snapshot_list_no_volume_configured(self,
mock_cinderclient):
@test.create_mocks({
api.cinder: [
('_cinderclient_with_generic_groups', 'cinderclient_groups'),
]
})
def test_volume_snapshot_list_no_volume_configured(self):
# remove volume from service catalog
catalog = self.service_catalog
for service in catalog:
@ -319,7 +326,7 @@ class CinderApiTests(test.APIMockTestCase):
search_opts = {'all_tenants': 1}
volume_snapshots = self.cinder_volume_snapshots.list()
cinderclient = mock_cinderclient.return_value
cinderclient = self.mock_cinderclient_groups.return_value
snapshots_mock = cinderclient.volume_snapshots.list
snapshots_mock.return_value = volume_snapshots