Enable changing volume type of a volume
Expose the functionality of the 'cinder retype' command in the UI. It allows user to change the volume type of a volume whose status is in-use or available when horizon's cinder API version is >= 2. cinder retype is only supported starting cinder v2. If enabled_backends is specified in /etc/cinder/cinder.conf, retype is actually performed by a specific driver. It depends on the drivers (backends) that are associated with volume types. Volume types are set through type-key extra specs. If enabled_backends in cinder.conf is not specified, volumes are created by LVM so retype is actually performaned in LVM. During retype, if cinder finds it can not retype, it will check if the migration policy is on_demand or never. If the policy is is never, then cinder does not do anything, otherwise, it will perform migration. By default, in the horizon retype dialog UI, migration policy is never which is also the default of the cinder cli command. Currently in horizon cinder api default version is 1. In order to test this functionallity, you need to update openstack_dashboard/local/local_settings.py to have the "volume" API to use version 2 so the "Change Volume Type" action menu shows up for the volume. If local_settings.py is not available, you need to copy the local_settings.py.example file, change it to local_settings.py, update other necessary settings and also update have the API version setting like the followings: OPENSTACK_API_VERSIONS = { #"data_processing": 1.1, #"identity": 3, "volume": 2 } Implements: blueprint volume-retype Change-Id: Id8bc539e1849f5910df34d7b76cc250ec82f9671
This commit is contained in:
parent
59e410eb24
commit
fa7105d0da
@ -93,6 +93,12 @@ class VolumeSnapshot(BaseCinderAPIResourceWrapper):
|
|||||||
'os-extended-snapshot-attributes:project_id']
|
'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):
|
class VolumeBackup(BaseCinderAPIResourceWrapper):
|
||||||
|
|
||||||
_attrs = ['id', 'name', 'description', 'container', 'size', 'status',
|
_attrs = ['id', 'name', 'description', 'container', 'size', 'status',
|
||||||
@ -162,6 +168,11 @@ def _replace_v2_parameters(data):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def version_get():
|
||||||
|
api_version = VERSIONS.get_active_version()
|
||||||
|
return api_version['version']
|
||||||
|
|
||||||
|
|
||||||
def volume_list(request, search_opts=None):
|
def volume_list(request, search_opts=None):
|
||||||
"""To see all volumes in the cloud as an admin you can pass in a special
|
"""To see all volumes in the cloud as an admin you can pass in a special
|
||||||
search option: {'all_tenants': 1}
|
search option: {'all_tenants': 1}
|
||||||
@ -212,6 +223,16 @@ def volume_delete(request, volume_id):
|
|||||||
return cinderclient(request).volumes.delete(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):
|
def volume_update(request, volume_id, name, description):
|
||||||
vol_data = {'name': name,
|
vol_data = {'name': name,
|
||||||
'description': description}
|
'description': description}
|
||||||
@ -399,3 +420,10 @@ def extension_supported(request, extension_name):
|
|||||||
if extension.name == extension_name:
|
if extension.name == extension_name:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@memoized
|
||||||
|
def retype_supported():
|
||||||
|
"""retype is only supported after cinder v2.
|
||||||
|
"""
|
||||||
|
return version_get() >= 2
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"volume:update_snapshot": [["rule:default"]],
|
"volume:update_snapshot": [["rule:default"]],
|
||||||
"volume:get_all_snapshots": [],
|
"volume:get_all_snapshots": [],
|
||||||
"volume:extend": [],
|
"volume:extend": [],
|
||||||
|
"volume:retype": [],
|
||||||
|
|
||||||
"volume_extension:types_manage": [["rule:admin_api"]],
|
"volume_extension:types_manage": [["rule:admin_api"]],
|
||||||
"volume_extension:types_extra_specs": [["rule:admin_api"]],
|
"volume_extension:types_extra_specs": [["rule:admin_api"]],
|
||||||
|
@ -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 %}
|
||||||
|
<div class="left">
|
||||||
|
<fieldset>
|
||||||
|
{% include "horizon/common/_form_fields.html" %}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<h3>{% trans "Description:" %}</h3>
|
||||||
|
<p>{% blocktrans %}
|
||||||
|
From here you can change the volume type of a volume after its creation.
|
||||||
|
This is equivalent to the <tt>cinder retype</tt> command.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<p>{% blocktrans %}
|
||||||
|
The "Volume Type" selected must be different from the current volume type.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</p>
|
||||||
|
<p>{% 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 %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modal-footer %}
|
||||||
|
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Change Volume Type" %}" />
|
||||||
|
<a href="{% url 'horizon:project:volumes:index' %}" class="btn btn-default secondary cancel close">{% trans "Cancel" %}</a>
|
||||||
|
{% endblock %}
|
@ -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 %}
|
@ -543,3 +543,69 @@ class ExtendForm(forms.SelfHandlingForm):
|
|||||||
exceptions.handle(request,
|
exceptions.handle(request,
|
||||||
_('Unable to extend volume.'),
|
_('Unable to extend volume.'),
|
||||||
redirect=redirect)
|
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
|
||||||
|
@ -216,6 +216,27 @@ class EditVolume(tables.LinkAction):
|
|||||||
return volume.status in ("available", "in-use")
|
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):
|
class UpdateRow(tables.Row):
|
||||||
ajax = True
|
ajax = True
|
||||||
|
|
||||||
@ -342,7 +363,8 @@ class VolumesTable(VolumesTableBase):
|
|||||||
row_class = UpdateRow
|
row_class = UpdateRow
|
||||||
table_actions = (CreateVolume, DeleteVolume, VolumesFilterAction)
|
table_actions = (CreateVolume, DeleteVolume, VolumesFilterAction)
|
||||||
row_actions = (EditVolume, ExtendVolume, LaunchVolume, EditAttachments,
|
row_actions = (EditVolume, ExtendVolume, LaunchVolume, EditAttachments,
|
||||||
CreateSnapshot, CreateBackup, DeleteVolume)
|
CreateSnapshot, CreateBackup, RetypeVolume,
|
||||||
|
DeleteVolume)
|
||||||
|
|
||||||
|
|
||||||
class DetachVolume(tables.BatchAction):
|
class DetachVolume(tables.BatchAction):
|
||||||
|
@ -1030,6 +1030,113 @@ class VolumeViewTests(test.TestCase):
|
|||||||
"New size must be greater than "
|
"New size must be greater than "
|
||||||
"current size.")
|
"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):
|
def test_encryption_false(self):
|
||||||
self._test_encryption(False)
|
self._test_encryption(False)
|
||||||
|
|
||||||
|
@ -43,4 +43,7 @@ urlpatterns = patterns(VIEWS_MOD,
|
|||||||
url(r'^(?P<volume_id>[^/]+)/update/$',
|
url(r'^(?P<volume_id>[^/]+)/update/$',
|
||||||
views.UpdateView.as_view(),
|
views.UpdateView.as_view(),
|
||||||
name='update'),
|
name='update'),
|
||||||
|
url(r'^(?P<volume_id>[^/]+)/retype/$',
|
||||||
|
views.RetypeView.as_view(),
|
||||||
|
name='retype'),
|
||||||
)
|
)
|
||||||
|
@ -242,3 +242,37 @@ class EditAttachmentsView(tables.DataTableView, forms.ModalFormView):
|
|||||||
return self.form_valid(form)
|
return self.form_valid(form)
|
||||||
else:
|
else:
|
||||||
return self.get(request, *args, **kwargs)
|
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}
|
||||||
|
@ -165,3 +165,18 @@ class CinderApiVersionTests(test.TestCase):
|
|||||||
self.assertIn('description', ret_data.keys())
|
self.assertIn('description', ret_data.keys())
|
||||||
self.assertNotIn('display_name', ret_data.keys())
|
self.assertNotIn('display_name', ret_data.keys())
|
||||||
self.assertNotIn('display_description', 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)
|
||||||
|
@ -93,6 +93,17 @@ def data(TEST):
|
|||||||
'volume_type': None,
|
'volume_type': None,
|
||||||
'attachments': [{"id": "1", "server_id": '1',
|
'attachments': [{"id": "1", "server_id": '1',
|
||||||
"device": "/dev/hda"}]})
|
"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'
|
volume.bootable = 'true'
|
||||||
nameless_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(volume))
|
||||||
TEST.cinder_volumes.add(api.cinder.Volume(nameless_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(other_volume))
|
||||||
|
TEST.cinder_volumes.add(api.cinder.Volume(volume_with_type))
|
||||||
|
|
||||||
vol_type1 = volume_types.VolumeType(volume_types.VolumeTypeManager(None),
|
vol_type1 = volume_types.VolumeType(volume_types.VolumeTypeManager(None),
|
||||||
{'id': u'1',
|
{'id': u'1',
|
||||||
|
Loading…
Reference in New Issue
Block a user