Refactor Project Volumes stand-alone panel

Note that there was state leaking from one of the existing
tests that was relied upon in other tests and moving that test
in the run order caused the others to fail. All related tests
have been altered to not leak state.

Change-Id: I972bc5650fa77044de8a027f570cf2cb41febef8
Implements: blueprint reorganise-volumes
This commit is contained in:
Richard Jones 2017-02-08 16:57:47 +11:00
parent 0b340eccc0
commit 46ad19dbf0
57 changed files with 988 additions and 1093 deletions

View File

@ -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

View File

@ -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")

View File

@ -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])

View File

@ -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

View File

@ -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):

View File

@ -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):

View File

@ -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

View File

@ -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"

View File

@ -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>

View File

@ -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)

View File

@ -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")

View File

@ -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"),)

View File

@ -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)

View File

@ -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"

View File

@ -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)

View File

@ -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)

View File

@ -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": {

View File

@ -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:

View File

@ -18,7 +18,6 @@ import horizon
class Volumes(horizon.Panel):
name = _("Volumes")
slug = 'volumes'
permissions = (

View File

@ -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",

View File

@ -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,)

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -1,4 +1,4 @@
{% extends "project/volumes/volumes/_limits.html" %}
{% extends "project/volumes/_limits.html" %}
{% load i18n horizon humanize %}
{% block title %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -3,5 +3,5 @@
{% block title %}{% trans "Create Volume" %}{% endblock %}
{% block main %}
{% include 'project/volumes/volumes/_create.html' %}
{% include 'project/volumes/_create.html' %}
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -3,5 +3,5 @@
{% block title %}{% trans "Extend Volume" %}{% endblock %}
{% block main %}
{% include 'project/volumes/volumes/_extend.html' %}
{% include 'project/volumes/_extend.html' %}
{% endblock %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -3,5 +3,5 @@
{% block title %}{% trans "Edit Volume" %}{% endblock %}
{% block main %}
{% include 'project/volumes/volumes/_update.html' %}
{% include 'project/volumes/_update.html' %}
{% endblock %}

View File

@ -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 %}

View File

@ -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)

View File

@ -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})

View File

@ -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'),
]

View File

@ -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

View File

@ -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,)

View File

@ -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'),
]

View File

@ -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

View File

@ -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'