Refactor Project Volumes stand-alone panel
Note that there was state leaking from one of the existing tests that was relied upon in other tests and moving that test in the run order caused the others to fail. All related tests have been altered to not leak state. Change-Id: I972bc5650fa77044de8a027f570cf2cb41febef8 Implements: blueprint reorganise-volumes
This commit is contained in:
parent
0b340eccc0
commit
46ad19dbf0
@ -20,7 +20,7 @@ from openstack_dashboard.api import keystone
|
||||
|
||||
from openstack_dashboard.dashboards.project.snapshots \
|
||||
import tables as snapshots_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.volumes \
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
import tables as volumes_tables
|
||||
|
||||
|
||||
|
@ -30,11 +30,11 @@ from openstack_dashboard.dashboards.admin.volumes.volume_types \
|
||||
from openstack_dashboard.dashboards.admin.volumes.volumes \
|
||||
import tables as volumes_tables
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
import tabs as volumes_tabs
|
||||
import views as volumes_views
|
||||
|
||||
|
||||
class VolumeTab(tables.PagedTableMixin, tabs.TableTab,
|
||||
volumes_tabs.VolumeTableMixIn, tables.DataTableView):
|
||||
volumes_views.VolumeTableMixIn, tables.DataTableView):
|
||||
table_classes = (volumes_tables.VolumesTable,)
|
||||
name = _("Volumes")
|
||||
slug = "volumes_tab"
|
||||
@ -105,7 +105,7 @@ class VolumeTab(tables.PagedTableMixin, tabs.TableTab,
|
||||
return filters
|
||||
|
||||
|
||||
class VolumeTypesTab(tabs.TableTab, volumes_tabs.VolumeTableMixIn):
|
||||
class VolumeTypesTab(tabs.TableTab, volumes_views.VolumeTableMixIn):
|
||||
table_classes = (volume_types_tables.VolumeTypesTable,
|
||||
volume_types_tables.QosSpecsTable)
|
||||
name = _("Volume Types")
|
||||
|
@ -26,7 +26,7 @@ from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.api import keystone
|
||||
from openstack_dashboard.dashboards.project.snapshots \
|
||||
import tables as snapshot_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.volumes \
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
import tables as volume_tables
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
@ -35,8 +35,16 @@ INDEX_URL = reverse('horizon:admin:volumes:index')
|
||||
|
||||
|
||||
class VolumeTests(test.BaseAdminViewTests):
|
||||
def tearDown(self):
|
||||
for volume in self.cinder_volumes.list():
|
||||
# VolumeTableMixIn._set_volume_attributes mutates data
|
||||
# and cinder_volumes.list() doesn't deep copy
|
||||
for att in volume.attachments:
|
||||
if 'instance' in att:
|
||||
del att['instance']
|
||||
super(VolumeTests, self).tearDown()
|
||||
|
||||
@test.create_stubs({api.nova: ('server_list',),
|
||||
@test.create_stubs({api.nova: ('server_list', 'server_get'),
|
||||
cinder: ('volume_list_paged',
|
||||
'volume_snapshot_list'),
|
||||
keystone: ('tenant_list',)})
|
||||
@ -45,6 +53,8 @@ class VolumeTests(test.BaseAdminViewTests):
|
||||
if instanceless_volumes:
|
||||
for volume in volumes:
|
||||
volume.attachments = []
|
||||
else:
|
||||
server = self.servers.first()
|
||||
|
||||
cinder.volume_list_paged(IsA(http.HttpRequest), sort_dir="desc",
|
||||
marker=None, paginate=True,
|
||||
@ -53,6 +63,8 @@ class VolumeTests(test.BaseAdminViewTests):
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest), search_opts={
|
||||
'all_tenants': True}).AndReturn([])
|
||||
if not instanceless_volumes:
|
||||
api.nova.server_get(IsA(http.HttpRequest),
|
||||
server.id).AndReturn(server)
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts={
|
||||
'all_tenants': True}, detailed=False) \
|
||||
.AndReturn([self.servers.list(), False])
|
||||
@ -72,13 +84,14 @@ class VolumeTests(test.BaseAdminViewTests):
|
||||
def test_index_with_attachments(self):
|
||||
self._test_index(instanceless_volumes=False)
|
||||
|
||||
@test.create_stubs({api.nova: ('server_list',),
|
||||
@test.create_stubs({api.nova: ('server_list', 'server_get'),
|
||||
cinder: ('volume_list_paged',
|
||||
'volume_snapshot_list'),
|
||||
keystone: ('tenant_list',)})
|
||||
def _test_index_paginated(self, marker, sort_dir, volumes, url,
|
||||
has_more, has_prev):
|
||||
vol_snaps = self.cinder_volume_snapshots.list()
|
||||
server = self.servers.first()
|
||||
cinder.volume_list_paged(IsA(http.HttpRequest), sort_dir=sort_dir,
|
||||
marker=marker, paginate=True,
|
||||
search_opts={'all_tenants': True}) \
|
||||
@ -88,6 +101,8 @@ class VolumeTests(test.BaseAdminViewTests):
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts={
|
||||
'all_tenants': True}, detailed=False) \
|
||||
.AndReturn([self.servers.list(), False])
|
||||
api.nova.server_get(IsA(http.HttpRequest),
|
||||
server.id).AndReturn(server)
|
||||
keystone.tenant_list(IsA(http.HttpRequest)) \
|
||||
.AndReturn([self.tenants.list(), False])
|
||||
|
||||
|
@ -27,7 +27,7 @@ from horizon.utils import validators as utils_validators
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.dashboards.admin.volumes.snapshots.forms \
|
||||
import populate_status_choices
|
||||
from openstack_dashboard.dashboards.project.volumes.volumes \
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
import forms as project_forms
|
||||
|
||||
|
||||
|
@ -16,7 +16,7 @@ from horizon import exceptions
|
||||
from horizon import tables
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.volumes import tables as volumes_tables
|
||||
import tables as volumes_tables
|
||||
|
||||
|
||||
class VolumesFilterAction(tables.FilterAction):
|
||||
|
@ -24,6 +24,15 @@ INDEX_URL = reverse('horizon:admin:volumes:volumes_tab')
|
||||
|
||||
|
||||
class VolumeViewTests(test.BaseAdminViewTests):
|
||||
def tearDown(self):
|
||||
for volume in self.cinder_volumes.list():
|
||||
# VolumeTableMixIn._set_volume_attributes mutates data
|
||||
# and cinder_volumes.list() doesn't deep copy
|
||||
for att in volume.attachments:
|
||||
if 'instance' in att:
|
||||
del att['instance']
|
||||
super(VolumeViewTests, self).tearDown()
|
||||
|
||||
@test.create_stubs({cinder: ('volume_reset_state',
|
||||
'volume_get')})
|
||||
def test_update_volume_status(self):
|
||||
|
@ -23,7 +23,7 @@ from openstack_dashboard.dashboards.admin.volumes.volumes \
|
||||
import forms as volumes_forms
|
||||
from openstack_dashboard.dashboards.admin.volumes.volumes \
|
||||
import tables as volumes_tables
|
||||
from openstack_dashboard.dashboards.project.volumes.volumes \
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
import views as volumes_views
|
||||
|
||||
|
||||
|
@ -147,8 +147,7 @@ class BackupsTable(tables.DataTable):
|
||||
display_choices=STATUS_DISPLAY_CHOICES)
|
||||
volume_name = BackupVolumeNameColumn("name",
|
||||
verbose_name=_("Volume Name"),
|
||||
link="horizon:project"
|
||||
":volumes:volumes:detail")
|
||||
link="horizon:project:volumes:detail")
|
||||
|
||||
class Meta(object):
|
||||
name = "volume_backups"
|
||||
|
@ -15,7 +15,7 @@
|
||||
{% if volume %}
|
||||
<dt>{% trans "Volume" %}</dt>
|
||||
<dd>
|
||||
<a href="{% url 'horizon:project:volumes:volumes:detail' backup.volume_id %}">
|
||||
<a href="{% url 'horizon:project:volumes:detail' backup.volume_id %}">
|
||||
{{ volume.name }}
|
||||
</a>
|
||||
</dd>
|
||||
|
@ -131,7 +131,7 @@ class VolumeBackupsViewTests(test.TestCase):
|
||||
'container_name': backup.container_name,
|
||||
'name': backup.name,
|
||||
'description': backup.description}
|
||||
url = reverse('horizon:project:volumes:volumes:create_backup',
|
||||
url = reverse('horizon:project:volumes:create_backup',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
|
@ -28,11 +28,11 @@ from openstack_dashboard.dashboards.project.backups \
|
||||
from openstack_dashboard.dashboards.project.backups \
|
||||
import tabs as backup_tabs
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
import tabs as volume_tabs
|
||||
import views as volume_views
|
||||
|
||||
|
||||
class BackupsView(tables.DataTableView, tables.PagedTableMixin,
|
||||
volume_tabs.VolumeTableMixIn):
|
||||
volume_views.VolumeTableMixIn):
|
||||
table_class = backup_tables.BackupsTable
|
||||
page_title = _("Volume Backups")
|
||||
|
||||
@ -61,7 +61,7 @@ class CreateBackupView(forms.ModalFormView):
|
||||
form_class = backup_forms.CreateBackupForm
|
||||
template_name = 'project/backups/create_backup.html'
|
||||
submit_label = _("Create Volume Backup")
|
||||
submit_url = "horizon:project:volumes:volumes:create_backup"
|
||||
submit_url = "horizon:project:volumes:create_backup"
|
||||
success_url = reverse_lazy("horizon:project:backups:index")
|
||||
page_title = _("Create Volume Backup")
|
||||
|
||||
|
@ -146,7 +146,7 @@ class EditImage(tables.LinkAction):
|
||||
class CreateVolumeFromImage(tables.LinkAction):
|
||||
name = "create_volume_from_image"
|
||||
verbose_name = _("Create Volume")
|
||||
url = "horizon:project:volumes:volumes:create"
|
||||
url = "horizon:project:volumes:create"
|
||||
classes = ("ajax-modal",)
|
||||
icon = "camera"
|
||||
policy_rules = (("volume", "volume:create"),)
|
||||
|
@ -306,7 +306,7 @@ class DetailView(tabs.TabView):
|
||||
redirect_url = 'horizon:project:instances:index'
|
||||
page_title = "{{ instance.name|default:instance.id }}"
|
||||
image_url = 'horizon:project:images:images:detail'
|
||||
volume_url = 'horizon:project:volumes:volumes:detail'
|
||||
volume_url = 'horizon:project:volumes:detail'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
|
@ -27,7 +27,7 @@ from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard import policy
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.volumes import tables as volume_tables
|
||||
import tables as volume_tables
|
||||
|
||||
|
||||
class LaunchSnapshot(volume_tables.LaunchVolume):
|
||||
@ -116,7 +116,7 @@ class EditVolumeSnapshot(policy.PolicyTargetMixin, tables.LinkAction):
|
||||
class CreateVolumeFromSnapshot(tables.LinkAction):
|
||||
name = "create_from_snapshot"
|
||||
verbose_name = _("Create Volume")
|
||||
url = "horizon:project:volumes:volumes:create"
|
||||
url = "horizon:project:volumes:create"
|
||||
classes = ("ajax-modal",)
|
||||
icon = "camera"
|
||||
policy_rules = (("volume", "volume:create"),)
|
||||
@ -193,7 +193,7 @@ class VolumeSnapshotsTable(volume_tables.VolumesTableBase):
|
||||
volume_name = SnapshotVolumeNameColumn(
|
||||
"name",
|
||||
verbose_name=_("Volume Name"),
|
||||
link="horizon:project:volumes:volumes:detail")
|
||||
link="horizon:project:volumes:detail")
|
||||
|
||||
class Meta(object):
|
||||
name = "volume_snapshots"
|
||||
|
@ -137,12 +137,11 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||
AndReturn(usage_limit)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:'
|
||||
'volumes:create_snapshot', args=[volume.id])
|
||||
url = reverse('horizon:project:volumes:create_snapshot',
|
||||
args=[volume.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertTemplateUsed(res, 'project/volumes/volumes/'
|
||||
'create_snapshot.html')
|
||||
self.assertTemplateUsed(res, 'project/volumes/create_snapshot.html')
|
||||
|
||||
@test.create_stubs({cinder: ('volume_get',
|
||||
'volume_snapshot_create',)})
|
||||
@ -165,7 +164,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||
'volume_id': volume.id,
|
||||
'name': snapshot.name,
|
||||
'description': snapshot.description}
|
||||
url = reverse('horizon:project:volumes:volumes:create_snapshot',
|
||||
url = reverse('horizon:project:volumes:create_snapshot',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
@ -191,7 +190,7 @@ class VolumeSnapshotsViewTests(test.TestCase):
|
||||
'volume_id': volume.id,
|
||||
'name': snapshot.name,
|
||||
'description': snapshot.description}
|
||||
url = reverse('horizon:project:volumes:volumes:create_snapshot',
|
||||
url = reverse('horizon:project:volumes:create_snapshot',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
@ -97,7 +97,7 @@ class DetailView(tabs.TabView):
|
||||
tab_group_class = vol_snapshot_tabs.SnapshotDetailTabs
|
||||
template_name = 'horizon/common/_detail.html'
|
||||
page_title = "{{ snapshot.name|default:snapshot.id }}"
|
||||
volume_url = 'horizon:project:volumes:volumes:detail'
|
||||
volume_url = 'horizon:project:volumes:detail'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
|
@ -44,13 +44,13 @@ resource_urls = {
|
||||
"AWS::EC2::Subnet": {
|
||||
'link': 'horizon:project:networks:subnets:detail'},
|
||||
"AWS::EC2::Volume": {
|
||||
'link': 'horizon:project:volumes:volumes:detail'},
|
||||
'link': 'horizon:project:volumes:detail'},
|
||||
"AWS::EC2::VPC": {
|
||||
'link': 'horizon:project:networks:detail'},
|
||||
"AWS::S3::Bucket": {
|
||||
'link': 'horizon:project:containers:index'},
|
||||
"OS::Cinder::Volume": {
|
||||
'link': 'horizon:project:volumes:volumes:detail'},
|
||||
'link': 'horizon:project:volumes:detail'},
|
||||
"OS::Heat::AccessPolicy": {
|
||||
'link': 'horizon:project:stacks:detail'},
|
||||
"OS::Heat::AutoScalingGroup": {
|
||||
|
@ -564,7 +564,7 @@ class CreateTransferForm(forms.SelfHandlingForm):
|
||||
msg = _('Created volume transfer: "%s".') % data['name']
|
||||
messages.success(request, msg)
|
||||
response = http.HttpResponseRedirect(
|
||||
reverse("horizon:project:volumes:volumes:show_transfer",
|
||||
reverse("horizon:project:volumes:show_transfer",
|
||||
args=(transfer.id, transfer.auth_key)))
|
||||
return response
|
||||
except Exception:
|
@ -18,7 +18,6 @@ import horizon
|
||||
|
||||
|
||||
class Volumes(horizon.Panel):
|
||||
|
||||
name = _("Volumes")
|
||||
slug = 'volumes'
|
||||
permissions = (
|
||||
|
@ -129,7 +129,7 @@ class DeleteVolume(VolumePolicyTargetMixin, tables.DeleteAction):
|
||||
class CreateVolume(tables.LinkAction):
|
||||
name = "create"
|
||||
verbose_name = _("Create Volume")
|
||||
url = "horizon:project:volumes:volumes:create"
|
||||
url = "horizon:project:volumes:create"
|
||||
classes = ("ajax-modal", "btn-create")
|
||||
icon = "plus"
|
||||
policy_rules = (("volume", "volume:create"),)
|
||||
@ -166,7 +166,7 @@ class CreateVolume(tables.LinkAction):
|
||||
class ExtendVolume(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
name = "extend"
|
||||
verbose_name = _("Extend Volume")
|
||||
url = "horizon:project:volumes:volumes:extend"
|
||||
url = "horizon:project:volumes:extend"
|
||||
classes = ("ajax-modal", "btn-extend")
|
||||
policy_rules = (("volume", "volume:extend"),)
|
||||
|
||||
@ -177,7 +177,7 @@ class ExtendVolume(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
class EditAttachments(tables.LinkAction):
|
||||
name = "attachments"
|
||||
verbose_name = _("Manage Attachments")
|
||||
url = "horizon:project:volumes:volumes:attach"
|
||||
url = "horizon:project:volumes:attach"
|
||||
classes = ("ajax-modal",)
|
||||
icon = "pencil"
|
||||
|
||||
@ -204,7 +204,7 @@ class EditAttachments(tables.LinkAction):
|
||||
class CreateSnapshot(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
name = "snapshots"
|
||||
verbose_name = _("Create Snapshot")
|
||||
url = "horizon:project:volumes:volumes:create_snapshot"
|
||||
url = "horizon:project:volumes:create_snapshot"
|
||||
classes = ("ajax-modal",)
|
||||
icon = "camera"
|
||||
policy_rules = (("volume", "volume:create_snapshot"),)
|
||||
@ -229,7 +229,7 @@ class CreateSnapshot(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
class CreateTransfer(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
name = "create_transfer"
|
||||
verbose_name = _("Create Transfer")
|
||||
url = "horizon:project:volumes:volumes:create_transfer"
|
||||
url = "horizon:project:volumes:create_transfer"
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "volume:create_transfer"),)
|
||||
|
||||
@ -240,7 +240,7 @@ class CreateTransfer(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
class CreateBackup(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
name = "backups"
|
||||
verbose_name = _("Create Backup")
|
||||
url = "horizon:project:volumes:volumes:create_backup"
|
||||
url = "horizon:project:volumes:create_backup"
|
||||
classes = ("ajax-modal",)
|
||||
policy_rules = (("volume", "backup:create"),)
|
||||
|
||||
@ -252,7 +252,7 @@ class CreateBackup(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
class UploadToImage(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
name = "upload_to_image"
|
||||
verbose_name = _("Upload to Image")
|
||||
url = "horizon:project:volumes:volumes:upload_to_image"
|
||||
url = "horizon:project:volumes:upload_to_image"
|
||||
classes = ("ajax-modal",)
|
||||
icon = "cloud-upload"
|
||||
policy_rules = (("volume",
|
||||
@ -269,7 +269,7 @@ class UploadToImage(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
class EditVolume(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
name = "edit"
|
||||
verbose_name = _("Edit Volume")
|
||||
url = "horizon:project:volumes:volumes:update"
|
||||
url = "horizon:project:volumes:update"
|
||||
classes = ("ajax-modal",)
|
||||
icon = "pencil"
|
||||
policy_rules = (("volume", "volume:update"),)
|
||||
@ -281,7 +281,7 @@ class EditVolume(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
class RetypeVolume(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
name = "retype"
|
||||
verbose_name = _("Change Volume Type")
|
||||
url = "horizon:project:volumes:volumes:retype"
|
||||
url = "horizon:project:volumes:retype"
|
||||
classes = ("ajax-modal",)
|
||||
icon = "pencil"
|
||||
policy_rules = (("volume", "volume:retype"),)
|
||||
@ -293,7 +293,7 @@ class RetypeVolume(VolumePolicyTargetMixin, tables.LinkAction):
|
||||
class AcceptTransfer(tables.LinkAction):
|
||||
name = "accept_transfer"
|
||||
verbose_name = _("Accept Transfer")
|
||||
url = "horizon:project:volumes:volumes:accept_transfer"
|
||||
url = "horizon:project:volumes:accept_transfer"
|
||||
classes = ("ajax-modal",)
|
||||
icon = "exchange"
|
||||
policy_rules = (("volume", "volume:accept_transfer"),)
|
||||
@ -401,7 +401,7 @@ def get_encrypted_value(volume):
|
||||
|
||||
def get_encrypted_link(volume):
|
||||
if hasattr(volume, 'encrypted') and volume.encrypted:
|
||||
return reverse("horizon:project:volumes:volumes:encryption_detail",
|
||||
return reverse("horizon:project:volumes:encryption_detail",
|
||||
kwargs={'volume_id': volume.id})
|
||||
|
||||
|
||||
@ -444,7 +444,7 @@ class VolumesTableBase(tables.DataTable):
|
||||
)
|
||||
name = tables.Column("name",
|
||||
verbose_name=_("Name"),
|
||||
link="horizon:project:volumes:volumes:detail")
|
||||
link="horizon:project:volumes:detail")
|
||||
description = tables.Column("description",
|
||||
verbose_name=_("Description"),
|
||||
truncate=40)
|
||||
@ -490,7 +490,7 @@ class UpdateMetadata(tables.LinkAction):
|
||||
class VolumesTable(VolumesTableBase):
|
||||
name = tables.WrappingColumn("name",
|
||||
verbose_name=_("Name"),
|
||||
link="horizon:project:volumes:volumes:detail")
|
||||
link="horizon:project:volumes:detail")
|
||||
volume_type = tables.Column(get_volume_type,
|
||||
verbose_name=_("Type"))
|
||||
attachments = AttachmentColumn("attachments",
|
@ -1,4 +1,4 @@
|
||||
# Copyright 2013 Nebula, Inc.
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# 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
|
||||
@ -12,114 +12,20 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon.tables import PagedTableMixin
|
||||
from horizon import tabs
|
||||
|
||||
from openstack_dashboard import api
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes.volumes \
|
||||
import tables as volume_tables
|
||||
class OverviewTab(tabs.Tab):
|
||||
name = _("Overview")
|
||||
slug = "overview"
|
||||
template_name = ("project/volumes/_detail_overview.html")
|
||||
|
||||
def get_context_data(self, request):
|
||||
return {"volume": self.tab_group.kwargs['volume']}
|
||||
|
||||
|
||||
class VolumeTableMixIn(object):
|
||||
_has_more_data = False
|
||||
_has_prev_data = False
|
||||
|
||||
def _get_volumes(self, search_opts=None):
|
||||
try:
|
||||
marker, sort_dir = self._get_marker()
|
||||
volumes, self._has_more_data, self._has_prev_data = \
|
||||
api.cinder.volume_list_paged(self.request, marker=marker,
|
||||
search_opts=search_opts,
|
||||
sort_dir=sort_dir, paginate=True)
|
||||
|
||||
if sort_dir == "asc":
|
||||
volumes.reverse()
|
||||
|
||||
return volumes
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume list.'))
|
||||
return []
|
||||
|
||||
def _get_instances(self, search_opts=None, instance_ids=None):
|
||||
if not instance_ids:
|
||||
return []
|
||||
try:
|
||||
# TODO(tsufiev): we should pass attached_instance_ids to
|
||||
# nova.server_list as soon as Nova API allows for this
|
||||
instances, has_more = api.nova.server_list(self.request,
|
||||
search_opts=search_opts,
|
||||
detailed=False)
|
||||
return instances
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve volume/instance "
|
||||
"attachment information"))
|
||||
return []
|
||||
|
||||
def _get_volumes_ids_with_snapshots(self, search_opts=None):
|
||||
try:
|
||||
volume_ids = []
|
||||
snapshots = api.cinder.volume_snapshot_list(
|
||||
self.request, search_opts=search_opts)
|
||||
if snapshots:
|
||||
# extract out the volume ids
|
||||
volume_ids = set([(s.volume_id) for s in snapshots])
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve snapshot list."))
|
||||
|
||||
return volume_ids
|
||||
|
||||
def _get_attached_instance_ids(self, volumes):
|
||||
attached_instance_ids = []
|
||||
for volume in volumes:
|
||||
for att in volume.attachments:
|
||||
server_id = att.get('server_id', None)
|
||||
if server_id is not None:
|
||||
attached_instance_ids.append(server_id)
|
||||
return attached_instance_ids
|
||||
|
||||
# set attachment string and if volume has snapshots
|
||||
def _set_volume_attributes(self,
|
||||
volumes,
|
||||
instances,
|
||||
volume_ids_with_snapshots):
|
||||
instances = OrderedDict([(inst.id, inst) for inst in instances])
|
||||
for volume in volumes:
|
||||
if volume_ids_with_snapshots:
|
||||
if volume.id in volume_ids_with_snapshots:
|
||||
setattr(volume, 'has_snapshot', True)
|
||||
if instances:
|
||||
for att in volume.attachments:
|
||||
server_id = att.get('server_id', None)
|
||||
att['instance'] = instances.get(server_id, None)
|
||||
|
||||
|
||||
class VolumeTab(PagedTableMixin, tabs.TableTab, VolumeTableMixIn):
|
||||
table_classes = (volume_tables.VolumesTable,)
|
||||
name = _("Volumes")
|
||||
slug = "volumes_tab"
|
||||
template_name = ("horizon/common/_detail_table.html")
|
||||
preload = False
|
||||
|
||||
def get_volumes_data(self):
|
||||
volumes = self._get_volumes()
|
||||
attached_instance_ids = self._get_attached_instance_ids(volumes)
|
||||
instances = self._get_instances(instance_ids=attached_instance_ids)
|
||||
volume_ids_with_snapshots = self._get_volumes_ids_with_snapshots()
|
||||
self._set_volume_attributes(
|
||||
volumes, instances, volume_ids_with_snapshots)
|
||||
return volumes
|
||||
|
||||
|
||||
class VolumeAndSnapshotTabs(tabs.TabGroup):
|
||||
slug = "volumes_and_snapshots"
|
||||
tabs = (VolumeTab, )
|
||||
sticky = True
|
||||
class VolumeDetailTabs(tabs.TabGroup):
|
||||
slug = "volume_details"
|
||||
tabs = (OverviewTab,)
|
||||
|
@ -3,6 +3,6 @@
|
||||
|
||||
{% block modal-body-right %}
|
||||
<div class="quota-dynamic">
|
||||
{% include "project/volumes/volumes/_limits.html" with usages=usages %}
|
||||
{% include "project/volumes/_limits.html" with usages=usages %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -3,7 +3,7 @@
|
||||
|
||||
{% block modal-body-right %}
|
||||
<div class="quota-dynamic">
|
||||
{% include "project/volumes/volumes/_snapshot_limits.html" with usages=usages snapshot_quota=True %}
|
||||
{% include "project/volumes/_snapshot_limits.html" with usages=usages snapshot_quota=True %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -31,7 +31,7 @@
|
||||
<dd>{{ volume.is_bootable|yesno|capfirst }}</dd>
|
||||
<dt>{% trans "Encrypted" %}</dt>
|
||||
{% if volume.encrypted %}
|
||||
<dd><a href="{% url 'horizon:project:volumes:volumes:encryption_detail' volume.id %}">{% trans "Yes" %}</a></dd>
|
||||
<dd><a href="{% url 'horizon:project:volumes:encryption_detail' volume.id %}">{% trans "Yes" %}</a></dd>
|
||||
{% else %}
|
||||
<dd>{% trans "No" %}</dd>
|
||||
{% endif %}
|
@ -3,6 +3,6 @@
|
||||
|
||||
{% block modal-body-right %}
|
||||
<div class="quota-dynamic">
|
||||
{% include "project/volumes/volumes/_extend_limits.html" with usages=usages %}
|
||||
{% include "project/volumes/_extend_limits.html" with usages=usages %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,4 +1,4 @@
|
||||
{% extends "project/volumes/volumes/_limits.html" %}
|
||||
{% extends "project/volumes/_limits.html" %}
|
||||
{% load i18n horizon humanize %}
|
||||
|
||||
{% block title %}
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Accept Volume Transfer" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/volumes/_accept_transfer.html' %}
|
||||
{% include 'project/volumes/_accept_transfer.html' %}
|
||||
{% endblock %}
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Manage Volume Attachments" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/volumes/_attach.html' %}
|
||||
{% include 'project/volumes/_attach.html' %}
|
||||
{% endblock %}
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Create Volume" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/volumes/_create.html' %}
|
||||
{% include 'project/volumes/_create.html' %}
|
||||
{% endblock %}
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Create Volume Snapshot" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/volumes/_create_snapshot.html' %}
|
||||
{% include 'project/volumes/_create_snapshot.html' %}
|
||||
{% endblock %}
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Create Volume Transfer" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/volumes/_create_transfer.html' %}
|
||||
{% include 'project/volumes/_create_transfer.html' %}
|
||||
{% endblock %}
|
@ -9,7 +9,7 @@
|
||||
{% block main %}
|
||||
<div class="row-fluid">
|
||||
<div class="col-sm-12">
|
||||
{% include "project/volumes/volumes/_encryption_detail_overview.html" %}
|
||||
{% include "project/volumes/_encryption_detail_overview.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Extend Volume" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/volumes/_extend.html' %}
|
||||
{% include 'project/volumes/_extend.html' %}
|
||||
{% endblock %}
|
@ -1,11 +0,0 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Volumes" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
{{ tab_group.render }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Change Volume Type" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/volumes/_retype.html' %}
|
||||
{% include 'project/volumes/_retype.html' %}
|
||||
{% endblock %}
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Volume Transfer Details" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/volumes/_show_transfer.html' %}
|
||||
{% include 'project/volumes/_show_transfer.html' %}
|
||||
{% endblock %}
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Edit Volume" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/volumes/_update.html' %}
|
||||
{% include 'project/volumes/_update.html' %}
|
||||
{% endblock %}
|
@ -3,5 +3,5 @@
|
||||
{% block title %}{% trans "Upload Volume to Image" %}{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
{% include 'project/volumes/volumes/_upload_to_image.html' %}
|
||||
{% include 'project/volumes/_upload_to_image.html' %}
|
||||
{% endblock %}
|
@ -1,175 +0,0 @@
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
import copy
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django import http
|
||||
from django.test.utils import override_settings
|
||||
from django.utils.http import urlunquote
|
||||
|
||||
from mox3.mox import IsA # noqa
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.dashboards.project.volumes.volumes \
|
||||
import tables as volume_tables
|
||||
from openstack_dashboard.test import helpers as test
|
||||
|
||||
|
||||
INDEX_URL = reverse('horizon:project:volumes:index')
|
||||
|
||||
|
||||
class VolumeAndSnapshotsAndBackupsTests(test.TestCase):
|
||||
@test.create_stubs({api.cinder: ('tenant_absolute_limits',
|
||||
'volume_list',
|
||||
'volume_list_paged',
|
||||
'volume_snapshot_list',
|
||||
'volume_backup_supported',
|
||||
'volume_backup_list_paged',
|
||||
),
|
||||
api.nova: ('server_list',)})
|
||||
def test_index(self, instanceless_volumes=False):
|
||||
vol_snaps = self.cinder_volume_snapshots.list()
|
||||
volumes = self.cinder_volumes.list()
|
||||
if instanceless_volumes:
|
||||
for volume in volumes:
|
||||
volume.attachments = []
|
||||
|
||||
api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\
|
||||
MultipleTimes().AndReturn(False)
|
||||
api.cinder.volume_list_paged(
|
||||
IsA(http.HttpRequest), marker=None, search_opts=None,
|
||||
sort_dir='desc', paginate=True).\
|
||||
AndReturn([volumes, False, False])
|
||||
if not instanceless_volumes:
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False).\
|
||||
AndReturn([self.servers.list(), False])
|
||||
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)).\
|
||||
AndReturn(vol_snaps)
|
||||
|
||||
api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)).\
|
||||
MultipleTimes().AndReturn(self.cinder_limits['absolute'])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTemplateUsed(res, 'project/volumes/index.html')
|
||||
|
||||
def test_index_no_volume_attachments(self):
|
||||
self.test_index(instanceless_volumes=True)
|
||||
|
||||
@test.create_stubs({api.cinder: ('tenant_absolute_limits',
|
||||
'volume_list_paged',
|
||||
'volume_backup_supported',
|
||||
'volume_snapshot_list'),
|
||||
api.nova: ('server_list',)})
|
||||
def _test_index_paginated(self, marker, sort_dir, volumes, url,
|
||||
has_more, has_prev):
|
||||
backup_supported = True
|
||||
vol_snaps = self.cinder_volume_snapshots.list()
|
||||
|
||||
api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\
|
||||
MultipleTimes().AndReturn(backup_supported)
|
||||
api.cinder.volume_list_paged(IsA(http.HttpRequest), marker=marker,
|
||||
sort_dir=sort_dir, search_opts=None,
|
||||
paginate=True).\
|
||||
AndReturn([volumes, has_more, has_prev])
|
||||
api.cinder.volume_snapshot_list(
|
||||
IsA(http.HttpRequest), search_opts=None).AndReturn(vol_snaps)
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False).\
|
||||
AndReturn([self.servers.list(), False])
|
||||
api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)).MultipleTimes().\
|
||||
AndReturn(self.cinder_limits['absolute'])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(urlunquote(url))
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTemplateUsed(res, 'project/volumes/index.html')
|
||||
|
||||
self.mox.UnsetStubs()
|
||||
return res
|
||||
|
||||
def ensure_attachments_exist(self, volumes):
|
||||
volumes = copy.copy(volumes)
|
||||
for volume in volumes:
|
||||
if not volume.attachments:
|
||||
volume.attachments.append({
|
||||
"id": "1", "server_id": '1', "device": "/dev/hda"})
|
||||
return volumes
|
||||
|
||||
@override_settings(API_RESULT_PAGE_SIZE=2)
|
||||
def test_index_paginated(self):
|
||||
mox_volumes = self.ensure_attachments_exist(self.cinder_volumes.list())
|
||||
size = settings.API_RESULT_PAGE_SIZE
|
||||
|
||||
# get first page
|
||||
expected_volumes = mox_volumes[:size]
|
||||
url = INDEX_URL
|
||||
res = self._test_index_paginated(marker=None, sort_dir="desc",
|
||||
volumes=expected_volumes, url=url,
|
||||
has_more=True, has_prev=False)
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, expected_volumes)
|
||||
|
||||
# get second page
|
||||
expected_volumes = mox_volumes[size:2 * size]
|
||||
marker = expected_volumes[0].id
|
||||
next = volume_tables.VolumesTable._meta.pagination_param
|
||||
url = "?".join([INDEX_URL, "=".join([next, marker])])
|
||||
res = self._test_index_paginated(marker=marker, sort_dir="desc",
|
||||
volumes=expected_volumes, url=url,
|
||||
has_more=True, has_prev=True)
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, expected_volumes)
|
||||
|
||||
# get last page
|
||||
expected_volumes = mox_volumes[-size:]
|
||||
marker = expected_volumes[0].id
|
||||
next = volume_tables.VolumesTable._meta.pagination_param
|
||||
url = "?".join([INDEX_URL, "=".join([next, marker])])
|
||||
res = self._test_index_paginated(marker=marker, sort_dir="desc",
|
||||
volumes=expected_volumes, url=url,
|
||||
has_more=False, has_prev=True)
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, expected_volumes)
|
||||
|
||||
@override_settings(API_RESULT_PAGE_SIZE=2)
|
||||
def test_index_paginated_prev_page(self):
|
||||
mox_volumes = self.ensure_attachments_exist(self.cinder_volumes.list())
|
||||
size = settings.API_RESULT_PAGE_SIZE
|
||||
|
||||
# prev from some page
|
||||
expected_volumes = mox_volumes[size:2 * size]
|
||||
marker = expected_volumes[0].id
|
||||
prev = volume_tables.VolumesTable._meta.prev_pagination_param
|
||||
url = "?".join([INDEX_URL, "=".join([prev, marker])])
|
||||
res = self._test_index_paginated(marker=marker, sort_dir="asc",
|
||||
volumes=expected_volumes, url=url,
|
||||
has_more=True, has_prev=True)
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, expected_volumes)
|
||||
|
||||
# back to first page
|
||||
expected_volumes = mox_volumes[:size]
|
||||
marker = expected_volumes[0].id
|
||||
prev = volume_tables.VolumesTable._meta.prev_pagination_param
|
||||
url = "?".join([INDEX_URL, "=".join([prev, marker])])
|
||||
res = self._test_index_paginated(marker=marker, sort_dir="asc",
|
||||
volumes=expected_volumes, url=url,
|
||||
has_more=True, has_prev=False)
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, expected_volumes)
|
@ -1,7 +1,3 @@
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -16,7 +12,12 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import six
|
||||
from six import moves
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.forms import widgets
|
||||
from django import http
|
||||
@ -25,22 +26,179 @@ from django.test.utils import override_settings
|
||||
from django.utils.http import urlunquote
|
||||
|
||||
from mox3.mox import IsA # noqa
|
||||
import six
|
||||
from six import moves
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
import tables as volume_tables
|
||||
from openstack_dashboard.test import helpers as test
|
||||
from openstack_dashboard.usage import quotas
|
||||
|
||||
|
||||
VOLUME_INDEX_URL = reverse('horizon:project:volumes:index')
|
||||
VOLUME_VOLUMES_TAB_URL = urlunquote(reverse(
|
||||
'horizon:project:volumes:volumes_tab'))
|
||||
INDEX_URL = reverse('horizon:project:volumes:index')
|
||||
SEARCH_OPTS = dict(status=api.cinder.VOLUME_STATE_AVAILABLE)
|
||||
|
||||
|
||||
class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
def tearDown(self):
|
||||
for volume in self.cinder_volumes.list():
|
||||
# VolumeTableMixIn._set_volume_attributes mutates data
|
||||
# and cinder_volumes.list() doesn't deep copy
|
||||
for att in volume.attachments:
|
||||
if 'instance' in att:
|
||||
del att['instance']
|
||||
super(VolumeViewTests, self).tearDown()
|
||||
|
||||
@test.create_stubs({api.cinder: ('tenant_absolute_limits',
|
||||
'volume_list',
|
||||
'volume_list_paged',
|
||||
'volume_snapshot_list',
|
||||
'volume_backup_supported',
|
||||
'volume_backup_list_paged',
|
||||
),
|
||||
api.nova: ('server_list', 'server_get')})
|
||||
def test_index(self, with_attachments=True):
|
||||
vol_snaps = self.cinder_volume_snapshots.list()
|
||||
volumes = self.cinder_volumes.list()
|
||||
if with_attachments:
|
||||
server = self.servers.first()
|
||||
else:
|
||||
for volume in volumes:
|
||||
volume.attachments = []
|
||||
|
||||
api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\
|
||||
MultipleTimes().AndReturn(False)
|
||||
api.cinder.volume_list_paged(
|
||||
IsA(http.HttpRequest), marker=None, search_opts=None,
|
||||
sort_dir='desc', paginate=True).\
|
||||
AndReturn([volumes, False, False])
|
||||
if with_attachments:
|
||||
api.nova.server_get(IsA(http.HttpRequest),
|
||||
server.id).AndReturn(server)
|
||||
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False).\
|
||||
AndReturn([self.servers.list(), False])
|
||||
api.cinder.volume_snapshot_list(IsA(http.HttpRequest)). \
|
||||
AndReturn(vol_snaps)
|
||||
|
||||
api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)).\
|
||||
MultipleTimes().AndReturn(self.cinder_limits['absolute'])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html')
|
||||
|
||||
def test_index_no_volume_attachments(self):
|
||||
self.test_index(with_attachments=False)
|
||||
|
||||
@test.create_stubs({api.cinder: ('tenant_absolute_limits',
|
||||
'volume_list_paged',
|
||||
'volume_backup_supported',
|
||||
'volume_snapshot_list'),
|
||||
api.nova: ('server_list', 'server_get')})
|
||||
def _test_index_paginated(self, marker, sort_dir, volumes, url,
|
||||
has_more, has_prev):
|
||||
backup_supported = True
|
||||
vol_snaps = self.cinder_volume_snapshots.list()
|
||||
server = self.servers.first()
|
||||
|
||||
api.cinder.volume_backup_supported(IsA(http.HttpRequest)).\
|
||||
MultipleTimes().AndReturn(backup_supported)
|
||||
api.cinder.volume_list_paged(IsA(http.HttpRequest), marker=marker,
|
||||
sort_dir=sort_dir, search_opts=None,
|
||||
paginate=True).\
|
||||
AndReturn([volumes, has_more, has_prev])
|
||||
api.cinder.volume_snapshot_list(
|
||||
IsA(http.HttpRequest), search_opts=None).AndReturn(vol_snaps)
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False).\
|
||||
AndReturn([self.servers.list(), False])
|
||||
api.nova.server_get(IsA(http.HttpRequest),
|
||||
server.id).AndReturn(server)
|
||||
api.cinder.tenant_absolute_limits(IsA(http.HttpRequest)).MultipleTimes().\
|
||||
AndReturn(self.cinder_limits['absolute'])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(urlunquote(url))
|
||||
self.assertEqual(res.status_code, 200)
|
||||
self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html')
|
||||
|
||||
self.mox.UnsetStubs()
|
||||
return res
|
||||
|
||||
def ensure_attachments_exist(self, volumes):
|
||||
volumes = copy.copy(volumes)
|
||||
for volume in volumes:
|
||||
if not volume.attachments:
|
||||
volume.attachments.append({
|
||||
"id": "1", "server_id": '1', "device": "/dev/hda"})
|
||||
return volumes
|
||||
|
||||
@override_settings(API_RESULT_PAGE_SIZE=2)
|
||||
def test_index_paginated(self):
|
||||
mox_volumes = self.ensure_attachments_exist(self.cinder_volumes.list())
|
||||
size = settings.API_RESULT_PAGE_SIZE
|
||||
|
||||
# get first page
|
||||
expected_volumes = mox_volumes[:size]
|
||||
url = INDEX_URL
|
||||
res = self._test_index_paginated(marker=None, sort_dir="desc",
|
||||
volumes=expected_volumes, url=url,
|
||||
has_more=True, has_prev=False)
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, expected_volumes)
|
||||
|
||||
# get second page
|
||||
expected_volumes = mox_volumes[size:2 * size]
|
||||
marker = expected_volumes[0].id
|
||||
next = volume_tables.VolumesTable._meta.pagination_param
|
||||
url = "?".join([INDEX_URL, "=".join([next, marker])])
|
||||
res = self._test_index_paginated(marker=marker, sort_dir="desc",
|
||||
volumes=expected_volumes, url=url,
|
||||
has_more=True, has_prev=True)
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, expected_volumes)
|
||||
|
||||
# get last page
|
||||
expected_volumes = mox_volumes[-size:]
|
||||
marker = expected_volumes[0].id
|
||||
next = volume_tables.VolumesTable._meta.pagination_param
|
||||
url = "?".join([INDEX_URL, "=".join([next, marker])])
|
||||
res = self._test_index_paginated(marker=marker, sort_dir="desc",
|
||||
volumes=expected_volumes, url=url,
|
||||
has_more=False, has_prev=True)
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, expected_volumes)
|
||||
|
||||
@override_settings(API_RESULT_PAGE_SIZE=2)
|
||||
def test_index_paginated_prev_page(self):
|
||||
mox_volumes = self.ensure_attachments_exist(self.cinder_volumes.list())
|
||||
size = settings.API_RESULT_PAGE_SIZE
|
||||
|
||||
# prev from some page
|
||||
expected_volumes = mox_volumes[size:2 * size]
|
||||
marker = expected_volumes[0].id
|
||||
prev = volume_tables.VolumesTable._meta.prev_pagination_param
|
||||
url = "?".join([INDEX_URL, "=".join([prev, marker])])
|
||||
res = self._test_index_paginated(marker=marker, sort_dir="asc",
|
||||
volumes=expected_volumes, url=url,
|
||||
has_more=True, has_prev=True)
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, expected_volumes)
|
||||
|
||||
# back to first page
|
||||
expected_volumes = mox_volumes[:size]
|
||||
marker = expected_volumes[0].id
|
||||
prev = volume_tables.VolumesTable._meta.prev_pagination_param
|
||||
url = "?".join([INDEX_URL, "=".join([prev, marker])])
|
||||
res = self._test_index_paginated(marker=marker, sort_dir="asc",
|
||||
volumes=expected_volumes, url=url,
|
||||
has_more=True, has_prev=False)
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, expected_volumes)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
'volume_snapshot_list',
|
||||
'volume_type_list',
|
||||
@ -108,11 +266,11 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
|
||||
redirect_url = VOLUME_VOLUMES_TAB_URL
|
||||
redirect_url = INDEX_URL
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
@ -181,10 +339,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
redirect_url = VOLUME_VOLUMES_TAB_URL
|
||||
redirect_url = INDEX_URL
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
@ -251,10 +409,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
redirect_url = VOLUME_VOLUMES_TAB_URL
|
||||
redirect_url = INDEX_URL
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
@ -301,12 +459,12 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# get snapshot from url
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post("?".join([url,
|
||||
"snapshot_id=" + str(snapshot.id)]),
|
||||
formData)
|
||||
|
||||
redirect_url = VOLUME_VOLUMES_TAB_URL
|
||||
redirect_url = INDEX_URL
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
@ -375,8 +533,8 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
source_volid=volume.id).AndReturn(volume)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
redirect_url = VOLUME_VOLUMES_TAB_URL
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
redirect_url = INDEX_URL
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(info=1)
|
||||
@ -451,10 +609,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# get snapshot from dropdown list
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
redirect_url = VOLUME_VOLUMES_TAB_URL
|
||||
redirect_url = INDEX_URL
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_snapshot_get',
|
||||
@ -497,7 +655,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post("?".join([url,
|
||||
"snapshot_id=" + str(snapshot.id)]),
|
||||
formData, follow=True)
|
||||
@ -555,12 +713,12 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# get image from url
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post("?".join([url,
|
||||
"image_id=" + str(image.id)]),
|
||||
formData)
|
||||
|
||||
redirect_url = VOLUME_VOLUMES_TAB_URL
|
||||
redirect_url = INDEX_URL
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_create',
|
||||
@ -632,10 +790,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# get image from dropdown list
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
redirect_url = VOLUME_VOLUMES_TAB_URL
|
||||
redirect_url = INDEX_URL
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_type_list',
|
||||
@ -683,7 +841,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post("?".join([url,
|
||||
"image_id=" + str(image.id)]),
|
||||
formData, follow=True)
|
||||
@ -738,7 +896,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post("?".join([url,
|
||||
"image_id=" + str(image.id)]),
|
||||
formData, follow=True)
|
||||
@ -835,7 +993,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
expected_error = [u'A volume of 5000GiB cannot be created as you only'
|
||||
@ -914,7 +1072,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:create')
|
||||
url = reverse('horizon:project:volumes:create')
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
expected_error = [u'You are already using all of your available'
|
||||
@ -942,8 +1100,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
search_opts=None).\
|
||||
AndReturn([])
|
||||
cinder.volume_delete(IsA(http.HttpRequest), volume.id)
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False).\
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn([self.servers.list(), False])
|
||||
cinder.volume_list_paged(
|
||||
IsA(http.HttpRequest), marker=None, paginate=True, sort_dir='desc',
|
||||
@ -951,15 +1108,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest),
|
||||
search_opts=None).\
|
||||
AndReturn([])
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False).\
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn([self.servers.list(), False])
|
||||
cinder.tenant_absolute_limits(IsA(http.HttpRequest)).MultipleTimes().\
|
||||
AndReturn(self.cinder_limits['absolute'])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = VOLUME_INDEX_URL
|
||||
url = INDEX_URL
|
||||
res = self.client.post(url, formData, follow=True)
|
||||
self.assertIn("Scheduled deletion of Volume: Volume name",
|
||||
[m.message for m in res.context['messages']])
|
||||
@ -977,7 +1133,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = (VOLUME_INDEX_URL +
|
||||
url = (INDEX_URL +
|
||||
"?action=row_update&table=volumes&obj_id=" + volume.id)
|
||||
|
||||
res = self.client.get(url, {}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
@ -1005,7 +1161,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:attach',
|
||||
url = reverse('horizon:project:volumes:attach',
|
||||
args=[volume.id])
|
||||
res = self.client.get(url)
|
||||
msg = 'Volume %s on instance %s' % (volume.name, servers[0].name)
|
||||
@ -1038,7 +1194,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:attach',
|
||||
url = reverse('horizon:project:volumes:attach',
|
||||
args=[volume.id])
|
||||
res = self.client.get(url)
|
||||
form = res.context['form']
|
||||
@ -1057,7 +1213,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
api.nova.server_list(IsA(http.HttpRequest)).AndReturn([servers, False])
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:attach',
|
||||
url = reverse('horizon:project:volumes:attach',
|
||||
args=[volume.id])
|
||||
res = self.client.get(url)
|
||||
# Assert the device field is hidden.
|
||||
@ -1080,7 +1236,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:attach',
|
||||
url = reverse('horizon:project:volumes:attach',
|
||||
args=[volume.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
@ -1120,7 +1276,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
cinder.tenant_absolute_limits(IsA(http.HttpRequest)).AndReturn(limits)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res_url = (VOLUME_INDEX_URL +
|
||||
res_url = (INDEX_URL +
|
||||
"?action=row_update&table=volumes&obj_id=" + volume.id)
|
||||
|
||||
res = self.client.get(res_url, {},
|
||||
@ -1128,7 +1284,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
snapshot_action = self._get_volume_row_action_from_ajax(
|
||||
res, 'snapshots', volume.id)
|
||||
self.assertEqual('horizon:project:volumes:volumes:create_snapshot',
|
||||
self.assertEqual('horizon:project:volumes:create_snapshot',
|
||||
snapshot_action.url)
|
||||
self.assertEqual(set(['ajax-modal']), set(snapshot_action.classes))
|
||||
self.assertEqual('Create Snapshot',
|
||||
@ -1147,7 +1303,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
cinder.tenant_absolute_limits(IsA(http.HttpRequest)).AndReturn(limits)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res_url = (VOLUME_INDEX_URL +
|
||||
res_url = (INDEX_URL +
|
||||
"?action=row_update&table=volumes&obj_id=" + volume.id)
|
||||
|
||||
res = self.client.get(res_url, {},
|
||||
@ -1177,15 +1333,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest),
|
||||
search_opts=None).\
|
||||
AndReturn([])
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False)\
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None)\
|
||||
.AndReturn([self.servers.list(), False])
|
||||
cinder.tenant_absolute_limits(IsA(http.HttpRequest))\
|
||||
.MultipleTimes().AndReturn(limits)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(VOLUME_INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'project/volumes/index.html')
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html')
|
||||
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, self.cinder_volumes.list())
|
||||
@ -1196,7 +1351,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
set(create_action.classes))
|
||||
self.assertEqual('Create Volume',
|
||||
six.text_type(create_action.verbose_name))
|
||||
self.assertEqual('horizon:project:volumes:volumes:create',
|
||||
self.assertEqual('horizon:project:volumes:create',
|
||||
create_action.url)
|
||||
self.assertEqual((('volume', 'volume:create'),),
|
||||
create_action.policy_rules)
|
||||
@ -1219,15 +1374,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest),
|
||||
search_opts=None).\
|
||||
AndReturn([])
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False)\
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None)\
|
||||
.AndReturn([self.servers.list(), False])
|
||||
cinder.tenant_absolute_limits(IsA(http.HttpRequest))\
|
||||
.MultipleTimes().AndReturn(limits)
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(VOLUME_INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'project/volumes/index.html')
|
||||
res = self.client.get(INDEX_URL)
|
||||
self.assertTemplateUsed(res, 'horizon/common/_data_table_view.html')
|
||||
|
||||
volumes = res.context['volumes_table'].data
|
||||
self.assertItemsEqual(volumes, self.cinder_volumes.list())
|
||||
@ -1257,7 +1411,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:detail',
|
||||
url = reverse('horizon:project:volumes:detail',
|
||||
args=[volume.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
@ -1277,7 +1431,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:encryption_detail',
|
||||
url = reverse('horizon:project:volumes:encryption_detail',
|
||||
args=[volume.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
@ -1305,7 +1459,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:encryption_detail',
|
||||
url = reverse('horizon:project:volumes:encryption_detail',
|
||||
args=[volume.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
@ -1329,7 +1483,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = (VOLUME_INDEX_URL +
|
||||
url = (INDEX_URL +
|
||||
"?action=row_update&table=volumes&obj_id=" + volume.id)
|
||||
|
||||
res = self.client.get(url, {},
|
||||
@ -1350,11 +1504,11 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:detail',
|
||||
url = reverse('horizon:project:volumes:detail',
|
||||
args=[volume.id])
|
||||
res = self.client.get(url)
|
||||
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_update',
|
||||
'volume_set_bootable',
|
||||
@ -1378,10 +1532,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
'description': volume.description,
|
||||
'bootable': False}
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:update',
|
||||
url = reverse('horizon:project:volumes:update',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_update',
|
||||
'volume_set_bootable',
|
||||
@ -1405,10 +1559,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
'description': volume.description,
|
||||
'bootable': False}
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:update',
|
||||
url = reverse('horizon:project:volumes:update',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_update',
|
||||
'volume_set_bootable',
|
||||
@ -1432,10 +1586,10 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
'description': 'update bootable flag',
|
||||
'bootable': True}
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:update',
|
||||
url = reverse('horizon:project:volumes:update',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertRedirectsNoFollow(res, VOLUME_INDEX_URL)
|
||||
self.assertRedirectsNoFollow(res, INDEX_URL)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_upload_to_image',
|
||||
'volume_get')})
|
||||
@ -1468,14 +1622,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:upload_to_image',
|
||||
url = reverse('horizon:project:volumes:upload_to_image',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, form_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertMessageCount(info=1)
|
||||
|
||||
redirect_url = VOLUME_INDEX_URL
|
||||
redirect_url = INDEX_URL
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_get',
|
||||
@ -1501,11 +1655,11 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:extend',
|
||||
url = reverse('horizon:project:volumes:extend',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
|
||||
redirect_url = VOLUME_INDEX_URL
|
||||
redirect_url = INDEX_URL
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
@test.create_stubs({cinder: ('volume_get',),
|
||||
@ -1527,7 +1681,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:extend',
|
||||
url = reverse('horizon:project:volumes:extend',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertFormErrors(res, 1,
|
||||
@ -1546,7 +1700,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = (VOLUME_INDEX_URL +
|
||||
url = (INDEX_URL +
|
||||
"?action=row_update&table=volumes&obj_id=" + volume.id)
|
||||
|
||||
res = self.client.get(url, {}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
|
||||
@ -1582,13 +1736,13 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:retype',
|
||||
url = reverse('horizon:project:volumes:retype',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, form_data)
|
||||
|
||||
self.assertNoFormErrors(res)
|
||||
|
||||
redirect_url = VOLUME_INDEX_URL
|
||||
redirect_url = INDEX_URL
|
||||
self.assertRedirectsNoFollow(res, redirect_url)
|
||||
|
||||
def test_encryption_false(self):
|
||||
@ -1618,15 +1772,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest),
|
||||
search_opts=None).\
|
||||
AndReturn(self.cinder_volume_snapshots.list())
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False)\
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None)\
|
||||
.AndReturn([self.servers.list(), False])
|
||||
cinder.tenant_absolute_limits(IsA(http.HttpRequest))\
|
||||
.MultipleTimes('limits').AndReturn(limits)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(VOLUME_INDEX_URL)
|
||||
res = self.client.get(INDEX_URL)
|
||||
rows = res.context['volumes_table'].get_rows()
|
||||
|
||||
if encryption:
|
||||
@ -1656,7 +1809,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:extend',
|
||||
url = reverse('horizon:project:volumes:extend',
|
||||
args=[volume.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertFormError(res, "form", "new_size",
|
||||
@ -1680,15 +1833,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
cinder.volume_snapshot_list(IsA(http.HttpRequest),
|
||||
search_opts=None).\
|
||||
AndReturn([])
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False)\
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None)\
|
||||
.AndReturn([self.servers.list(), False])
|
||||
cinder.tenant_absolute_limits(IsA(http.HttpRequest))\
|
||||
.MultipleTimes().AndReturn(limits)
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
res = self.client.get(VOLUME_INDEX_URL)
|
||||
res = self.client.get(INDEX_URL)
|
||||
table = res.context['volumes_table']
|
||||
|
||||
# Verify that the create transfer action is present if and only if
|
||||
@ -1713,7 +1865,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
# Create a transfer for the first available volume
|
||||
url = reverse('horizon:project:volumes:volumes:create_transfer',
|
||||
url = reverse('horizon:project:volumes:create_transfer',
|
||||
args=[volToTransfer.id])
|
||||
res = self.client.post(url, formData)
|
||||
self.assertNoFormErrors(res)
|
||||
@ -1747,15 +1899,14 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
search_opts=None).\
|
||||
AndReturn([])
|
||||
cinder.transfer_delete(IsA(http.HttpRequest), transfer.id)
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None,
|
||||
detailed=False).\
|
||||
api.nova.server_list(IsA(http.HttpRequest), search_opts=None).\
|
||||
AndReturn([self.servers.list(), False])
|
||||
cinder.tenant_absolute_limits(IsA(http.HttpRequest)).MultipleTimes().\
|
||||
AndReturn(self.cinder_limits['absolute'])
|
||||
|
||||
self.mox.ReplayAll()
|
||||
|
||||
url = VOLUME_INDEX_URL
|
||||
url = INDEX_URL
|
||||
res = self.client.post(url, formData, follow=True)
|
||||
self.assertNoFormErrors(res)
|
||||
self.assertIn('Successfully deleted volume transfer "test transfer"',
|
||||
@ -1770,7 +1921,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
self.mox.ReplayAll()
|
||||
|
||||
formData = {'transfer_id': transfer.id, 'auth_key': transfer.auth_key}
|
||||
url = reverse('horizon:project:volumes:volumes:accept_transfer')
|
||||
url = reverse('horizon:project:volumes:accept_transfer')
|
||||
res = self.client.post(url, formData, follow=True)
|
||||
self.assertNoFormErrors(res)
|
||||
|
||||
@ -1786,7 +1937,7 @@ class VolumeViewTests(test.ResetImageAPIVersionMixin, test.TestCase):
|
||||
|
||||
filename = "{}.txt".format(slugify(transfer.id))
|
||||
|
||||
url = reverse('horizon:project:volumes:volumes:'
|
||||
url = reverse('horizon:project:volumes:'
|
||||
'download_transfer_creds',
|
||||
kwargs={'transfer_id': transfer.id,
|
||||
'auth_key': transfer.auth_key})
|
@ -12,18 +12,52 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from django.conf.urls import include
|
||||
from django.conf.urls import url
|
||||
|
||||
from openstack_dashboard.dashboards.project.backups \
|
||||
import views as backup_views
|
||||
from openstack_dashboard.dashboards.project.volumes import views
|
||||
from openstack_dashboard.dashboards.project.volumes.volumes \
|
||||
import urls as volume_urls
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.IndexView.as_view(), name='index'),
|
||||
url(r'^\?tab=volumes_and_snapshots__volumes_tab$',
|
||||
views.IndexView.as_view(), name='volumes_tab'),
|
||||
url(r'', include(
|
||||
volume_urls,
|
||||
namespace='volumes')),
|
||||
url(r'^$', views.VolumesView.as_view(), name='index'),
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<volume_id>[^/]+)/extend/$',
|
||||
views.ExtendView.as_view(),
|
||||
name='extend'),
|
||||
url(r'^(?P<volume_id>[^/]+)/attach/$',
|
||||
views.EditAttachmentsView.as_view(),
|
||||
name='attach'),
|
||||
url(r'^(?P<volume_id>[^/]+)/create_snapshot/$',
|
||||
views.CreateSnapshotView.as_view(),
|
||||
name='create_snapshot'),
|
||||
url(r'^(?P<volume_id>[^/]+)/create_transfer/$',
|
||||
views.CreateTransferView.as_view(),
|
||||
name='create_transfer'),
|
||||
url(r'^accept_transfer/$',
|
||||
views.AcceptTransferView.as_view(),
|
||||
name='accept_transfer'),
|
||||
url(r'^(?P<transfer_id>[^/]+)/auth/(?P<auth_key>[^/]+)/$',
|
||||
views.ShowTransferView.as_view(),
|
||||
name='show_transfer'),
|
||||
url(r'^(?P<volume_id>[^/]+)/create_backup/$',
|
||||
backup_views.CreateBackupView.as_view(),
|
||||
name='create_backup'),
|
||||
url(r'^(?P<volume_id>[^/]+)/$',
|
||||
views.DetailView.as_view(),
|
||||
name='detail'),
|
||||
url(r'^(?P<volume_id>[^/]+)/upload_to_image/$',
|
||||
views.UploadToImageView.as_view(),
|
||||
name='upload_to_image'),
|
||||
url(r'^(?P<volume_id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(),
|
||||
name='update'),
|
||||
url(r'^(?P<volume_id>[^/]+)/retype/$',
|
||||
views.RetypeView.as_view(),
|
||||
name='retype'),
|
||||
url(r'^(?P<volume_id>[^/]+)/encryption_detail/$',
|
||||
views.EncryptionDetailView.as_view(),
|
||||
name='encryption_detail'),
|
||||
url(r'^(?P<transfer_id>[^/]+)/download_creds/(?P<auth_key>[^/]+)$',
|
||||
views.DownloadTransferCreds.as_view(),
|
||||
name='download_transfer_creds'),
|
||||
]
|
||||
|
@ -12,15 +12,626 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Views for managing volumes.
|
||||
"""
|
||||
|
||||
from collections import OrderedDict
|
||||
import json
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django import http
|
||||
from django.template.defaultfilters import slugify # noqa
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils import encoding
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views import generic
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
from horizon import tabs
|
||||
from horizon.utils import memoized
|
||||
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard.api import nova
|
||||
from openstack_dashboard import exceptions as dashboard_exception
|
||||
from openstack_dashboard.usage import quotas
|
||||
from openstack_dashboard.utils import filters
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
import forms as volume_forms
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
import tables as volume_tables
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
import tabs as project_tabs
|
||||
|
||||
|
||||
class IndexView(tabs.TabbedTableView):
|
||||
tab_group_class = project_tabs.VolumeAndSnapshotTabs
|
||||
template_name = 'project/volumes/index.html'
|
||||
class VolumeTableMixIn(object):
|
||||
_has_more_data = False
|
||||
_has_prev_data = False
|
||||
|
||||
def _get_volumes(self, search_opts=None):
|
||||
try:
|
||||
marker, sort_dir = self._get_marker()
|
||||
volumes, self._has_more_data, self._has_prev_data = \
|
||||
cinder.volume_list_paged(self.request, marker=marker,
|
||||
search_opts=search_opts,
|
||||
sort_dir=sort_dir, paginate=True)
|
||||
|
||||
if sort_dir == "asc":
|
||||
volumes.reverse()
|
||||
|
||||
return volumes
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume list.'))
|
||||
return []
|
||||
|
||||
def _get_instances(self, search_opts=None, instance_ids=None):
|
||||
if not instance_ids:
|
||||
return []
|
||||
try:
|
||||
# TODO(tsufiev): we should pass attached_instance_ids to
|
||||
# nova.server_list as soon as Nova API allows for this
|
||||
instances, has_more = nova.server_list(self.request,
|
||||
search_opts=search_opts)
|
||||
return instances
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve volume/instance "
|
||||
"attachment information"))
|
||||
return []
|
||||
|
||||
def _get_volumes_ids_with_snapshots(self, search_opts=None):
|
||||
try:
|
||||
volume_ids = []
|
||||
snapshots = cinder.volume_snapshot_list(
|
||||
self.request, search_opts=search_opts)
|
||||
if snapshots:
|
||||
# extract out the volume ids
|
||||
volume_ids = set([(s.volume_id) for s in snapshots])
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve snapshot list."))
|
||||
|
||||
return volume_ids
|
||||
|
||||
def _get_attached_instance_ids(self, volumes):
|
||||
attached_instance_ids = []
|
||||
for volume in volumes:
|
||||
for att in volume.attachments:
|
||||
server_id = att.get('server_id', None)
|
||||
if server_id is not None:
|
||||
attached_instance_ids.append(server_id)
|
||||
return attached_instance_ids
|
||||
|
||||
# set attachment string and if volume has snapshots
|
||||
def _set_volume_attributes(self,
|
||||
volumes,
|
||||
instances,
|
||||
volume_ids_with_snapshots):
|
||||
instances = OrderedDict([(inst.id, inst) for inst in instances])
|
||||
for volume in volumes:
|
||||
if volume_ids_with_snapshots:
|
||||
if volume.id in volume_ids_with_snapshots:
|
||||
setattr(volume, 'has_snapshot', True)
|
||||
if instances:
|
||||
for att in volume.attachments:
|
||||
server_id = att.get('server_id', None)
|
||||
att['instance'] = instances.get(server_id, None)
|
||||
|
||||
|
||||
class VolumesView(tables.PagedTableMixin, VolumeTableMixIn,
|
||||
tables.DataTableView):
|
||||
table_class = volume_tables.VolumesTable
|
||||
page_title = _("Volumes")
|
||||
|
||||
def get_data(self):
|
||||
volumes = self._get_volumes()
|
||||
attached_instance_ids = self._get_attached_instance_ids(volumes)
|
||||
instances = self._get_instances(instance_ids=attached_instance_ids)
|
||||
volume_ids_with_snapshots = self._get_volumes_ids_with_snapshots()
|
||||
self._set_volume_attributes(
|
||||
volumes, instances, volume_ids_with_snapshots)
|
||||
return volumes
|
||||
|
||||
|
||||
class DetailView(tabs.TabView):
|
||||
tab_group_class = project_tabs.VolumeDetailTabs
|
||||
template_name = 'horizon/common/_detail.html'
|
||||
page_title = "{{ volume.name|default:volume.id }}"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
volume = self.get_data()
|
||||
table = volume_tables.VolumesTable(self.request)
|
||||
context["volume"] = volume
|
||||
context["url"] = self.get_redirect_url()
|
||||
context["actions"] = table.render_row_actions(volume)
|
||||
choices = volume_tables.VolumesTableBase.STATUS_DISPLAY_CHOICES
|
||||
volume.status_label = filters.get_display_label(choices, volume.status)
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
volume_id = self.kwargs['volume_id']
|
||||
volume = cinder.volume_get(self.request, volume_id)
|
||||
snapshots = cinder.volume_snapshot_list(
|
||||
self.request, search_opts={'volume_id': volume.id})
|
||||
if snapshots:
|
||||
setattr(volume, 'has_snapshot', True)
|
||||
for att in volume.attachments:
|
||||
att['instance'] = nova.server_get(self.request,
|
||||
att['server_id'])
|
||||
except Exception:
|
||||
redirect = self.get_redirect_url()
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume details.'),
|
||||
redirect=redirect)
|
||||
return volume
|
||||
|
||||
def get_redirect_url(self):
|
||||
return reverse('horizon:project:volumes:index')
|
||||
|
||||
def get_tabs(self, request, *args, **kwargs):
|
||||
volume = self.get_data()
|
||||
return self.tab_group_class(request, volume=volume, **kwargs)
|
||||
|
||||
|
||||
class CreateView(forms.ModalFormView):
|
||||
form_class = volume_forms.CreateForm
|
||||
template_name = 'project/volumes/create.html'
|
||||
submit_label = _("Create Volume")
|
||||
submit_url = reverse_lazy("horizon:project:volumes:create")
|
||||
success_url = reverse_lazy('horizon:project:volumes:index')
|
||||
page_title = _("Create Volume")
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(CreateView, self).get_initial()
|
||||
self.default_vol_type = None
|
||||
try:
|
||||
self.default_vol_type = cinder.volume_type_default(self.request)
|
||||
initial['type'] = self.default_vol_type.name
|
||||
except dashboard_exception.NOT_FOUND:
|
||||
pass
|
||||
return initial
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CreateView, self).get_context_data(**kwargs)
|
||||
try:
|
||||
context['usages'] = quotas.tenant_limit_usages(self.request)
|
||||
context['volume_types'] = self._get_volume_types()
|
||||
except Exception:
|
||||
exceptions.handle(self.request)
|
||||
return context
|
||||
|
||||
def _get_volume_types(self):
|
||||
volume_types = []
|
||||
try:
|
||||
volume_types = cinder.volume_type_list(self.request)
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume type list.'))
|
||||
|
||||
# check if we have default volume type so we can present the
|
||||
# description of no volume type differently
|
||||
no_type_description = None
|
||||
if self.default_vol_type is None:
|
||||
message = \
|
||||
_("If \"No volume type\" is selected, the volume will be "
|
||||
"created without a volume type.")
|
||||
|
||||
no_type_description = encoding.force_text(message)
|
||||
|
||||
type_descriptions = [{'name': '',
|
||||
'description': no_type_description}] + \
|
||||
[{'name': type.name,
|
||||
'description': getattr(type, "description", "")}
|
||||
for type in volume_types]
|
||||
|
||||
return json.dumps(type_descriptions)
|
||||
|
||||
|
||||
class ExtendView(forms.ModalFormView):
|
||||
form_class = volume_forms.ExtendForm
|
||||
template_name = 'project/volumes/extend.html'
|
||||
submit_label = _("Extend Volume")
|
||||
submit_url = "horizon:project:volumes:extend"
|
||||
success_url = reverse_lazy("horizon:project:volumes:index")
|
||||
page_title = _("Extend Volume")
|
||||
|
||||
def get_object(self):
|
||||
if not hasattr(self, "_object"):
|
||||
volume_id = self.kwargs['volume_id']
|
||||
try:
|
||||
self._object = cinder.volume_get(self.request, volume_id)
|
||||
except Exception:
|
||||
self._object = None
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume information.'))
|
||||
return self._object
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ExtendView, self).get_context_data(**kwargs)
|
||||
context['volume'] = self.get_object()
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
try:
|
||||
usages = quotas.tenant_limit_usages(self.request)
|
||||
usages['gigabytesUsed'] = (usages['gigabytesUsed']
|
||||
- context['volume'].size)
|
||||
context['usages'] = usages
|
||||
except Exception:
|
||||
exceptions.handle(self.request)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
volume = self.get_object()
|
||||
return {'id': self.kwargs['volume_id'],
|
||||
'name': volume.name,
|
||||
'orig_size': volume.size}
|
||||
|
||||
|
||||
class CreateSnapshotView(forms.ModalFormView):
|
||||
form_class = volume_forms.CreateSnapshotForm
|
||||
template_name = 'project/volumes/create_snapshot.html'
|
||||
submit_url = "horizon:project:volumes:create_snapshot"
|
||||
success_url = reverse_lazy('horizon:project:snapshots:index')
|
||||
page_title = _("Create Volume Snapshot")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CreateSnapshotView, self).get_context_data(**kwargs)
|
||||
context['volume_id'] = self.kwargs['volume_id']
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
try:
|
||||
volume = cinder.volume_get(self.request, context['volume_id'])
|
||||
if (volume.status == 'in-use'):
|
||||
context['attached'] = True
|
||||
context['form'].set_warning(_("This volume is currently "
|
||||
"attached to an instance. "
|
||||
"In some cases, creating a "
|
||||
"snapshot from an attached "
|
||||
"volume can result in a "
|
||||
"corrupted snapshot."))
|
||||
context['usages'] = quotas.tenant_limit_usages(self.request)
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume information.'))
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
return {'volume_id': self.kwargs["volume_id"]}
|
||||
|
||||
|
||||
class UploadToImageView(forms.ModalFormView):
|
||||
form_class = volume_forms.UploadToImageForm
|
||||
template_name = 'project/volumes/upload_to_image.html'
|
||||
submit_label = _("Upload")
|
||||
submit_url = "horizon:project:volumes:upload_to_image"
|
||||
success_url = reverse_lazy("horizon:project:volumes:index")
|
||||
page_title = _("Upload Volume to Image")
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
volume_id = self.kwargs['volume_id']
|
||||
volume = cinder.volume_get(self.request, volume_id)
|
||||
except Exception:
|
||||
error_message = _(
|
||||
'Unable to retrieve volume information for volume: "%s"') \
|
||||
% volume_id
|
||||
exceptions.handle(self.request,
|
||||
error_message,
|
||||
redirect=self.success_url)
|
||||
|
||||
return volume
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UploadToImageView, self).get_context_data(**kwargs)
|
||||
context['volume'] = self.get_data()
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
volume = self.get_data()
|
||||
|
||||
return {'id': self.kwargs['volume_id'],
|
||||
'name': volume.name,
|
||||
'status': volume.status}
|
||||
|
||||
|
||||
class CreateTransferView(forms.ModalFormView):
|
||||
form_class = volume_forms.CreateTransferForm
|
||||
template_name = 'project/volumes/create_transfer.html'
|
||||
success_url = reverse_lazy('horizon:project:volumes:index')
|
||||
modal_id = "create_volume_transfer_modal"
|
||||
submit_label = _("Create Volume Transfer")
|
||||
submit_url = "horizon:project:volumes:create_transfer"
|
||||
page_title = _("Create Volume Transfer")
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super(CreateTransferView, self).get_context_data(**kwargs)
|
||||
volume_id = self.kwargs['volume_id']
|
||||
context['volume_id'] = volume_id
|
||||
context['submit_url'] = reverse(self.submit_url, args=[volume_id])
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
return {'volume_id': self.kwargs["volume_id"]}
|
||||
|
||||
|
||||
class AcceptTransferView(forms.ModalFormView):
|
||||
form_class = volume_forms.AcceptTransferForm
|
||||
template_name = 'project/volumes/accept_transfer.html'
|
||||
success_url = reverse_lazy('horizon:project:volumes:index')
|
||||
modal_id = "accept_volume_transfer_modal"
|
||||
submit_label = _("Accept Volume Transfer")
|
||||
submit_url = reverse_lazy(
|
||||
"horizon:project:volumes:accept_transfer")
|
||||
page_title = _("Accept Volume Transfer")
|
||||
|
||||
|
||||
class ShowTransferView(forms.ModalFormView):
|
||||
form_class = volume_forms.ShowTransferForm
|
||||
template_name = 'project/volumes/show_transfer.html'
|
||||
success_url = reverse_lazy('horizon:project:volumes:index')
|
||||
modal_id = "show_volume_transfer_modal"
|
||||
submit_url = "horizon:project:volumes:show_transfer"
|
||||
cancel_label = _("Close")
|
||||
download_label = _("Download transfer credentials")
|
||||
page_title = _("Volume Transfer Details")
|
||||
|
||||
def get_object(self):
|
||||
try:
|
||||
return self._object
|
||||
except AttributeError:
|
||||
transfer_id = self.kwargs['transfer_id']
|
||||
try:
|
||||
self._object = cinder.transfer_get(self.request, transfer_id)
|
||||
return self._object
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume transfer.'))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ShowTransferView, self).get_context_data(**kwargs)
|
||||
context['transfer_id'] = self.kwargs['transfer_id']
|
||||
context['auth_key'] = self.kwargs['auth_key']
|
||||
context['submit_url'] = reverse(self.submit_url, args=[
|
||||
context['transfer_id'], context['auth_key']])
|
||||
context['download_label'] = self.download_label
|
||||
context['download_url'] = reverse(
|
||||
'horizon:project:volumes:download_transfer_creds',
|
||||
args=[context['transfer_id'], context['auth_key']]
|
||||
)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
transfer = self.get_object()
|
||||
return {'id': transfer.id,
|
||||
'name': transfer.name,
|
||||
'auth_key': self.kwargs['auth_key']}
|
||||
|
||||
|
||||
class UpdateView(forms.ModalFormView):
|
||||
form_class = volume_forms.UpdateForm
|
||||
modal_id = "update_volume_modal"
|
||||
template_name = 'project/volumes/update.html'
|
||||
submit_url = "horizon:project:volumes:update"
|
||||
success_url = reverse_lazy("horizon:project:volumes:index")
|
||||
page_title = _("Edit Volume")
|
||||
|
||||
def get_object(self):
|
||||
if not hasattr(self, "_object"):
|
||||
vol_id = self.kwargs['volume_id']
|
||||
try:
|
||||
self._object = cinder.volume_get(self.request, vol_id)
|
||||
except Exception:
|
||||
msg = _('Unable to retrieve volume.')
|
||||
url = reverse('horizon:project:volumes:index')
|
||||
exceptions.handle(self.request, msg, redirect=url)
|
||||
return self._object
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UpdateView, self).get_context_data(**kwargs)
|
||||
context['volume'] = self.get_object()
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
volume = self.get_object()
|
||||
return {'volume_id': self.kwargs["volume_id"],
|
||||
'name': volume.name,
|
||||
'description': volume.description,
|
||||
'bootable': volume.is_bootable}
|
||||
|
||||
|
||||
class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
|
||||
table_class = volume_tables.AttachmentsTable
|
||||
form_class = volume_forms.AttachForm
|
||||
form_id = "attach_volume_form"
|
||||
modal_id = "attach_volume_modal"
|
||||
template_name = 'project/volumes/attach.html'
|
||||
submit_url = "horizon:project:volumes:attach"
|
||||
success_url = reverse_lazy("horizon:project:volumes:index")
|
||||
page_title = _("Manage Volume Attachments")
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_object(self):
|
||||
volume_id = self.kwargs['volume_id']
|
||||
try:
|
||||
return cinder.volume_get(self.request, volume_id)
|
||||
except Exception:
|
||||
self._object = None
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume information.'))
|
||||
|
||||
def get_data(self):
|
||||
attachments = []
|
||||
volume = self.get_object()
|
||||
if volume is not None:
|
||||
for att in volume.attachments:
|
||||
att['volume_name'] = getattr(volume, 'name', att['device'])
|
||||
attachments.append(att)
|
||||
return attachments
|
||||
|
||||
def get_initial(self):
|
||||
try:
|
||||
instances, has_more = nova.server_list(self.request)
|
||||
except Exception:
|
||||
instances = []
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve attachment information."))
|
||||
return {'volume': self.get_object(),
|
||||
'instances': instances}
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_form(self, **kwargs):
|
||||
form_class = kwargs.get('form_class', self.get_form_class())
|
||||
return super(EditAttachmentsView, self).get_form(form_class)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EditAttachmentsView, self).get_context_data(**kwargs)
|
||||
context['form'] = self.get_form()
|
||||
volume = self.get_object()
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
if volume and volume.status == 'available':
|
||||
context['show_attach'] = True
|
||||
else:
|
||||
context['show_attach'] = False
|
||||
context['volume'] = volume
|
||||
if self.request.is_ajax():
|
||||
context['hide'] = True
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Table action handling
|
||||
handled = self.construct_tables()
|
||||
if handled:
|
||||
return handled
|
||||
return self.render_to_response(self.get_context_data(**kwargs))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetypeView(forms.ModalFormView):
|
||||
form_class = volume_forms.RetypeForm
|
||||
modal_id = "retype_volume_modal"
|
||||
template_name = 'project/volumes/retype.html'
|
||||
submit_label = _("Change Volume Type")
|
||||
submit_url = "horizon:project:volumes:retype"
|
||||
success_url = reverse_lazy("horizon:project:volumes:index")
|
||||
page_title = _("Change Volume Type")
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
volume_id = self.kwargs['volume_id']
|
||||
volume = cinder.volume_get(self.request, volume_id)
|
||||
except Exception:
|
||||
error_message = _(
|
||||
'Unable to retrieve volume information for volume: "%s"') \
|
||||
% volume_id
|
||||
exceptions.handle(self.request,
|
||||
error_message,
|
||||
redirect=self.success_url)
|
||||
|
||||
return volume
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RetypeView, self).get_context_data(**kwargs)
|
||||
context['volume'] = self.get_data()
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
volume = self.get_data()
|
||||
|
||||
return {'id': self.kwargs['volume_id'],
|
||||
'name': volume.name,
|
||||
'volume_type': volume.volume_type}
|
||||
|
||||
|
||||
class EncryptionDetailView(generic.TemplateView):
|
||||
template_name = 'project/volumes/encryption_detail.html'
|
||||
page_title = _("Volume Encryption Details: {{ volume.name }}")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EncryptionDetailView, self).get_context_data(**kwargs)
|
||||
volume = self.get_volume_data()
|
||||
context["encryption_metadata"] = self.get_encryption_data()
|
||||
context["volume"] = volume
|
||||
context["page_title"] = _("Volume Encryption Details: "
|
||||
"%(volume_name)s") % {'volume_name':
|
||||
volume.name}
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_encryption_data(self):
|
||||
try:
|
||||
volume_id = self.kwargs['volume_id']
|
||||
self._encryption_metadata = \
|
||||
cinder.volume_get_encryption_metadata(self.request,
|
||||
volume_id)
|
||||
except Exception:
|
||||
redirect = self.get_redirect_url()
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume encryption '
|
||||
'details.'),
|
||||
redirect=redirect)
|
||||
return self._encryption_metadata
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_volume_data(self):
|
||||
try:
|
||||
volume_id = self.kwargs['volume_id']
|
||||
volume = cinder.volume_get(self.request, volume_id)
|
||||
except Exception:
|
||||
redirect = self.get_redirect_url()
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume details.'),
|
||||
redirect=redirect)
|
||||
return volume
|
||||
|
||||
def get_redirect_url(self):
|
||||
return reverse('horizon:project:volumes:index')
|
||||
|
||||
|
||||
class DownloadTransferCreds(generic.View):
|
||||
# TODO(Itxaka): Remove cache_control in django >= 1.9
|
||||
# https://code.djangoproject.com/ticket/13008
|
||||
@method_decorator(cache_control(max_age=0, no_cache=True,
|
||||
no_store=True, must_revalidate=True))
|
||||
@method_decorator(never_cache)
|
||||
def get(self, request, transfer_id, auth_key):
|
||||
try:
|
||||
transfer = cinder.transfer_get(self.request, transfer_id)
|
||||
except Exception:
|
||||
transfer = None
|
||||
response = http.HttpResponse(content_type='application/text')
|
||||
response['Content-Disposition'] = \
|
||||
'attachment; filename=%s.txt' % slugify(transfer_id)
|
||||
response.write('%s: %s\n%s: %s\n%s: %s' % (
|
||||
_("Transfer name"),
|
||||
getattr(transfer, 'name', ''),
|
||||
_("Transfer ID"),
|
||||
transfer_id,
|
||||
_("Authorization Key"),
|
||||
auth_key))
|
||||
response['Content-Length'] = str(len(response.content))
|
||||
return response
|
||||
|
@ -1,31 +0,0 @@
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# 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 tabs
|
||||
|
||||
|
||||
class OverviewTab(tabs.Tab):
|
||||
name = _("Overview")
|
||||
slug = "overview"
|
||||
template_name = ("project/volumes/volumes/_detail_overview.html")
|
||||
|
||||
def get_context_data(self, request):
|
||||
return {"volume": self.tab_group.kwargs['volume']}
|
||||
|
||||
|
||||
class VolumeDetailTabs(tabs.TabGroup):
|
||||
slug = "volume_details"
|
||||
tabs = (OverviewTab,)
|
@ -1,64 +0,0 @@
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# 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.project.volumes \
|
||||
.volumes import views
|
||||
from openstack_dashboard.dashboards.project.backups \
|
||||
import views as backup_views
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^create/$', views.CreateView.as_view(), name='create'),
|
||||
url(r'^(?P<volume_id>[^/]+)/extend/$',
|
||||
views.ExtendView.as_view(),
|
||||
name='extend'),
|
||||
url(r'^(?P<volume_id>[^/]+)/attach/$',
|
||||
views.EditAttachmentsView.as_view(),
|
||||
name='attach'),
|
||||
url(r'^(?P<volume_id>[^/]+)/create_snapshot/$',
|
||||
views.CreateSnapshotView.as_view(),
|
||||
name='create_snapshot'),
|
||||
url(r'^(?P<volume_id>[^/]+)/create_transfer/$',
|
||||
views.CreateTransferView.as_view(),
|
||||
name='create_transfer'),
|
||||
url(r'^accept_transfer/$',
|
||||
views.AcceptTransferView.as_view(),
|
||||
name='accept_transfer'),
|
||||
url(r'^(?P<transfer_id>[^/]+)/auth/(?P<auth_key>[^/]+)/$',
|
||||
views.ShowTransferView.as_view(),
|
||||
name='show_transfer'),
|
||||
url(r'^(?P<volume_id>[^/]+)/create_backup/$',
|
||||
backup_views.CreateBackupView.as_view(),
|
||||
name='create_backup'),
|
||||
url(r'^(?P<volume_id>[^/]+)/$',
|
||||
views.DetailView.as_view(),
|
||||
name='detail'),
|
||||
url(r'^(?P<volume_id>[^/]+)/upload_to_image/$',
|
||||
views.UploadToImageView.as_view(),
|
||||
name='upload_to_image'),
|
||||
url(r'^(?P<volume_id>[^/]+)/update/$',
|
||||
views.UpdateView.as_view(),
|
||||
name='update'),
|
||||
url(r'^(?P<volume_id>[^/]+)/retype/$',
|
||||
views.RetypeView.as_view(),
|
||||
name='retype'),
|
||||
url(r'^(?P<volume_id>[^/]+)/encryption_detail/$',
|
||||
views.EncryptionDetailView.as_view(),
|
||||
name='encryption_detail'),
|
||||
url(r'^(?P<transfer_id>[^/]+)/download_creds/(?P<auth_key>[^/]+)$',
|
||||
views.DownloadTransferCreds.as_view(),
|
||||
name='download_transfer_creds'),
|
||||
]
|
@ -1,547 +0,0 @@
|
||||
# Copyright 2012 Nebula, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Views for managing volumes.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.urlresolvers import reverse_lazy
|
||||
from django import http
|
||||
from django.template.defaultfilters import slugify # noqa
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils import encoding
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views import generic
|
||||
|
||||
from horizon import exceptions
|
||||
from horizon import forms
|
||||
from horizon import tables
|
||||
from horizon import tabs
|
||||
from horizon.utils import memoized
|
||||
|
||||
from openstack_dashboard import api
|
||||
from openstack_dashboard.api import cinder
|
||||
from openstack_dashboard import exceptions as dashboard_exception
|
||||
from openstack_dashboard.usage import quotas
|
||||
from openstack_dashboard.utils import filters
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.volumes import forms as project_forms
|
||||
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.volumes import tables as project_tables
|
||||
from openstack_dashboard.dashboards.project.volumes \
|
||||
.volumes import tabs as project_tabs
|
||||
|
||||
|
||||
class DetailView(tabs.TabView):
|
||||
tab_group_class = project_tabs.VolumeDetailTabs
|
||||
template_name = 'horizon/common/_detail.html'
|
||||
page_title = "{{ volume.name|default:volume.id }}"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DetailView, self).get_context_data(**kwargs)
|
||||
volume = self.get_data()
|
||||
table = project_tables.VolumesTable(self.request)
|
||||
context["volume"] = volume
|
||||
context["url"] = self.get_redirect_url()
|
||||
context["actions"] = table.render_row_actions(volume)
|
||||
choices = project_tables.VolumesTableBase.STATUS_DISPLAY_CHOICES
|
||||
volume.status_label = filters.get_display_label(choices, volume.status)
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
volume_id = self.kwargs['volume_id']
|
||||
volume = cinder.volume_get(self.request, volume_id)
|
||||
snapshots = cinder.volume_snapshot_list(
|
||||
self.request, search_opts={'volume_id': volume.id})
|
||||
if snapshots:
|
||||
setattr(volume, 'has_snapshot', True)
|
||||
for att in volume.attachments:
|
||||
att['instance'] = api.nova.server_get(self.request,
|
||||
att['server_id'])
|
||||
except Exception:
|
||||
redirect = self.get_redirect_url()
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume details.'),
|
||||
redirect=redirect)
|
||||
return volume
|
||||
|
||||
def get_redirect_url(self):
|
||||
return reverse('horizon:project:volumes:index')
|
||||
|
||||
def get_tabs(self, request, *args, **kwargs):
|
||||
volume = self.get_data()
|
||||
return self.tab_group_class(request, volume=volume, **kwargs)
|
||||
|
||||
|
||||
class CreateView(forms.ModalFormView):
|
||||
form_class = project_forms.CreateForm
|
||||
template_name = 'project/volumes/volumes/create.html'
|
||||
submit_label = _("Create Volume")
|
||||
submit_url = reverse_lazy("horizon:project:volumes:volumes:create")
|
||||
success_url = reverse_lazy('horizon:project:volumes:volumes_tab')
|
||||
page_title = _("Create Volume")
|
||||
|
||||
def get_initial(self):
|
||||
initial = super(CreateView, self).get_initial()
|
||||
self.default_vol_type = None
|
||||
try:
|
||||
self.default_vol_type = cinder.volume_type_default(self.request)
|
||||
initial['type'] = self.default_vol_type.name
|
||||
except dashboard_exception.NOT_FOUND:
|
||||
pass
|
||||
return initial
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CreateView, self).get_context_data(**kwargs)
|
||||
try:
|
||||
context['usages'] = quotas.tenant_limit_usages(self.request)
|
||||
context['volume_types'] = self._get_volume_types()
|
||||
except Exception:
|
||||
exceptions.handle(self.request)
|
||||
return context
|
||||
|
||||
def _get_volume_types(self):
|
||||
volume_types = []
|
||||
try:
|
||||
volume_types = cinder.volume_type_list(self.request)
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume type list.'))
|
||||
|
||||
# check if we have default volume type so we can present the
|
||||
# description of no volume type differently
|
||||
no_type_description = None
|
||||
if self.default_vol_type is None:
|
||||
message = \
|
||||
_("If \"No volume type\" is selected, the volume will be "
|
||||
"created without a volume type.")
|
||||
|
||||
no_type_description = encoding.force_text(message)
|
||||
|
||||
type_descriptions = [{'name': '',
|
||||
'description': no_type_description}] + \
|
||||
[{'name': type.name,
|
||||
'description': getattr(type, "description", "")}
|
||||
for type in volume_types]
|
||||
|
||||
return json.dumps(type_descriptions)
|
||||
|
||||
|
||||
class ExtendView(forms.ModalFormView):
|
||||
form_class = project_forms.ExtendForm
|
||||
template_name = 'project/volumes/volumes/extend.html'
|
||||
submit_label = _("Extend Volume")
|
||||
submit_url = "horizon:project:volumes:volumes:extend"
|
||||
success_url = reverse_lazy("horizon:project:volumes:index")
|
||||
page_title = _("Extend Volume")
|
||||
|
||||
def get_object(self):
|
||||
if not hasattr(self, "_object"):
|
||||
volume_id = self.kwargs['volume_id']
|
||||
try:
|
||||
self._object = cinder.volume_get(self.request, volume_id)
|
||||
except Exception:
|
||||
self._object = None
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume information.'))
|
||||
return self._object
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ExtendView, self).get_context_data(**kwargs)
|
||||
context['volume'] = self.get_object()
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
try:
|
||||
usages = quotas.tenant_limit_usages(self.request)
|
||||
usages['gigabytesUsed'] = (usages['gigabytesUsed']
|
||||
- context['volume'].size)
|
||||
context['usages'] = usages
|
||||
except Exception:
|
||||
exceptions.handle(self.request)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
volume = self.get_object()
|
||||
return {'id': self.kwargs['volume_id'],
|
||||
'name': volume.name,
|
||||
'orig_size': volume.size}
|
||||
|
||||
|
||||
class CreateSnapshotView(forms.ModalFormView):
|
||||
form_class = project_forms.CreateSnapshotForm
|
||||
template_name = 'project/volumes/volumes/create_snapshot.html'
|
||||
submit_url = "horizon:project:volumes:volumes:create_snapshot"
|
||||
success_url = reverse_lazy('horizon:project:snapshots:index')
|
||||
page_title = _("Create Volume Snapshot")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(CreateSnapshotView, self).get_context_data(**kwargs)
|
||||
context['volume_id'] = self.kwargs['volume_id']
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
try:
|
||||
volume = cinder.volume_get(self.request, context['volume_id'])
|
||||
if (volume.status == 'in-use'):
|
||||
context['attached'] = True
|
||||
context['form'].set_warning(_("This volume is currently "
|
||||
"attached to an instance. "
|
||||
"In some cases, creating a "
|
||||
"snapshot from an attached "
|
||||
"volume can result in a "
|
||||
"corrupted snapshot."))
|
||||
context['usages'] = quotas.tenant_limit_usages(self.request)
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume information.'))
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
return {'volume_id': self.kwargs["volume_id"]}
|
||||
|
||||
|
||||
class UploadToImageView(forms.ModalFormView):
|
||||
form_class = project_forms.UploadToImageForm
|
||||
template_name = 'project/volumes/volumes/upload_to_image.html'
|
||||
submit_label = _("Upload")
|
||||
submit_url = "horizon:project:volumes:volumes:upload_to_image"
|
||||
success_url = reverse_lazy("horizon:project:volumes:index")
|
||||
page_title = _("Upload Volume to Image")
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
volume_id = self.kwargs['volume_id']
|
||||
volume = cinder.volume_get(self.request, volume_id)
|
||||
except Exception:
|
||||
error_message = _(
|
||||
'Unable to retrieve volume information for volume: "%s"') \
|
||||
% volume_id
|
||||
exceptions.handle(self.request,
|
||||
error_message,
|
||||
redirect=self.success_url)
|
||||
|
||||
return volume
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UploadToImageView, self).get_context_data(**kwargs)
|
||||
context['volume'] = self.get_data()
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
volume = self.get_data()
|
||||
|
||||
return {'id': self.kwargs['volume_id'],
|
||||
'name': volume.name,
|
||||
'status': volume.status}
|
||||
|
||||
|
||||
class CreateTransferView(forms.ModalFormView):
|
||||
form_class = project_forms.CreateTransferForm
|
||||
template_name = 'project/volumes/volumes/create_transfer.html'
|
||||
success_url = reverse_lazy('horizon:project:volumes:volumes_tab')
|
||||
modal_id = "create_volume_transfer_modal"
|
||||
submit_label = _("Create Volume Transfer")
|
||||
submit_url = "horizon:project:volumes:volumes:create_transfer"
|
||||
page_title = _("Create Volume Transfer")
|
||||
|
||||
def get_context_data(self, *args, **kwargs):
|
||||
context = super(CreateTransferView, self).get_context_data(**kwargs)
|
||||
volume_id = self.kwargs['volume_id']
|
||||
context['volume_id'] = volume_id
|
||||
context['submit_url'] = reverse(self.submit_url, args=[volume_id])
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
return {'volume_id': self.kwargs["volume_id"]}
|
||||
|
||||
|
||||
class AcceptTransferView(forms.ModalFormView):
|
||||
form_class = project_forms.AcceptTransferForm
|
||||
template_name = 'project/volumes/volumes/accept_transfer.html'
|
||||
success_url = reverse_lazy('horizon:project:volumes:volumes_tab')
|
||||
modal_id = "accept_volume_transfer_modal"
|
||||
submit_label = _("Accept Volume Transfer")
|
||||
submit_url = reverse_lazy(
|
||||
"horizon:project:volumes:volumes:accept_transfer")
|
||||
page_title = _("Accept Volume Transfer")
|
||||
|
||||
|
||||
class ShowTransferView(forms.ModalFormView):
|
||||
form_class = project_forms.ShowTransferForm
|
||||
template_name = 'project/volumes/volumes/show_transfer.html'
|
||||
success_url = reverse_lazy('horizon:project:volumes:volumes_tab')
|
||||
modal_id = "show_volume_transfer_modal"
|
||||
submit_url = "horizon:project:volumes:volumes:show_transfer"
|
||||
cancel_label = _("Close")
|
||||
download_label = _("Download transfer credentials")
|
||||
page_title = _("Volume Transfer Details")
|
||||
|
||||
def get_object(self):
|
||||
try:
|
||||
return self._object
|
||||
except AttributeError:
|
||||
transfer_id = self.kwargs['transfer_id']
|
||||
try:
|
||||
self._object = cinder.transfer_get(self.request, transfer_id)
|
||||
return self._object
|
||||
except Exception:
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume transfer.'))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(ShowTransferView, self).get_context_data(**kwargs)
|
||||
context['transfer_id'] = self.kwargs['transfer_id']
|
||||
context['auth_key'] = self.kwargs['auth_key']
|
||||
context['submit_url'] = reverse(self.submit_url, args=[
|
||||
context['transfer_id'], context['auth_key']])
|
||||
context['download_label'] = self.download_label
|
||||
context['download_url'] = reverse(
|
||||
'horizon:project:volumes:volumes:download_transfer_creds',
|
||||
args=[context['transfer_id'], context['auth_key']]
|
||||
)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
transfer = self.get_object()
|
||||
return {'id': transfer.id,
|
||||
'name': transfer.name,
|
||||
'auth_key': self.kwargs['auth_key']}
|
||||
|
||||
|
||||
class UpdateView(forms.ModalFormView):
|
||||
form_class = project_forms.UpdateForm
|
||||
modal_id = "update_volume_modal"
|
||||
template_name = 'project/volumes/volumes/update.html'
|
||||
submit_url = "horizon:project:volumes:volumes:update"
|
||||
success_url = reverse_lazy("horizon:project:volumes:index")
|
||||
page_title = _("Edit Volume")
|
||||
|
||||
def get_object(self):
|
||||
if not hasattr(self, "_object"):
|
||||
vol_id = self.kwargs['volume_id']
|
||||
try:
|
||||
self._object = cinder.volume_get(self.request, vol_id)
|
||||
except Exception:
|
||||
msg = _('Unable to retrieve volume.')
|
||||
url = reverse('horizon:project:volumes:index')
|
||||
exceptions.handle(self.request, msg, redirect=url)
|
||||
return self._object
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(UpdateView, self).get_context_data(**kwargs)
|
||||
context['volume'] = self.get_object()
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
volume = self.get_object()
|
||||
return {'volume_id': self.kwargs["volume_id"],
|
||||
'name': volume.name,
|
||||
'description': volume.description,
|
||||
'bootable': volume.is_bootable}
|
||||
|
||||
|
||||
class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
|
||||
table_class = project_tables.AttachmentsTable
|
||||
form_class = project_forms.AttachForm
|
||||
form_id = "attach_volume_form"
|
||||
modal_id = "attach_volume_modal"
|
||||
template_name = 'project/volumes/volumes/attach.html'
|
||||
submit_url = "horizon:project:volumes:volumes:attach"
|
||||
success_url = reverse_lazy("horizon:project:volumes:index")
|
||||
page_title = _("Manage Volume Attachments")
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_object(self):
|
||||
volume_id = self.kwargs['volume_id']
|
||||
try:
|
||||
return cinder.volume_get(self.request, volume_id)
|
||||
except Exception:
|
||||
self._object = None
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume information.'))
|
||||
|
||||
def get_data(self):
|
||||
attachments = []
|
||||
volume = self.get_object()
|
||||
if volume is not None:
|
||||
for att in volume.attachments:
|
||||
att['volume_name'] = getattr(volume, 'name', att['device'])
|
||||
attachments.append(att)
|
||||
return attachments
|
||||
|
||||
def get_initial(self):
|
||||
try:
|
||||
instances, has_more = api.nova.server_list(self.request)
|
||||
except Exception:
|
||||
instances = []
|
||||
exceptions.handle(self.request,
|
||||
_("Unable to retrieve attachment information."))
|
||||
return {'volume': self.get_object(),
|
||||
'instances': instances}
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_form(self, **kwargs):
|
||||
form_class = kwargs.get('form_class', self.get_form_class())
|
||||
return super(EditAttachmentsView, self).get_form(form_class)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EditAttachmentsView, self).get_context_data(**kwargs)
|
||||
context['form'] = self.get_form()
|
||||
volume = self.get_object()
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
if volume and volume.status == 'available':
|
||||
context['show_attach'] = True
|
||||
else:
|
||||
context['show_attach'] = False
|
||||
context['volume'] = volume
|
||||
if self.request.is_ajax():
|
||||
context['hide'] = True
|
||||
return context
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
# Table action handling
|
||||
handled = self.construct_tables()
|
||||
if handled:
|
||||
return handled
|
||||
return self.render_to_response(self.get_context_data(**kwargs))
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RetypeView(forms.ModalFormView):
|
||||
form_class = project_forms.RetypeForm
|
||||
modal_id = "retype_volume_modal"
|
||||
template_name = 'project/volumes/volumes/retype.html'
|
||||
submit_label = _("Change Volume Type")
|
||||
submit_url = "horizon:project:volumes:volumes:retype"
|
||||
success_url = reverse_lazy("horizon:project:volumes:index")
|
||||
page_title = _("Change Volume Type")
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_data(self):
|
||||
try:
|
||||
volume_id = self.kwargs['volume_id']
|
||||
volume = cinder.volume_get(self.request, volume_id)
|
||||
except Exception:
|
||||
error_message = _(
|
||||
'Unable to retrieve volume information for volume: "%s"') \
|
||||
% volume_id
|
||||
exceptions.handle(self.request,
|
||||
error_message,
|
||||
redirect=self.success_url)
|
||||
|
||||
return volume
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(RetypeView, self).get_context_data(**kwargs)
|
||||
context['volume'] = self.get_data()
|
||||
args = (self.kwargs['volume_id'],)
|
||||
context['submit_url'] = reverse(self.submit_url, args=args)
|
||||
return context
|
||||
|
||||
def get_initial(self):
|
||||
volume = self.get_data()
|
||||
|
||||
return {'id': self.kwargs['volume_id'],
|
||||
'name': volume.name,
|
||||
'volume_type': volume.volume_type}
|
||||
|
||||
|
||||
class EncryptionDetailView(generic.TemplateView):
|
||||
template_name = 'project/volumes/volumes/encryption_detail.html'
|
||||
page_title = _("Volume Encryption Details: {{ volume.name }}")
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(EncryptionDetailView, self).get_context_data(**kwargs)
|
||||
volume = self.get_volume_data()
|
||||
context["encryption_metadata"] = self.get_encryption_data()
|
||||
context["volume"] = volume
|
||||
context["page_title"] = _("Volume Encryption Details: "
|
||||
"%(volume_name)s") % {'volume_name':
|
||||
volume.name}
|
||||
return context
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_encryption_data(self):
|
||||
try:
|
||||
volume_id = self.kwargs['volume_id']
|
||||
self._encryption_metadata = \
|
||||
cinder.volume_get_encryption_metadata(self.request,
|
||||
volume_id)
|
||||
except Exception:
|
||||
redirect = self.get_redirect_url()
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume encryption '
|
||||
'details.'),
|
||||
redirect=redirect)
|
||||
return self._encryption_metadata
|
||||
|
||||
@memoized.memoized_method
|
||||
def get_volume_data(self):
|
||||
try:
|
||||
volume_id = self.kwargs['volume_id']
|
||||
volume = cinder.volume_get(self.request, volume_id)
|
||||
except Exception:
|
||||
redirect = self.get_redirect_url()
|
||||
exceptions.handle(self.request,
|
||||
_('Unable to retrieve volume details.'),
|
||||
redirect=redirect)
|
||||
return volume
|
||||
|
||||
def get_redirect_url(self):
|
||||
return reverse('horizon:project:volumes:index')
|
||||
|
||||
|
||||
class DownloadTransferCreds(generic.View):
|
||||
# TODO(Itxaka): Remove cache_control in django >= 1.9
|
||||
# https://code.djangoproject.com/ticket/13008
|
||||
@method_decorator(cache_control(max_age=0, no_cache=True,
|
||||
no_store=True, must_revalidate=True))
|
||||
@method_decorator(never_cache)
|
||||
def get(self, request, transfer_id, auth_key):
|
||||
try:
|
||||
transfer = cinder.transfer_get(self.request, transfer_id)
|
||||
except Exception:
|
||||
transfer = None
|
||||
response = http.HttpResponse(content_type='application/text')
|
||||
response['Content-Disposition'] = \
|
||||
'attachment; filename=%s.txt' % slugify(transfer_id)
|
||||
response.write('%s: %s\n%s: %s\n%s: %s' % (
|
||||
_("Transfer name"),
|
||||
getattr(transfer, 'name', ''),
|
||||
_("Transfer ID"),
|
||||
transfer_id,
|
||||
_("Authorization Key"),
|
||||
auth_key))
|
||||
response['Content-Length'] = str(len(response.content))
|
||||
return response
|
@ -3,7 +3,7 @@ PANEL = 'volumes'
|
||||
# The slug of the dashboard the PANEL associated with. Required.
|
||||
PANEL_DASHBOARD = 'project'
|
||||
# The slug of the panel group the PANEL is associated with.
|
||||
PANEL_GROUP = 'compute'
|
||||
PANEL_GROUP = 'volumes'
|
||||
|
||||
# Python panel class of the PANEL to be added.
|
||||
ADD_PANEL = 'openstack_dashboard.dashboards.project.volumes.panel.Volumes'
|
Loading…
Reference in New Issue
Block a user