Merge "Enable volumes metadata update"

This commit is contained in:
Jenkins 2016-10-03 23:47:30 +00:00 committed by Gerrit Code Review
commit 1d14504de9
16 changed files with 434 additions and 20 deletions

View File

@ -355,6 +355,14 @@ def volume_update(request, volume_id, name, description):
**vol_data)
def volume_set_metadata(request, volume_id, metadata):
return cinderclient(request).volumes.set_metadata(volume_id, metadata)
def volume_delete_metadata(request, volume_id, keys):
return cinderclient(request).volumes.delete_metadata(volume_id, keys)
def volume_reset_state(request, volume_id, state):
return cinderclient(request).volumes.reset_state(volume_id, state)
@ -445,6 +453,16 @@ def volume_snapshot_update(request, snapshot_id, name, description):
**snapshot_data)
def volume_snapshot_set_metadata(request, snapshot_id, metadata):
return cinderclient(request).volume_snapshots.set_metadata(
snapshot_id, metadata)
def volume_snapshot_delete_metadata(request, snapshot_id, keys):
return cinderclient(request).volume_snapshots.delete_metadata(
snapshot_id, keys)
def volume_snapshot_reset_state(request, snapshot_id, state):
return cinderclient(request).volume_snapshots.reset_state(
snapshot_id, state)
@ -792,7 +810,7 @@ def volume_type_extra_set(request, type_id, metadata):
def volume_type_extra_delete(request, type_id, keys):
vol_type = volume_type_get(request, type_id)
return vol_type.unset_keys([keys])
return vol_type.unset_keys(keys)
def qos_spec_list(request):

View File

@ -132,6 +132,34 @@ class VolumeTypes(generic.View):
return {'items': [api.cinder.VolumeType(u).to_dict() for u in result]}
@urls.register
class VolumeMetadata(generic.View):
"""API for volume metadata"""
url_regex = r'cinder/volumes/(?P<volume_id>[^/]+)/metadata$'
@rest_utils.ajax()
def get(self, request, volume_id):
"""Get a specific volume's metadata
http://localhost/api/cinder/volumes/1/metadata
"""
return api.cinder.volume_get(request,
volume_id).to_dict().get('metadata')
@rest_utils.ajax()
def patch(self, request, volume_id):
"""Update metadata items for specific volume
http://localhost/api/cinder/volumes/1/metadata
"""
updated = request.DATA['updated']
removed = request.DATA['removed']
if updated:
api.cinder.volume_set_metadata(request, volume_id, updated)
if removed:
api.cinder.volume_delete_metadata(request, volume_id, removed)
@urls.register
class VolumeType(generic.View):
"""API for getting a volume type.
@ -179,6 +207,74 @@ class VolumeSnapshots(generic.View):
return {'items': [u.to_dict() for u in result]}
@urls.register
class VolumeSnapshotMetadata(generic.View):
"""API for getting snapshots metadata"""
url_regex = r'cinder/volumesnapshots/' \
r'(?P<volume_snapshot_id>[^/]+)/metadata$'
@rest_utils.ajax()
def get(self, request, volume_snapshot_id):
"""Get a specific volumes snapshot metadata
http://localhost/api/cinder/volumesnapshots/1/metadata
"""
result = api.cinder.volume_snapshot_get(request,
volume_snapshot_id).\
to_dict().get('metadata')
return result
@rest_utils.ajax()
def patch(self, request, volume_snapshot_id):
"""Update metadata for specific volume snapshot
http://localhost/api/cinder/volumesnapshots/1/metadata
"""
updated = request.DATA['updated']
removed = request.DATA['removed']
if updated:
api.cinder.volume_snapshot_set_metadata(request,
volume_snapshot_id,
updated)
if removed:
api.cinder.volume_snapshot_delete_metadata(request,
volume_snapshot_id,
removed)
@urls.register
class VolumeTypeMetadata(generic.View):
"""API for getting snapshots metadata"""
url_regex = r'cinder/volumetypes/(?P<volume_type_id>[^/]+)/metadata$'
@rest_utils.ajax()
def get(self, request, volume_type_id):
"""Get a specific volume's metadata
http://localhost/api/cinder/volumetypes/1/metadata
"""
metadata = api.cinder.volume_type_extra_get(request, volume_type_id)
result = {x.key: x.value for x in metadata}
return result
@rest_utils.ajax()
def patch(self, request, volume_type_id):
"""Update metadata for specific volume
http://localhost/api/cinder/volumetypes/1/metadata
"""
updated = request.DATA['updated']
removed = request.DATA['removed']
if updated:
api.cinder.volume_type_extra_set(request,
volume_type_id,
updated)
if removed:
api.cinder.volume_type_extra_delete(request,
volume_type_id,
removed)
@urls.register
class Extensions(generic.View):
"""API for cinder extensions.

View File

@ -72,7 +72,8 @@ class VolumeSnapshotsTable(volumes_tables.VolumesTableBase):
table_actions = (snapshots_tables.VolumeSnapshotsFilterAction,
snapshots_tables.DeleteVolumeSnapshot,)
row_actions = (snapshots_tables.DeleteVolumeSnapshot,
UpdateVolumeSnapshotStatus,)
UpdateVolumeSnapshotStatus,
snapshots_tables.UpdateMetadata)
row_class = UpdateRow
status_columns = ("status",)
columns = ('tenant', 'host', 'name', 'description', 'size', 'status',

View File

@ -19,7 +19,7 @@ from openstack_dashboard.test import helpers as test
from openstack_dashboard.dashboards.admin.volumes.snapshots import forms
INDEX_URL = reverse('horizon:admin:volumes:index')
INDEX_URL = 'horizon:admin:volumes:index'
class VolumeSnapshotsViewTests(test.BaseAdminViewTests):
@ -79,7 +79,7 @@ class VolumeSnapshotsViewTests(test.BaseAdminViewTests):
self.assertNoFormErrors(res)
self.assertMessageCount(error=1)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertRedirectsNoFollow(res, reverse(INDEX_URL))
@test.create_stubs({cinder: ('volume_snapshot_get',
'volume_get')})
@ -101,7 +101,7 @@ class VolumeSnapshotsViewTests(test.BaseAdminViewTests):
self.assertNoFormErrors(res)
self.assertMessageCount(error=1)
self.assertRedirectsNoFollow(res, INDEX_URL)
self.assertRedirectsNoFollow(res, reverse(INDEX_URL))
def test_get_snapshot_status_choices_without_current(self):
current_status = {'status': 'available'}

View File

@ -195,6 +195,23 @@ class UpdateRow(tables.Row):
return volume_type
class UpdateMetadata(tables.LinkAction):
name = "update_metadata"
verbose_name = _("Update Metadata")
ajax = False
attrs = {"ng-controller": "MetadataModalHelperController as modal"}
def __init__(self, **kwargs):
kwargs['preempt'] = True
super(UpdateMetadata, self).__init__(**kwargs)
def get_link_url(self, datum):
obj_id = self.table.get_object_id(datum)
self.attrs['ng-click'] = (
"modal.openMetadataModal('volume_type', '%s', true)" % obj_id)
return "javascript:void(0);"
class VolumeTypesTable(tables.DataTable):
name = tables.WrappingColumn("name", verbose_name=_("Name"),
form_field=forms.CharField(max_length=64))
@ -233,7 +250,8 @@ class VolumeTypesTable(tables.DataTable):
EditVolumeType,
UpdateVolumeTypeEncryption,
DeleteVolumeTypeEncryption,
DeleteVolumeType,)
DeleteVolumeType,
UpdateMetadata)
row_class = UpdateRow

View File

@ -111,6 +111,7 @@ class VolumesTable(volumes_tables.VolumesTable):
row_actions = (volumes_tables.DeleteVolume,
UpdateVolumeStatusAction,
UnmanageVolumeAction,
MigrateVolume)
MigrateVolume,
volumes_tables.UpdateMetadata)
columns = ('tenant', 'host', 'name', 'size', 'status', 'volume_type',
'attachments', 'bootable', 'encryption',)

View File

@ -132,6 +132,24 @@ class CreateVolumeFromSnapshot(tables.LinkAction):
return False
class UpdateMetadata(tables.LinkAction):
name = "update_metadata"
verbose_name = _("Update Metadata")
ajax = False
attrs = {"ng-controller": "MetadataModalHelperController as modal"}
def __init__(self, **kwargs):
kwargs['preempt'] = True
super(UpdateMetadata, self).__init__(**kwargs)
def get_link_url(self, datum):
obj_id = self.table.get_object_id(datum)
self.attrs['ng-click'] = (
"modal.openMetadataModal('volume_snapshot', '%s', true)" % obj_id)
return "javascript:void(0);"
class UpdateRow(tables.Row):
ajax = True
@ -191,7 +209,8 @@ class VolumeSnapshotsTable(volume_tables.VolumesTableBase):
launch_actions = (LaunchSnapshotNG,) + launch_actions
row_actions = ((CreateVolumeFromSnapshot,) + launch_actions +
(EditVolumeSnapshot, DeleteVolumeSnapshot))
(EditVolumeSnapshot, DeleteVolumeSnapshot,
UpdateMetadata))
row_class = UpdateRow
status_columns = ("status",)
permissions = [(

View File

@ -469,6 +469,23 @@ class VolumesFilterAction(tables.FilterAction):
if q in volume.name.lower()]
class UpdateMetadata(tables.LinkAction):
name = "update_metadata"
verbose_name = _("Update Metadata")
ajax = False
attrs = {"ng-controller": "MetadataModalHelperController as modal"}
def __init__(self, **kwargs):
kwargs['preempt'] = True
super(UpdateMetadata, self).__init__(**kwargs)
def get_link_url(self, datum):
obj_id = self.table.get_object_id(datum)
self.attrs['ng-click'] = (
"modal.openMetadataModal('volume', '%s', true)" % obj_id)
return "javascript:void(0);"
class VolumesTable(VolumesTableBase):
name = tables.WrappingColumn("name",
verbose_name=_("Name"),
@ -504,7 +521,7 @@ class VolumesTable(VolumesTableBase):
launch_actions +
(EditAttachments, CreateSnapshot, CreateBackup,
RetypeVolume, UploadToImage, CreateTransfer,
DeleteTransfer, DeleteVolume))
DeleteTransfer, DeleteVolume, UpdateMetadata))
class DetachVolume(tables.BatchAction):

View File

@ -22,7 +22,8 @@
metadataService.$inject = [
'horizon.app.core.openstack-service-api.nova',
'horizon.app.core.openstack-service-api.glance'
'horizon.app.core.openstack-service-api.glance',
'horizon.app.core.openstack-service-api.cinder'
];
/**
@ -32,7 +33,7 @@
*
* Unified acquisition and modification of metadata.
*/
function metadataService(nova, glance) {
function metadataService(nova, glance, cinder) {
var service = {
getMetadata: getMetadata,
editMetadata: editMetadata,
@ -52,7 +53,10 @@
aggregate: nova.getAggregateExtraSpecs,
flavor: nova.getFlavorExtraSpecs,
image: glance.getImageProps,
instance: nova.getInstanceMetadata
instance: nova.getInstanceMetadata,
volume: cinder.getVolumeMetadata,
volume_snapshot: cinder.getVolumeSnapshotMetadata,
volume_type: cinder.getVolumeTypeMetadata
}[resource](id);
}
@ -69,7 +73,10 @@
aggregate: nova.editAggregateExtraSpecs,
flavor: nova.editFlavorExtraSpecs,
image: glance.editImageProps,
instance: nova.editInstanceMetadata
instance: nova.editInstanceMetadata,
volume: cinder.editVolumeMetadata,
volume_snapshot: cinder.editVolumeSnapshotMetadata,
volume_type: cinder.editVolumeTypeMetadata
}[resource](id, updated, removed);
}
@ -86,7 +93,10 @@
aggregate: 'OS::Nova::Aggregate',
flavor: 'OS::Nova::Flavor',
image: 'OS::Glance::Image',
instance: 'OS::Nova::Server'
instance: 'OS::Nova::Server',
volume: 'OS::Cinder::Volume',
volume_snapshot: 'OS::Cinder::Snapshot',
volume_type: 'OS:Cinder::VolumeType'
}[resource]
};
if (propertiesTarget) {

View File

@ -31,10 +31,17 @@
editImageProps: function() {},
getNamespaces: function() {}};
var cinder = {getVolumeMetadata:function() {},
getVolumeSnapshotMetadata:function() {},
getVolumeTypeMetadata:function() {},
editVolumeMetadata: function() {},
editVolumeSnapshotMetadata: function() {}};
beforeEach(function() {
module(function($provide) {
$provide.value('horizon.app.core.openstack-service-api.nova', nova);
$provide.value('horizon.app.core.openstack-service-api.glance', glance);
$provide.value('horizon.app.core.openstack-service-api.cinder', cinder);
});
});
@ -97,6 +104,18 @@
expect(glance.editImageProps).toHaveBeenCalledWith('1', 'updated', ['removed']);
});
it('should edit volume metadata', function() {
spyOn(cinder, 'editVolumeMetadata');
metadataService.editMetadata('volume', '1', 'updated', ['removed']);
expect(cinder.editVolumeMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']);
});
it('should edit volume snapshot metadata', function() {
spyOn(cinder, 'editVolumeSnapshotMetadata');
metadataService.editMetadata('volume_snapshot', '1', 'updated', ['removed']);
expect(cinder.editVolumeSnapshotMetadata).toHaveBeenCalledWith('1', 'updated', ['removed']);
});
it('should get image namespace', function() {
spyOn(glance, 'getNamespaces');
metadataService.getNamespaces('image');
@ -111,6 +130,13 @@
expect(actual).toBe(expected);
});
it('should get volume metadata', function() {
var expected = 'volume metadata';
spyOn(cinder, 'getVolumeMetadata').and.returnValue(expected);
var actual = metadataService.getMetadata('volume', '1');
expect(actual).toBe(expected);
});
it('should edit instance metadata', function() {
spyOn(nova, 'editInstanceMetadata');
metadataService.editMetadata('instance', '1', 'updated', ['removed']);

View File

@ -7,6 +7,9 @@
<span translate ng-if="modal.resourceType==='flavor'">Update Flavor Metadata</span>
<span translate ng-if="modal.resourceType==='image'">Update Image Metadata</span>
<span translate ng-if="modal.resourceType==='instance'">Update Instance Metadata</span>
<span translate ng-if="modal.resourceType==='volume'">Update Volume Metadata</span>
<span translate ng-if="modal.resourceType==='volume_snapshot'">Update Volume Snapshot Metadata</span>
<span translate ng-if="modal.resourceType==='volume_type'">Update Volume Type Metadata</span>
</h3>
</div>
<div class="modal-body">

View File

@ -38,6 +38,9 @@
getVolumes: getVolumes,
getVolume: getVolume,
getVolumeTypes: getVolumeTypes,
getVolumeMetadata: getVolumeMetadata,
getVolumeSnapshotMetadata: getVolumeSnapshotMetadata,
getVolumeTypeMetadata: getVolumeTypeMetadata,
getVolumeType: getVolumeType,
getDefaultVolumeType: getDefaultVolumeType,
getVolumeSnapshots: getVolumeSnapshots,
@ -48,7 +51,10 @@
getServices: getServices,
getDefaultQuotaSets: getDefaultQuotaSets,
setDefaultQuotaSets: setDefaultQuotaSets,
updateProjectQuota: updateProjectQuota
updateProjectQuota: updateProjectQuota,
editVolumeMetadata: editVolumeMetadata,
editVolumeSnapshotMetadata: editVolumeSnapshotMetadata,
editVolumeTypeMetadata:editVolumeTypeMetadata
};
return service;
@ -146,6 +152,63 @@
});
}
function getVolumeMetadata(id) {
return apiService.get('/api/cinder/volumes/' + id + '/metadata')
.error(function () {
toastService.add('error', gettext('Unable to retrieve the volume metadata.'));
});
}
function getVolumeSnapshotMetadata(id) {
return apiService.get('/api/cinder/volumesnapshots/' + id + '/metadata')
.error(function () {
toastService.add('error', gettext('Unable to retrieve the snapshot metadata.'));
});
}
function getVolumeTypeMetadata(id) {
return apiService.get('/api/cinder/volumetypes/' + id + '/metadata')
.error(function () {
toastService.add('error', gettext('Unable to retrieve the volume type metadata.'));
});
}
function editVolumeMetadata(id, updated, removed) {
return apiService.patch(
'/api/cinder/volumes/' + id + '/metadata',
{
updated: updated,
removed: removed
}
).error(function () {
toastService.add('error', gettext('Unable to edit volume metadata.'));
});
}
function editVolumeSnapshotMetadata(id, updated, removed) {
return apiService.patch(
'/api/cinder/volumesnapshots/' + id + '/metadata',
{
updated: updated,
removed: removed
}
).error(function () {
toastService.add('error', gettext('Unable to edit snapshot metadata.'));
});
}
function editVolumeTypeMetadata(id, updated, removed) {
return apiService.patch(
'/api/cinder/volumetypes/' + id + '/metadata',
{
updated: updated,
removed: removed
}
).error(function () {
toastService.add('error', gettext('Unable to edit volume type metadata.'));
});
}
/**
* @name getVolumeType
* @description

View File

@ -62,6 +62,13 @@
error: 'Unable to retrieve the volume.',
testInput: [1]
},
{
func: 'getVolumeMetadata',
method: 'get',
path: '/api/cinder/volumes/1/metadata',
error: 'Unable to retrieve the volume metadata.',
testInput: [1]
},
{
func: 'getVolumeTypes',
method: 'get',
@ -76,6 +83,13 @@
error: 'Unable to retrieve the volume type.',
testInput: [1]
},
{
func: 'getVolumeTypeMetadata',
method: 'get',
path: '/api/cinder/volumetypes/1/metadata',
error: 'Unable to retrieve the volume type metadata.',
testInput: [1]
},
{
func: 'getDefaultVolumeType',
method: 'get',
@ -112,6 +126,13 @@
error: 'Unable to retrieve the volume snapshots.',
testInput: [ 'config' ]
},
{
func: 'getVolumeSnapshotMetadata',
method: 'get',
path: '/api/cinder/volumesnapshots/1/metadata',
error: 'Unable to retrieve the snapshot metadata.',
testInput: [1]
},
{
func: 'createVolume',
method: 'post',
@ -165,6 +186,42 @@
data: {'volumes': 42},
error: 'Unable to update project quota data.',
testInput: [{'volumes': 42}, 42]
},
{ func: 'editVolumeMetadata',
method: 'patch',
path: '/api/cinder/volumes/42/metadata',
data: {
"updated": {a: '1', b: '2'},
"removed": ['c', 'd']
},
error: "Unable to edit volume metadata.",
testInput: [
42, {a: '1', b: '2'}, ['c', 'd']
]
},
{ func: 'editVolumeSnapshotMetadata',
method: 'patch',
path: '/api/cinder/volumesnapshots/42/metadata',
data: {
"updated": {a: '1', b: '2'},
"removed": ['c', 'd']
},
error: "Unable to edit snapshot metadata.",
testInput: [
42, {a: '1', b: '2'}, ['c', 'd']
]
},
{ func: 'editVolumeTypeMetadata',
method: 'patch',
path: '/api/cinder/volumetypes/42/metadata',
data: {
"updated": {a: '1', b: '2'},
"removed": ['c', 'd']
},
error: "Unable to edit volume type metadata.",
testInput: [
42, {a: '1', b: '2'}, ['c', 'd']
]
}
];

View File

@ -18,6 +18,7 @@ from django.conf import settings
from openstack_dashboard import api
from openstack_dashboard.api.base import Quota
from openstack_dashboard.api.cinder import VolTypeExtraSpec
from openstack_dashboard.api.rest import cinder
from openstack_dashboard.test import helpers as test
@ -99,6 +100,33 @@ class CinderRestTestCase(test.TestCase):
self.assertStatusCode(response, 201)
self.assertEqual(response.content.decode("utf-8"), mock_post_response)
@mock.patch.object(cinder.api, 'cinder')
def test_volume_get_metadata(self, cc):
request = self.mock_rest_request(**{'GET': {}})
cc.volume_get.return_value = mock.Mock(
**{'to_dict.return_value': {'id': 'one',
'metadata': {'foo': 'bar'}}})
response = cinder.VolumeMetadata().get(request, '1')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'foo': 'bar'})
cc.volume_get.assert_called_once_with(request, '1')
@mock.patch.object(cinder.api, 'cinder')
def test_volume_update_metadata(self, cc):
request = self.mock_rest_request(
body='{"updated": {"a": "1", "b": "2"}, '
'"removed": ["c", "d"]}'
)
response = cinder.VolumeMetadata().patch(request, '1')
self.assertStatusCode(response, 204)
self.assertEqual(b'', response.content)
cc.volume_set_metadata.assert_called_once_with(
request, '1', {'a': '1', 'b': '2'}
)
cc.volume_delete_metadata.assert_called_once_with(
request, '1', ['c', 'd']
)
#
# Volume Types
#
@ -138,6 +166,35 @@ class CinderRestTestCase(test.TestCase):
cc.volume_type_default.assert_called_once_with(request)
cc.VolumeType.assert_called_once_with({'name': 'one'})
@mock.patch.object(cinder.api, 'cinder')
def test_volume_type_get_metadata(self, cc):
request = self.mock_rest_request(**{'GET': {}})
cc.volume_type_extra_get = mock.Mock()
cc.volume_type_extra_get.return_value = \
[VolTypeExtraSpec(1, 'foo', 'bar')]
# cc.volume_type_extra_get.side_effect = [{'foo': 'bar'}]
response = cinder.VolumeTypeMetadata().get(request, '1')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'foo': 'bar'})
cc.volume_type_extra_get.assert_called_once_with(request, '1')
@mock.patch.object(cinder.api, 'cinder')
def test_volume_type_update_metadata(self, cc):
request = self.mock_rest_request(
body='{"updated": {"a": "1", "b": "2"}, '
'"removed": ["c", "d"]}'
)
response = cinder.VolumeTypeMetadata().patch(request, '1')
self.assertStatusCode(response, 204)
self.assertEqual(b'', response.content)
cc.volume_type_extra_set.assert_called_once_with(
request, '1', {'a': '1', 'b': '2'}
)
cc.volume_type_extra_delete.assert_called_once_with(
request, '1', ['c', 'd']
)
#
# Volume Snapshots
#
@ -170,6 +227,33 @@ class CinderRestTestCase(test.TestCase):
cc.volume_snapshot_list.assert_called_once_with(request,
search_opts=filters)
@mock.patch.object(cinder.api, 'cinder')
def test_volume_snapshot_get_metadata(self, cc):
request = self.mock_rest_request(**{'GET': {}})
cc.volume_snapshot_get.return_value = mock.Mock(
**{'to_dict.return_value': {'id': 'one',
'metadata': {'foo': 'bar'}}})
response = cinder.VolumeSnapshotMetadata().get(request, '1')
self.assertStatusCode(response, 200)
self.assertEqual(response.json, {'foo': 'bar'})
cc.volume_snapshot_get.assert_called_once_with(request, '1')
@mock.patch.object(cinder.api, 'cinder')
def test_volume_snapshot_update_metadata(self, cc):
request = self.mock_rest_request(
body='{"updated": {"a": "1", "b": "2"}, '
'"removed": ["c", "d"]}'
)
response = cinder.VolumeSnapshotMetadata().patch(request, '1')
self.assertStatusCode(response, 204)
self.assertEqual(b'', response.content)
cc.volume_snapshot_set_metadata.assert_called_once_with(
request, '1', {'a': '1', 'b': '2'}
)
cc.volume_snapshot_delete_metadata.assert_called_once_with(
request, '1', ['c', 'd']
)
#
# Extensions
#

View File

@ -286,8 +286,8 @@ class GlanceApiTests(test.APITestCase):
def test_metadefs_namespace_list_with_properties_target(self):
metadata_defs = self.metadata_defs.list()
limit = getattr(settings, 'API_RESULT_LIMIT', 1000)
filters = {'resource_types': ['mock name'],
'properties_target': 'mock properties target'}
filters = {'resource_types': ['OS::Cinder::Volume'],
'properties_target': 'user'}
glanceclient = self.stub_glanceclient()
glanceclient.metadefs_namespace = self.mox.CreateMockAnything()
@ -295,7 +295,7 @@ class GlanceApiTests(test.APITestCase):
limit=limit,
filters=filters,
sort_dir='asc',
sort_key='namespace',) \
sort_key='namespace', ) \
.AndReturn(metadata_defs)
self.mox.ReplayAll()

View File

@ -404,8 +404,9 @@ def data(TEST):
{
'created_at': '2014-08-21T08:39:43Z',
'prefix': 'mock',
'name': 'mock name',
'properties_target': 'mock properties target'
'name': 'OS::Cinder::Volume',
'properties_target': 'user'
}
],
'visibility': 'public',