# License for the specific language governing permissions and limitations
# under the License.
-
import mock
from cinder import exception
from cinder import test
+from cinder.tests.unit import fake_snapshot
from cinder.volume.drivers.emc import xtremio
'snapshots': 'vol-id',
'initiators': 'initiator-id',
'initiator-groups': 'ig-id',
- 'lun-maps': 'mapping-id'}
+ 'lun-maps': 'mapping-id',
+ 'consistency-groups': 'cg-id',
+ 'consistency-group-volumes': 'cg-vol-id',
+ }
xms_data = {'xms': {1: {'version': '4.0.0'}},
- 'clusters': {'cluster1':
- {'name': 'cluster1',
- 'sys-sw-version': "3.0.0-devel_ba23ee5381eeab73",
- 'ud-ssd-space': '8146708710',
- 'ud-ssd-space-in-use': '708710',
- 'vol-size': '29884416',
- 'chap-authentication-mode': 'disabled',
- 'chap-discovery-mode': 'disabled',
- "index": 1},
- 1: {'name': 'cluster1',
- 'sys-sw-version': "3.0.0-devel_ba23ee5381eeab73",
+ 'clusters': {1: {'name': 'brick1',
+ 'sys-sw-version': "4.0.0-devel_ba23ee5381eeab73",
'ud-ssd-space': '8146708710',
'ud-ssd-space-in-use': '708710',
'vol-size': '29884416',
'chap-authentication-mode': 'disabled',
'chap-discovery-mode': 'disabled',
- "index": 1}},
- 'target-groups': {'Default': {"index": 1, }},
+ "index": 1,
+ },
+ },
+ 'target-groups': {'Default': {"index": 1, },
+ },
'iscsi-portals': {'10.205.68.5/16':
{"port-address":
"iqn.2008-05.com.xtremio:001e67939c34",
"ip-port": 3260,
"ip-addr": "10.205.68.5/16",
"name": "10.205.68.5/16",
- "index": 1}},
+ "index": 1,
+ },
+ },
'targets': {'X1-SC2-fc1': {'index': 1, "name": "X1-SC2-fc1",
"port-address":
"21:00:00:24:ff:57:b2:36",
- 'port-state': 'up'},
+ 'port-state': 'up',
+ },
'X1-SC2-fc2': {'index': 2, "name": "X1-SC2-fc2",
"port-address":
"21:00:00:24:ff:57:b2:55",
- 'port-state': 'up'}
+ 'port-state': 'up',
+ }
},
'volumes': {},
'initiator-groups': {},
'initiators': {},
'lun-maps': {},
+ 'consistency-groups': {},
+ 'consistency-group-volumes': {},
}
+def get_xms_obj_by_name(typ, name):
+ for item in xms_data[typ].values():
+ if 'name' in item and item['name'] == name:
+ return item
+ raise exception.NotFound()
+
+
def clean_xms_data():
xms_data['volumes'] = {}
xms_data['initiator-groups'] = {}
xms_data['initiators'] = {}
xms_data['lun-maps'] = {}
+ xms_data['consistency-group-volumes'] = {}
+ xms_data['consistency-groups'] = {}
def fix_data(data, object_type):
d[typ2id[object_type]] = ["a91e8c81c2d14ae4865187ce4f866f8a",
d.get('name'),
- len(xms_data[object_type]) + 1]
+ len(xms_data.get(object_type, [])) + 1]
d['index'] = len(xms_data[object_type]) + 1
return d
return key
+def get_obj(typ, name, idx):
+ if name:
+ return {"content": get_xms_obj_by_name(typ, name)}
+ elif idx:
+ if idx not in xms_data.get(typ, {}):
+ raise exception.NotFound()
+ return {"content": xms_data[typ][idx]}
+
+
def xms_request(object_type='volumes', request_typ='GET', data=None,
- name=None, idx=None):
+ name=None, idx=None, ver='v1'):
if object_type == 'snapshots':
object_type = 'volumes'
- obj_key = name if name else idx
+ try:
+ res = xms_data[object_type]
+ except KeyError:
+ raise exception.VolumeDriverException
if request_typ == 'GET':
- try:
- res = xms_data[object_type]
- except KeyError:
- raise exception.VolumeDriverException
if name or idx:
- if obj_key not in res:
- raise exception.NotFound()
- return {"content": res[obj_key]}
+ return get_obj(object_type, name, idx)
else:
if data and data.get('full') == 1:
- return {object_type: res.values()}
+ return {object_type: list(res.values())}
else:
return {object_type: [{"href": "/%s/%d" % (object_type,
obj['index']),
for obj in res.values()]}
elif request_typ == 'POST':
data = fix_data(data, object_type)
+ name_key = get_xms_obj_key(data)
+ try:
+ if name_key and get_xms_obj_by_name(object_type, data[name_key]):
+ raise (exception
+ .VolumeBackendAPIException
+ ('Volume by this name already exists'))
+ except exception.NotFound:
+ pass
data['index'] = len(xms_data[object_type]) + 1
xms_data[object_type][data['index']] = data
# find the name key
- name_key = get_xms_obj_key(data)
+ if name_key:
+ data['name'] = data[name_key]
if object_type == 'lun-maps':
data['ig-name'] = data['ig-id']
- if name_key:
- if data[name_key] in xms_data[object_type]:
- raise (exception
- .VolumeBackendAPIException
- ('Volume by this name already exists'))
- xms_data[object_type][data[name_key]] = data
return {"links": [{"href": "/%s/%d" %
(object_type, data[typ2id[object_type]][2])}]}
elif request_typ == 'DELETE':
- if obj_key in xms_data[object_type]:
- data = xms_data[object_type][obj_key]
+ if object_type == 'consistency-group-volumes':
+ data = [cgv for cgv in
+ xms_data['consistency-group-volumes'].values()
+ if cgv['vol-id'] == data['vol-id']
+ and cgv['cg-id'] == data['cg-id']][0]
+ else:
+ data = get_obj(object_type, name, idx)['content']
+ if data:
del xms_data[object_type][data['index']]
- del xms_data[object_type][data[typ2id[object_type]][1]]
else:
raise exception.NotFound()
elif request_typ == 'PUT':
- if obj_key in xms_data[object_type]:
- obj = xms_data[object_type][obj_key]
- obj.update(data)
- key = get_xms_obj_key(data)
- if key:
- xms_data[object_type][data[key]] = obj
- else:
- raise exception.NotFound()
+ obj = get_obj(object_type, name, idx)['content']
+ data = fix_data(data, object_type)
+ del data['index']
+ obj.update(data)
def xms_bad_request(object_type='volumes', request_typ='GET', data=None,
- name=None, idx=None):
+ name=None, idx=None, ver='v1'):
if request_typ == 'GET':
raise exception.NotFound()
elif request_typ == 'POST':
def xms_failed_rename_snapshot_request(object_type='volumes',
request_typ='GET', data=None,
- name=None, idx=None):
+ name=None, idx=None, ver='v1'):
if request_typ == 'POST':
xms_data['volumes'][27] = {}
return {
"href": "https://host/api/json/v2/types/snapshots/27",
"rel": "self"}]}
elif request_typ == 'PUT':
- raise exception.VolumeBackendAPIException(msg='Failed to delete')
+ raise exception.VolumeBackendAPIException(data='Failed to delete')
elif request_typ == 'DELETE':
del xms_data['volumes'][27]
'initiator': 'iqn.1993-08.org.debian:01:222',
'wwpns': ["123456789012345", "123456789054321"],
'wwnns': ["223456789012345", "223456789054321"],
- 'host': 'fakehost'}
+ 'host': 'fakehost',
+ }
test_volume = {'name': 'vol1',
'size': 1,
'project_id': 'project',
'display_name': 'vol1',
'display_description': 'test volume',
- 'volume_type_id': None}
+ 'volume_type_id': None,
+ 'consistencygroup_id':
+ '192eb39b-6c2f-420c-bae3-3cfd117f0345',
+ }
test_snapshot = D()
test_snapshot.update({'name': 'snapshot1',
'size': 1,
'id': '192eb39b-6c2f-420c-bae3-3cfd117f0002',
'volume_name': 'vol-vol1',
'volume_id': '192eb39b-6c2f-420c-bae3-3cfd117f0001',
- 'project_id': 'project'})
+ 'project_id': 'project',
+ 'consistencygroup_id':
+ '192eb39b-6c2f-420c-bae3-3cfd117f0345',
+ })
test_snapshot.__dict__.update(test_snapshot)
test_volume2 = {'name': 'vol2',
'size': 1,
'project_id': 'project',
'display_name': 'vol2',
'display_description': 'test volume 2',
- 'volume_type_id': None}
+ 'volume_type_id': None,
+ 'consistencygroup_id':
+ '192eb39b-6c2f-420c-bae3-3cfd117f0345',
+ }
test_clone = {'name': 'clone1',
'size': 1,
'volume_name': 'vol3',
'project_id': 'project',
'display_name': 'clone1',
'display_description': 'volume created from snapshot',
- 'volume_type_id': None}
+ 'volume_type_id': None,
+ 'consistencygroup_id':
+ '192eb39b-6c2f-420c-bae3-3cfd117f0345',
+ }
unmanaged1 = {'id': 'unmanaged1',
'name': 'unmanaged1',
- 'size': 3}
+ 'size': 3,
+ }
+ context = {'user': 'admin', }
+ group = {'id': '192eb39b-6c2f-420c-bae3-3cfd117f0345',
+ 'name': 'cg1',
+ 'status': 'OK',
+ }
+ cgsnapshot = mock.Mock(id='192eb39b-6c2f-420c-bae3-3cfd117f9876',
+ consistencygroup_id=group['id'])
+
+ def cgsnap_getitem(self, val):
+ return self.__dict__[val]
+
+ cgsnapshot.__getitem__ = cgsnap_getitem
@mock.patch('cinder.volume.drivers.emc.xtremio.XtremIOClient.req')
class EMCXIODriverISCSITestCase(test.TestCase):
def setUp(self):
super(EMCXIODriverISCSITestCase, self).setUp()
+ clean_xms_data()
- configuration = mock.Mock()
- configuration.san_login = ''
- configuration.san_password = ''
- configuration.san_ip = ''
- configuration.xtremio_cluster_name = ''
- configuration.xtremio_provisioning_factor = 20.0
+ config = mock.Mock()
+ config.san_login = ''
+ config.san_password = ''
+ config.san_ip = ''
+ config.xtremio_cluster_name = 'brick1'
+ config.xtremio_provisioning_factor = 20.0
def safe_get(key):
- getattr(configuration, key)
-
- configuration.safe_get = safe_get
- self.driver = xtremio.XtremIOISCSIDriver(configuration=configuration)
+ getattr(config, key)
+ config.safe_get = safe_get
+ self.driver = xtremio.XtremIOISCSIDriver(configuration=config)
+ self.driver.client = xtremio.XtremIOClient4(config,
+ config
+ .xtremio_cluster_name)
self.data = CommonData()
def test_check_for_setup_error(self, req):
def test_create_extend_delete_volume(self, req):
req.side_effect = xms_request
- clean_xms_data()
self.driver.create_volume(self.data.test_volume)
self.driver.extend_volume(self.data.test_volume, 5)
self.driver.delete_volume(self.data.test_volume)
def test_create_delete_snapshot(self, req):
req.side_effect = xms_request
- clean_xms_data()
self.driver.create_volume(self.data.test_volume)
self.driver.create_snapshot(self.data.test_snapshot)
self.assertEqual(self.data.test_snapshot['id'],
- xms_data['volumes'][3]['name'])
+ xms_data['volumes'][2]['name'])
self.driver.delete_snapshot(self.data.test_snapshot)
self.driver.delete_volume(self.data.test_volume)
def test_failed_rename_snapshot(self, req):
req.side_effect = xms_failed_rename_snapshot_request
- self.driver.create_snapshot(self.data.test_snapshot)
- self.assertIn(27, xms_data['volumes'])
- clean_xms_data()
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.create_snapshot,
+ self.data.test_snapshot)
+ self.assertEqual(0, len(xms_data['volumes']))
def test_volume_from_snapshot(self, req):
req.side_effect = xms_request
- clean_xms_data()
xms_data['volumes'] = {}
self.driver.create_volume(self.data.test_volume)
self.driver.create_snapshot(self.data.test_snapshot)
def test_clone_volume(self, req):
req.side_effect = xms_request
- clean_xms_data()
self.driver.create_volume(self.data.test_volume)
self.driver.create_cloned_volume(self.data.test_clone,
self.data.test_volume)
def test_duplicate_volume(self, req):
req.side_effect = xms_request
- clean_xms_data()
self.driver.create_volume(self.data.test_volume)
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.create_volume, self.data.test_volume)
def test_no_portals_configured(self, req):
req.side_effect = xms_request
- clean_xms_data()
portals = xms_data['iscsi-portals'].copy()
xms_data['iscsi-portals'].clear()
lunmap = {'lun': 4}
def test_initialize_terminate_connection(self, req):
req.side_effect = xms_request
- clean_xms_data()
self.driver.create_volume(self.data.test_volume)
map_data = self.driver.initialize_connection(self.data.test_volume,
self.data.connector)
def test_initialize_connection_bad_ig(self, req):
req.side_effect = xms_bad_request
- clean_xms_data()
self.assertRaises(exception.VolumeBackendAPIException,
self.driver.initialize_connection,
self.data.test_volume,
def test_get_stats(self, req):
req.side_effect = xms_request
- clean_xms_data()
stats = self.driver.get_volume_stats(True)
self.assertEqual(stats['volume_backend_name'],
self.driver.backend_name)
def test_manage_unmanage(self, req):
req.side_effect = xms_request
- clean_xms_data()
- xms_data['volumes'] = {'unmanaged1': {'vol-name': 'unmanaged1',
- 'index': 'unmanaged1',
- 'vol-size': '3'}}
+ xms_data['volumes'] = {1: {'name': 'unmanaged1',
+ 'index': 1,
+ 'vol-size': '3',
+ },
+ }
ref_vol = {"source-name": "unmanaged1"}
invalid_ref = {"source-name": "invalid"}
self.assertRaises(exception.ManageExistingInvalidReference,
self.data.test_volume2)
self.driver.unmanage(self.data.test_volume)
+ @mock.patch('cinder.objects.snapshot.SnapshotList.get_all_for_cgsnapshot')
+ def test_cg_operations(self, get_all_for_cgsnapshot, req):
+ req.side_effect = xms_request
+ d = self.data
+ snapshot_obj = fake_snapshot.fake_snapshot_obj(d.context)
+ snapshot_obj.consistencygroup_id = d.group['id']
+ get_all_for_cgsnapshot.return_value = [snapshot_obj]
+
+ self.driver.create_consistencygroup(d.context, d.group)
+ self.assertEqual(1, len(xms_data['consistency-groups']))
+ self.driver.update_consistencygroup(d.context, d.group,
+ add_volumes=[d.test_volume,
+ d.test_volume2])
+ self.assertEqual(2, len(xms_data['consistency-group-volumes']))
+ self.driver.update_consistencygroup(d.context, d.group,
+ remove_volumes=[d.test_volume2])
+ self.assertEqual(1, len(xms_data['consistency-group-volumes']))
+ self.driver.db = mock.Mock()
+ (self.driver.db.
+ volume_get_all_by_group.return_value) = [mock.MagicMock()]
+ self.driver.create_cgsnapshot(d.context, d.cgsnapshot)
+ snaps_name = self.driver._get_cgsnap_name(d.cgsnapshot)
+ snaps = xms_data['volumes'][1]
+ snaps['index'] = 1
+ xms_data['snapshot-sets'] = {snaps_name: snaps, 1: snaps}
+ self.assertRaises(exception.InvalidInput,
+ self.driver.create_consistencygroup_from_src,
+ d.context, d.group, [])
+ self.driver.delete_cgsnapshot(d.context, d.cgsnapshot)
+ self.driver.delete_consistencygroup(d.context, d.group)
+
@mock.patch('cinder.volume.drivers.emc.xtremio.XtremIOClient.req')
class EMCXIODriverFibreChannelTestCase(test.TestCase):
def setUp(self):
super(EMCXIODriverFibreChannelTestCase, self).setUp()
+ clean_xms_data()
- configuration = mock.Mock()
- configuration.san_login = ''
- configuration.san_password = ''
- configuration.san_ip = ''
- configuration.xtremio_cluster_name = ''
- configuration.xtremio_provisioning_factor = 20.0
+ config = mock.Mock()
+ config.san_login = ''
+ config.san_password = ''
+ config.san_ip = ''
+ config.xtremio_cluster_name = ''
+ config.xtremio_provisioning_factor = 20.0
self.driver = xtremio.XtremIOFibreChannelDriver(
- configuration=configuration)
+ configuration=config)
+ self.driver.client = xtremio.XtremIOClient4(config,
+ config.
+ xtremio_cluster_name)
self.data = CommonData()
def test_initialize_terminate_connection(self, req):
req.side_effect = xms_request
- clean_xms_data()
self.driver.create_volume(self.data.test_volume)
map_data = self.driver.initialize_connection(self.data.test_volume,
self.data.connector)
1.0.3 - update logging level, add translation
1.0.4 - support for FC zones
1.0.5 - add support for XtremIO 4.0
-1.0.6 - add support for iSCSI and CA validation
+1.0.6 - add support for iSCSI multipath, CA validation, consistency groups,
+ R/O snapshots
"""
import json
from cinder import exception
from cinder.i18n import _, _LE, _LI, _LW
+from cinder import objects
from cinder.volume import driver
from cinder.volume.drivers.san import san
from cinder.zonemanager import utils as fczm_utils
"""
raise NotImplementedError()
+ def get_extra_capabilities(self):
+ return {}
+
class XtremIOClient3(XtremIOClient):
def __init__(self, configuration, cluster_id):
super(XtremIOClient4, self).__init__(configuration, cluster_id)
self._cluster_name = None
+ def get_extra_capabilities(self):
+ return {'consistencygroup_support': True}
+
def find_lunmap(self, ig_name, vol_name):
try:
return (self.req('lun-maps',
self.req(typ, 'DELETE', idx=int(idx))
raise
+ def add_vol_to_cg(self, vol_id, cg_id):
+ add_data = {'vol-id': vol_id, 'cg-id': cg_id}
+ self.req('consistency-group-volumes', 'POST', add_data, ver='v2')
+
class XtremIOVolumeDriver(san.SanDriver):
"""Executes commands relating to Volumes."""
data = {'vol-name': volume['id'],
'vol-size': str(volume['size']) + 'g'
}
-
self.client.req('volumes', 'POST', data)
+ if volume.get('consistencygroup_id'):
+ self.client.add_vol_to_cg(volume['id'],
+ volume['consistencygroup_id'])
+
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
self.client.create_snapshot(snapshot.id, volume['id'])
+ if snapshot.get('consistencygroup_id'):
+ self.client.add_vol_to_cg(volume['id'],
+ snapshot['consistencygroup_id'])
+
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
self.client.create_snapshot(src_vref['id'], volume['id'])
+ if volume.get('consistencygroup_id'):
+ self.client.add_vol_to_cg(volume['id'],
+ volume['consistencygroup_id'])
+
def delete_volume(self, volume):
"""Deletes a volume."""
try:
'thick_provisioning_support': False,
'reserved_percentage':
self.configuration.reserved_percentage,
- 'QoS_support': False}
+ 'QoS_support': False,
+ }
+ self._stats.update(self.client.get_extra_capabilities())
def get_volume_stats(self, refresh=False):
"""Get volume stats.
self.client.req('volumes', 'PUT', data, name=volume['id'])
except exception.NotFound:
msg = _("can't find the volume to extend")
- raise (exception.VolumeDriverException(message=msg))
+ raise exception.VolumeDriverException(message=msg)
def check_for_export(self, context, volume_id):
"""Make sure volume is exported."""
def _get_ig(self, connector):
raise NotImplementedError()
+ def create_consistencygroup(self, context, group):
+ """Creates a consistency group.
+
+ :param context: the context
+ :param group: the group object to be created
+ :returns: dict -- modelUpdate = {'status': 'available'}
+ :raises: VolumeBackendAPIException
+ """
+ create_data = {'consistency-group-name': group['id']}
+ self.client.req('consistency-groups', 'POST', data=create_data,
+ ver='v2')
+ return {'status': 'available'}
+
+ def delete_consistencygroup(self, context, group):
+ """Deletes a consistency group."""
+ self.client.req('consistency-groups', 'DELETE', name=group['id'],
+ ver='v2')
+
+ volumes = self.db.volume_get_all_by_group(context, group['id'])
+
+ for volume in volumes:
+ self.delete_volume(volume)
+ volume.status = 'deleted'
+
+ model_update = {'status': group['status']}
+
+ return model_update, volumes
+
+ def create_consistencygroup_from_src(self, context, group, volumes,
+ cgsnapshot=None, snapshots=None):
+ """Creates a consistencygroup from source.
+
+ :param context: the context of the caller.
+ :param group: the dictionary of the consistency group to be created.
+ :param volumes: a list of volume dictionaries in the group.
+ :param cgsnapshot: the dictionary of the cgsnapshot as source.
+ :param snapshots: a list of snapshot dictionaries in the cgsnapshot.
+ :return model_update, volumes_model_update
+ """
+ if cgsnapshot and snapshots:
+ for volume, snapshot in zip(volumes, snapshots):
+ self.create_volume_from_snapshot(volume, snapshot)
+ create_data = {'consistency-group-name': group['id'],
+ 'vol-list': [v['id'] for v in volumes]}
+ self.client.req('consistency-groups', 'POST', data=create_data,
+ ver='v2')
+ else:
+ msg = _("create_consistencygroup_from_src only supports a"
+ " cgsnapshot source, other sources cannot be used.")
+ raise exception.InvalidInput(msg)
+
+ return None, None
+
+ def update_consistencygroup(self, context, group,
+ add_volumes=None, remove_volumes=None):
+ """Updates a consistency group.
+
+ :param context: the context of the caller.
+ :param group: the dictionary of the consistency group to be updated.
+ :param add_volumes: a list of volume dictionaries to be added.
+ :param remove_volumes: a list of volume dictionaries to be removed.
+ :return model_update, add_volumes_update, remove_volumes_update
+ """
+ add_volumes = add_volumes if add_volumes else []
+ remove_volumes = remove_volumes if remove_volumes else []
+ for vol in add_volumes:
+ add_data = {'vol-id': vol['id'], 'cg-id': group['id']}
+ self.client.req('consistency-group-volumes', 'POST', add_data,
+ ver='v2')
+ for vol in remove_volumes:
+ remove_data = {'vol-id': vol['id'], 'cg-id': group['id']}
+ self.client.req('consistency-group-volumes', 'DELETE', remove_data,
+ name=group['id'], ver='v2')
+ return None, None, None
+
+ def _get_cgsnap_name(self, cgsnapshot):
+ return '%(cg)s%(snap)s' % {'cg': cgsnapshot['consistencygroup_id']
+ .replace('-', ''),
+ 'snap': cgsnapshot['id'].replace('-', '')}
+
+ def create_cgsnapshot(self, context, cgsnapshot):
+ """Creates a cgsnapshot."""
+ data = {'consistency-group-id': cgsnapshot['consistencygroup_id'],
+ 'snapshot-set-name': self._get_cgsnap_name(cgsnapshot)}
+ self.client.req('snapshots', 'POST', data, ver='v2')
+
+ snapshots = objects.SnapshotList().get_all_for_cgsnapshot(
+ context, cgsnapshot['id'])
+
+ for snapshot in snapshots:
+ snapshot.status = 'available'
+
+ model_update = {'status': 'available'}
+
+ return model_update, snapshots
+
+ def delete_cgsnapshot(self, context, cgsnapshot):
+ """Deletes a cgsnapshot."""
+ self.client.req('snapshot-sets', 'DELETE',
+ name=self._get_cgsnap_name(cgsnapshot), ver='v2')
+
+ snapshots = objects.SnapshotList().get_all_for_cgsnapshot(
+ context, cgsnapshot['id'])
+
+ for snapshot in snapshots:
+ snapshot.status = 'deleted'
+
+ model_update = {'status': cgsnapshot.status}
+
+ return model_update, snapshots
+
class XtremIOISCSIDriver(XtremIOVolumeDriver, driver.ISCSIDriver):
"""Executes commands relating to ISCSI volumes.