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:
parent
32d463a298
commit
ef4d8d69c9
|
@ -100,7 +100,7 @@ class Volume(BaseCinderAPIResourceWrapper):
|
||||||
class VolumeSnapshot(BaseCinderAPIResourceWrapper):
|
class VolumeSnapshot(BaseCinderAPIResourceWrapper):
|
||||||
|
|
||||||
_attrs = ['id', 'name', 'description', 'size', 'status',
|
_attrs = ['id', 'name', 'description', 'size', 'status',
|
||||||
'created_at', 'volume_id',
|
'created_at', 'volume_id', 'group_snapshot_id',
|
||||||
'os-extended-snapshot-attributes:project_id',
|
'os-extended-snapshot-attributes:project_id',
|
||||||
'metadata']
|
'metadata']
|
||||||
|
|
||||||
|
@ -344,7 +344,8 @@ def volume_list_paged(request, search_opts=None, marker=None, paginate=False,
|
||||||
|
|
||||||
@profiler.trace
|
@profiler.trace
|
||||||
def volume_get(request, volume_id):
|
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:
|
for attachment in volume_data.attachments:
|
||||||
if "server_id" in attachment:
|
if "server_id" in attachment:
|
||||||
|
@ -455,7 +456,8 @@ def volume_migrate(request, volume_id, host, force_host_copy=False,
|
||||||
|
|
||||||
@profiler.trace
|
@profiler.trace
|
||||||
def volume_snapshot_get(request, snapshot_id):
|
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)
|
return VolumeSnapshot(snapshot)
|
||||||
|
|
||||||
|
|
||||||
|
@ -473,7 +475,7 @@ def volume_snapshot_list_paged(request, search_opts=None, marker=None,
|
||||||
has_more_data = False
|
has_more_data = False
|
||||||
has_prev_data = False
|
has_prev_data = False
|
||||||
snapshots = []
|
snapshots = []
|
||||||
c_client = cinderclient(request)
|
c_client = _cinderclient_with_generic_groups(request)
|
||||||
if c_client is None:
|
if c_client is None:
|
||||||
return snapshots, has_more_data, has_more_data
|
return snapshots, has_more_data, has_more_data
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,13 @@ class DeleteVolumeSnapshot(policy.PolicyTargetMixin, tables.DeleteAction):
|
||||||
def delete(self, request, obj_id):
|
def delete(self, request, obj_id):
|
||||||
api.cinder.volume_snapshot_delete(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):
|
class EditVolumeSnapshot(policy.PolicyTargetMixin, tables.LinkAction):
|
||||||
name = "edit"
|
name = "edit"
|
||||||
|
@ -159,6 +166,11 @@ class UpdateRow(tables.Row):
|
||||||
def get_data(self, request, snapshot_id):
|
def get_data(self, request, snapshot_id):
|
||||||
snapshot = cinder.volume_snapshot_get(request, snapshot_id)
|
snapshot = cinder.volume_snapshot_get(request, snapshot_id)
|
||||||
snapshot._volume = cinder.volume_get(request, snapshot.volume_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
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@ -174,6 +186,17 @@ class SnapshotVolumeNameColumn(tables.WrappingColumn):
|
||||||
return reverse(self.link, args=(volume_id,))
|
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):
|
class VolumeSnapshotsFilterAction(tables.FilterAction):
|
||||||
|
|
||||||
def filter(self, table, snapshots, filter_string):
|
def filter(self, table, snapshots, filter_string):
|
||||||
|
@ -184,6 +207,10 @@ class VolumeSnapshotsFilterAction(tables.FilterAction):
|
||||||
|
|
||||||
|
|
||||||
class VolumeDetailsSnapshotsTable(volume_tables.VolumesTableBase):
|
class VolumeDetailsSnapshotsTable(volume_tables.VolumesTableBase):
|
||||||
|
group_snapshot = GroupSnapshotNameColumn(
|
||||||
|
"name",
|
||||||
|
verbose_name=_("Group Snapshot"),
|
||||||
|
link="horizon:project:vg_snapshots:detail")
|
||||||
name = tables.WrappingColumn(
|
name = tables.WrappingColumn(
|
||||||
"name",
|
"name",
|
||||||
verbose_name=_("Name"),
|
verbose_name=_("Name"),
|
||||||
|
|
|
@ -36,7 +36,8 @@ class OverviewTab(tabs.Tab):
|
||||||
_('Unable to retrieve snapshot details.'),
|
_('Unable to retrieve snapshot details.'),
|
||||||
redirect=redirect)
|
redirect=redirect)
|
||||||
return {"snapshot": snapshot,
|
return {"snapshot": snapshot,
|
||||||
"volume": volume}
|
"volume": volume,
|
||||||
|
"group_snapshot": snapshot.group_snapshot}
|
||||||
|
|
||||||
def get_redirect_url(self):
|
def get_redirect_url(self):
|
||||||
return reverse('horizon:project:snapshots:index')
|
return reverse('horizon:project:snapshots:index')
|
||||||
|
|
|
@ -22,6 +22,12 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</a>
|
</a>
|
||||||
</dd>
|
</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>
|
</dl>
|
||||||
|
|
||||||
<h4>{% trans "Specs" %}</h4>
|
<h4>{% trans "Specs" %}</h4>
|
||||||
|
|
|
@ -34,15 +34,18 @@ INDEX_URL = reverse('horizon:project:snapshots:index')
|
||||||
|
|
||||||
class VolumeSnapshotsViewTests(test.TestCase):
|
class VolumeSnapshotsViewTests(test.TestCase):
|
||||||
@test.create_mocks({api.cinder: ('volume_snapshot_list_paged',
|
@test.create_mocks({api.cinder: ('volume_snapshot_list_paged',
|
||||||
'volume_list'),
|
'volume_list',
|
||||||
|
'group_snapshot_list'),
|
||||||
api.base: ('is_service_enabled',)})
|
api.base: ('is_service_enabled',)})
|
||||||
def _test_snapshots_index_paginated(self, marker, sort_dir, snapshots, url,
|
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_is_service_enabled.return_value = True
|
||||||
self.mock_volume_snapshot_list_paged.return_value = [snapshots,
|
self.mock_volume_snapshot_list_paged.return_value = [snapshots,
|
||||||
has_more,
|
has_more,
|
||||||
has_prev]
|
has_prev]
|
||||||
self.mock_volume_list.return_value = self.cinder_volumes.list()
|
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))
|
res = self.client.get(urlunquote(url))
|
||||||
self.assertEqual(res.status_code, 200)
|
self.assertEqual(res.status_code, 200)
|
||||||
|
@ -56,17 +59,21 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||||
paginate=True)
|
paginate=True)
|
||||||
self.mock_volume_list.assert_called_once_with(test.IsHttpRequest())
|
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
|
return res
|
||||||
|
|
||||||
@override_settings(API_RESULT_PAGE_SIZE=1)
|
@override_settings(API_RESULT_PAGE_SIZE=1)
|
||||||
def test_snapshots_index_paginated(self):
|
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
|
size = settings.API_RESULT_PAGE_SIZE
|
||||||
base_url = INDEX_URL
|
base_url = INDEX_URL
|
||||||
next = snapshot_tables.VolumeSnapshotsTable._meta.pagination_param
|
next = snapshot_tables.VolumeSnapshotsTable._meta.pagination_param
|
||||||
|
|
||||||
# get first page
|
# get first page
|
||||||
expected_snapshots = mox_snapshots[:size]
|
expected_snapshots = mock_snapshots[:size]
|
||||||
res = self._test_snapshots_index_paginated(
|
res = self._test_snapshots_index_paginated(
|
||||||
marker=None, sort_dir="desc", snapshots=expected_snapshots,
|
marker=None, sort_dir="desc", snapshots=expected_snapshots,
|
||||||
url=base_url, has_more=True, has_prev=False)
|
url=base_url, has_more=True, has_prev=False)
|
||||||
|
@ -74,7 +81,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||||
self.assertItemsEqual(snapshots, expected_snapshots)
|
self.assertItemsEqual(snapshots, expected_snapshots)
|
||||||
|
|
||||||
# get second page
|
# get second page
|
||||||
expected_snapshots = mox_snapshots[size:2 * size]
|
expected_snapshots = mock_snapshots[size:2 * size]
|
||||||
marker = expected_snapshots[0].id
|
marker = expected_snapshots[0].id
|
||||||
|
|
||||||
url = base_url + "?%s=%s" % (next, marker)
|
url = base_url + "?%s=%s" % (next, marker)
|
||||||
|
@ -85,7 +92,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||||
self.assertItemsEqual(snapshots, expected_snapshots)
|
self.assertItemsEqual(snapshots, expected_snapshots)
|
||||||
|
|
||||||
# get last page
|
# get last page
|
||||||
expected_snapshots = mox_snapshots[-size:]
|
expected_snapshots = mock_snapshots[-size:]
|
||||||
marker = expected_snapshots[0].id
|
marker = expected_snapshots[0].id
|
||||||
url = base_url + "?%s=%s" % (next, marker)
|
url = base_url + "?%s=%s" % (next, marker)
|
||||||
res = self._test_snapshots_index_paginated(
|
res = self._test_snapshots_index_paginated(
|
||||||
|
@ -94,15 +101,29 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||||
snapshots = res.context['volume_snapshots_table'].data
|
snapshots = res.context['volume_snapshots_table'].data
|
||||||
self.assertItemsEqual(snapshots, expected_snapshots)
|
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)
|
@override_settings(API_RESULT_PAGE_SIZE=1)
|
||||||
def test_snapshots_index_paginated_prev_page(self):
|
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
|
size = settings.API_RESULT_PAGE_SIZE
|
||||||
base_url = INDEX_URL
|
base_url = INDEX_URL
|
||||||
prev = snapshot_tables.VolumeSnapshotsTable._meta.prev_pagination_param
|
prev = snapshot_tables.VolumeSnapshotsTable._meta.prev_pagination_param
|
||||||
|
|
||||||
# prev from some page
|
# prev from some page
|
||||||
expected_snapshots = mox_snapshots[size:2 * size]
|
expected_snapshots = mock_snapshots[size:2 * size]
|
||||||
marker = expected_snapshots[0].id
|
marker = expected_snapshots[0].id
|
||||||
url = base_url + "?%s=%s" % (prev, marker)
|
url = base_url + "?%s=%s" % (prev, marker)
|
||||||
res = self._test_snapshots_index_paginated(
|
res = self._test_snapshots_index_paginated(
|
||||||
|
@ -112,7 +133,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||||
self.assertItemsEqual(snapshots, expected_snapshots)
|
self.assertItemsEqual(snapshots, expected_snapshots)
|
||||||
|
|
||||||
# back to first page
|
# back to first page
|
||||||
expected_snapshots = mox_snapshots[:size]
|
expected_snapshots = mock_snapshots[:size]
|
||||||
marker = expected_snapshots[0].id
|
marker = expected_snapshots[0].id
|
||||||
url = base_url + "?%s=%s" % (prev, marker)
|
url = base_url + "?%s=%s" % (prev, marker)
|
||||||
res = self._test_snapshots_index_paginated(
|
res = self._test_snapshots_index_paginated(
|
||||||
|
|
|
@ -38,6 +38,7 @@ class SnapshotsView(tables.PagedTableMixin, tables.DataTableView):
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
snapshots = []
|
snapshots = []
|
||||||
volumes = {}
|
volumes = {}
|
||||||
|
needs_gs = False
|
||||||
if cinder.is_volume_service_enabled(self.request):
|
if cinder.is_volume_service_enabled(self.request):
|
||||||
try:
|
try:
|
||||||
marker, sort_dir = self._get_marker()
|
marker, sort_dir = self._get_marker()
|
||||||
|
@ -45,15 +46,36 @@ class SnapshotsView(tables.PagedTableMixin, tables.DataTableView):
|
||||||
cinder.volume_snapshot_list_paged(
|
cinder.volume_snapshot_list_paged(
|
||||||
self.request, paginate=True, marker=marker,
|
self.request, paginate=True, marker=marker,
|
||||||
sort_dir=sort_dir)
|
sort_dir=sort_dir)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(self.request,
|
||||||
|
_("Unable to retrieve volume snapshots."))
|
||||||
|
try:
|
||||||
volumes = cinder.volume_list(self.request)
|
volumes = cinder.volume_list(self.request)
|
||||||
volumes = dict((v.id, v) for v in volumes)
|
volumes = dict((v.id, v) for v in volumes)
|
||||||
except Exception:
|
except Exception:
|
||||||
exceptions.handle(self.request, _("Unable to retrieve "
|
exceptions.handle(self.request,
|
||||||
"volume snapshots."))
|
_("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:
|
for snapshot in snapshots:
|
||||||
volume = volumes.get(snapshot.volume_id)
|
volume = volumes.get(snapshot.volume_id)
|
||||||
setattr(snapshot, '_volume', volume)
|
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
|
return snapshots
|
||||||
|
|
||||||
|
@ -127,6 +149,11 @@ class DetailView(tabs.TabView):
|
||||||
snapshot_id)
|
snapshot_id)
|
||||||
snapshot._volume = cinder.volume_get(self.request,
|
snapshot._volume = cinder.volume_get(self.request,
|
||||||
snapshot.volume_id)
|
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:
|
except Exception:
|
||||||
redirect = self.get_redirect_url()
|
redirect = self.get_redirect_url()
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
|
|
|
@ -61,21 +61,21 @@ class UpdateRow(tables.Row):
|
||||||
vg_snapshot = cinder.group_snapshot_get(request, vg_snapshot_id)
|
vg_snapshot = cinder.group_snapshot_get(request, vg_snapshot_id)
|
||||||
if getattr(vg_snapshot, 'group_id', None):
|
if getattr(vg_snapshot, 'group_id', None):
|
||||||
try:
|
try:
|
||||||
vg_snapshot._group = cinder.group_get(request,
|
vg_snapshot.group = cinder.group_get(request,
|
||||||
vg_snapshot.group_id)
|
vg_snapshot.group_id)
|
||||||
except Exception:
|
except Exception:
|
||||||
exceptions.handle(request, _("Unable to retrieve group"))
|
exceptions.handle(request, _("Unable to retrieve group"))
|
||||||
vg_snapshot._group = None
|
vg_snapshot.group = None
|
||||||
return vg_snapshot
|
return vg_snapshot
|
||||||
|
|
||||||
|
|
||||||
class GroupNameColumn(tables.WrappingColumn):
|
class GroupNameColumn(tables.WrappingColumn):
|
||||||
def get_raw_data(self, snapshot):
|
def get_raw_data(self, snapshot):
|
||||||
group = snapshot._group
|
group = snapshot.group
|
||||||
return group.name_or_id if group else _("-")
|
return group.name_or_id if group else _("-")
|
||||||
|
|
||||||
def get_link_url(self, snapshot):
|
def get_link_url(self, snapshot):
|
||||||
group = snapshot._group
|
group = snapshot.group
|
||||||
if group:
|
if group:
|
||||||
return reverse(self.link, args=(group.id,))
|
return reverse(self.link, args=(group.id,))
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ class IndexView(tables.DataTableView):
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
_("Unable to retrieve volume groups."))
|
_("Unable to retrieve volume groups."))
|
||||||
for gs in vg_snapshots:
|
for gs in vg_snapshots:
|
||||||
gs._group = groups.get(gs.group_id)
|
gs.group = groups.get(gs.group_id)
|
||||||
return vg_snapshots
|
return vg_snapshots
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -119,7 +119,9 @@ class DeleteVolume(VolumePolicyTargetMixin, tables.DeleteAction):
|
||||||
# Can't delete volume if part of consistency group
|
# Can't delete volume if part of consistency group
|
||||||
if getattr(volume, 'consistencygroup_id', None):
|
if getattr(volume, 'consistencygroup_id', None):
|
||||||
return False
|
return False
|
||||||
|
# Can't delete volume if part of volume group
|
||||||
|
if volume.group:
|
||||||
|
return False
|
||||||
return (volume.status in DELETABLE_STATES and
|
return (volume.status in DELETABLE_STATES and
|
||||||
not getattr(volume, 'has_snapshot', False))
|
not getattr(volume, 'has_snapshot', False))
|
||||||
return True
|
return True
|
||||||
|
@ -339,6 +341,14 @@ class UpdateRow(tables.Row):
|
||||||
|
|
||||||
def get_data(self, request, volume_id):
|
def get_data(self, request, volume_id):
|
||||||
volume = cinder.volume_get(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
|
return volume
|
||||||
|
|
||||||
|
|
||||||
|
@ -393,6 +403,17 @@ class AttachmentColumn(tables.WrappingColumn):
|
||||||
return safestring.mark_safe(", ".join(attachments))
|
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):
|
def get_volume_type(volume):
|
||||||
return volume.volume_type if volume.volume_type != "None" else None
|
return volume.volume_type if volume.volume_type != "None" else None
|
||||||
|
|
||||||
|
@ -500,6 +521,10 @@ class VolumesTable(VolumesTableBase):
|
||||||
name = tables.WrappingColumn("name",
|
name = tables.WrappingColumn("name",
|
||||||
verbose_name=_("Name"),
|
verbose_name=_("Name"),
|
||||||
link="horizon:project:volumes:detail")
|
link="horizon:project:volumes:detail")
|
||||||
|
group = GroupNameColumn(
|
||||||
|
"name",
|
||||||
|
verbose_name=_("Group"),
|
||||||
|
link="horizon:project:volume_groups:detail")
|
||||||
volume_type = tables.Column(get_volume_type,
|
volume_type = tables.Column(get_volume_type,
|
||||||
verbose_name=_("Type"))
|
verbose_name=_("Type"))
|
||||||
attachments = AttachmentColumn("attachments",
|
attachments = AttachmentColumn("attachments",
|
||||||
|
|
|
@ -14,8 +14,10 @@
|
||||||
|
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
|
|
||||||
|
from horizon import exceptions
|
||||||
from horizon import tabs
|
from horizon import tabs
|
||||||
|
|
||||||
|
from openstack_dashboard.api import cinder
|
||||||
from openstack_dashboard.dashboards.project.snapshots import tables
|
from openstack_dashboard.dashboards.project.snapshots import tables
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,8 +27,10 @@ class OverviewTab(tabs.Tab):
|
||||||
template_name = ("project/volumes/_detail_overview.html")
|
template_name = ("project/volumes/_detail_overview.html")
|
||||||
|
|
||||||
def get_context_data(self, request):
|
def get_context_data(self, request):
|
||||||
|
volume = self.tab_group.kwargs['volume']
|
||||||
return {
|
return {
|
||||||
'volume': self.tab_group.kwargs['volume'],
|
'volume': volume,
|
||||||
|
'group': volume.group,
|
||||||
'detail_url': {
|
'detail_url': {
|
||||||
'instance': 'horizon:project:instances:detail',
|
'instance': 'horizon:project:instances:detail',
|
||||||
'image': 'horizon:project:images:images:detail',
|
'image': 'horizon:project:images:images:detail',
|
||||||
|
@ -47,9 +51,28 @@ class SnapshotTab(tabs.TableTab):
|
||||||
snapshots = self.tab_group.kwargs['snapshots']
|
snapshots = self.tab_group.kwargs['snapshots']
|
||||||
volume = self.tab_group.kwargs['volume']
|
volume = self.tab_group.kwargs['volume']
|
||||||
|
|
||||||
if volume is not None:
|
if volume is None:
|
||||||
for snapshot in snapshots:
|
return snapshots
|
||||||
snapshot._volume = volume
|
|
||||||
|
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
|
return snapshots
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,12 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<dt>{% trans "Status" %}</dt>
|
<dt>{% trans "Status" %}</dt>
|
||||||
<dd>{{ volume.status_label|capfirst }}</dd>
|
<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>
|
</dl>
|
||||||
|
|
||||||
<h4>{% trans "Specs" %}</h4>
|
<h4>{% trans "Specs" %}</h4>
|
||||||
|
|
|
@ -44,9 +44,10 @@ class VolumeIndexViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||||
'volume_backup_supported',
|
'volume_backup_supported',
|
||||||
'volume_snapshot_list',
|
'volume_snapshot_list',
|
||||||
'volume_list_paged',
|
'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()
|
vol_snaps = self.cinder_volume_snapshots.list()
|
||||||
volumes = self.cinder_volumes.list()
|
volumes = self.cinder_volumes.list()
|
||||||
if with_attachments:
|
if with_attachments:
|
||||||
|
@ -56,6 +57,10 @@ class VolumeIndexViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||||
volume.attachments = []
|
volume.attachments = []
|
||||||
|
|
||||||
self.mock_volume_backup_supported.return_value = False
|
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]
|
self.mock_volume_list_paged.return_value = [volumes, False, False]
|
||||||
if with_attachments:
|
if with_attachments:
|
||||||
self.mock_server_get.return_value = server
|
self.mock_server_get.return_value = server
|
||||||
|
@ -73,6 +78,9 @@ class VolumeIndexViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||||
search_opts=None)
|
search_opts=None)
|
||||||
self.mock_volume_snapshot_list.assert_called_once()
|
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(
|
self.mock_volume_backup_supported.assert_called_with(
|
||||||
test.IsHttpRequest())
|
test.IsHttpRequest())
|
||||||
self.mock_volume_list_paged.assert_called_once_with(
|
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):
|
def test_index_no_volume_attachments(self):
|
||||||
self._test_index(False)
|
self._test_index(False)
|
||||||
|
|
||||||
|
def test_index_with_volume_groups(self):
|
||||||
|
self._test_index(with_groups=True)
|
||||||
|
|
||||||
@test.create_mocks({
|
@test.create_mocks({
|
||||||
api.nova: ['server_get', 'server_list'],
|
api.nova: ['server_get', 'server_list'],
|
||||||
cinder: ['tenant_absolute_limits',
|
cinder: ['tenant_absolute_limits',
|
||||||
|
|
|
@ -109,6 +109,24 @@ class VolumeTableMixIn(object):
|
||||||
attached_instance_ids.append(server_id)
|
attached_instance_ids.append(server_id)
|
||||||
return attached_instance_ids
|
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
|
# set attachment string and if volume has snapshots
|
||||||
def _set_volume_attributes(self,
|
def _set_volume_attributes(self,
|
||||||
volumes,
|
volumes,
|
||||||
|
@ -137,6 +155,7 @@ class VolumesView(tables.PagedTableMixin, VolumeTableMixIn,
|
||||||
volume_ids_with_snapshots = self._get_volumes_ids_with_snapshots()
|
volume_ids_with_snapshots = self._get_volumes_ids_with_snapshots()
|
||||||
self._set_volume_attributes(
|
self._set_volume_attributes(
|
||||||
volumes, instances, volume_ids_with_snapshots)
|
volumes, instances, volume_ids_with_snapshots)
|
||||||
|
self._get_groups(volumes)
|
||||||
return volumes
|
return volumes
|
||||||
|
|
||||||
|
|
||||||
|
@ -172,6 +191,10 @@ class DetailView(tabs.TabbedTableView):
|
||||||
for att in volume.attachments:
|
for att in volume.attachments:
|
||||||
att['instance'] = nova.server_get(self.request,
|
att['instance'] = nova.server_get(self.request,
|
||||||
att['server_id'])
|
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:
|
except Exception:
|
||||||
redirect = self.get_redirect_url()
|
redirect = self.get_redirect_url()
|
||||||
exceptions.handle(self.request,
|
exceptions.handle(self.request,
|
||||||
|
|
|
@ -62,6 +62,7 @@ def data(TEST):
|
||||||
TEST.cinder_group_types = utils.TestDataContainer()
|
TEST.cinder_group_types = utils.TestDataContainer()
|
||||||
TEST.cinder_group_snapshots = utils.TestDataContainer()
|
TEST.cinder_group_snapshots = utils.TestDataContainer()
|
||||||
TEST.cinder_group_volumes = utils.TestDataContainer()
|
TEST.cinder_group_volumes = utils.TestDataContainer()
|
||||||
|
TEST.cinder_volume_snapshots_with_groups = utils.TestDataContainer()
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
service_1 = services.Service(services.ServiceManager(None), {
|
service_1 = services.Service(services.ServiceManager(None), {
|
||||||
|
@ -566,3 +567,17 @@ def data(TEST):
|
||||||
'attachments': []})
|
'attachments': []})
|
||||||
TEST.cinder_group_volumes.add(group_volume_1)
|
TEST.cinder_group_volumes.add(group_volume_1)
|
||||||
TEST.cinder_group_volumes.add(group_volume_2)
|
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))
|
||||||
|
|
|
@ -296,11 +296,15 @@ class CinderApiTests(test.APIMockTestCase):
|
||||||
self.assertTrue(more_data)
|
self.assertTrue(more_data)
|
||||||
self.assertFalse(prev_data)
|
self.assertFalse(prev_data)
|
||||||
|
|
||||||
@mock.patch.object(api.cinder, 'cinderclient')
|
@test.create_mocks({
|
||||||
def test_volume_snapshot_list(self, mock_cinderclient):
|
api.cinder: [
|
||||||
|
('_cinderclient_with_generic_groups', 'cinderclient_groups'),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
def test_volume_snapshot_list(self):
|
||||||
search_opts = {'all_tenants': 1}
|
search_opts = {'all_tenants': 1}
|
||||||
volume_snapshots = self.cinder_volume_snapshots.list()
|
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 = cinderclient.volume_snapshots.list
|
||||||
snapshots_mock.return_value = volume_snapshots
|
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)
|
api.cinder.volume_snapshot_list(self.request, search_opts=search_opts)
|
||||||
snapshots_mock.assert_called_once_with(search_opts=search_opts)
|
snapshots_mock.assert_called_once_with(search_opts=search_opts)
|
||||||
|
|
||||||
@mock.patch.object(api.cinder, 'cinderclient')
|
@test.create_mocks({
|
||||||
def test_volume_snapshot_list_no_volume_configured(self,
|
api.cinder: [
|
||||||
mock_cinderclient):
|
('_cinderclient_with_generic_groups', 'cinderclient_groups'),
|
||||||
|
]
|
||||||
|
})
|
||||||
|
def test_volume_snapshot_list_no_volume_configured(self):
|
||||||
# remove volume from service catalog
|
# remove volume from service catalog
|
||||||
catalog = self.service_catalog
|
catalog = self.service_catalog
|
||||||
for service in catalog:
|
for service in catalog:
|
||||||
|
@ -319,7 +326,7 @@ class CinderApiTests(test.APIMockTestCase):
|
||||||
search_opts = {'all_tenants': 1}
|
search_opts = {'all_tenants': 1}
|
||||||
volume_snapshots = self.cinder_volume_snapshots.list()
|
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 = cinderclient.volume_snapshots.list
|
||||||
snapshots_mock.return_value = volume_snapshots
|
snapshots_mock.return_value = volume_snapshots
|
||||||
|
|
Loading…
Reference in New Issue