Add support for volume transfers
Cinder supports volume transfers, which are transfers of ownership from one project to another. The normal flow is that a donor creates a volume transfer, captures the resulting transfer id and secret authentication key, and passes that information out of band to the recipient (such as by email, text message, etc.). The recipient then accepts the transfer, supplying the transfer id and authentication key. The ownership of the volume is then transferred from the donor to the recipient, and the volome is no longer visible to the donor. It is important to know of some limitations about the cinder API for volume transfers and their impact on the UI design. 1. When creating a volume transfer, you cannot specify who the intended recipient will be, and anyone with the transfer ID and auth key can claim the volume. Therefore there is no UI to prompt for a recipient. 2. Current volume transfers are only visible to the donor; users in other projects are unable to view these transfers. This makes it pointless to create a project table to view and accept volume transfers, since the current transfers are not visible. Instead, the transfer information has been added to the volume details, which are visible by the donor, and the volume state clearly reflects that a transfer has been created. It it also impossible to present to the recipient a pull-down list of transfers to accept. 3. The only time that the authorization key is visible to the donor is in the response from the creation of the transfer; after creation it is impossible for even the donor to recover it. Since the donor must capture the transfer ID and authorization key in order to send it to the recipient, an extra form was created to present this information to the donor immediately after the transfer has been created. It is not enough to put the information in a success message, since these are typically only displayed for a small number of seconds before they disappear. Co-Authored-By: Gary W. Smith <gary.w.smith@hp.com> Co-Authored-By: Julie Gravel <julie.gravel@hp.com> Implements blueprint cinder-volume-transfer Change-Id: I0134f90c93a8d804d2c87fe9713bcef9e1629d79
This commit is contained in:
parent
3fb0836c31
commit
18ec30107c
@ -79,7 +79,7 @@ class Volume(BaseCinderAPIResourceWrapper):
|
|||||||
'volume_type', 'availability_zone', 'imageRef', 'bootable',
|
'volume_type', 'availability_zone', 'imageRef', 'bootable',
|
||||||
'snapshot_id', 'source_volid', 'attachments', 'tenant_name',
|
'snapshot_id', 'source_volid', 'attachments', 'tenant_name',
|
||||||
'os-vol-host-attr:host', 'os-vol-tenant-attr:tenant_id',
|
'os-vol-host-attr:host', 'os-vol-tenant-attr:tenant_id',
|
||||||
'metadata', 'volume_image_metadata', 'encrypted']
|
'metadata', 'volume_image_metadata', 'encrypted', 'transfer']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_bootable(self):
|
def is_bootable(self):
|
||||||
@ -129,6 +129,11 @@ class QosSpec(object):
|
|||||||
self.value = val
|
self.value = val
|
||||||
|
|
||||||
|
|
||||||
|
class VolumeTransfer(base.APIResourceWrapper):
|
||||||
|
|
||||||
|
_attrs = ['id', 'name', 'created_at', 'volume_id', 'auth_key']
|
||||||
|
|
||||||
|
|
||||||
@memoized
|
@memoized
|
||||||
def cinderclient(request):
|
def cinderclient(request):
|
||||||
api_version = VERSIONS.get_active_version()
|
api_version = VERSIONS.get_active_version()
|
||||||
@ -181,7 +186,17 @@ def volume_list(request, search_opts=None):
|
|||||||
c_client = cinderclient(request)
|
c_client = cinderclient(request)
|
||||||
if c_client is None:
|
if c_client is None:
|
||||||
return []
|
return []
|
||||||
return [Volume(v) for v in c_client.volumes.list(search_opts=search_opts)]
|
|
||||||
|
# build a dictionary of volume_id -> transfer
|
||||||
|
transfers = {t.volume_id: t
|
||||||
|
for t in transfer_list(request, search_opts=search_opts)}
|
||||||
|
|
||||||
|
volumes = []
|
||||||
|
for v in c_client.volumes.list(search_opts=search_opts):
|
||||||
|
v.transfer = transfers.get(v.id)
|
||||||
|
volumes.append(Volume(v))
|
||||||
|
|
||||||
|
return volumes
|
||||||
|
|
||||||
|
|
||||||
def volume_get(request, volume_id):
|
def volume_get(request, volume_id):
|
||||||
@ -196,6 +211,14 @@ def volume_get(request, volume_id):
|
|||||||
# the lack a server_id property; to work around that we'll
|
# the lack a server_id property; to work around that we'll
|
||||||
# give the attached instance a generic name.
|
# give the attached instance a generic name.
|
||||||
attachment['instance_name'] = _("Unknown instance")
|
attachment['instance_name'] = _("Unknown instance")
|
||||||
|
|
||||||
|
volume_data.transfer = None
|
||||||
|
if volume_data.status == 'awaiting-transfer':
|
||||||
|
for transfer in transfer_list(request):
|
||||||
|
if transfer.volume_id == volume_id:
|
||||||
|
volume_data.transfer = transfer
|
||||||
|
break
|
||||||
|
|
||||||
return Volume(volume_data)
|
return Volume(volume_data)
|
||||||
|
|
||||||
|
|
||||||
@ -521,3 +544,30 @@ def extension_supported(request, extension_name):
|
|||||||
if extension.name == extension_name:
|
if extension.name == extension_name:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_list(request, detailed=True, search_opts=None):
|
||||||
|
"""To see all volumes transfers as an admin pass in a special
|
||||||
|
search option: {'all_tenants': 1}
|
||||||
|
"""
|
||||||
|
c_client = cinderclient(request)
|
||||||
|
return [VolumeTransfer(v) for v in c_client.transfers.list(
|
||||||
|
detailed=detailed, search_opts=search_opts)]
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_get(request, transfer_id):
|
||||||
|
transfer_data = cinderclient(request).transfers.get(transfer_id)
|
||||||
|
return VolumeTransfer(transfer_data)
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_create(request, transfer_id, name):
|
||||||
|
volume = cinderclient(request).transfers.create(transfer_id, name)
|
||||||
|
return VolumeTransfer(volume)
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_accept(request, transfer_id, auth_key):
|
||||||
|
return cinderclient(request).transfers.accept(transfer_id, auth_key)
|
||||||
|
|
||||||
|
|
||||||
|
def transfer_delete(request, transfer_id):
|
||||||
|
return cinderclient(request).transfers.delete(transfer_id)
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-body-right %}
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans "Ownership of a volume can be transferred from one project to another. Accepting a transfer requires obtaining the Transfer ID and Authorization Key from the donor. This is equivalent to the <tt>cinder transfer-accept</tt> command." %}</p>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,7 @@
|
|||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-body-right %}
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans 'Ownership of a volume can be transferred from one project to another. Once a volume transfer is created in a donor project, it then can be "accepted" by a recipient project. This is equivalent to the <tt>cinder transfer-create</tt> command.' %}</p>
|
||||||
|
{% endblock %}
|
@ -26,7 +26,7 @@
|
|||||||
<dl>
|
<dl>
|
||||||
<dt>{% trans "Size" %}</dt>
|
<dt>{% trans "Size" %}</dt>
|
||||||
<dd>{{ volume.size }} {% trans "GB" %}</dd>
|
<dd>{{ volume.size }} {% trans "GB" %}</dd>
|
||||||
<dt>{% trans "Created" %}</dt>
|
<dt>{% trans "Created" context "Created time" %}</dt>
|
||||||
<dd>{{ volume.created_at|parse_date }}</dd>
|
<dd>{{ volume.created_at|parse_date }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
@ -78,3 +78,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if volume.transfer %}
|
||||||
|
<div class="status row detail">
|
||||||
|
<h4>{% trans "Volume Transfer" %}</h4>
|
||||||
|
<hr class="header_rule">
|
||||||
|
<dl>
|
||||||
|
<dt>{% trans "ID" %}</dt>
|
||||||
|
<dd>{{ volume.transfer.id }}</dd>
|
||||||
|
</dl>
|
||||||
|
<dl>
|
||||||
|
<dt>{% trans "Name" %}</dt>
|
||||||
|
<dd>{{ volume.transfer.name }}</dd>
|
||||||
|
</dl>
|
||||||
|
<dl>
|
||||||
|
<dt>{% trans "Created" context "Created time" %}</dt>
|
||||||
|
<dd>{{ volume.transfer.created_at|parse_date }}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
{% extends "horizon/common/_modal_form.html" %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block modal-body-right %}
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% trans "The Transfer ID and the Authorization Key are needed by the recipient in order to accept the transfer. Please capture both the Transfer ID and the Authorization Key and provide them to your transfer recipient." %}</p>
|
||||||
|
<p class="alert alert-warning">{% trans "The Authorization Key will not be available after closing this page, so you must capture it now, or else you will be unable to use the transfer." %}</p>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-footer %}
|
||||||
|
<a href="{% url 'horizon:project:volumes:volumes_tab' %}" class="btn btn-default secondary cancel close">{% trans "Close" %}</a>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Accept Volume Transfer" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Accept Volume Transfer") %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include 'project/volumes/volumes/_accept_transfer.html' %}
|
||||||
|
{% endblock %}
|
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Create Volume Transfer" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Create a Volume Transfer") %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include 'project/volumes/volumes/_create_transfer.html' %}
|
||||||
|
{% endblock %}
|
@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load i18n %}
|
||||||
|
{% block title %}{% trans "Volume Transfer Details" %}{% endblock %}
|
||||||
|
|
||||||
|
{% block page_header %}
|
||||||
|
{% include "horizon/common/_page_header.html" with title=_("Volume Transfer Details") %}
|
||||||
|
{% endblock page_header %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
{% include 'project/volumes/volumes/_show_transfer.html' %}
|
||||||
|
{% endblock %}
|
@ -20,6 +20,7 @@ Views for managing volumes.
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.forms import ValidationError # noqa
|
from django.forms import ValidationError # noqa
|
||||||
|
from django import http
|
||||||
from django.template.defaultfilters import filesizeformat # noqa
|
from django.template.defaultfilters import filesizeformat # noqa
|
||||||
from django.utils.translation import pgettext_lazy
|
from django.utils.translation import pgettext_lazy
|
||||||
from django.utils.translation import ugettext_lazy as _
|
from django.utils.translation import ugettext_lazy as _
|
||||||
@ -520,6 +521,65 @@ class CreateSnapshotForm(forms.SelfHandlingForm):
|
|||||||
redirect=redirect)
|
redirect=redirect)
|
||||||
|
|
||||||
|
|
||||||
|
class CreateTransferForm(forms.SelfHandlingForm):
|
||||||
|
name = forms.CharField(max_length=255, label=_("Transfer Name"),
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
try:
|
||||||
|
volume_id = self.initial['volume_id']
|
||||||
|
transfer = cinder.transfer_create(request, volume_id, data['name'])
|
||||||
|
|
||||||
|
if data['name']:
|
||||||
|
msg = _('Created volume transfer: "%s".') % data['name']
|
||||||
|
else:
|
||||||
|
msg = _('Created volume transfer.')
|
||||||
|
messages.success(request, msg)
|
||||||
|
response = http.HttpResponseRedirect(
|
||||||
|
reverse("horizon:project:volumes:volumes:show_transfer",
|
||||||
|
args=(transfer.id, transfer.auth_key)))
|
||||||
|
return response
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, _('Unable to create volume transfer.'))
|
||||||
|
|
||||||
|
|
||||||
|
class AcceptTransferForm(forms.SelfHandlingForm):
|
||||||
|
# These max lengths correspond to the sizes in cinder
|
||||||
|
transfer_id = forms.CharField(max_length=36, label=_("Transfer ID"))
|
||||||
|
auth_key = forms.CharField(max_length=16, label=_("Authorization Key"))
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
try:
|
||||||
|
transfer = cinder.transfer_accept(request,
|
||||||
|
data['transfer_id'],
|
||||||
|
data['auth_key'])
|
||||||
|
|
||||||
|
msg = (_('Successfully accepted volume transfer: "%s"')
|
||||||
|
% data['transfer_id'])
|
||||||
|
messages.success(request, msg)
|
||||||
|
return transfer
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, _('Unable to accept volume transfer.'))
|
||||||
|
|
||||||
|
|
||||||
|
class ShowTransferForm(forms.SelfHandlingForm):
|
||||||
|
name = forms.CharField(
|
||||||
|
label=_("Transfer Name"),
|
||||||
|
widget=forms.TextInput(attrs={'readonly': 'readonly'}),
|
||||||
|
required=False)
|
||||||
|
id = forms.CharField(
|
||||||
|
label=_("Transfer ID"),
|
||||||
|
widget=forms.TextInput(attrs={'readonly': 'readonly'}),
|
||||||
|
required=False)
|
||||||
|
auth_key = forms.CharField(
|
||||||
|
label=_("Authorization Key"),
|
||||||
|
widget=forms.TextInput(attrs={'readonly': 'readonly'}),
|
||||||
|
required=False)
|
||||||
|
|
||||||
|
def handle(self, request, data):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UpdateForm(forms.SelfHandlingForm):
|
class UpdateForm(forms.SelfHandlingForm):
|
||||||
name = forms.CharField(max_length=255, label=_("Volume Name"),
|
name = forms.CharField(max_length=255, label=_("Volume Name"),
|
||||||
required=False)
|
required=False)
|
||||||
|
@ -24,6 +24,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||||||
from django.utils.translation import ungettext_lazy
|
from django.utils.translation import ungettext_lazy
|
||||||
|
|
||||||
from horizon import exceptions
|
from horizon import exceptions
|
||||||
|
from horizon import messages
|
||||||
from horizon import tables
|
from horizon import tables
|
||||||
|
|
||||||
from openstack_dashboard import api
|
from openstack_dashboard import api
|
||||||
@ -186,6 +187,17 @@ class CreateSnapshot(VolumePolicyTargetMixin, tables.LinkAction):
|
|||||||
return volume.status in ("available", "in-use")
|
return volume.status in ("available", "in-use")
|
||||||
|
|
||||||
|
|
||||||
|
class CreateTransfer(VolumePolicyTargetMixin, tables.LinkAction):
|
||||||
|
name = "create_transfer"
|
||||||
|
verbose_name = _("Create Transfer")
|
||||||
|
url = "horizon:project:volumes:volumes:create_transfer"
|
||||||
|
classes = ("ajax-modal",)
|
||||||
|
policy_rules = (("volume", "volume:create_transfer"),)
|
||||||
|
|
||||||
|
def allowed(self, request, volume=None):
|
||||||
|
return volume.status == "available"
|
||||||
|
|
||||||
|
|
||||||
class CreateBackup(VolumePolicyTargetMixin, tables.LinkAction):
|
class CreateBackup(VolumePolicyTargetMixin, tables.LinkAction):
|
||||||
name = "backups"
|
name = "backups"
|
||||||
verbose_name = _("Create Backup")
|
verbose_name = _("Create Backup")
|
||||||
@ -238,6 +250,48 @@ class RetypeVolume(VolumePolicyTargetMixin, tables.LinkAction):
|
|||||||
return volume.status in ("available", "in-use")
|
return volume.status in ("available", "in-use")
|
||||||
|
|
||||||
|
|
||||||
|
class AcceptTransfer(tables.LinkAction):
|
||||||
|
name = "accept_transfer"
|
||||||
|
verbose_name = _("Accept Transfer")
|
||||||
|
url = "horizon:project:volumes:volumes:accept_transfer"
|
||||||
|
classes = ("ajax-modal",)
|
||||||
|
icon = "exchange"
|
||||||
|
policy_rules = (("volume", "volume:accept_transfer"),)
|
||||||
|
ajax = True
|
||||||
|
|
||||||
|
def single(self, table, request, object_id=None):
|
||||||
|
return HttpResponse(self.render())
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteTransfer(VolumePolicyTargetMixin, tables.Action):
|
||||||
|
# This class inherits from tables.Action instead of the more obvious
|
||||||
|
# tables.DeleteAction due to the confirmation message. When the delete
|
||||||
|
# is successful, DeleteAction automatically appends the name of the
|
||||||
|
# volume to the message, e.g. "Deleted volume transfer 'volume'". But
|
||||||
|
# we are deleting the volume *transfer*, whose name is different.
|
||||||
|
name = "delete_transfer"
|
||||||
|
verbose_name = _("Cancel Transfer")
|
||||||
|
policy_rules = (("volume", "volume:delete_transfer"),)
|
||||||
|
classes = ('btn-danger',)
|
||||||
|
|
||||||
|
def allowed(self, request, volume):
|
||||||
|
return (volume.status == "awaiting-transfer" and
|
||||||
|
getattr(volume, 'transfer', None))
|
||||||
|
|
||||||
|
def single(self, table, request, volume_id):
|
||||||
|
volume = table.get_object_by_id(volume_id)
|
||||||
|
try:
|
||||||
|
cinder.transfer_delete(request, volume.transfer.id)
|
||||||
|
if volume.transfer.name:
|
||||||
|
msg = _('Successfully deleted volume transfer "%s"'
|
||||||
|
) % volume.transfer.name
|
||||||
|
else:
|
||||||
|
msg = _("Successfully deleted volume transfer")
|
||||||
|
messages.success(request, msg)
|
||||||
|
except Exception:
|
||||||
|
exceptions.handle(request, _("Unable to delete volume transfer."))
|
||||||
|
|
||||||
|
|
||||||
class UpdateRow(tables.Row):
|
class UpdateRow(tables.Row):
|
||||||
ajax = True
|
ajax = True
|
||||||
|
|
||||||
@ -361,10 +415,12 @@ class VolumesTable(VolumesTableBase):
|
|||||||
verbose_name = _("Volumes")
|
verbose_name = _("Volumes")
|
||||||
status_columns = ["status"]
|
status_columns = ["status"]
|
||||||
row_class = UpdateRow
|
row_class = UpdateRow
|
||||||
table_actions = (CreateVolume, DeleteVolume, VolumesFilterAction)
|
table_actions = (CreateVolume, AcceptTransfer, DeleteVolume,
|
||||||
|
VolumesFilterAction)
|
||||||
row_actions = (EditVolume, ExtendVolume, LaunchVolume, EditAttachments,
|
row_actions = (EditVolume, ExtendVolume, LaunchVolume, EditAttachments,
|
||||||
CreateSnapshot, CreateBackup, RetypeVolume,
|
CreateSnapshot, CreateBackup, RetypeVolume,
|
||||||
UploadToImage, DeleteVolume)
|
UploadToImage, CreateTransfer, DeleteTransfer,
|
||||||
|
DeleteVolume)
|
||||||
|
|
||||||
|
|
||||||
class DetachVolume(tables.BatchAction):
|
class DetachVolume(tables.BatchAction):
|
||||||
|
@ -1033,7 +1033,8 @@ class VolumeViewTests(test.TestCase):
|
|||||||
self.assertContains(res, expected_string, html=True,
|
self.assertContains(res, expected_string, html=True,
|
||||||
msg_prefix="The create button is not disabled")
|
msg_prefix="The create button is not disabled")
|
||||||
|
|
||||||
@test.create_stubs({cinder: ('volume_get', 'tenant_absolute_limits'),
|
@test.create_stubs({cinder: ('tenant_absolute_limits',
|
||||||
|
'volume_get',),
|
||||||
api.nova: ('server_get',)})
|
api.nova: ('server_get',)})
|
||||||
def test_detail_view(self):
|
def test_detail_view(self):
|
||||||
volume = self.cinder_volumes.first()
|
volume = self.cinder_volumes.first()
|
||||||
@ -1398,3 +1399,100 @@ class VolumeViewTests(test.TestCase):
|
|||||||
self.assertFormError(res, "form", "new_size",
|
self.assertFormError(res, "form", "new_size",
|
||||||
"Volume cannot be extended to 1000GB as you only "
|
"Volume cannot be extended to 1000GB as you only "
|
||||||
"have 80GB of your quota available.")
|
"have 80GB of your quota available.")
|
||||||
|
|
||||||
|
@test.create_stubs({cinder: ('volume_backup_supported',
|
||||||
|
'volume_list',
|
||||||
|
'tenant_absolute_limits'),
|
||||||
|
api.nova: ('server_list',)})
|
||||||
|
def test_create_transfer_availability(self):
|
||||||
|
limits = self.cinder_limits['absolute']
|
||||||
|
|
||||||
|
cinder.volume_backup_supported(IsA(http.HttpRequest))\
|
||||||
|
.MultipleTimes().AndReturn(False)
|
||||||
|
cinder.volume_list(IsA(http.HttpRequest), search_opts=None)\
|
||||||
|
.AndReturn(self.volumes.list())
|
||||||
|
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)
|
||||||
|
table = res.context['volumes_table']
|
||||||
|
|
||||||
|
# Verify that the create transfer action is present if and only if
|
||||||
|
# the volume is available
|
||||||
|
for vol in table.data:
|
||||||
|
actions = [a.name for a in table.get_row_actions(vol)]
|
||||||
|
self.assertEqual('create_transfer' in actions,
|
||||||
|
vol.status == 'available')
|
||||||
|
|
||||||
|
@test.create_stubs({cinder: ('transfer_create',)})
|
||||||
|
def test_create_transfer(self):
|
||||||
|
volumes = self.volumes.list()
|
||||||
|
volToTransfer = [v for v in volumes if v.status == 'available'][0]
|
||||||
|
formData = {'volume_id': volToTransfer.id,
|
||||||
|
'name': u'any transfer name'}
|
||||||
|
|
||||||
|
cinder.transfer_create(IsA(http.HttpRequest),
|
||||||
|
formData['volume_id'],
|
||||||
|
formData['name']).AndReturn(
|
||||||
|
self.cinder_volume_transfers.first())
|
||||||
|
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
# Create a transfer for the first available volume
|
||||||
|
url = reverse('horizon:project:volumes:volumes:create_transfer',
|
||||||
|
args=[volToTransfer.id])
|
||||||
|
res = self.client.post(url, formData)
|
||||||
|
self.assertNoFormErrors(res)
|
||||||
|
|
||||||
|
@test.create_stubs({cinder: ('volume_backup_supported',
|
||||||
|
'volume_list',
|
||||||
|
'transfer_delete',
|
||||||
|
'tenant_absolute_limits'),
|
||||||
|
api.nova: ('server_list',)})
|
||||||
|
def test_delete_transfer(self):
|
||||||
|
transfer = self.cinder_volume_transfers.first()
|
||||||
|
volumes = []
|
||||||
|
# Attach the volume transfer to the relevant volume
|
||||||
|
for v in self.cinder_volumes.list():
|
||||||
|
if v.id == transfer.volume_id:
|
||||||
|
v.status = 'awaiting-transfer'
|
||||||
|
v.transfer = transfer
|
||||||
|
volumes.append(v)
|
||||||
|
|
||||||
|
formData = {'action':
|
||||||
|
'volumes__delete_transfer__%s' % transfer.volume_id}
|
||||||
|
|
||||||
|
cinder.volume_backup_supported(IsA(http.HttpRequest))\
|
||||||
|
.MultipleTimes().AndReturn(False)
|
||||||
|
cinder.volume_list(IsA(http.HttpRequest), search_opts=None)\
|
||||||
|
.AndReturn(volumes)
|
||||||
|
cinder.transfer_delete(IsA(http.HttpRequest), transfer.id)
|
||||||
|
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
|
||||||
|
res = self.client.post(url, formData, follow=True)
|
||||||
|
self.assertNoFormErrors(res)
|
||||||
|
self.assertIn('Successfully deleted volume transfer "test transfer"',
|
||||||
|
[m.message for m in res.context['messages']])
|
||||||
|
|
||||||
|
@test.create_stubs({cinder: ('transfer_accept',)})
|
||||||
|
def test_accept_transfer(self):
|
||||||
|
transfer = self.cinder_volume_transfers.first()
|
||||||
|
|
||||||
|
cinder.transfer_accept(IsA(http.HttpRequest), transfer.id,
|
||||||
|
transfer.auth_key)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
formData = {'transfer_id': transfer.id, 'auth_key': transfer.auth_key}
|
||||||
|
url = reverse('horizon:project:volumes:volumes:accept_transfer')
|
||||||
|
res = self.client.post(url, formData, follow=True)
|
||||||
|
self.assertNoFormErrors(res)
|
||||||
|
@ -35,6 +35,15 @@ urlpatterns = patterns(
|
|||||||
url(r'^(?P<volume_id>[^/]+)/create_snapshot/$',
|
url(r'^(?P<volume_id>[^/]+)/create_snapshot/$',
|
||||||
views.CreateSnapshotView.as_view(),
|
views.CreateSnapshotView.as_view(),
|
||||||
name='create_snapshot'),
|
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/$',
|
url(r'^(?P<volume_id>[^/]+)/create_backup/$',
|
||||||
backup_views.CreateBackupView.as_view(),
|
backup_views.CreateBackupView.as_view(),
|
||||||
name='create_backup'),
|
name='create_backup'),
|
||||||
|
@ -190,6 +190,72 @@ class UploadToImageView(forms.ModalFormView):
|
|||||||
'status': volume.status}
|
'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"
|
||||||
|
modal_header = _("Create Volume Transfer")
|
||||||
|
submit_label = _("Create Volume Transfer")
|
||||||
|
submit_url = "horizon:project:volumes:volumes:create_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"
|
||||||
|
modal_header = _("Accept Volume Transfer")
|
||||||
|
submit_label = _("Accept Volume Transfer")
|
||||||
|
submit_url = reverse_lazy(
|
||||||
|
"horizon:project:volumes:volumes:accept_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"
|
||||||
|
modal_header = _("Volume Transfer")
|
||||||
|
submit_url = "horizon:project:volumes:volumes:show_transfer"
|
||||||
|
|
||||||
|
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']])
|
||||||
|
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):
|
class UpdateView(forms.ModalFormView):
|
||||||
form_class = project_forms.UpdateForm
|
form_class = project_forms.UpdateForm
|
||||||
template_name = 'project/volumes/volumes/update.html'
|
template_name = 'project/volumes/volumes/update.html'
|
||||||
|
@ -22,12 +22,19 @@ from openstack_dashboard.test import helpers as test
|
|||||||
|
|
||||||
|
|
||||||
class CinderApiTests(test.APITestCase):
|
class CinderApiTests(test.APITestCase):
|
||||||
|
|
||||||
def test_volume_list(self):
|
def test_volume_list(self):
|
||||||
search_opts = {'all_tenants': 1}
|
search_opts = {'all_tenants': 1}
|
||||||
|
detailed = True
|
||||||
volumes = self.cinder_volumes.list()
|
volumes = self.cinder_volumes.list()
|
||||||
|
volume_transfers = self.cinder_volume_transfers.list()
|
||||||
cinderclient = self.stub_cinderclient()
|
cinderclient = self.stub_cinderclient()
|
||||||
cinderclient.volumes = self.mox.CreateMockAnything()
|
cinderclient.volumes = self.mox.CreateMockAnything()
|
||||||
cinderclient.volumes.list(search_opts=search_opts,).AndReturn(volumes)
|
cinderclient.volumes.list(search_opts=search_opts,).AndReturn(volumes)
|
||||||
|
cinderclient.transfers = self.mox.CreateMockAnything()
|
||||||
|
cinderclient.transfers.list(
|
||||||
|
detailed=detailed,
|
||||||
|
search_opts=search_opts,).AndReturn(volume_transfers)
|
||||||
self.mox.ReplayAll()
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
# No assertions are necessary. Verification is handled by mox.
|
# No assertions are necessary. Verification is handled by mox.
|
||||||
|
@ -19,6 +19,7 @@ from cinderclient.v2 import services
|
|||||||
from cinderclient.v2 import volume_backups as vol_backups
|
from cinderclient.v2 import volume_backups as vol_backups
|
||||||
from cinderclient.v2 import volume_encryption_types as vol_enc_types
|
from cinderclient.v2 import volume_encryption_types as vol_enc_types
|
||||||
from cinderclient.v2 import volume_snapshots as vol_snaps
|
from cinderclient.v2 import volume_snapshots as vol_snaps
|
||||||
|
from cinderclient.v2 import volume_transfers
|
||||||
from cinderclient.v2 import volume_types
|
from cinderclient.v2 import volume_types
|
||||||
from cinderclient.v2 import volumes
|
from cinderclient.v2 import volumes
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ def data(TEST):
|
|||||||
TEST.cinder_quotas = utils.TestDataContainer()
|
TEST.cinder_quotas = utils.TestDataContainer()
|
||||||
TEST.cinder_quota_usages = utils.TestDataContainer()
|
TEST.cinder_quota_usages = utils.TestDataContainer()
|
||||||
TEST.cinder_availability_zones = utils.TestDataContainer()
|
TEST.cinder_availability_zones = utils.TestDataContainer()
|
||||||
|
TEST.cinder_volume_transfers = utils.TestDataContainer()
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
service_1 = services.Service(services.ServiceManager(None), {
|
service_1 = services.Service(services.ServiceManager(None), {
|
||||||
@ -272,3 +274,13 @@ def data(TEST):
|
|||||||
TEST.cinder_qos_specs.add(qos_spec1, qos_spec2)
|
TEST.cinder_qos_specs.add(qos_spec1, qos_spec2)
|
||||||
vol_type1.associated_qos_spec = qos_spec1.name
|
vol_type1.associated_qos_spec = qos_spec1.name
|
||||||
TEST.cinder_qos_spec_associations.add(vol_type1)
|
TEST.cinder_qos_spec_associations.add(vol_type1)
|
||||||
|
|
||||||
|
# volume_transfers
|
||||||
|
transfer_1 = volume_transfers.VolumeTransfer(
|
||||||
|
volume_transfers.VolumeTransferManager(None), {
|
||||||
|
'id': '99999999-8888-7777-6666-555555555555',
|
||||||
|
'name': 'test transfer',
|
||||||
|
'volume_id': volume.id,
|
||||||
|
'auth_key': 'blah',
|
||||||
|
'created_at': ''})
|
||||||
|
TEST.cinder_volume_transfers.add(transfer_1)
|
||||||
|
Loading…
Reference in New Issue
Block a user