diff --git a/openstack_dashboard/api/cinder.py b/openstack_dashboard/api/cinder.py
index 55bfa745d3..660fde2fcc 100644
--- a/openstack_dashboard/api/cinder.py
+++ b/openstack_dashboard/api/cinder.py
@@ -93,6 +93,12 @@ class VolumeSnapshot(BaseCinderAPIResourceWrapper):
'os-extended-snapshot-attributes:project_id']
+class VolumeType(BaseCinderAPIResourceWrapper):
+
+ _attrs = ['id', 'name', 'extra_specs', 'created_at',
+ 'os-extended-snapshot-attributes:project_id']
+
+
class VolumeBackup(BaseCinderAPIResourceWrapper):
_attrs = ['id', 'name', 'description', 'container', 'size', 'status',
@@ -162,6 +168,11 @@ def _replace_v2_parameters(data):
return data
+def version_get():
+ api_version = VERSIONS.get_active_version()
+ return api_version['version']
+
+
def volume_list(request, search_opts=None):
"""To see all volumes in the cloud as an admin you can pass in a special
search option: {'all_tenants': 1}
@@ -212,6 +223,16 @@ def volume_delete(request, volume_id):
return cinderclient(request).volumes.delete(volume_id)
+def volume_retype(request, volume_id, new_type, migration_policy):
+
+ if not retype_supported():
+ raise exceptions.NotAvailable
+
+ return cinderclient(request).volumes.retype(volume_id,
+ new_type,
+ migration_policy)
+
+
def volume_update(request, volume_id, name, description):
vol_data = {'name': name,
'description': description}
@@ -399,3 +420,10 @@ def extension_supported(request, extension_name):
if extension.name == extension_name:
return True
return False
+
+
+@memoized
+def retype_supported():
+ """retype is only supported after cinder v2.
+ """
+ return version_get() >= 2
diff --git a/openstack_dashboard/conf/cinder_policy.json b/openstack_dashboard/conf/cinder_policy.json
index 7d1ef6317c..0f2b0af02b 100644
--- a/openstack_dashboard/conf/cinder_policy.json
+++ b/openstack_dashboard/conf/cinder_policy.json
@@ -19,6 +19,7 @@
"volume:update_snapshot": [["rule:default"]],
"volume:get_all_snapshots": [],
"volume:extend": [],
+ "volume:retype": [],
"volume_extension:types_manage": [["rule:admin_api"]],
"volume_extension:types_extra_specs": [["rule:admin_api"]],
diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_retype.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_retype.html
new file mode 100644
index 0000000000..80af3cd826
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/_retype.html
@@ -0,0 +1,41 @@
+{% extends "horizon/common/_modal_form.html" %}
+{% load i18n %}
+{% load url from future %}
+
+{% block form_id %}{% endblock %}
+{% block form_action %}{% url 'horizon:project:volumes:volumes:retype' volume.id %}{% endblock %}
+
+{% block modal_id %}retype_volume_modal{% endblock %}
+{% block modal-header %}{% trans "Change Volume Type" %}{% endblock %}
+
+{% block modal-body %}
+
+
+ {% include "horizon/common/_form_fields.html" %}
+
+
+
+
{% trans "Description:" %}
+
{% blocktrans %}
+ From here you can change the volume type of a volume after its creation.
+ This is equivalent to the cinder retype command.
+ {% endblocktrans %}
+
+
{% blocktrans %}
+ The "Volume Type" selected must be different from the current volume type.
+ {% endblocktrans %}
+
+
{% blocktrans %}
+ The "Migration Policy" is only used if the volume retype cannot be
+ completed. If the "Migration Policy" is "On Demand", the back end will
+ perform volume migration. Note that migration may take a significant
+ amount of time to complete, in some cases hours.
+ {% endblocktrans %}
+
+
+{% endblock %}
+
+{% block modal-footer %}
+
+ {% trans "Cancel" %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/retype.html b/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/retype.html
new file mode 100644
index 0000000000..829ab7496d
--- /dev/null
+++ b/openstack_dashboard/dashboards/project/volumes/templates/volumes/volumes/retype.html
@@ -0,0 +1,11 @@
+{% extends 'base.html' %}
+{% load i18n %}
+{% block title %}{% trans "Change Volume Type" %}{% endblock %}
+
+{% block page_header %}
+ {% include "horizon/common/_page_header.html" with title=_("Change Volume Type") %}
+{% endblock page_header %}
+
+{% block main %}
+ {% include 'project/volumes/volumes/_retype.html' %}
+{% endblock %}
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/forms.py b/openstack_dashboard/dashboards/project/volumes/volumes/forms.py
index 28f68d990d..1074d57132 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/forms.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/forms.py
@@ -543,3 +543,69 @@ class ExtendForm(forms.SelfHandlingForm):
exceptions.handle(request,
_('Unable to extend volume.'),
redirect=redirect)
+
+
+class RetypeForm(forms.SelfHandlingForm):
+ name = forms.CharField(label=_('Volume Name'),
+ widget=forms.TextInput(
+ attrs={'readonly': 'readonly'}))
+ volume_type = forms.ChoiceField(label=_('Type'),
+ required=True)
+ MIGRATION_POLICY_CHOICES = [('never', _('Never')),
+ ('on-demand', _('On Demand'))]
+ migration_policy = forms.ChoiceField(label=_('Migration Policy'),
+ widget=forms.Select(),
+ choices=(MIGRATION_POLICY_CHOICES),
+ initial='never',
+ required=False)
+
+ def __init__(self, request, *args, **kwargs):
+ super(RetypeForm, self).__init__(request, *args, **kwargs)
+
+ try:
+ volume_types = cinder.volume_type_list(request)
+ self.fields['volume_type'].choices = [(t.name, t.name)
+ for t in volume_types]
+ self.fields['volume_type'].initial = self.initial['volume_type']
+
+ except Exception:
+ redirect_url = reverse("horizon:project:volumes:index")
+ error_message = _('Unable to retrieve the volume type list.')
+ exceptions.handle(request, error_message, redirect=redirect_url)
+
+ def clean_volume_type(self):
+ cleaned_volume_type = self.cleaned_data['volume_type']
+ origin_type = self.initial['volume_type']
+
+ if cleaned_volume_type == origin_type:
+ error_message = _(
+ 'New volume type must be different from '
+ 'the original volume type "%s".') % cleaned_volume_type
+ raise ValidationError(error_message)
+
+ return cleaned_volume_type
+
+ def handle(self, request, data):
+ volume_id = self.initial['id']
+
+ try:
+ cinder.volume_retype(request,
+ volume_id,
+ data['volume_type'],
+ data['migration_policy'])
+
+ message = _(
+ 'Successfully sent the request to change the volume '
+ 'type to "%(vtype)s" for volume: "%(name)s"')
+ params = {'name': data['name'],
+ 'vtype': data['volume_type']}
+ messages.info(request, message % params)
+
+ return True
+ except Exception:
+ error_message = _(
+ 'Unable to change the volume type for volume: "%s"') \
+ % data['name']
+ exceptions.handle(request, error_message)
+
+ return False
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py b/openstack_dashboard/dashboards/project/volumes/volumes/tables.py
index 9a9199baea..dbcb41a09f 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/tables.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/tables.py
@@ -216,6 +216,27 @@ class EditVolume(tables.LinkAction):
return volume.status in ("available", "in-use")
+class RetypeVolume(tables.LinkAction):
+ name = "retype"
+ verbose_name = _("Change Volume Type")
+ url = "horizon:project:volumes:volumes:retype"
+ classes = ("ajax-modal",)
+ icon = "pencil"
+ policy_rules = (("volume", "volume:retype"),)
+
+ def get_policy_target(self, request, datum=None):
+ project_id = None
+ if datum:
+ project_id = getattr(datum, "os-vol-tenant-attr:tenant_id", None)
+
+ return {"project_id": project_id}
+
+ def allowed(self, request, volume=None):
+ retype_supported = cinder.retype_supported()
+
+ return volume.status in ("available", "in-use") and retype_supported
+
+
class UpdateRow(tables.Row):
ajax = True
@@ -342,7 +363,8 @@ class VolumesTable(VolumesTableBase):
row_class = UpdateRow
table_actions = (CreateVolume, DeleteVolume, VolumesFilterAction)
row_actions = (EditVolume, ExtendVolume, LaunchVolume, EditAttachments,
- CreateSnapshot, CreateBackup, DeleteVolume)
+ CreateSnapshot, CreateBackup, RetypeVolume,
+ DeleteVolume)
class DetachVolume(tables.BatchAction):
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
index 2a1f9ecd06..d344d45ebf 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/tests.py
@@ -1030,6 +1030,113 @@ class VolumeViewTests(test.TestCase):
"New size must be greater than "
"current size.")
+ @test.create_stubs({cinder: ('volume_get',
+ 'retype_supported'),
+ api.nova: ('server_get',)})
+ def test_retype_volume_not_supported_no_action_item(self):
+ volume = self.cinder_volumes.get(name='my_volume')
+ server = self.servers.first()
+
+ cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
+ cinder.retype_supported().AndReturn(False)
+ api.nova.server_get(IsA(http.HttpRequest), server.id).AndReturn(server)
+
+ self.mox.ReplayAll()
+
+ url = VOLUME_INDEX_URL + \
+ "?action=row_update&table=volumes&obj_id=" + volume.id
+
+ res = self.client.get(url, {}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+ self.assertEqual(res.status_code, 200)
+
+ self.assertNotContains(res, 'Change Volume Type')
+ self.assertNotContains(res, 'retype')
+
+ @test.create_stubs({cinder: ('volume_get',
+ 'retype_supported')})
+ def test_retype_volume_supported_action_item(self):
+ volume = self.cinder_volumes.get(name='v2_volume')
+
+ cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
+ cinder.retype_supported().AndReturn(True)
+
+ self.mox.ReplayAll()
+
+ url = VOLUME_INDEX_URL + \
+ "?action=row_update&table=volumes&obj_id=" + volume.id
+
+ res = self.client.get(url, {}, HTTP_X_REQUESTED_WITH='XMLHttpRequest')
+
+ self.assertEqual(res.status_code, 200)
+
+ self.assertContains(res, 'Change Volume Type')
+ self.assertContains(res, 'retype')
+
+ @test.create_stubs({cinder: ('volume_get',
+ 'volume_retype',
+ 'volume_type_list')})
+ def test_retype_volume(self):
+ volume = self.cinder_volumes.get(name='my_volume2')
+
+ volume_type = self.cinder_volume_types.get(name='vol_type_1')
+
+ form_data = {'id': volume.id,
+ 'name': volume.name,
+ 'volume_type': volume_type.name,
+ 'migration_policy': 'on-demand'}
+
+ cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
+
+ cinder.volume_type_list(
+ IsA(http.HttpRequest)).AndReturn(self.cinder_volume_types.list())
+
+ cinder.volume_retype(
+ IsA(http.HttpRequest),
+ volume.id,
+ form_data['volume_type'],
+ form_data['migration_policy']).AndReturn(True)
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:project:volumes:volumes:retype',
+ args=[volume.id])
+ res = self.client.post(url, form_data)
+
+ self.assertNoFormErrors(res)
+
+ redirect_url = VOLUME_INDEX_URL
+ self.assertRedirectsNoFollow(res, redirect_url)
+
+ @test.create_stubs({cinder: ('volume_get',
+ 'volume_type_list')})
+ def test_retype_volume_same_type(self):
+ volume = self.cinder_volumes.get(name='my_volume2')
+
+ volume_type = self.cinder_volume_types.get(name='vol_type_2')
+
+ form_data = {'id': volume.id,
+ 'name': volume.name,
+ 'volume_type': volume_type.name,
+ 'migration_policy': 'on-demand'}
+
+ cinder.volume_get(IsA(http.HttpRequest), volume.id).AndReturn(volume)
+
+ cinder.volume_type_list(
+ IsA(http.HttpRequest)).AndReturn(self.cinder_volume_types.list())
+
+ self.mox.ReplayAll()
+
+ url = reverse('horizon:project:volumes:volumes:retype',
+ args=[volume.id])
+ res = self.client.post(url, form_data)
+
+ self.assertFormError(res,
+ 'form',
+ 'volume_type',
+ 'New volume type must be different from the '
+ 'original volume type "%s".' % volume_type.name)
+
def test_encryption_false(self):
self._test_encryption(False)
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/urls.py b/openstack_dashboard/dashboards/project/volumes/volumes/urls.py
index a2d8ce314c..8c3aed2b7a 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/urls.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/urls.py
@@ -43,4 +43,7 @@ urlpatterns = patterns(VIEWS_MOD,
url(r'^(?P[^/]+)/update/$',
views.UpdateView.as_view(),
name='update'),
+ url(r'^(?P[^/]+)/retype/$',
+ views.RetypeView.as_view(),
+ name='retype'),
)
diff --git a/openstack_dashboard/dashboards/project/volumes/volumes/views.py b/openstack_dashboard/dashboards/project/volumes/volumes/views.py
index b1ef144e80..a3c02751ee 100644
--- a/openstack_dashboard/dashboards/project/volumes/volumes/views.py
+++ b/openstack_dashboard/dashboards/project/volumes/volumes/views.py
@@ -242,3 +242,37 @@ class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
return self.form_valid(form)
else:
return self.get(request, *args, **kwargs)
+
+
+class RetypeView(forms.ModalFormView):
+ form_class = project_forms.RetypeForm
+ template_name = 'project/volumes/volumes/retype.html'
+ success_url = reverse_lazy("horizon:project:volumes:index")
+
+ @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()
+
+ return context
+
+ def get_initial(self):
+ volume = self.get_data()
+
+ return {'id': self.kwargs['volume_id'],
+ 'name': volume.name,
+ 'volume_type': volume.volume_type}
diff --git a/openstack_dashboard/test/api_tests/cinder_tests.py b/openstack_dashboard/test/api_tests/cinder_tests.py
index 0392e17533..1fd1892187 100644
--- a/openstack_dashboard/test/api_tests/cinder_tests.py
+++ b/openstack_dashboard/test/api_tests/cinder_tests.py
@@ -165,3 +165,18 @@ class CinderApiVersionTests(test.TestCase):
self.assertIn('description', ret_data.keys())
self.assertNotIn('display_name', ret_data.keys())
self.assertNotIn('display_description', ret_data.keys())
+
+ @override_settings(OPENSTACK_API_VERSIONS={'volume': 1})
+ def test_version_get_1(self):
+ version = api.cinder.version_get()
+ self.assertEqual(version, 1)
+
+ @override_settings(OPENSTACK_API_VERSIONS={'volume': 2})
+ def test_version_get_2(self):
+ version = api.cinder.version_get()
+ self.assertEqual(version, 2)
+
+ @override_settings(OPENSTACK_API_VERSIONS={'volume': 1})
+ def test_retype_not_supported(self):
+ retype_supported = api.cinder.retype_supported()
+ self.assertFalse(retype_supported)
diff --git a/openstack_dashboard/test/test_data/cinder_data.py b/openstack_dashboard/test/test_data/cinder_data.py
index 7dcd5a5797..40167c03c1 100644
--- a/openstack_dashboard/test/test_data/cinder_data.py
+++ b/openstack_dashboard/test/test_data/cinder_data.py
@@ -93,6 +93,17 @@ def data(TEST):
'volume_type': None,
'attachments': [{"id": "1", "server_id": '1',
"device": "/dev/hda"}]})
+ volume_with_type = volumes.Volume(volumes.VolumeManager(None),
+ {'id': "7dcb47fd-07d9-42c2-9647-be5eab799ebe",
+ 'name': 'my_volume2',
+ 'status': 'in-use',
+ 'size': 10,
+ 'display_name': u'my_volume2',
+ 'display_description': '',
+ 'created_at': '2013-04-01 10:30:00',
+ 'volume_type': 'vol_type_2',
+ 'attachments': [{"id": "2", "server_id": '2',
+ "device": "/dev/hdb"}]})
volume.bootable = 'true'
nameless_volume.bootable = 'true'
@@ -101,6 +112,7 @@ def data(TEST):
TEST.cinder_volumes.add(api.cinder.Volume(volume))
TEST.cinder_volumes.add(api.cinder.Volume(nameless_volume))
TEST.cinder_volumes.add(api.cinder.Volume(other_volume))
+ TEST.cinder_volumes.add(api.cinder.Volume(volume_with_type))
vol_type1 = volume_types.VolumeType(volume_types.VolumeTypeManager(None),
{'id': u'1',