Updated Dell SC support to version 2.1.
Also removed direct DB calls in consistency
groups.
ManageableSnapshotsVD support added.
Updated versions to 2.5.0.
Change-Id: I525bba93a04cc01db92af8711e6b2917e80a93d0
mock_close_connection,
mock_open_connection,
mock_init):
- self.driver.db = mock.Mock()
mock_volume = mock.MagicMock()
expected_volumes = [mock_volume]
- self.driver.db.volume_get_all_by_group.return_value = expected_volumes
context = {}
group = {'id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
'status': fields.ConsistencyGroupStatus.DELETED}
- model_update, volumes = self.driver.delete_consistencygroup(context,
- group,
- [])
+ model_update, volumes = self.driver.delete_consistencygroup(
+ context, group, [mock_volume])
mock_find_replay_profile.assert_called_once_with(group['id'])
mock_delete_replay_profile.assert_called_once_with(self.SCRPLAYPROFILE)
mock_delete_volume.assert_called_once_with(mock_volume)
mock_close_connection,
mock_open_connection,
mock_init):
- self.driver.db = mock.Mock()
- mock_volume = mock.MagicMock()
- expected_volumes = [mock_volume]
- self.driver.db.volume_get_all_by_group.return_value = expected_volumes
context = {}
group = {'id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
'status': fields.ConsistencyGroupStatus.DELETED}
[])
mock_find_replay_profile.assert_called_once_with(group['id'])
self.assertFalse(mock_delete_replay_profile.called)
- mock_delete_volume.assert_called_once_with(mock_volume)
+ self.assertFalse(mock_delete_volume.called)
self.assertEqual(group['status'], model_update['status'])
- self.assertEqual(expected_volumes, volumes)
+ self.assertEqual([], volumes)
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'update_cg_volumes',
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'find_replay_profile',
return_value=SCRPLAYPROFILE)
- @mock.patch('cinder.objects.snapshot.SnapshotList.get_all_for_cgsnapshot')
def test_create_cgsnapshot(self,
- mock_get_all_for_cgsnapshot,
mock_find_replay_profile,
mock_snap_cg_replay,
mock_close_connection,
mock_init):
mock_snapshot = mock.MagicMock()
expected_snapshots = [mock_snapshot]
- mock_get_all_for_cgsnapshot.return_value = (expected_snapshots)
context = {}
cggrp = {'consistencygroup_id': 'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
'id': '100'}
- model_update, snapshots = self.driver.create_cgsnapshot(context, cggrp,
- [])
+ model_update, snapshots = self.driver.create_cgsnapshot(
+ context, cggrp, [mock_snapshot])
mock_find_replay_profile.assert_called_once_with(
cggrp['consistencygroup_id'])
mock_snap_cg_replay.assert_called_once_with(self.SCRPLAYPROFILE,
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'find_replay_profile',
return_value=SCRPLAYPROFILE)
- @mock.patch('cinder.objects.snapshot.SnapshotList.get_all_for_cgsnapshot')
def test_delete_cgsnapshot(self,
- mock_get_all_for_cgsnapshot,
mock_find_replay_profile,
mock_delete_cg_replay,
mock_close_connection,
mock_init):
mock_snapshot = mock.MagicMock()
expected_snapshots = [mock_snapshot]
- mock_get_all_for_cgsnapshot.return_value = (expected_snapshots)
context = {}
cgsnap = {'consistencygroup_id':
'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
'id': '100',
'status': 'deleted'}
- model_update, snapshots = self.driver.delete_cgsnapshot(context,
- cgsnap,
- [])
+ model_update, snapshots = self.driver.delete_cgsnapshot(
+ context, cgsnap, [mock_snapshot])
mock_find_replay_profile.assert_called_once_with(
cgsnap['consistencygroup_id'])
mock_delete_cg_replay.assert_called_once_with(self.SCRPLAYPROFILE,
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'find_replay_profile',
return_value=None)
- @mock.patch('cinder.objects.snapshot.SnapshotList.get_all_for_cgsnapshot')
def test_delete_cgsnapshot_profile_not_found(self,
- mock_get_all_for_cgsnapshot,
mock_find_replay_profile,
mock_delete_cg_replay,
mock_close_connection,
mock_init):
mock_snapshot = mock.MagicMock()
expected_snapshots = [mock_snapshot]
- mock_get_all_for_cgsnapshot.return_value = (expected_snapshots)
context = {}
cgsnap = {'consistencygroup_id':
'fc8f2fec-fab2-4e34-9148-c094c913b9a3',
'id': '100',
'status': 'deleted'}
- model_update, snapshots = self.driver.delete_cgsnapshot(context,
- cgsnap,
- [])
+ model_update, snapshots = self.driver.delete_cgsnapshot(
+ context, cgsnap, [mock_snapshot])
mock_find_replay_profile.assert_called_once_with(
cgsnap['consistencygroup_id'])
self.VOLUME, 'B')
self.assertTrue(res)
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'resume_replication')
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'find_volume',
- return_value=VOLUME)
- @mock.patch.object(dell_storagecenter_iscsi.DellStorageCenterISCSIDriver,
- '_do_repl')
- def test_replication_enable(self,
- mock_do_repl,
- mock_find_volume,
- mock_resume_replication,
- mock_close_connection,
- mock_open_connection,
- mock_init):
- # Note that since we do nothing with sync or async here
- # at all we do not bother testing it.
- mock_do_repl.side_effect = [(False, False), # No run.
- (True, False), # Good run.
- (True, False), # Bad run.
- (True, False), # Multiple replications.
- (True, False)] # Multiple fail.
- mock_resume_replication.side_effect = [True, # Good run.
- False, # Bad run.
- True, # Multiple replications.
- True,
- False] # Multiple fail.
- vref = {'replication_driver_data': '',
- 'id': 'guid'}
- model_update = {}
- # No run
- ret = self.driver.replication_enable({}, vref)
- self.assertEqual(model_update, ret)
- # we didn't try to resume, right?
- self.assertEqual(0, mock_resume_replication.call_count)
- # Good run
- vref = {'replication_driver_data': '12345',
- 'id': 'guid'}
- ret = self.driver.replication_enable({}, vref)
- self.assertEqual(model_update, ret)
- # Hard to distinguish good from bad. Make sure we tried.
- self.assertEqual(1, mock_resume_replication.call_count)
- # Bad run
- model_update = {'replication_status': 'error'}
- ret = self.driver.replication_enable({}, vref)
- self.assertEqual(model_update, ret)
- # Make sure we actually sent this down.
- self.assertEqual(2, mock_resume_replication.call_count)
- mock_resume_replication.assert_called_with(self.VOLUME, 12345)
- # Multiple replications.
- vref = {'replication_driver_data': '12345,67890',
- 'id': 'guid'}
- model_update = {}
- ret = self.driver.replication_enable({}, vref)
- self.assertEqual(model_update, ret)
- # Should be called two more times.
- self.assertEqual(4, mock_resume_replication.call_count)
- # This checks the last call
- mock_resume_replication.assert_called_with(self.VOLUME, 67890)
- # Multiple fail.
- model_update = {'replication_status': 'error'}
- ret = self.driver.replication_enable({}, vref)
- self.assertEqual(model_update, ret)
- # We are set to fail on the first call so one more.
- self.assertEqual(5, mock_resume_replication.call_count)
- # This checks the last call.
- mock_resume_replication.assert_called_with(self.VOLUME, 12345)
-
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'pause_replication')
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'find_volume',
- return_value=VOLUME)
- @mock.patch.object(dell_storagecenter_iscsi.DellStorageCenterISCSIDriver,
- '_do_repl')
- def test_replication_disable(self,
- mock_do_repl,
- mock_find_volume,
- mock_pause_replication,
- mock_close_connection,
- mock_open_connection,
- mock_init):
- # Note that since we do nothing with sync or async here
- # at all we do not bother testing it.
- mock_do_repl.side_effect = [(False, False), # No run.
- (True, False), # Good run.
- (True, False), # Bad run.
- (True, False), # Multiple replications.
- (True, False)] # Multiple fail.
- mock_pause_replication.side_effect = [True, # Good run.
- False, # Bad run.
- True, # Multiple replications.
- True,
- False] # Multiple fail.
- vref = {'replication_driver_data': '',
- 'id': 'guid'}
- model_update = {}
- # No run
- ret = self.driver.replication_disable({}, vref)
- self.assertEqual(model_update, ret)
- # we didn't try to resume, right?
- self.assertEqual(0, mock_pause_replication.call_count)
- # Good run
- vref = {'replication_driver_data': '12345',
- 'id': 'guid'}
- ret = self.driver.replication_disable({}, vref)
- self.assertEqual(model_update, ret)
- # Hard to distinguish good from bad. Make sure we tried.
- self.assertEqual(1, mock_pause_replication.call_count)
- # Bad run
- model_update = {'replication_status': 'error'}
- ret = self.driver.replication_disable({}, vref)
- self.assertEqual(model_update, ret)
- # Make sure we actually sent this down.
- self.assertEqual(2, mock_pause_replication.call_count)
- mock_pause_replication.assert_called_with(self.VOLUME, 12345)
- # Multiple replications.
- vref = {'replication_driver_data': '12345,67890',
- 'id': 'guid'}
- model_update = {}
- ret = self.driver.replication_disable({}, vref)
- self.assertEqual(model_update, ret)
- # Should be called two more times.
- self.assertEqual(4, mock_pause_replication.call_count)
- # This checks the last call
- mock_pause_replication.assert_called_with(self.VOLUME, 67890)
- # Multiple fail.
- model_update = {'replication_status': 'error'}
- ret = self.driver.replication_disable({}, vref)
- self.assertEqual(model_update, ret)
- # We are set to fail on the first call so one more.
- self.assertEqual(5, mock_pause_replication.call_count)
- # This checks the last call.
- mock_pause_replication.assert_called_with(self.VOLUME, 12345)
-
- def test__find_host(self,
- mock_close_connection,
- mock_open_connection,
- mock_init):
- backends = self.driver.backends
- self.driver.backends = [{'target_device_id': '12345',
- 'managed_backend_name': 'host@dell1',
- 'qosnode': 'cinderqos'},
- {'target_device_id': '67890',
- 'managed_backend_name': 'host@dell2',
- 'qosnode': 'cinderqos'}]
- # Just make sure we are turning the correct bit..
- # Good run
- expected = 'host@dell2'
- ret = self.driver._find_host('67890')
- self.assertEqual(expected, ret)
- # Bad run
- ret = self.driver._find_host('54321')
- self.assertIsNone(ret)
- self.driver.backends = backends
-
def test__parse_secondary(self,
mock_close_connection,
mock_open_connection,
mock_init):
backends = self.driver.backends
- vref = {'id': 'guid', 'replication_driver_data': '67890'}
self.driver.backends = [{'target_device_id': '12345',
- 'managed_backend_name': 'host@dell1',
'qosnode': 'cinderqos'},
{'target_device_id': '67890',
- 'managed_backend_name': 'host@dell2',
'qosnode': 'cinderqos'}]
mock_api = mock.MagicMock()
# Good run. Secondary in replication_driver_data and backend. sc up.
- destssn, host = self.driver._parse_secondary(mock_api, vref, '67890')
+ destssn = self.driver._parse_secondary(mock_api, '67890')
self.assertEqual(67890, destssn)
- self.assertEqual('host@dell2', host)
- # Bad run. Secondary not in replication_driver_data
- destssn, host = self.driver._parse_secondary(mock_api, vref, '12345')
- self.assertIsNone(destssn)
- self.assertIsNone(host)
# Bad run. Secondary not in backend.
- vref['replication_driver_data'] = '67891'
- destssn, host = self.driver._parse_secondary(mock_api, vref, '67890')
- self.assertIsNone(destssn)
- self.assertIsNone(host)
- # Bad run. no driver data
- vref['replication_driver_data'] = ''
- destssn, host = self.driver._parse_secondary(mock_api, vref, '67890')
+ destssn = self.driver._parse_secondary(mock_api, '99999')
self.assertIsNone(destssn)
- self.assertIsNone(host)
- # Good run. No secondary selected.
- vref['replication_driver_data'] = '12345'
- destssn, host = self.driver._parse_secondary(mock_api, vref, '12345')
+ # Good run.
+ destssn = self.driver._parse_secondary(mock_api, '12345')
self.assertEqual(12345, destssn)
- self.assertEqual('host@dell1', host)
self.driver.backends = backends
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
mock_open_connection,
mock_init):
backends = self.driver.backends
- vref = {'id': 'guid', 'replication_driver_data': '12345'}
self.driver.backends = [{'target_device_id': '12345',
- 'managed_backend_name': 'host@dell1',
'qosnode': 'cinderqos'},
{'target_device_id': '67890',
- 'managed_backend_name': 'host@dell2',
'qosnode': 'cinderqos'}]
mock_api = mock.MagicMock()
# Bad run. Good selection. SC down.
- vref['replication_driver_data'] = '12345'
mock_api.find_sc = mock.MagicMock(
side_effect=exception.VolumeBackendAPIException(data='1234'))
- destssn, host = self.driver._parse_secondary(mock_api, vref, '12345')
+ destssn = self.driver._parse_secondary(mock_api, '12345')
self.assertIsNone(destssn)
- self.assertIsNone(host)
self.driver.backends = backends
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'break_replication')
@mock.patch.object(dell_storagecenter_iscsi.DellStorageCenterISCSIDriver,
'_parse_secondary')
- @mock.patch.object(dell_storagecenter_iscsi.DellStorageCenterISCSIDriver,
- '_do_repl')
- def test_replication_failover(self,
- mock_do_repl,
- mock_parse_secondary,
- mock_break_replication,
- mock_close_connection,
- mock_open_connection,
- mock_init):
- mock_parse_secondary.side_effect = [(12345, 'host@host#be'), # Good.
- (12345, 'host@host#be'), # Bad.
- (None, None)] # Not found.
- mock_break_replication.side_effect = [True, # Good run.
- False] # Bad run.
- mock_do_repl.side_effect = [(False, False), # No run.
- (True, False), # Good run.
- (True, False), # Bad run.
- (True, False)] # Secondary not found.
- vref = {'id': 'guid'}
+ @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+ 'find_volume')
+ @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+ 'remove_mappings')
+ def test_failover_host(self,
+ mock_remove_mappings,
+ mock_find_volume,
+ mock_parse_secondary,
+ mock_break_replication,
+ mock_close_connection,
+ mock_open_connection,
+ mock_init):
+ self.driver.replication_enabled = False
+ self.driver.failed_over = False
+ volumes = [{'id': 'guid1', 'replication_driver_data': '12345'},
+ {'id': 'guid2', 'replication_driver_data': '12345'}]
# No run. Not doing repl. Should raise.
- self.assertRaises(exception.ReplicationError,
- self.driver.replication_failover,
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.failover_host,
{},
- vref,
+ volumes,
'12345')
# Good run
- expected = {'host': 'host@host#be',
- 'replication_driver_data': None}
- ret = self.driver.replication_failover({}, vref, '12345')
- self.assertEqual(expected, ret)
- # Bad run. (break_replication fails)
- self.assertRaises(exception.ReplicationError,
- self.driver.replication_failover,
- {},
- vref,
- '12345')
+ self.driver.replication_enabled = True
+ mock_parse_secondary.return_value = 12345
+ expected_destssn = 12345
+ expected_volume_update = [{'volume_id': 'guid1', 'updates':
+ {'replication_status': 'failed-over'}},
+ {'volume_id': 'guid2', 'updates':
+ {'replication_status': 'failed-over'}}]
+ destssn, volume_update = self.driver.failover_host(
+ {}, volumes, '12345')
+ self.assertEqual(expected_destssn, destssn)
+ self.assertEqual(expected_volume_update, volume_update)
+ # Good run. Not all volumes replicated.
+ volumes = [{'id': 'guid1', 'replication_driver_data': '12345'},
+ {'id': 'guid2', 'replication_driver_data': ''}]
+ expected_volume_update = [{'volume_id': 'guid1', 'updates':
+ {'replication_status': 'failed-over'}},
+ {'volume_id': 'guid2', 'updates':
+ {'status': 'error'}}]
+ destssn, volume_update = self.driver.failover_host(
+ {}, volumes, '12345')
+ self.assertEqual(expected_destssn, destssn)
+ self.assertEqual(expected_volume_update, volume_update)
+ # Good run. Not all volumes replicated. No replication_driver_data.
+ volumes = [{'id': 'guid1', 'replication_driver_data': '12345'},
+ {'id': 'guid2'}]
+ expected_volume_update = [{'volume_id': 'guid1', 'updates':
+ {'replication_status': 'failed-over'}},
+ {'volume_id': 'guid2', 'updates':
+ {'status': 'error'}}]
+ destssn, volume_update = self.driver.failover_host(
+ {}, volumes, '12345')
+ self.assertEqual(expected_destssn, destssn)
+ self.assertEqual(expected_volume_update, volume_update)
+ # Good run. No volumes replicated. No replication_driver_data.
+ volumes = [{'id': 'guid1'},
+ {'id': 'guid2'}]
+ expected_volume_update = [{'volume_id': 'guid1', 'updates':
+ {'status': 'error'}},
+ {'volume_id': 'guid2', 'updates':
+ {'status': 'error'}}]
+ destssn, volume_update = self.driver.failover_host(
+ {}, volumes, '12345')
+ self.assertEqual(expected_destssn, destssn)
+ self.assertEqual(expected_volume_update, volume_update)
# Secondary not found.
- self.assertRaises(exception.ReplicationError,
- self.driver.replication_failover,
+ mock_parse_secondary.return_value = None
+ self.assertRaises(exception.InvalidInput,
+ self.driver.failover_host,
{},
- vref,
+ volumes,
'54321')
+ # Already failed over.
+ self.driver.failed_over = True
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.failover_host,
+ {},
+ volumes,
+ '12345')
+ self.driver.replication_enabled = False
+
+ def test__get_unmanaged_replay(self,
+ mock_close_connection,
+ mock_open_connection,
+ mock_init):
+ mock_api = mock.MagicMock()
+ existing_ref = None
+ self.assertRaises(exception.ManageExistingInvalidReference,
+ self.driver._get_unmanaged_replay,
+ mock_api,
+ 'guid',
+ existing_ref)
+ existing_ref = {'source-id': 'Not a source-name'}
+ self.assertRaises(exception.ManageExistingInvalidReference,
+ self.driver._get_unmanaged_replay,
+ mock_api,
+ 'guid',
+ existing_ref)
+ existing_ref = {'source-name': 'name'}
+ mock_api.find_volume = mock.MagicMock(return_value=None)
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver._get_unmanaged_replay,
+ mock_api,
+ 'guid',
+ existing_ref)
+ mock_api.find_volume.return_value = {'instanceId': '1'}
+ mock_api.find_replay = mock.MagicMock(return_value=None)
+ self.assertRaises(exception.ManageExistingInvalidReference,
+ self.driver._get_unmanaged_replay,
+ mock_api,
+ 'guid',
+ existing_ref)
+ mock_api.find_replay.return_value = {'instanceId': 2}
+ ret = self.driver._get_unmanaged_replay(mock_api, 'guid', existing_ref)
+ self.assertEqual({'instanceId': 2}, ret)
@mock.patch.object(dell_storagecenter_iscsi.DellStorageCenterISCSIDriver,
- '_do_repl')
- def test_list_replication_targets(self,
- mock_do_repl,
+ '_get_unmanaged_replay')
+ @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+ 'manage_replay')
+ def test_manage_existing_snapshot(self,
+ mock_manage_replay,
+ mock_get_unmanaged_replay,
mock_close_connection,
mock_open_connection,
mock_init):
- mock_do_repl.side_effect = [(False, False), # No repl.
- (True, False), # Good run.
- (True, False)] # Target not found.
- backends = self.driver.backends
- self.driver.backends = [{'target_device_id': '12345',
- 'managed_backend_name': 'host@dell1',
- 'qosnode': 'cinderqos'},
- {'target_device_id': '67890',
- 'managed_backend_name': 'host@dell2',
- 'qosnode': 'cinderqos'}]
- # No repl.
- expected = {'volume_id': 'guid',
- 'targets': []}
- vref = {'replication_driver_data': '',
- 'id': 'guid'}
- ret = self.driver.list_replication_targets({}, vref)
- self.assertEqual(expected, ret)
- # Good run.
- expected = {'volume_id': 'guid',
- 'targets': [{'type': 'managed',
- 'target_device_id': '12345',
- 'backend_name': 'host@dell1'},
- {'type': 'managed',
- 'target_device_id': '67890',
- 'backend_name': 'host@dell2'}]}
- vref = {'replication_driver_data': '12345,67890',
- 'id': 'guid'}
- ret = self.driver.list_replication_targets({}, vref)
- self.assertEqual(expected, ret)
- # Target not found.
- # We find one target but not another. This could happen for a variety
- # of reasons most of them administrator negligence. But the main one
- # is that someone reconfigured their backends without taking into
- # account how this would affect the children.
- expected = {'volume_id': 'guid',
- 'targets': [{'type': 'managed',
- 'target_device_id': '12345',
- 'backend_name': 'host@dell1'}]}
- vref = {'replication_driver_data': '12345,99999',
- 'id': 'guid'}
- ret = self.driver.list_replication_targets({}, vref)
- self.assertEqual(expected, ret)
+ snapshot = {'volume_id': 'guida',
+ 'id': 'guidb'}
+ existing_ref = {'source-name': 'name'}
+ screplay = {'description': 'name'}
+ mock_get_unmanaged_replay.return_value = screplay
+ mock_manage_replay.return_value = True
+ self.driver.manage_existing_snapshot(snapshot, existing_ref)
+ self.assertEqual(1, mock_get_unmanaged_replay.call_count)
+ mock_manage_replay.assert_called_once_with(screplay, 'guidb')
+ mock_manage_replay.return_value = False
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.manage_existing_snapshot,
+ snapshot,
+ existing_ref)
- self.driver.backends = backends
+ @mock.patch.object(dell_storagecenter_iscsi.DellStorageCenterISCSIDriver,
+ '_get_unmanaged_replay')
+ def test_manage_existing_snapshot_get_size(self,
+ mock_get_unmanaged_replay,
+ mock_close_connection,
+ mock_open_connection,
+ mock_init):
+ snapshot = {'volume_id': 'a',
+ 'id': 'b'}
+ existing_ref = {'source-name'}
+ # Good size.
+ mock_get_unmanaged_replay.return_value = {'size':
+ '1.073741824E9 Bytes'}
+ ret = self.driver.manage_existing_snapshot_get_size(snapshot,
+ existing_ref)
+ self.assertEqual(1, ret)
+ # Not on 1GB boundries.
+ mock_get_unmanaged_replay.return_value = {'size':
+ '2.073741824E9 Bytes'}
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.manage_existing_snapshot_get_size,
+ snapshot,
+ existing_ref)
+
+ @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+ 'find_volume')
+ @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+ 'find_replay')
+ @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
+ 'unmanage_replay')
+ def test_unmanage_snapshot(self,
+ mock_unmanage_replay,
+ mock_find_replay,
+ mock_find_volume,
+ mock_close_connection,
+ mock_open_connection,
+ mock_init):
+ snapshot = {'volume_id': 'guida',
+ 'id': 'guidb'}
+ screplay = {'description': 'guidb'}
+ mock_find_volume.return_value = None
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.unmanage_snapshot,
+ snapshot)
+ mock_find_volume.return_value = {'name': 'guida'}
+ mock_find_replay.return_value = None
+ self.assertRaises(exception.VolumeBackendAPIException,
+ self.driver.unmanage_snapshot,
+ snapshot)
+ mock_find_replay.return_value = screplay
+ self.driver.unmanage_snapshot(snapshot)
+ mock_unmanage_replay.assert_called_once_with(screplay)
self.scapi.ssn = self.configuration.dell_sc_ssn
self.scapi.sfname = self.configuration.dell_sc_server_folder
self.scapi.vfname = self.configuration.dell_sc_volume_folder
+ # Note that we set this to True (or not) on the replication tests.
+ self.scapi.failed_over = False
self.volid = str(uuid.uuid4())
self.volume_name = "volume" + self.volid
mock_open_connection,
mock_init):
# Test calling find_volume with result of no volume found
- mock_get_volume_list.side_effect = [[], [], []]
+ mock_get_volume_list.side_effect = [[], []]
res = self.scapi.find_volume(self.volume_name)
self.assertIsNone(res, 'None expected')
mock_close_connection,
mock_open_connection,
mock_init):
+ self.scapi.failed_over = True
mock_get_volume_list.side_effect = [[], [], self.VOLUME_LIST]
res = self.scapi.find_volume(self.volume_name)
self.assertEqual(self.VOLUME, res, 'Unexpected volume')
+ self.scapi.failed_over = False
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'_import_one',
mock_close_connection,
mock_open_connection,
mock_init):
+ self.scapi.failed_over = True
mock_get_volume_list.side_effect = [[], [], self.VOLUME_LIST]
self.assertRaises(exception.VolumeBackendAPIException,
self.scapi.find_volume, self.volume_name)
+ self.scapi.failed_over = False
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'_get_volume_list')
mock_get_volume_list.side_effect = [[],
[],
self.VOLUME_LIST_MULTI_VOLS]
+ self.scapi.failed_over = True
self.assertRaises(exception.VolumeBackendAPIException,
self.scapi.find_volume, self.volume_name)
+ self.scapi.failed_over = False
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'_get_volume_list',
mock_close_connection,
mock_open_connection,
mock_init):
- gb, rem = self.scapi._size_to_gb('1.073741824E9 Byte')
+ gb, rem = self.scapi.size_to_gb('1.073741824E9 Byte')
self.assertEqual(1, gb)
self.assertEqual(0, rem)
self.assertRaises(exception.VolumeBackendAPIException,
- self.scapi._size_to_gb,
+ self.scapi.size_to_gb,
'banana')
- gb, rem = self.scapi._size_to_gb('1.073741924E9 Byte')
+ gb, rem = self.scapi.size_to_gb('1.073741924E9 Byte')
self.assertEqual(1, gb)
self.assertEqual(100, rem)
return_value=[{'configuredSize':
'1.073741824E9 Bytes'}])
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- '_size_to_gb',
+ 'size_to_gb',
return_value=(1, 0))
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'_find_mappings',
return_value=[{'configuredSize':
'1.073741924E9 Bytes'}])
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- '_size_to_gb',
+ 'size_to_gb',
return_value=(1, 100))
def test_manage_existing_bad_size(self,
mock_size_to_gb,
return_value=[{'configuredSize':
'1.073741824E9 Bytes'}])
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- '_size_to_gb',
+ 'size_to_gb',
return_value=(1, 0))
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'_find_mappings',
return_value=[{'configuredSize':
'1.073741824E9 Bytes'}])
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- '_size_to_gb',
+ 'size_to_gb',
return_value=(1, 0))
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'_find_mappings',
return_value=[{'configuredSize':
'1.073741824E9 Bytes'}])
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- '_size_to_gb',
+ 'size_to_gb',
return_value=(1, 0))
def test_get_unmanaged_volume_size(self,
mock_size_to_gb,
return_value=[{'configuredSize':
'1.073741924E9 Bytes'}])
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- '_size_to_gb',
+ 'size_to_gb',
return_value=(1, 100))
def test_get_unmanaged_volume_size_bad_size(self,
mock_size_to_gb,
mock_post.assert_any_call('StorageCenter/ScReplication', payload)
self.assertIsNone(ret)
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'get_screplication',
- return_value=SCREPL)
- @mock.patch.object(dell_storagecenter_api.HttpClient,
- 'post',
- return_value=RESPONSE_200)
- def test_pause_replication(self,
- mock_post,
- mock_get_screplication,
- mock_close_connection,
- mock_open_connection,
- mock_init):
- # Not much to test here without an SC.
- ret = self.scapi.pause_replication(self.VOLUME, 65495)
- self.assertTrue(ret)
-
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'get_screplication',
- return_value=SCREPL)
- @mock.patch.object(dell_storagecenter_api.HttpClient,
- 'post',
- return_value=RESPONSE_400)
- def test_pause_replication_error(self,
- mock_post,
- mock_get_screplication,
- mock_close_connection,
- mock_open_connection,
- mock_init):
- # Not much to test here without an SC.
- ret = self.scapi.pause_replication(self.VOLUME, 65495)
- self.assertFalse(ret)
-
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'get_screplication',
- return_value=None)
- def test_pause_replication_not_found(self,
- mock_get_screplication,
- mock_close_connection,
- mock_open_connection,
- mock_init):
- # Not much to test here without an SC.
- ret = self.scapi.pause_replication(self.VOLUME, 65495)
- self.assertFalse(ret)
-
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'get_screplication',
- return_value=SCREPL)
- @mock.patch.object(dell_storagecenter_api.HttpClient,
- 'post',
- return_value=RESPONSE_200)
- def test_resume_replication(self,
- mock_post,
- mock_get_screplication,
- mock_close_connection,
- mock_open_connection,
- mock_init):
- # Not much to test here without an SC.
- ret = self.scapi.resume_replication(self.VOLUME, 65495)
- self.assertTrue(ret)
-
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'get_screplication',
- return_value=SCREPL)
- @mock.patch.object(dell_storagecenter_api.HttpClient,
- 'post',
- return_value=RESPONSE_400)
- def test_resume_replication_error(self,
- mock_post,
- mock_get_screplication,
- mock_close_connection,
- mock_open_connection,
- mock_init):
- # Not much to test here without an SC.
- ret = self.scapi.resume_replication(self.VOLUME, 65495)
- self.assertFalse(ret)
-
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'get_screplication',
- return_value=None)
- def test_resume_replication_not_found(self,
- mock_get_screplication,
- mock_close_connection,
- mock_open_connection,
- mock_init):
- # Not much to test here without an SC.
- ret = self.scapi.resume_replication(self.VOLUME, 65495)
- self.assertFalse(ret)
-
@mock.patch.object(dell_storagecenter_api.HttpClient,
'post',
return_value=RESPONSE_200)
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'get_screplication')
- @mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- 'rename_volume')
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'find_repl_volume')
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'find_volume')
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
- '_remove_mappings')
+ 'remove_mappings')
def test_break_replication(self,
mock_remove_mappings,
mock_find_volume,
mock_find_repl_volume,
- mock_rename_volume,
mock_get_screplication,
mock_close_connection,
mock_open_connection,
mock_find_volume.side_effect = [self.VOLUME, # 1
self.VOLUME, # 2
None, # 3
- None, # 4
- None] # 5
+ None] # 4
# Much like find volume we do not gate on this.
mock_get_screplication.side_effect = [self.SCREPL[0], # 1
None, # 2
None, # 3
- None, # 4
- None] # 5
+ None] # 4
# This
mock_find_repl_volume.side_effect = [self.VOLUME, # 1
self.VOLUME, # 2
self.VOLUME, # 3
- self.VOLUME, # 4
- None] # 5
+ self.VOLUME] # 4
mock_remove_mappings.side_effect = [True, # 1
True,
True, # 2
False,
True, # 3
True,
- True, # 4
- True,
- False] # 5
+ False] # 4
- mock_rename_volume.side_effect = [True, # 1
- True, # 2
- True, # 3
- False] # 4
# Good path.
ret = self.scapi.break_replication('name', 65495)
self.assertTrue(ret)
- self.assertEqual(1, mock_rename_volume.call_count)
# Source found, screpl not found.
ret = self.scapi.break_replication('name', 65495)
self.assertTrue(ret)
- self.assertEqual(2, mock_rename_volume.call_count)
# No source vol good path.
ret = self.scapi.break_replication('name', 65495)
self.assertTrue(ret)
- self.assertEqual(3, mock_rename_volume.call_count)
- # rename fail
- ret = self.scapi.break_replication('name', 65495)
- self.assertFalse(ret)
- self.assertEqual(4, mock_rename_volume.call_count)
# fail remove mappings
ret = self.scapi.break_replication('name', 65495)
self.assertFalse(ret)
- self.assertEqual(4, mock_rename_volume.call_count)
@mock.patch.object(dell_storagecenter_api.StorageCenterApi,
'_get_user_preferences')
scvol,
'a,b')
+ @mock.patch.object(dell_storagecenter_api.HttpClient,
+ 'put')
+ def test_manage_replay(self,
+ mock_put,
+ mock_close_connection,
+ mock_open_connection,
+ mock_init):
+ screplay = {'description': 'notguid',
+ 'instanceId': 1}
+ payload = {'description': 'guid',
+ 'expireTime': 0}
+ mock_put.return_value = self.RESPONSE_200
+ ret = self.scapi.manage_replay(screplay, 'guid')
+ self.assertTrue(ret)
+ mock_put.assert_called_once_with('StorageCenter/ScReplay/1', payload)
+ mock_put.return_value = self.RESPONSE_400
+ ret = self.scapi.manage_replay(screplay, 'guid')
+ self.assertFalse(ret)
+
+ @mock.patch.object(dell_storagecenter_api.HttpClient,
+ 'put')
+ def test_unmanage_replay(self,
+ mock_put,
+ mock_close_connection,
+ mock_open_connection,
+ mock_init):
+ screplay = {'description': 'guid',
+ 'instanceId': 1}
+ payload = {'expireTime': 1440}
+ mock_put.return_value = self.RESPONSE_200
+ ret = self.scapi.unmanage_replay(screplay)
+ self.assertTrue(ret)
+ mock_put.assert_called_once_with('StorageCenter/ScReplay/1', payload)
+ mock_put.return_value = self.RESPONSE_400
+ ret = self.scapi.unmanage_replay(screplay)
+ self.assertFalse(ret)
+
class DellSCSanAPIConnectionTestCase(test.TestCase):
connection to the Dell REST API.
"""
- def __init__(self, config):
+ def __init__(self, config, active_backend_id):
self.config = config
+ # Now that active_backend_id is set on failover.
+ # Use that if set. Mark the backend as failed over.
+ self.active_backend_id = active_backend_id
+ self.ssn = self.config.dell_sc_ssn
def open_connection(self):
"""Creates the StorageCenterApi object.
:raises: VolumeBackendAPIException
"""
connection = None
- ssn = self.config.dell_sc_ssn
LOG.info(_LI('open_connection to %(ssn)s at %(ip)s'),
- {'ssn': ssn,
+ {'ssn': self.ssn,
'ip': self.config.san_ip})
- if ssn:
+ if self.ssn:
"""Open connection to REST API."""
connection = StorageCenterApi(self.config.san_ip,
self.config.dell_sc_api_port,
# This instance is for a single backend. That backend has a
# few items of information we should save rather than passing them
# about.
- connection.ssn = ssn
connection.vfname = self.config.dell_sc_volume_folder
connection.sfname = self.config.dell_sc_server_folder
+ # Set appropriate ssn and failover state.
+ if self.active_backend_id:
+ connection.ssn = self.active_backend_id
+ connection.failed_over = True
+ else:
+
+ connection.ssn = self.ssn
+ connection.failed_over = False
+ # Open connection.
connection.open_connection()
else:
raise exception.VolumeBackendAPIException(
2.3.0 - Added Legacy Port Mode Support
2.3.1 - Updated error handling.
2.4.0 - Added Replication V2 support.
+ 2.4.1 - Updated Replication support to V2.1.
+ 2.5.0 - ManageableSnapshotsVD implemented.
"""
- APIVERSION = '2.4.0'
+ APIVERSION = '2.5.0'
def __init__(self, host, port, user, password, verify):
"""This creates a connection to Dell SC or EM.
"""
self.notes = 'Created by Dell Cinder Driver'
self.repl_prefix = 'Cinder repl of '
- self.failover_prefix = 'Cinder failover '
self.ssn = None
+ self.failed_over = False
self.vfname = 'openstack'
self.sfname = 'openstack'
self.legacypayloadfilters = False
for the volume first. If not found it searches the entire array for
the volume.
+ Remember that in the case of a failover we have already been switched
+ to our new SSN. So the initial searches are valid.
+
:param name: Name of the volume to search for. This is the cinder
volume ID.
:returns: Dell Volume object or None if not found.
{'n': name,
'v': self.vfname})
vollist = self._get_volume_list(name, None, False)
- # Failover Check.
- # If an empty list was returned then either there is no such volume
- # or we are in a failover state. Look for failover volume.
- if not vollist:
+ # If we found nothing and are failed over then we might not have
+ # completed our replication failover. Look for the replication
+ # volume. We are already pointing at that SC.
+ if not vollist and self.failed_over:
LOG.debug('Unable to locate volume. Checking for failover.')
- # Get our failover name.
- fn = self._failover_name(name)
+ # Get our replay name.
+ fn = self._repl_name(name)
vollist = self._get_volume_list(fn, None, False)
# Same deal as the rest of these. If 0 not found. If greater than
# one we have multiple copies and cannot return a valid result.
if len(vollist) == 1:
- # So we are in failover. Rename the volume and move it to our
- # volume folder.
LOG.info(_LI('Found failover volume. Competing failover.'))
# Import our found volume. This completes our failover.
scvolume = self._import_one(vollist[0], name)
{'fail': fn,
'guid': name})
return scvolume
- msg = _('Unable to complete import of %s.') % fn
+ msg = _('Unable to complete failover of %s.') % fn
raise exception.VolumeBackendAPIException(data=msg)
# If multiple volumes of the same name are found we need to error.
# 201 expected.
if self._check_result(r):
# Server was created
- LOG.info(_LI('SC server created %s'), scserver)
scserver = self._first_result(r)
+ LOG.info(_LI('SC server created %s'), scserver)
# Add hba to our server
if scserver is not None:
return None
+ def manage_replay(self, screplay, replayid):
+ """Basically renames the screplay and sets it to never expire.
+
+ :param screplay: DellSC object.
+ :param replayid: New name for replay.
+ :return: True on success. False on fail.
+ """
+ if screplay and replayid:
+ payload = {}
+ payload['description'] = replayid
+ payload['expireTime'] = 0
+ r = self.client.put('StorageCenter/ScReplay/%s' %
+ self._get_id(screplay),
+ payload)
+ if self._check_result(r):
+ return True
+ LOG.error(_LE('Error managing replay %s'),
+ screplay.get('description'))
+ return False
+
+ def unmanage_replay(self, screplay):
+ """Basically sets the expireTime
+
+ :param screplay: DellSC object.
+ :return: True on success. False on fail.
+ """
+ if screplay:
+ payload = {}
+ payload['expireTime'] = 1440
+ r = self.client.put('StorageCenter/ScReplay/%s' %
+ self._get_id(screplay),
+ payload)
+ if self._check_result(r):
+ return True
+ LOG.error(_LE('Error unmanaging replay %s'),
+ screplay.get('description'))
+ return False
+
def delete_replay(self, scvolume, replayid):
"""Finds a Dell replay by replayid string and expires it.
' for Consistency Group support')
raise NotImplementedError(data=msg)
- def _size_to_gb(self, spacestring):
+ @staticmethod
+ def size_to_gb(spacestring):
"""Splits a SC size string into GB and a remainder.
Space is returned in a string like ...
if count == 1:
# First thing to check is if the size is something we can
# work with.
- sz, rem = self._size_to_gb(vollist[0]['configuredSize'])
+ sz, rem = self.size_to_gb(vollist[0]['configuredSize'])
if rem > 0:
raise exception.VolumeBackendAPIException(
data=_('Volume size must multiple of 1 GB.'))
count = len(vollist)
# If we found one volume with that name we can work with it.
if count == 1:
- sz, rem = self._size_to_gb(vollist[0]['configuredSize'])
+ sz, rem = self.size_to_gb(vollist[0]['configuredSize'])
if rem > 0:
raise exception.VolumeBackendAPIException(
data=_('Volume size must multiple of 1 GB.'))
def _repl_name(self, name):
return self.repl_prefix + name
- def _failover_name(self, name):
- return self.failover_prefix + name
-
def _get_disk_folder(self, ssn, foldername):
# TODO(tswanson): Harden this.
diskfolder = None
'destsc': destssn})
return screpl
- def pause_replication(self, scvolume, destssn):
- # destssn should probably be part of the object.
- replication = self.get_screplication(scvolume, destssn)
- if replication:
- r = self.client.post('StorageCenter/ScReplication/%s/Pause' %
- self._get_id(replication), {})
- if self._check_result(r):
- return True
- return False
-
- def resume_replication(self, scvolume, destssn):
- # destssn should probably be part of the object.
- replication = self.get_screplication(scvolume, destssn)
- if replication:
- r = self.client.post('StorageCenter/ScReplication/%s/Resume' %
- self._get_id(replication), {})
- if self._check_result(r):
- return True
- return False
-
def find_repl_volume(self, guid, destssn, instance_id=None):
+ """Find our replay destination volume on the destssn.
+
+ :param guid: Volume ID.
+ :param destssn: Where to look for the volume.
+ :param instance_id: If we know our exact volume ID use that.
+ :return: SC Volume object or None
+ """
# Do a normal volume search.
pf = self._get_payload_filter()
pf.append('scSerialNumber', destssn)
pf.append('instanceId', instance_id)
else:
# Try the name.
- pf.append('Name', self.repl_prefix + guid)
+ pf.append('Name', self._repl_name(guid))
r = self.client.post('StorageCenter/ScVolume/GetList',
pf.payload)
if self._check_result(r):
return volumes[0]
return None
- def _remove_mappings(self, scvol):
+ def remove_mappings(self, scvol):
"""Peels all the mappings off of scvol.
:param scvol:
self._get_id(scvol),
{})
return self._check_result(r)
- return None
+ return False
def break_replication(self, volumename, destssn):
"""This just breaks the replication.
every time this goes south.
:param volumename:
- :param destssn:
- :return: True False
+ :return:
"""
ret = False
replid = None
# stuffing it into the recycle bin.
# Instead we try to unmap the destination volume which will break
# the replication but leave the replication object on the SC.
- ret = self._remove_mappings(screplvol)
+ ret = self.remove_mappings(screplvol)
# If the volume is free of replication.
if ret:
- # Move and rename it.
- ret = self.rename_volume(screplvol,
- self._failover_name(volumename))
# Try to kill mappings on the source.
# We don't care that this succeeded or failed. Just move on.
- self._remove_mappings(scvolume)
+ self.remove_mappings(scvolume)
return ret
from oslo_utils import excutils
from cinder import exception
-from cinder import objects
from cinder.i18n import _, _LE, _LI, _LW
from cinder.volume import driver
from cinder.volume.drivers.dell import dell_storagecenter_api
class DellCommonDriver(driver.ConsistencyGroupVD, driver.ManageableVD,
- driver.ExtendVD,
+ driver.ExtendVD, driver.ManageableSnapshotsVD,
driver.SnapshotVD, driver.BaseVD):
def __init__(self, *args, **kwargs):
self.backends = self.configuration.safe_get('replication_device')
self.replication_enabled = True if self.backends else False
self.is_direct_connect = False
+ self.active_backend_id = kwargs.get('active_backend_id', None)
+ self.failed_over = (self.active_backend_id is not None)
def _bytes_to_gb(self, spacestring):
"""Space is returned in a string like ...
specific helpers.
"""
self._client = dell_storagecenter_api.StorageCenterApiHelper(
- self.configuration)
+ self.configuration, self.active_backend_id)
def check_for_setup_error(self):
"""Validates the configuration information."""
'not supported with direct connect.')
raise exception.InvalidHost(reason=msg)
- if self.replication_enabled:
+ # If we are a healthy replicated system make sure our backend
+ # is alive.
+ if self.replication_enabled and not self.failed_over:
# Check that our replication destinations are available.
- # TODO(tswanson): Check if we need a diskfolder. (Or not.)
- # TODO(tswanson): Can we check that the backend specifies
- # TODO(tswanson): the same ssn as target_device_id.
for backend in self.backends:
replssn = backend['target_device_id']
try:
"""
do_repl = False
sync = False
- if not self.is_direct_connect:
+ # Repl does not work with direct connect.
+ if not self.failed_over and not self.is_direct_connect:
specs = self._get_volume_extra_specs(volume)
do_repl = specs.get('replication_enabled') == '<is> True'
sync = specs.get('replication_type') == '<in> sync'
return model_update
- def _split(self, replication_driver_data):
+ def _split_driver_data(self, replication_driver_data):
+ """Splits the replication_driver_data into an array of ssn strings.
+
+ :param replication_driver_data: A string of comma separated SSNs.
+ :returns: SSNs in an array of strings.
+ """
ssnstrings = []
+ # We have any replication_driver_data.
if replication_driver_data:
+ # Split the array and wiffle through the entries.
for str in replication_driver_data.split(','):
+ # Strip any junk from the string.
ssnstring = str.strip()
+ # Anything left?
if ssnstring:
+ # Add it to our array.
ssnstrings.append(ssnstring)
return ssnstrings
scvol = api.find_volume(volume_name)
replication_driver_data = volume.get('replication_driver_data')
# This is just a string of ssns separated by commas.
- ssnstrings = self._split(replication_driver_data)
+ ssnstrings = self._split_driver_data(replication_driver_data)
# Trundle through these and delete them all.
for ssnstring in ssnstrings:
ssn = int(ssnstring)
if self.replication_enabled:
data['replication_type'] = ['async', 'sync']
data['replication_count'] = len(self.backends)
+ replication_targets = []
+ # Trundle through our backends.
+ for backend in self.backends:
+ target_device_id = backend.get('target_device_id')
+ if target_device_id:
+ replication_targets.append(target_device_id)
+ data['replication_targets'] = replication_targets
self._stats = data
LOG.debug('Total cap %(total)s Free cap %(free)s',
# If we are here because we found no profile that should be fine
# as we are trying to delete it anyway.
- # Now whack the volumes. So get our list.
- volumes = self.db.volume_get_all_by_group(context, gid)
# Trundle through the list deleting the volumes.
for volume in volumes:
self.delete_volume(volume)
:param context: the context of the caller.
:param cgsnapshot: Information about the snapshot to take.
+ :param snapshots: List of snapshots for this cgsnapshot.
:returns: Updated model_update, snapshots.
:raises: VolumeBackendAPIException.
"""
if profile:
LOG.debug('profile %s replayid %s', profile, snapshotid)
if api.snap_cg_replay(profile, snapshotid, 0):
- snapshots = objects.SnapshotList().get_all_for_cgsnapshot(
- context, snapshotid)
for snapshot in snapshots:
snapshot.status = 'available'
% snapshotid)
raise exception.VolumeBackendAPIException(data=msg)
- snapshots = objects.SnapshotList().get_all_for_cgsnapshot(
- context, snapshotid)
for snapshot in snapshots:
snapshot.status = 'deleted'
return model_update
return True
- def replication_enable(self, context, vref):
- """Re-enable replication on vref.
-
- :param context: NA
- :param vref: Cinder volume reference.
- :return: model_update.
- """
- volumename = vref.get('id')
- LOG.info(_LI('Enabling replication on %s'), volumename)
- model_update = {}
- with self._client.open_connection() as api:
- replication_driver_data = vref.get('replication_driver_data')
- destssns = self._split(replication_driver_data)
- do_repl, sync = self._do_repl(api, vref)
- if destssns and do_repl:
- scvolume = api.find_volume(volumename)
- if scvolume:
- for destssn in destssns:
- if not api.resume_replication(scvolume, int(destssn)):
- LOG.error(_LE('Unable to resume replication on '
- 'volume %(vol)s to SC %(ssn)s'),
- {'vol': volumename,
- 'ssn': destssn})
- model_update['replication_status'] = 'error'
- break
- else:
- LOG.error(_LE('Volume %s not found'), volumename)
- else:
- LOG.error(_LE('Replication not enabled or no replication '
- 'destinations found. %s'),
- volumename)
- return model_update
-
- def replication_disable(self, context, vref):
- """Disable replication on vref.
-
- :param context: NA
- :param vref: Cinder volume reference.
- :return: model_update.
- """
- volumename = vref.get('id')
- LOG.info(_LI('Disabling replication on %s'), volumename)
- model_update = {}
- with self._client.open_connection() as api:
- replication_driver_data = vref.get('replication_driver_data')
- destssns = self._split(replication_driver_data)
- do_repl, sync = self._do_repl(api, vref)
- if destssns and do_repl:
- scvolume = api.find_volume(volumename)
- if scvolume:
- for destssn in destssns:
- if not api.pause_replication(scvolume, int(destssn)):
- LOG.error(_LE('Unable to pause replication on '
- 'volume %(vol)s to SC %(ssn)s'),
- {'vol': volumename,
- 'ssn': destssn})
- model_update['replication_status'] = 'error'
- break
- else:
- LOG.error(_LE('Volume %s not found'), volumename)
- else:
- LOG.error(_LE('Replication not enabled or no replication '
- 'destinations found. %s'),
- volumename)
- return model_update
-
- def _find_host(self, ssnstring):
- """Find the backend associated with this ssnstring.
-
- :param ssnstring: The ssn of the storage center we are looking for.
- :return: The managed_backend_name associated with said storage center.
- """
- for backend in self.backends:
- if ssnstring == backend['target_device_id']:
- return backend['managed_backend_name']
- return None
-
- def _parse_secondary(self, api, vref, secondary):
+ def _parse_secondary(self, api, secondary):
"""Find the replication destination associated with secondary.
:param api: Dell StorageCenterApi
- :param vref: Cinder Volume
:param secondary: String indicating the secondary to failover to.
- :return: Destination SSN and the host string for the given secondary.
+ :return: Destination SSN for the given secondary.
"""
LOG.debug('_parse_secondary. Looking for %s.', secondary)
- replication_driver_data = vref['replication_driver_data']
destssn = None
- host = None
- ssnstrings = self._split(replication_driver_data)
- # Trundle through these and delete them all.
- for ssnstring in ssnstrings:
+ # Trundle through these looking for our secondary.
+ for backend in self.backends:
+ ssnstring = backend['target_device_id']
# If they list a secondary it has to match.
# If they do not list a secondary we return the first
# replication on a working system.
# way to pick a destination to failover to. So just
# look for one that is just up.
try:
- # If the SC ssn exists check if we are configured to
- # use it.
+ # If the SC ssn exists use it.
if api.find_sc(ssn):
- host = self._find_host(ssnstring)
- # If host then we are configured.
- if host:
- # Save our ssn and get out of here.
- destssn = ssn
- break
+ destssn = ssn
+ break
except exception.VolumeBackendAPIException:
LOG.warning(_LW('SSN %s appears to be down.'), ssn)
- LOG.info(_LI('replication failover secondary is %(ssn)s %(host)s'),
- {'ssn': destssn,
- 'host': host})
- return destssn, host
+ LOG.info(_LI('replication failover secondary is %(ssn)s'),
+ {'ssn': destssn})
+ return destssn
- def replication_failover(self, context, vref, secondary):
+ def failover_host(self, context, volumes, secondary_id=None):
"""Failover to secondary.
- The flow is as follows.
- 1.The user explicitly requests a failover of a replicated volume.
- 2.Driver breaks replication.
- a. Neatly by deleting the SCReplication object if the
- primary is still up.
- b. Brutally by unmapping the replication volume if it isn't.
- 3.We rename the volume to "Cinder failover <Volume GUID>"
- 4.Change Cinder DB entry for which backend controls the volume
- to the backend listed in the replication_device.
- 5.That's it.
-
- Completion of the failover is done on first use on the new backend.
- We do this by modifying the find_volume function.
-
- Find volume searches the following places in order:
- 1. "<Volume GUID>" in the backend's volume folder.
- 2. "<Volume GUID>" outside of the volume folder.
- 3. "Cinder failover <Volume GUID>" anywhere on the system.
-
- If "Cinder failover <Volume GUID>" was found:
- 1.Volume is renamed to "<Volume GUID>".
- 2.Volume is moved to the new backend's volume folder.
- 3.The volume is now available on the secondary backend.
-
- :param context;
- :param vref: Cinder volume reference.
- :param secondary: SSN of the destination Storage Center
- :return: model_update on failover.
+ :param context: security context
+ :param secondary_id: Specifies rep target to fail over to
+ :param volumes: List of volumes serviced by this backend.
+ :returns : destssn, volume_updates data structure
+
+ Example volume_updates data structure:
+ [{'volume_id': <cinder-uuid>,
+ 'updates': {'provider_id': 8,
+ 'replication_status': 'failed-over',
+ 'replication_extended_status': 'whatever',...}},]
"""
- LOG.info(_LI('Failing replication %(vol)s to %(sec)s'),
- {'vol': vref.get('id'),
- 'sec': secondary})
- # If we fall through this is our error.
- msg = _('Unable to failover replication.')
- with self._client.open_connection() as api:
- # Basic check. We should never get here.
- do_repl, sync = self._do_repl(api, vref)
- if not do_repl:
- # If we did get here then there is a disconnect. Set our
- # message and raise (below).
- msg = _('Unable to failover unreplicated volume.')
- else:
+
+ # We do not allow failback. Dragons be there.
+ if self.failed_over:
+ raise exception.VolumeBackendAPIException(message=_(
+ 'Backend has already been failed over. Unable to fail back.'))
+
+ LOG.info(_LI('Failing backend to %s'), secondary_id)
+ # basic check
+ if self.replication_enabled:
+ with self._client.open_connection() as api:
# Look for the specified secondary.
- destssn, host = self._parse_secondary(api, vref, secondary)
- if destssn and host:
- volumename = vref.get('id')
- # This will break the replication on the SC side. At the
- # conclusion of this the destination volume will be
- # renamed to indicate failover is in progress. We will
- # pick the volume up on the destination backend later.
- if api.break_replication(volumename, destssn):
+ destssn = self._parse_secondary(api, secondary_id)
+ if destssn:
+ # We roll through trying to break replications.
+ # Is failing here a complete failure of failover?
+ volume_updates = []
+ for volume in volumes:
model_update = {}
- model_update['host'] = host
- model_update['replication_driver_data'] = None
- return model_update
- # We are here. Nothing went well.
- LOG.error(_LE('Unable to break replication from '
- '%(from)s to %(to)d.'),
- {'from': volumename,
- 'to': destssn})
+ if volume.get('replication_driver_data'):
+ ret = api.break_replication(volume['id'], destssn)
+ LOG.info(_LI('Failing over volume %(id)s '
+ 'replication: %(res)s.'),
+ {'id': volume['id'],
+ 'res': ('FAILED', 'SUCCESS')[ret]})
+ # We should note that we are now failed over.
+ model_update = {
+ 'replication_status': 'failed-over'}
+ else:
+ # Not a replicated volume. Try to unmap it.
+ scvolume = api.find_volume(volume['id'])
+ api.remove_mappings(scvolume)
+ model_update = {'status': 'error'}
+ # Either we are failed over or our status is now error.
+ volume_updates.append({'volume_id': volume['id'],
+ 'updates': model_update})
+
+ # this is it.
+ return destssn, volume_updates
else:
- LOG.error(_LE('Unable to find valid destination.'))
+ raise exception.InvalidInput(message=(
+ _('replication_failover failed. %s not found.') %
+ secondary_id))
+ # I don't think we should ever get here.
+ raise exception.VolumeBackendAPIException(message=(
+ _('replication_failover failed. '
+ 'Backend not configured for failover')))
+
+ def _get_unmanaged_replay(self, api, volume_name, existing_ref):
+ replay_name = None
+ if existing_ref:
+ replay_name = existing_ref.get('source-name')
+ if not replay_name:
+ msg = _('_get_unmanaged_replay: Must specify source-name.')
+ LOG.error(msg)
+ raise exception.ManageExistingInvalidReference(
+ existing_ref=existing_ref, reason=msg)
+ # Find our volume.
+ scvolume = api.find_volume(volume_name)
+ if not scvolume:
+ # Didn't find it.
+ msg = (_('_get_unmanaged_replay: Cannot find volume id %s')
+ % volume_name)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ # Find our replay.
+ screplay = api.find_replay(scvolume, replay_name)
+ if not screplay:
+ # Didn't find it. Reference must be invalid.
+ msg = (_('_get_unmanaged_replay: Cannot '
+ 'find snapshot named %s') % replay_name)
+ LOG.error(msg)
+ raise exception.ManageExistingInvalidReference(
+ existing_ref=existing_ref, reason=msg)
+ return screplay
+
+ def manage_existing_snapshot(self, snapshot, existing_ref):
+ """Brings an existing backend storage object under Cinder management.
+
+ existing_ref is passed straight through from the API request's
+ manage_existing_ref value, and it is up to the driver how this should
+ be interpreted. It should be sufficient to identify a storage object
+ that the driver should somehow associate with the newly-created cinder
+ snapshot structure.
- # We raise to indicate something bad happened.
- raise exception.ReplicationError(volume_id=vref.get('id'),
- reason=msg)
+ There are two ways to do this:
- def list_replication_targets(self, context, vref):
- """Lists replication targets for the given vref.
+ 1. Rename the backend storage object so that it matches the
+ snapshot['name'] which is how drivers traditionally map between a
+ cinder snapshot and the associated backend storage object.
- We return targets the volume has been setup to replicate to and that
- are configured on this backend.
+ 2. Place some metadata on the snapshot, or somewhere in the backend,
+ that allows other driver requests (e.g. delete) to locate the
+ backend storage object when required.
- :param context: NA
- :param vref: Cinder volume object.
- :return: A dict of the form {'volume_id': id,
- 'targets': [ {'type': xxx,
- 'target_device_id': xxx,
- 'backend_name': xxx}]}
+ If the existing_ref doesn't make sense, or doesn't refer to an existing
+ backend storage object, raise a ManageExistingInvalidReference
+ exception.
"""
- LOG.debug('list_replication_targets for volume %s', vref.get('id'))
- targets = []
+ volume_name = snapshot.get('volume_id')
+ snapshot_id = snapshot.get('id')
with self._client.open_connection() as api:
- do_repl, sync = self._do_repl(api, vref)
- # If we have no replication_driver_data then we have no replication
- # targets
- replication_driver_data = vref.get('replication_driver_data')
- ssnstrings = self._split(replication_driver_data)
- # If we have data.
- if ssnstrings:
- # Trundle through our backends.
- for backend in self.backends:
- # If we find a backend then we report it.
- if ssnstrings.count(backend['target_device_id']):
- target = {}
- target['type'] = 'managed'
- target['target_device_id'] = (
- backend['target_device_id'])
- target['backend_name'] = (
- backend['managed_backend_name'])
- targets.append(target)
- else:
- # We note if the source is not replicated to a
- # configured destination for the backend.
- LOG.info(_LI('Volume %(guid)s not replicated to '
- 'backend %(name)s'),
- {'guid': vref['id'],
- 'name': backend['managed_backend_name']})
- # At this point we note that what we found and what we
- # expected to find were two different things.
- if len(ssnstrings) != len(targets):
- LOG.warning(_LW('Expected replication count %(rdd)d does '
- 'match configured replication count '
- '%(tgt)d.'),
- {'rdd': len(ssnstrings),
- 'tgt': len(targets)})
- # Format response.
- replication_targets = {'volume_id': vref.get('id'), 'targets': targets}
- LOG.info(_LI('list_replication_targets: %s'), replication_targets)
- return replication_targets
+ # Find our unmanaged snapshot. This will raise on error.
+ screplay = self._get_unmanaged_replay(api, volume_name,
+ existing_ref)
+ # Manage means update description and update expiration.
+ if not api.manage_replay(screplay, snapshot_id):
+ # That didn't work. Error.
+ msg = (_('manage_existing_snapshot: Error managing '
+ 'existing replay %(ss)s on volume %(vol)s') %
+ {'ss': screplay.get('description'),
+ 'vol': volume_name})
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+
+ # Life is good. Let the world know what we've done.
+ LOG.info(_LI('manage_existing_snapshot: snapshot %(exist)s on '
+ 'volume %(volume)s has been renamed to %(id)s and is '
+ 'now managed by Cinder.'),
+ {'exist': screplay.get('description'),
+ 'volume': volume_name,
+ 'id': snapshot_id})
+
+ # NOTE: Can't use abstractmethod before all drivers implement it
+ def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
+ """Return size of snapshot to be managed by manage_existing.
+
+ When calculating the size, round up to the next GB.
+ """
+ volume_name = snapshot.get('volume_id')
+ with self._client.open_connection() as api:
+ screplay = self._get_unmanaged_replay(api, volume_name,
+ existing_ref)
+ sz, rem = dell_storagecenter_api.StorageCenterApi.size_to_gb(
+ screplay['size'])
+ if rem > 0:
+ raise exception.VolumeBackendAPIException(
+ data=_('Volume size must be a multiple of 1 GB.'))
+ return sz
+
+ # NOTE: Can't use abstractmethod before all drivers implement it
+ def unmanage_snapshot(self, snapshot):
+ """Removes the specified snapshot from Cinder management.
+
+ Does not delete the underlying backend storage object.
+
+ NOTE: We do set the expire countdown to 1 day. Once a snapshot is
+ unmanaged it will expire 24 hours later.
+ """
+ volume_name = snapshot.get('volume_id')
+ snapshot_id = snapshot.get('id')
+ with self._client.open_connection() as api:
+ # Find our volume.
+ scvolume = api.find_volume(volume_name)
+ if not scvolume:
+ # Didn't find it.
+ msg = (_('unmanage_snapshot: Cannot find volume id %s')
+ % volume_name)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ # Find our replay.
+ screplay = api.find_replay(scvolume, snapshot_id)
+ if not screplay:
+ # Didn't find it. Reference must be invalid.
+ msg = (_('unmanage_snapshot: Cannot find snapshot named %s')
+ % snapshot_id)
+ LOG.error(msg)
+ raise exception.VolumeBackendAPIException(data=msg)
+ # Free our snapshot.
+ api.unmanage_replay(screplay)
+ # Do not check our result.
2.3.0 - Added Legacy Port Mode Support
2.3.1 - Updated error handling.
2.4.0 - Added Replication V2 support.
+ 2.4.1 - Updated Replication support to V2.1.
+ 2.5.0 - ManageableSnapshotsVD implemented.
"""
- VERSION = '2.4.0'
+ VERSION = '2.5.0'
def __init__(self, *args, **kwargs):
super(DellStorageCenterFCDriver, self).__init__(*args, **kwargs)
2.3.0 - Added Legacy Port Mode Support
2.3.1 - Updated error handling.
2.4.0 - Added Replication V2 support.
+ 2.4.1 - Updated Replication support to V2.1.
+ 2.5.0 - ManageableSnapshotsVD implemented.
"""
- VERSION = '2.4.0'
+ VERSION = '2.5.0'
def __init__(self, *args, **kwargs):
super(DellStorageCenterISCSIDriver, self).__init__(*args, **kwargs)
self.backend_name = (
- self.configuration.safe_get('volume_backend_name')
- or 'Dell-iSCSI')
+ self.configuration.safe_get('volume_backend_name') or 'Dell-iSCSI')
def initialize_connection(self, volume, connector):
# Initialize_connection will find or create a server identified by the
+++ /dev/null
----
-features:
- - Added replication v2 support to the Dell Storage Center drivers.
--- /dev/null
+---
+features:
+ - Added replication v2.1 support to the Dell Storage Center drivers.