Merge "Refactor Project Volumes stand-alone panel"
This commit is contained in:
commit
03a81c54d0
@ -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…
x
Reference in New Issue
Block a user