Merge "Refactor Project Volumes stand-alone panel"

This commit is contained in:
Jenkins 2017-03-09 12:51:08 +00:00 committed by Gerrit Code Review
commit 03a81c54d0
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'