diff --git a/cinder/tests/unit/test_hpe3par.py b/cinder/tests/unit/test_hpe3par.py index 3eabe366f0f..747b17952ba 100644 --- a/cinder/tests/unit/test_hpe3par.py +++ b/cinder/tests/unit/test_hpe3par.py @@ -112,10 +112,10 @@ class HPE3PARBaseDriver(object): CGSNAPSHOT_BASE_NAME = 'oss-6Rxe1druToSHJByeMeeh8g' CLIENT_ID = "12345" REPLICATION_CLIENT_ID = "54321" + REPLICATION_BACKEND_ID = 'target' # fake host on the 3par FAKE_HOST = 'fakehost' FAKE_CINDER_HOST = 'fakehost@foo#' + HPE3PAR_CPG - FAKE_FAILOVER_HOST = 'fakefailover@foo#destfakepool' USER_ID = '2689d9a913974c008b1d859013f23607' PROJECT_ID = 'fac88235b9d64685a3530f73e490348f' VOLUME_ID_SNAP = '761fc5e5-5191-4ec7-aeba-33e36de44156' @@ -170,7 +170,7 @@ class HPE3PARBaseDriver(object): 'volume_type': 'replicated', 'volume_type_id': VOLUME_TYPE_ID_REPLICATED} - replication_targets = [{'target_device_id': 'target', + replication_targets = [{'backend_id': REPLICATION_BACKEND_ID, 'cpg_map': HPE3PAR_CPG_MAP, 'hpe3par_api_url': 'https://1.1.1.1/api/v1', 'hpe3par_username': HPE3PAR_USER_NAME, @@ -180,23 +180,9 @@ class HPE3PARBaseDriver(object): 'san_password': HPE3PAR_USER_PASS, 'san_ssh_port': HPE3PAR_SAN_SSH_PORT, 'ssh_conn_timeout': HPE3PAR_SAN_SSH_CON_TIMEOUT, - 'san_private_key': HPE3PAR_SAN_SSH_PRIVATE, - 'managed_backend_name': FAKE_FAILOVER_HOST}] + 'san_private_key': HPE3PAR_SAN_SSH_PRIVATE}] - list_rep_targets = [{'target_device_id': 'target'}] - - replication_devs_unmgd = [{'target_device_id': 'target', - 'cpg_map': HPE3PAR_CPG_MAP, - 'hpe3par_api_url': 'https://1.1.1.1/api/v1', - 'hpe3par_username': HPE3PAR_USER_NAME, - 'hpe3par_password': HPE3PAR_USER_PASS, - 'san_ip': HPE3PAR_SAN_IP, - 'san_login': HPE3PAR_USER_NAME, - 'san_password': HPE3PAR_USER_PASS, - 'san_ssh_port': HPE3PAR_SAN_SSH_PORT, - 'ssh_conn_timeout': HPE3PAR_SAN_SSH_CON_TIMEOUT, - 'san_private_key': HPE3PAR_SAN_SSH_PRIVATE, - 'managed_backend_name': None}] + list_rep_targets = [{'backend_id': 'target'}] volume_encrypted = {'name': VOLUME_NAME, 'id': VOLUME_ID, @@ -976,8 +962,7 @@ class HPE3PARBaseDriver(object): self.assertIsNone(return_model) @mock.patch.object(volume_types, 'get_volume_type') - def test_create_volume_replicated_managed_periodic(self, - _mock_volume_types): + def test_create_volume_replicated_periodic(self, _mock_volume_types): # setup_mock_client drive with default configuration # and return the mock HTTP 3PAR client conf = self.setup_configuration() @@ -1020,7 +1005,7 @@ class HPE3PARBaseDriver(object): "qos": {}, "type": "OpenStack"}) - target_device_id = self.replication_targets[0]['target_device_id'] + backend_id = self.replication_targets[0]['backend_id'] expected = [ mock.call.createVolume( self.VOLUME_3PAR_NAME, @@ -1035,7 +1020,7 @@ class HPE3PARBaseDriver(object): mock.call.createRemoteCopyGroup( self.RCG_3PAR_NAME, [{'userCPG': HPE3PAR_CPG_REMOTE, - 'targetName': target_device_id, + 'targetName': backend_id, 'mode': PERIODIC_MODE, 'snapCPG': HPE3PAR_CPG_REMOTE}], {'localUserCPG': HPE3PAR_CPG, @@ -1044,12 +1029,12 @@ class HPE3PARBaseDriver(object): self.RCG_3PAR_NAME, self.VOLUME_3PAR_NAME, [{'secVolumeName': self.VOLUME_3PAR_NAME, - 'targetName': target_device_id}], + 'targetName': backend_id}], optional={'volumeAutoCreation': True}), mock.call.modifyRemoteCopyGroup( self.RCG_3PAR_NAME, {'targets': [{'syncPeriod': SYNC_PERIOD, - 'targetName': target_device_id}]}), + 'targetName': backend_id}]}), mock.call.startRemoteCopy(self.RCG_3PAR_NAME)] mock_client.assert_has_calls( self.get_id_login + @@ -1062,8 +1047,68 @@ class HPE3PARBaseDriver(object): return_model) @mock.patch.object(volume_types, 'get_volume_type') - def test_create_volume_replicated_managed_sync(self, - _mock_volume_types): + def test_delete_volume_replicated_failedover(self, _mock_volume_types): + # setup_mock_client drive with default configuration + # and return the mock HTTP 3PAR client + conf = self.setup_configuration() + self.replication_targets[0]['replication_mode'] = 'periodic' + conf.replication_device = self.replication_targets + mock_client = self.setup_driver(config=conf) + mock_client.getStorageSystemInfo.return_value = ( + {'id': self.CLIENT_ID}) + mock_client.getRemoteCopyGroup.return_value = ( + {'targets': [{'targetName': 'tgt'}]}) + mock_client.getCPG.return_value = {'domain': None} + mock_replicated_client = self.setup_driver(config=conf) + mock_replicated_client.getStorageSystemInfo.return_value = ( + {'id': self.REPLICATION_CLIENT_ID}) + + _mock_volume_types.return_value = { + 'name': 'replicated', + 'extra_specs': { + 'replication_enabled': ' True', + 'replication:mode': 'periodic', + 'replication:sync_period': '900', + 'volume_type': self.volume_type_replicated}} + + with mock.patch.object( + hpecommon.HPE3PARCommon, + '_create_client') as mock_create_client, \ + mock.patch.object( + hpecommon.HPE3PARCommon, + '_create_replication_client') as mock_replication_client: + mock_create_client.return_value = mock_client + mock_replication_client.return_value = mock_replicated_client + + volume = self.volume_replicated.copy() + volume['replication_status'] = 'failed-over' + self.driver.delete_volume(volume) + + rcg_name = self.RCG_3PAR_NAME + ".r" + self.CLIENT_ID + expected = [ + mock.call.getRemoteCopyGroup(rcg_name), + mock.call.toggleRemoteCopyConfigMirror( + 'tgt', + mirror_config=False), + mock.call.stopRemoteCopy(rcg_name), + mock.call.removeVolumeFromRemoteCopyGroup( + rcg_name, + self.VOLUME_3PAR_NAME, + removeFromTarget=True), + mock.call.removeRemoteCopyGroup(rcg_name), + mock.call.deleteVolume(self.VOLUME_3PAR_NAME), + mock.call.toggleRemoteCopyConfigMirror( + 'tgt', + mirror_config=True)] + mock_client.assert_has_calls( + self.get_id_login + + self.standard_logout + + self.standard_login + + expected + + self.standard_logout) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_create_volume_replicated_sync(self, _mock_volume_types): # setup_mock_client drive with default configuration # and return the mock HTTP 3PAR client conf = self.setup_configuration() @@ -1105,7 +1150,7 @@ class HPE3PARBaseDriver(object): "qos": {}, "type": "OpenStack"}) - target_device_id = self.replication_targets[0]['target_device_id'] + backend_id = self.replication_targets[0]['backend_id'] expected = [ mock.call.createVolume( self.VOLUME_3PAR_NAME, @@ -1120,7 +1165,7 @@ class HPE3PARBaseDriver(object): mock.call.createRemoteCopyGroup( self.RCG_3PAR_NAME, [{'userCPG': HPE3PAR_CPG_REMOTE, - 'targetName': target_device_id, + 'targetName': backend_id, 'mode': SYNC_MODE, 'snapCPG': HPE3PAR_CPG_REMOTE}], {'localUserCPG': HPE3PAR_CPG, @@ -1129,178 +1174,7 @@ class HPE3PARBaseDriver(object): self.RCG_3PAR_NAME, self.VOLUME_3PAR_NAME, [{'secVolumeName': self.VOLUME_3PAR_NAME, - 'targetName': target_device_id}], - optional={'volumeAutoCreation': True}), - mock.call.startRemoteCopy(self.RCG_3PAR_NAME)] - mock_client.assert_has_calls( - self.get_id_login + - self.standard_logout + - self.standard_login + - expected + - self.standard_logout) - self.assertEqual({'replication_status': 'enabled', - 'provider_location': self.CLIENT_ID}, - return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_create_volume_replicated_unmanaged_periodic(self, - _mock_volume_types): - # setup_mock_client drive with default configuration - # and return the mock HTTP 3PAR client - conf = self.setup_configuration() - self.replication_devs_unmgd[0]['replication_mode'] = 'periodic' - conf.replication_device = self.replication_devs_unmgd - mock_client = self.setup_driver(config=conf) - mock_client.getStorageSystemInfo.return_value = {'id': self.CLIENT_ID} - mock_client.getRemoteCopyGroup.side_effect = hpeexceptions.HTTPNotFound - mock_client.getCPG.return_value = {'domain': None} - mock_replicated_client = self.setup_driver(config=conf) - mock_replicated_client.getStorageSystemInfo.return_value = ( - {'id': self.REPLICATION_CLIENT_ID}) - - _mock_volume_types.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'cpg': HPE3PAR_CPG, - 'snap_cpg': HPE3PAR_CPG_SNAP, - 'replication_enabled': ' True', - 'replication:mode': 'periodic', - 'replication:sync_period': '900', - 'volume_type': self.volume_type_replicated}} - - with mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_client') as mock_create_client, \ - mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_replication_client') as mock_replication_client: - mock_create_client.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - return_model = self.driver.create_volume(self.volume_replicated) - comment = Comment({ - "volume_type_name": "replicated", - "display_name": "Foo Volume", - "name": "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7", - "volume_type_id": "be9181f1-4040-46f2-8298-e7532f2bf9db", - "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7", - "qos": {}, - "type": "OpenStack"}) - - target_device_id = self.replication_targets[0]['target_device_id'] - expected = [ - mock.call.getCPG(HPE3PAR_CPG), - mock.call.createVolume( - self.VOLUME_3PAR_NAME, - HPE3PAR_CPG, - 2048, { - 'comment': comment, - 'tpvv': True, - 'tdvv': False, - 'snapCPG': HPE3PAR_CPG_SNAP}), - mock.call.getRemoteCopyGroup(self.RCG_3PAR_NAME), - mock.call.getCPG(HPE3PAR_CPG), - mock.call.getCPG(HPE3PAR_CPG), - mock.call.createRemoteCopyGroup( - self.RCG_3PAR_NAME, - [{'userCPG': HPE3PAR_CPG_REMOTE, - 'targetName': target_device_id, - 'mode': PERIODIC_MODE, - 'snapCPG': HPE3PAR_CPG_REMOTE}], - {'localUserCPG': HPE3PAR_CPG, - 'localSnapCPG': HPE3PAR_CPG_SNAP}), - mock.call.addVolumeToRemoteCopyGroup( - self.RCG_3PAR_NAME, - self.VOLUME_3PAR_NAME, - [{'secVolumeName': self.VOLUME_3PAR_NAME, - 'targetName': target_device_id}], - optional={'volumeAutoCreation': True}), - mock.call.modifyRemoteCopyGroup( - self.RCG_3PAR_NAME, - {'targets': [{'syncPeriod': SYNC_PERIOD, - 'targetName': target_device_id}]}), - mock.call.startRemoteCopy(self.RCG_3PAR_NAME)] - mock_client.assert_has_calls( - self.get_id_login + - self.standard_logout + - self.standard_login + - expected + - self.standard_logout) - self.assertEqual({'replication_status': 'enabled', - 'provider_location': self.CLIENT_ID}, - return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_create_volume_replicated_unmanaged_sync(self, - _mock_volume_types): - # setup_mock_client drive with default configuration - # and return the mock HTTP 3PAR client - conf = self.setup_configuration() - self.replication_devs_unmgd[0]['replication_mode'] = 'sync' - conf.replication_device = self.replication_devs_unmgd - mock_client = self.setup_driver(config=conf) - mock_client.getStorageSystemInfo.return_value = {'id': self.CLIENT_ID} - mock_client.getRemoteCopyGroup.side_effect = hpeexceptions.HTTPNotFound - mock_client.getCPG.return_value = {'domain': None} - mock_replicated_client = self.setup_driver(config=conf) - mock_replicated_client.getStorageSystemInfo.return_value = ( - {'id': self.REPLICATION_CLIENT_ID}) - - _mock_volume_types.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'cpg': HPE3PAR_CPG, - 'snap_cpg': HPE3PAR_CPG_SNAP, - 'replication_enabled': ' True', - 'replication:mode': 'sync', - 'volume_type': self.volume_type_replicated}} - - with mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_client') as mock_create_client, \ - mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_replication_client') as mock_replication_client: - mock_create_client.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - return_model = self.driver.create_volume(self.volume_replicated) - comment = Comment({ - "volume_type_name": "replicated", - "display_name": "Foo Volume", - "name": "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7", - "volume_type_id": "be9181f1-4040-46f2-8298-e7532f2bf9db", - "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7", - "qos": {}, - "type": "OpenStack"}) - - target_device_id = self.replication_targets[0]['target_device_id'] - expected = [ - mock.call.getCPG(HPE3PAR_CPG), - mock.call.createVolume( - self.VOLUME_3PAR_NAME, - HPE3PAR_CPG, - 2048, { - 'comment': comment, - 'tpvv': True, - 'tdvv': False, - 'snapCPG': HPE3PAR_CPG_SNAP}), - mock.call.getRemoteCopyGroup(self.RCG_3PAR_NAME), - mock.call.getCPG(HPE3PAR_CPG), - mock.call.getCPG(HPE3PAR_CPG), - mock.call.createRemoteCopyGroup( - self.RCG_3PAR_NAME, - [{'userCPG': HPE3PAR_CPG_REMOTE, - 'targetName': target_device_id, - 'mode': SYNC_MODE, - 'snapCPG': HPE3PAR_CPG_REMOTE}], - {'localUserCPG': HPE3PAR_CPG, - 'localSnapCPG': HPE3PAR_CPG_SNAP}), - mock.call.addVolumeToRemoteCopyGroup( - self.RCG_3PAR_NAME, - self.VOLUME_3PAR_NAME, - [{'secVolumeName': self.VOLUME_3PAR_NAME, - 'targetName': target_device_id}], + 'targetName': backend_id}], optional={'volumeAutoCreation': True}), mock.call.startRemoteCopy(self.RCG_3PAR_NAME)] mock_client.assert_has_calls( @@ -4336,371 +4210,9 @@ class HPE3PARBaseDriver(object): self.standard_logout) @mock.patch.object(volume_types, 'get_volume_type') - def test_replication_enable_not_in_rcopy(self, _mock_volume_types): - # Managed vs. unmanaged and periodic vs. sync are not relevant when - # enabling/disabling replication and listing replication targets. - # We will use managed and periodic as the default. - conf = self.setup_configuration() - self.replication_targets[0]['replication_mode'] = 'periodic' - conf.replication_device = self.replication_targets - mock_client = self.setup_driver(config=conf) - mock_client.getStorageSystemInfo.return_value = ( - {'id': self.CLIENT_ID}) - mock_client.getRemoteCopyGroup.side_effect = ( - hpeexceptions.HTTPNotFound) - mock_client.getCPG.return_value = {'domain': None} - mock_replicated_client = self.setup_driver(config=conf) - mock_replicated_client.getStorageSystemInfo.return_value = ( - {'id': self.REPLICATION_CLIENT_ID}) - - _mock_volume_types.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'cpg': HPE3PAR_CPG, - 'snap_cpg': HPE3PAR_CPG_SNAP, - 'replication_enabled': ' True', - 'replication:mode': 'periodic', - 'replication:sync_period': '900', - 'volume_type': self.volume_type_replicated}} - - with mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_client') as mock_create_client, \ - mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_replication_client') as mock_replication_client: - mock_create_client.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - return_model = self.driver.replication_enable( - context.get_admin_context(), - self.volume_replicated) - - target_device_id = self.replication_targets[0]['target_device_id'] - expected = [ - mock.call.getRemoteCopyGroup(self.RCG_3PAR_NAME), - mock.call.getCPG(HPE3PAR_CPG), - mock.call.getCPG(HPE3PAR_CPG), - mock.call.createRemoteCopyGroup( - self.RCG_3PAR_NAME, - [{'userCPG': HPE3PAR_CPG_REMOTE, - 'targetName': target_device_id, - 'mode': PERIODIC_MODE, - 'snapCPG': HPE3PAR_CPG_REMOTE}], - {'localUserCPG': HPE3PAR_CPG, - 'localSnapCPG': HPE3PAR_CPG_SNAP}), - mock.call.addVolumeToRemoteCopyGroup( - self.RCG_3PAR_NAME, - self.VOLUME_3PAR_NAME, - [{'secVolumeName': self.VOLUME_3PAR_NAME, - 'targetName': target_device_id}], - optional={'volumeAutoCreation': True}), - mock.call.modifyRemoteCopyGroup( - self.RCG_3PAR_NAME, - {'targets': [{'syncPeriod': SYNC_PERIOD, - 'targetName': target_device_id}]}), - mock.call.startRemoteCopy(self.RCG_3PAR_NAME)] - mock_client.assert_has_calls( - self.get_id_login + - self.standard_logout + - self.standard_login + - expected + - self.standard_logout) - self.assertEqual({'replication_status': 'enabled', - 'provider_location': self.CLIENT_ID}, - return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_replication_enable_in_rcopy(self, _mock_volume_types): - # Managed vs. unmanaged and periodic vs. sync are not relevant when - # enabling/disabling replication and listing replication targets. - # We will use managed and periodic as the default. - conf = self.setup_configuration() - self.replication_targets[0]['replication_mode'] = 'periodic' - conf.replication_device = self.replication_targets - mock_client = self.setup_driver(config=conf) - mock_client.getStorageSystemInfo.return_value = ( - {'id': self.CLIENT_ID}) - mock_replicated_client = self.setup_driver(config=conf) - mock_replicated_client.getStorageSystemInfo.return_value = ( - {'id': self.REPLICATION_CLIENT_ID}) - - _mock_volume_types.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True', - 'replication:mode': 'periodic', - 'replication:sync_period': '900', - 'volume_type': self.volume_type_replicated}} - - with mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_client') as mock_create_client, \ - mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_replication_client') as mock_replication_client: - mock_create_client.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - return_model = self.driver.replication_enable( - context.get_admin_context(), - self.volume_replicated) - - expected = [ - mock.call.getRemoteCopyGroup(self.RCG_3PAR_NAME), - mock.call.startRemoteCopy(self.RCG_3PAR_NAME)] - mock_client.assert_has_calls( - self.get_id_login + - self.standard_logout + - self.standard_login + - expected + - self.standard_logout) - self.assertEqual({'replication_status': 'enabled', - 'provider_location': self.CLIENT_ID}, - return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_replication_enable_non_replicated_type(self, _mock_volume_types): - # Managed vs. unmanaged and periodic vs. sync are not relevant when - # enabling/disabling replication and listing replication targets. - # We will use managed and periodic as the default. - conf = self.setup_configuration() - self.replication_targets[0]['replication_mode'] = 'periodic' - conf.replication_device = self.replication_targets - mock_client = self.setup_driver(config=conf) - - _mock_volume_types.return_value = { - 'name': 'NOT_replicated', - 'extra_specs': { - 'volume_type': self.volume_type}} - - with mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_client') as mock_create_client: - mock_create_client.return_value = mock_client - - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.replication_enable, - context.get_admin_context(), - self.volume_replicated) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_replication_disable(self, _mock_volume_types): - # Managed vs. unmanaged and periodic vs. sync are not relevant when - # enabling/disabling replication and listing replication targets. - # We will use managed and periodic as the default. - conf = self.setup_configuration() - self.replication_targets[0]['replication_mode'] = 'periodic' - conf.replication_device = self.replication_targets - mock_client = self.setup_driver(config=conf) - mock_client.getStorageSystemInfo.return_value = ( - {'id': self.CLIENT_ID}) - mock_replicated_client = self.setup_driver(config=conf) - mock_replicated_client.getStorageSystemInfo.return_value = ( - {'id': self.REPLICATION_CLIENT_ID}) - - _mock_volume_types.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True', - 'replication:mode': 'periodic', - 'replication:sync_period': '900', - 'volume_type': self.volume_type_replicated}} - - with mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_client') as mock_create_client, \ - mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_replication_client') as mock_replication_client: - mock_create_client.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - return_model = self.driver.replication_disable( - context.get_admin_context(), - self.volume_replicated) - - expected = [ - mock.call.stopRemoteCopy(self.RCG_3PAR_NAME)] - mock_client.assert_has_calls( - self.get_id_login + - self.standard_logout + - self.standard_login + - expected + - self.standard_logout) - self.assertEqual({'replication_status': 'disabled'}, - return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_replication_disable_fail(self, _mock_volume_types): - # Managed vs. unmanaged and periodic vs. sync are not relevant when - # enabling/disabling replication and listing replication targets. - # We will use managed and periodic as the default. - conf = self.setup_configuration() - self.replication_targets[0]['replication_mode'] = 'periodic' - conf.replication_device = self.replication_targets - mock_client = self.setup_driver(config=conf) - mock_client.stopRemoteCopy.side_effect = ( - Exception("Error: Remote Copy could not be stopped.")) - mock_client.getStorageSystemInfo.return_value = ( - {'id': self.CLIENT_ID}) - mock_replicated_client = self.setup_driver(config=conf) - mock_replicated_client.getStorageSystemInfo.return_value = ( - {'id': self.REPLICATION_CLIENT_ID}) - - _mock_volume_types.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True', - 'replication:mode': 'periodic', - 'replication:sync_period': '900', - 'volume_type': self.volume_type_replicated}} - - with mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_client') as mock_create_client, \ - mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_replication_client') as mock_replication_client: - mock_create_client.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - return_model = self.driver.replication_disable( - context.get_admin_context(), - self.volume_replicated) - - expected = [ - mock.call.stopRemoteCopy(self.RCG_3PAR_NAME)] - mock_client.assert_has_calls( - self.get_id_login + - self.standard_logout + - self.standard_login + - expected + - self.standard_logout) - self.assertEqual({'replication_status': 'disable_failed'}, - return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_replication_disable_non_replicated_type(self, _mock_volume_types): - # Managed vs. unmanaged and periodic vs. sync are not relevant when - # enabling/disabling replication and listing replication targets. - # We will use managed and periodic as the default. - conf = self.setup_configuration() - self.replication_targets[0]['replication_mode'] = 'periodic' - conf.replication_device = self.replication_targets - mock_client = self.setup_driver(config=conf) - - _mock_volume_types.return_value = { - 'name': 'NOT_replicated', - 'extra_specs': { - 'volume_type': self.volume_type}} - - with mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_client') as mock_create_client: - mock_create_client.return_value = mock_client - - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.replication_disable, - context.get_admin_context(), - self.volume_replicated) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_list_replication_targets(self, _mock_volume_types): - # Managed vs. unmanaged and periodic vs. sync are not relevant when - # enabling/disabling replication and listing replication targets. - # We will use managed and periodic as the default. - target_device_id = self.replication_targets[0]['target_device_id'] - conf = self.setup_configuration() - self.replication_targets[0]['replication_mode'] = 'periodic' - conf.replication_device = self.replication_targets - mock_client = self.setup_driver(config=conf) - mock_client.getRemoteCopyGroup.return_value = ( - {'targets': [{'targetName': target_device_id}]}) - mock_client.getStorageSystemInfo.return_value = ( - {'id': self.CLIENT_ID}) - mock_client.getCPG.return_value = {'domain': None} - mock_replicated_client = self.setup_driver(config=conf) - mock_replicated_client.getStorageSystemInfo.return_value = ( - {'id': self.REPLICATION_CLIENT_ID}) - - _mock_volume_types.return_value = { - 'name': 'replicated', - 'extra_specs': { - 'replication_enabled': ' True', - 'replication:mode': 'periodic', - 'replication:sync_period': '900', - 'volume_type': self.volume_type_replicated}} - - with mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_client') as mock_create_client, \ - mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_replication_client') as mock_replication_client: - mock_create_client.return_value = mock_client - mock_replication_client.return_value = mock_replicated_client - - return_model = self.driver.list_replication_targets( - context.get_admin_context(), - self.volume_replicated) - - expected = [ - mock.call.getRemoteCopyGroup(self.RCG_3PAR_NAME)] - mock_client.assert_has_calls( - self.get_id_login + - self.standard_logout + - self.standard_login + - expected + - self.standard_logout) - - targets = self.list_rep_targets - self.assertEqual({'volume_id': self.volume_replicated['id'], - 'targets': targets}, - return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_list_replication_targets_non_replicated_type(self, - _mock_volume_types): - # Managed vs. unmanaged and periodic vs. sync are not relevant when - # enabling/disabling replication and listing replication targets. - # We will use managed and periodic as the default. - conf = self.setup_configuration() - self.replication_targets[0]['replication_mode'] = 'periodic' - conf.replication_device = self.replication_targets - mock_client = self.setup_driver(config=conf) - mock_client.getStorageSystemInfo.return_value = ( - {'id': self.CLIENT_ID}) - - _mock_volume_types.return_value = { - 'name': 'NOT_replicated', - 'extra_specs': { - 'volume_type': self.volume_type}} - - with mock.patch.object( - hpecommon.HPE3PARCommon, - '_create_client') as mock_create_client: - mock_create_client.return_value = mock_client - - return_model = self.driver.list_replication_targets( - context.get_admin_context(), - self.volume_replicated) - - mock_client.assert_has_calls( - self.get_id_login + - self.standard_logout + - self.standard_login + - self.standard_logout) - - self.assertEqual([], return_model) - - @mock.patch.object(volume_types, 'get_volume_type') - def test_replication_failover_managed(self, _mock_volume_types): + def test_failover_host(self, _mock_volume_types): # periodic vs. sync is not relevant when conducting a failover. We # will just use periodic. - provider_location = self.CLIENT_ID + ":" + self.REPLICATION_CLIENT_ID conf = self.setup_configuration() self.replication_targets[0]['replication_mode'] = 'periodic' conf.replication_device = self.replication_targets @@ -4727,33 +4239,36 @@ class HPE3PARBaseDriver(object): '_create_replication_client') as mock_replication_client: mock_create_client.return_value = mock_client mock_replication_client.return_value = mock_replicated_client - valid_target_device_id = ( - self.replication_targets[0]['target_device_id']) - invalid_target_device_id = 'INVALID' + valid_backend_id = ( + self.replication_targets[0]['backend_id']) + invalid_backend_id = 'INVALID' - # test invalid secondary target + volumes = [self.volume_replicated] + # Test invalid secondary target. self.assertRaises( exception.VolumeBackendAPIException, - self.driver.replication_failover, + self.driver.failover_host, context.get_admin_context(), - self.volume_replicated, - invalid_target_device_id) + volumes, + invalid_backend_id) - # test no secondary target + # Test no secondary target. self.assertRaises( exception.VolumeBackendAPIException, - self.driver.replication_failover, + self.driver.failover_host, context.get_admin_context(), - self.volume_replicated, + volumes, None) - # test a successful failover - volume = self.volume_replicated - volume['provider_location'] = self.CLIENT_ID - return_model = self.driver.replication_failover( + # Test a successful failover. + expected_model = (self.REPLICATION_BACKEND_ID, + [{'updates': {'replication_status': + 'failed-over'}, + 'volume_id': self.VOLUME_ID}]) + return_model = self.driver.failover_host( context.get_admin_context(), - volume, - valid_target_device_id) + volumes, + valid_backend_id) expected = [ mock.call.stopRemoteCopy(self.RCG_3PAR_NAME)] mock_client.assert_has_calls( @@ -4762,31 +4277,16 @@ class HPE3PARBaseDriver(object): self.standard_login + expected + self.standard_logout) - self.assertEqual({'replication_status': 'inactive', - 'provider_location': provider_location, - 'host': self.FAKE_FAILOVER_HOST}, - return_model) - - # test a unsuccessful failover - mock_replicated_client.recoverRemoteCopyGroupFromDisaster.\ - side_effect = ( - exception.VolumeBackendAPIException( - "Error: Failover was unsuccessful.")) - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.replication_failover, - context.get_admin_context(), - self.volume_replicated, - valid_target_device_id) + self.assertEqual(expected_model, return_model) @mock.patch.object(volume_types, 'get_volume_type') - def test_replication_failover_unmanaged(self, _mock_volume_types): - # periodic vs. sync is not relevant when conducting a failover. We - # will just use periodic. - provider_location = self.CLIENT_ID + ":" + self.REPLICATION_CLIENT_ID + def test_replication_failback_ready(self, _mock_volume_types): + # Managed vs. unmanaged and periodic vs. sync are not relevant when + # failing back a volume. + # We will use managed and periodic as the default. conf = self.setup_configuration() - self.replication_devs_unmgd[0]['replication_mode'] = 'periodic' - conf.replication_device = self.replication_devs_unmgd + self.replication_targets[0]['replication_mode'] = 'periodic' + conf.replication_device = self.replication_targets mock_client = self.setup_driver(config=conf) mock_client.getStorageSystemInfo.return_value = ( {'id': self.CLIENT_ID}) @@ -4810,56 +4310,65 @@ class HPE3PARBaseDriver(object): '_create_replication_client') as mock_replication_client: mock_create_client.return_value = mock_client mock_replication_client.return_value = mock_replicated_client - valid_target_device_id = ( - self.replication_targets[0]['target_device_id']) - invalid_target_device_id = 'INVALID' - # test invalid secondary target + # Test a successful fail-back. + volume = self.volume_replicated.copy() + volume['replication_status'] = 'failed-over' + return_model = self.driver.failover_host( + context.get_admin_context(), + [volume], + 'default') + expected_model = (None, + [{'updates': {'replication_status': + 'available'}, + 'volume_id': self.VOLUME_ID}]) + self.assertEqual(expected_model, return_model) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_replication_failback_not_ready(self, _mock_volume_types): + # Managed vs. unmanaged and periodic vs. sync are not relevant when + # failing back a volume. + # We will use managed and periodic as the default. + conf = self.setup_configuration() + self.replication_targets[0]['replication_mode'] = 'periodic' + conf.replication_device = self.replication_targets + mock_client = self.setup_driver(config=conf) + mock_client.getStorageSystemInfo.return_value = ( + {'id': self.CLIENT_ID}) + mock_replicated_client = self.setup_driver(config=conf) + mock_replicated_client.getStorageSystemInfo.return_value = ( + {'id': self.REPLICATION_CLIENT_ID}) + + _mock_volume_types.return_value = { + 'name': 'replicated', + 'extra_specs': { + 'replication_enabled': ' True', + 'replication:mode': 'periodic', + 'replication:sync_period': '900', + 'volume_type': self.volume_type_replicated}} + + with mock.patch.object( + hpecommon.HPE3PARCommon, + '_create_client') as mock_create_client, \ + mock.patch.object( + hpecommon.HPE3PARCommon, + '_create_replication_client') as mock_replication_client: + mock_create_client.return_value = mock_client + mock_client.getRemoteCopyGroup.side_effect = ( + exception.VolumeBackendAPIException( + "Error: Remote Copy Group not Ready.")) + mock_replication_client.return_value = mock_replicated_client + + # Test an unsuccessful fail-back. + volume = self.volume_replicated.copy() + volume['replication_status'] = 'failed-over' + self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.replication_failover, + exception.VolumeDriverException, + self.driver.failover_host, context.get_admin_context(), - self.volume_replicated, - invalid_target_device_id) - - # test no secondary target - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.replication_failover, - context.get_admin_context(), - self.volume_replicated, - None) - - # test a successful failover - volume = self.volume_replicated - volume['provider_location'] = self.CLIENT_ID - return_model = self.driver.replication_failover( - context.get_admin_context(), - volume, - valid_target_device_id) - expected = [ - mock.call.stopRemoteCopy(self.RCG_3PAR_NAME)] - mock_client.assert_has_calls( - self.get_id_login + - self.standard_logout + - self.standard_login + - expected + - self.standard_logout) - self.assertEqual({'replication_status': 'inactive', - 'provider_location': provider_location}, - return_model) - - # test a unsuccessful failover - mock_replicated_client.recoverRemoteCopyGroupFromDisaster.\ - side_effect = ( - exception.VolumeBackendAPIException( - "Error: Failover was unsuccessful.")) - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.replication_failover, - context.get_admin_context(), - self.volume_replicated, - valid_target_device_id) + [volume], + 'default') class TestHPE3PARFCDriver(HPE3PARBaseDriver, test.TestCase): diff --git a/cinder/volume/drivers/hpe/hpe_3par_common.py b/cinder/volume/drivers/hpe/hpe_3par_common.py index 205f7bb8164..27dbd59e42d 100644 --- a/cinder/volume/drivers/hpe/hpe_3par_common.py +++ b/cinder/volume/drivers/hpe/hpe_3par_common.py @@ -227,10 +227,11 @@ class HPE3PARCommon(object): 3.0.12 - Remove client version checks for replication 3.0.13 - Support creating a cg from a source cg 3.0.14 - Comparison of WWNs now handles case difference. bug #1546453 + 3.0.15 - Update replication to version 2.1 """ - VERSION = "3.0.14" + VERSION = "3.0.15" stats = {} @@ -253,6 +254,11 @@ class HPE3PARCommon(object): EXTRA_SPEC_REP_MODE = "replication:mode" EXTRA_SPEC_REP_SYNC_PERIOD = "replication:sync_period" RC_ACTION_CHANGE_TO_PRIMARY = 7 + DEFAULT_REP_MODE = 'periodic' + DEFAULT_SYNC_PERIOD = 900 + RC_GROUP_STARTED = 3 + SYNC_STATUS_COMPLETED = 3 + FAILBACK_VALUE = 'default' # License values for reported capabilities PRIORITY_OPT_LIC = "Priority Optimization" @@ -279,13 +285,14 @@ class HPE3PARCommon(object): hpe3par_valid_keys = ['cpg', 'snap_cpg', 'provisioning', 'persona', 'vvs', 'flash_cache'] - def __init__(self, config): + def __init__(self, config, active_backend_id=None): self.config = config self.client = None self.uuid = uuid.uuid4() self._client_conf = {} self._replication_targets = [] self._replication_enabled = False + self._active_backend_id = active_backend_id def get_version(self): return self.VERSION @@ -381,7 +388,7 @@ class HPE3PARCommon(object): if client is not None: client.logout() - def do_setup(self, context, volume=None, timeout=None, stats=None): + def do_setup(self, context, timeout=None, stats=None): if hpe3parclient is None: msg = _('You must install hpe3parclient before using 3PAR' ' drivers. Run "pip install python-3parclient" to' @@ -393,7 +400,7 @@ class HPE3PARCommon(object): # to communicate with the 3PAR array. It will contain either # the values for the primary array or secondary array in the # case of a fail-over. - self._get_3par_config(volume) + self._get_3par_config() self.client = self._create_client(timeout=timeout) wsapi_version = self.client.getWsApiVersion() self.API_VERSION = wsapi_version['build'] @@ -452,7 +459,7 @@ class HPE3PARCommon(object): if self.client: self.client_login() try: - cpg_names = self.config.hpe3par_cpg + cpg_names = self._client_conf['hpe3par_cpg'] for cpg_name in cpg_names: self.validate_cpg(cpg_name) @@ -1218,7 +1225,7 @@ class HPE3PARCommon(object): valid_licenses, self.REMOTE_COPY_LIC, "Replication") - for cpg_name in self.config.hpe3par_cpg: + for cpg_name in self._client_conf['hpe3par_cpg']: try: cpg = self.client.getCPG(cpg_name) if (self.API_VERSION >= SRSTATLD_API_VERSION): @@ -1315,6 +1322,8 @@ class HPE3PARCommon(object): 'vendor_name': 'Hewlett Packard Enterprise', 'volume_backend_name': None, 'array_id': info['id'], + 'replication_enabled': self._replication_enabled, + 'replication_targets': self._get_replication_targets(), 'pools': pools} def _check_license_enabled(self, valid_licenses, @@ -1678,7 +1687,7 @@ class HPE3PARCommon(object): # Default to pool extracted from host. # If that doesn't work use the 1st CPG in the config as the default. - default_cpg = pool or self.config.hpe3par_cpg[0] + default_cpg = pool or self._client_conf['hpe3par_cpg'][0] cpg = self._get_key_value(hpe3par_keys, 'cpg', default_cpg) if cpg is not default_cpg: @@ -2158,8 +2167,8 @@ class HPE3PARCommon(object): flash_cache = self.get_flash_cache_policy(hpe3par_keys) if qos or vvs_name or flash_cache is not None: - cpg_names = self._get_key_value(hpe3par_keys, 'cpg', - self.config.hpe3par_cpg) + cpg_names = self._get_key_value( + hpe3par_keys, 'cpg', self._client_conf['hpe3par_cpg']) try: self._add_volume_to_volume_set(volume, volume_name, cpg_names[0], vvs_name, @@ -2850,172 +2859,142 @@ class HPE3PARCommon(object): return existing_vluns # v2 replication methods - def replication_enable(self, context, volume): - """Enable replication on a replication capable volume.""" - if not self._volume_of_replicated_type(volume): - msg = _("Unable to enable volume replication because volume is " - "not of replicated type.") - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - model_update = {"provider_location": self.client.id} - # If replication is not enabled and the volume is of replicated type, - # we treat this as an error. - if not self._replication_enabled: - msg = _LE("Enabling replication failed because replication is " - "not properly configured.") - LOG.error(msg) - model_update['replication_status'] = "error" - else: - if self._do_volume_replication_setup(volume): - model_update['replication_status'] = "enabled" - else: - model_update['replication_status'] = "error" - - return model_update - - def replication_disable(self, context, volume): - """Disable replication on the specified volume.""" - if not self._volume_of_replicated_type(volume): - msg = _("Unable to disable volume replication because volume is " - "not of replicated type.") - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - model_update = {} - # If replication is not enabled and the volume is of replicated type, - # we treat this as an error. - if self._replication_enabled: - model_update['replication_status'] = 'disabled' - rcg_name = self._get_3par_rcg_name(volume['id']) - vol_name = self._get_3par_vol_name(volume['id']) - - try: - self.client.stopRemoteCopy(rcg_name) - except Exception as ex: - msg = (_LE("There was a problem disabling replication on " - "volume '%(name)s': %(error)s") % - {'name': vol_name, - 'error': six.text_type(ex)}) - LOG.error(msg) - model_update['replication_status'] = 'disable_failed' - else: - msg = _LE("Disabling replication failed because replication is " - "not properly configured.") - LOG.error(msg) - model_update['replication_status'] = 'error' - - return model_update - - def replication_failover(self, context, volume, secondary): + def failover_host(self, context, volumes, secondary_backend_id): """Force failover to a secondary replication target.""" - if not self._volume_of_replicated_type(volume): - msg = _("Unable to failover because volume is not of " - "replicated type.") - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - - # If replication is not enabled and the volume is of replicated type, - # we treat this as an error. + # Ensure replication is enabled before we try and failover. if not self._replication_enabled: msg = _LE("Issuing a fail-over failed because replication is " "not properly configured.") LOG.error(msg) - model_update = {"replication_status": "error"} - return model_update - - failover_target = None - for target in self._replication_targets: - if target['target_device_id'] == secondary: - failover_target = target - break - - if not failover_target: - msg = _("A valid secondary target MUST be specified in order " - "to failover.") - LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) - if self.client is not None and failover_target['id'] == self.client.id: - msg = _("The failover array cannot be the primary array.") + # Check to see if the user requested to failback. + if secondary_backend_id == self.FAILBACK_VALUE: + volume_update_list = self._replication_failback(volumes) + target_id = None + else: + # Find the failover target. + failover_target = None + for target in self._replication_targets: + if target['backend_id'] == secondary_backend_id: + failover_target = target + break + if not failover_target: + msg = _("A valid secondary target MUST be specified in order " + "to failover.") + LOG.error(msg) + raise exception.VolumeBackendAPIException(data=msg) + + target_id = failover_target['backend_id'] + # For each volume, if it is replicated, we want to fail it over. + volume_update_list = [] + for volume in volumes: + if self._volume_of_replicated_type(volume): + try: + # Try and stop remote-copy on main array. We eat the + # exception here because when an array goes down, the + # groups will stop automatically. + rcg_name = self._get_3par_rcg_name(volume['id']) + self.client.stopRemoteCopy(rcg_name) + except Exception: + pass + + try: + # Failover to secondary array. + remote_rcg_name = self._get_3par_remote_rcg_name( + volume['id'], volume['provider_location']) + cl = self._create_replication_client(failover_target) + cl.recoverRemoteCopyGroupFromDisaster( + remote_rcg_name, self.RC_ACTION_CHANGE_TO_PRIMARY) + volume_update_list.append( + {'volume_id': volume['id'], + 'updates': {'replication_status': 'failed-over'}}) + except Exception as ex: + msg = (_LE("There was a problem with the failover " + "(%(error)s) and it was unsuccessful. " + "Volume '%(volume)s will not be available " + "on the failed over target."), + {'error': six.text_type(ex), + 'volume': volume['id']}) + LOG.error(msg) + volume_update_list.append( + {'volume_id': volume['id'], + 'updates': {'replication_status': 'error'}}) + finally: + self._destroy_replication_client(cl) + else: + # If the volume is not of replicated type, we need to + # force the status into error state so a user knows they + # do not have access to the volume. + volume_update_list.append( + {'volume_id': volume['id'], + 'updates': {'status': 'error'}}) + + return target_id, volume_update_list + + def _replication_failback(self, volumes): + # Make sure the proper steps on the backend have been completed before + # we allow a fail-over. + if not self._is_host_ready_for_failback(volumes): + msg = _("The host is not ready to be failed back. Please " + "resynchronize the volumes and resume replication on the " + "3PAR backends.") LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) + raise exception.VolumeDriverException(data=msg) - try: - # Try and stop remote-copy on main array. - rcg_name = self._get_3par_rcg_name(volume['id']) - self.client.stopRemoteCopy(rcg_name) - except Exception: - pass + # Update the volumes status to available. + volume_update_list = [] + for volume in volumes: + if self._volume_of_replicated_type(volume): + volume_update_list.append( + {'volume_id': volume['id'], + 'updates': {'replication_status': 'available'}}) + else: + # Upon failing back, we can move the non-replicated volumes + # back into available state. + volume_update_list.append( + {'volume_id': volume['id'], + 'updates': {'status': 'available'}}) - try: - # Failover to secondary array. - remote_rcg_name = self._get_3par_remote_rcg_name( - volume['id'], volume['provider_location']) - cl = self._create_replication_client(failover_target) - cl.recoverRemoteCopyGroupFromDisaster( - remote_rcg_name, self.RC_ACTION_CHANGE_TO_PRIMARY) - new_location = volume['provider_location'] + ":" + ( - failover_target['id']) + return volume_update_list - model_update = {"provider_location": new_location, - "replication_status": "inactive"} - if failover_target['managed_backend_name']: - # We want to update the volumes host if our target is managed. - model_update['host'] = failover_target['managed_backend_name'] + def _is_host_ready_for_failback(self, volumes): + """Checks to make sure the volume has been synchronized - except Exception as ex: - msg = _("There was a problem with the failover (%s) and it was " - "unsuccessful.") % six.text_type(ex) - LOG.error(msg) - raise exception.VolumeBackendAPIException(data=msg) - finally: - self._destroy_replication_client(cl) - - return model_update - - def list_replication_targets(self, context, volume): - """Provides a means to obtain replication targets for a volume. - - This will query all enabled targets on a 3PAR backend and cross - reference them with all entries in cinder.conf. It will return - only those that appear on both, aka enabled replication targets. + This ensures that all the remote copy targets have been restored + to their natural direction, and all of the volumes have been + fully synchronized. """ - if not self._volume_of_replicated_type(volume): - return [] - - allowed_names = [] - # If the primary target is offline we can not ask it what targets are - # available. Our only option is to list all cinder.conf entries. try: - rcg_name = self._get_3par_rcg_name(volume['id']) - rcg = self.client.getRemoteCopyGroup(rcg_name) - rcg_targets = rcg['targets'] - for target in rcg_targets: - allowed_names.append(target['targetName']) + for volume in volumes: + if self._volume_of_replicated_type(volume): + location = volume.get('provider_location') + remote_rcg_name = self._get_3par_remote_rcg_name( + volume['id'], + location) + rcg = self.client.getRemoteCopyGroup(remote_rcg_name) + + # Make sure all targets are in their natural direction. + targets = rcg['targets'] + for target in targets: + if target['roleReversed'] or ( + target['state'] != self.RC_GROUP_STARTED): + return False + + # Make sure all volumes are fully synced. + volumes = rcg['volumes'] + for volume in volumes: + remote_volumes = volume['remoteVolumes'] + for remote_volume in remote_volumes: + if remote_volume['syncStatus'] != ( + self.SYNC_STATUS_COMPLETED): + return False except Exception: - LOG.warning(_LW("The primary array is currently unreachable. All " - "targets returned from list_replication_targets " - "are pulled directly from cinder.conf and are not " - "guarenteed to be available because they could " - "not be verified with the primary array.")) + # If there was a problem, we will return false so we can + # log an error in the parent function. + return False - replication_targets = [] - volume_type = self._get_volume_type(volume["volume_type_id"]) - extra_specs = volume_type.get("extra_specs") - replication_mode = extra_specs.get(self.EXTRA_SPEC_REP_MODE) - replication_mode_num = self._get_remote_copy_mode_num( - replication_mode) - - for target in self._replication_targets: - if not allowed_names and replication_mode_num == ( - target['replication_mode']) or ( - target['target_device_id'] in allowed_names): - list_vals = {'target_device_id': target['target_device_id']} - replication_targets.append(list_vals) - - return {'volume_id': volume['id'], - 'targets': replication_targets} + return True def _do_replication_setup(self): replication_targets = [] @@ -3042,7 +3021,7 @@ class HPE3PARCommon(object): # Format hpe3par_iscsi_chap_enabled as a bool remote_array['hpe3par_iscsi_chap_enabled'] = ( dev.get('hpe3par_iscsi_chap_enabled') == 'True') - array_name = remote_array['target_device_id'] + array_name = remote_array['backend_id'] # Make sure we can log into the array, that it has been # correctly configured, and its API version meets the @@ -3066,7 +3045,7 @@ class HPE3PARCommon(object): LOG.warning(msg) elif not self._is_valid_replication_array(remote_array): msg = (_LW("'%s' is not a valid replication array. " - "In order to be valid, target_device_id, " + "In order to be valid, backend_id, " "replication_mode, " "hpe3par_api_url, hpe3par_username, " "hpe3par_password, cpg_map, san_ip, " @@ -3091,7 +3070,7 @@ class HPE3PARCommon(object): def _is_valid_replication_array(self, target): required_flags = ['hpe3par_api_url', 'hpe3par_username', 'hpe3par_password', 'san_ip', 'san_login', - 'san_password', 'target_device_id', + 'san_password', 'backend_id', 'replication_mode', 'cpg_map'] try: self.check_replication_flags(target, required_flags) @@ -3156,19 +3135,14 @@ class HPE3PARCommon(object): ret_mode = self.PERIODIC return ret_mode - def _get_3par_config(self, volume): + def _get_3par_config(self): self._do_replication_setup() conf = None - if self._replication_enabled and volume: - provider_location = volume.get('provider_location') - if provider_location: - if volume.get('replication_status') == 'failed-over': - _, provider_location = provider_location.split(':') - - for target in self._replication_targets: - if target['id'] == provider_location: - conf = target - break + if self._replication_enabled: + for target in self._replication_targets: + if target['backend_id'] == self._active_backend_id: + conf = target + break self._build_3par_config(conf) def _build_3par_config(self, conf=None): @@ -3182,6 +3156,8 @@ class HPE3PARCommon(object): with unmanaged replication. """ if conf: + self._client_conf['hpe3par_cpg'] = self._generate_hpe3par_cpgs( + conf.get('cpg_map')) self._client_conf['hpe3par_username'] = ( conf.get('hpe3par_username')) self._client_conf['hpe3par_password'] = ( @@ -3202,6 +3178,8 @@ class HPE3PARCommon(object): conf.get('iscsi_ip_address')) self._client_conf['iscsi_port'] = conf.get('iscsi_port') else: + self._client_conf['hpe3par_cpg'] = ( + self.config.hpe3par_cpg) self._client_conf['hpe3par_username'] = ( self.config.hpe3par_username) self._client_conf['hpe3par_password'] = ( @@ -3234,6 +3212,22 @@ class HPE3PARCommon(object): return ret_target_cpg + def _generate_hpe3par_cpgs(self, cpg_map): + hpe3par_cpgs = [] + cpg_pairs = cpg_map.split(' ') + for cpg_pair in cpg_pairs: + cpgs = cpg_pair.split(':') + hpe3par_cpgs.append(cpgs[1]) + + return hpe3par_cpgs + + def _get_replication_targets(self): + replication_targets = [] + for target in self._replication_targets: + replication_targets.append(target['backend_id']) + + return replication_targets + def _do_volume_replication_setup(self, volume): """This function will do or ensure the following: @@ -3262,11 +3256,12 @@ class HPE3PARCommon(object): # are set correctly. volume_type = self._get_volume_type(volume["volume_type_id"]) extra_specs = volume_type.get("extra_specs") - replication_mode = extra_specs.get(self.EXTRA_SPEC_REP_MODE) + replication_mode = extra_specs.get( + self.EXTRA_SPEC_REP_MODE, self.DEFAULT_REP_MODE) replication_mode_num = self._get_remote_copy_mode_num( replication_mode) replication_sync_period = extra_specs.get( - self.EXTRA_SPEC_REP_SYNC_PERIOD) + self.EXTRA_SPEC_REP_SYNC_PERIOD, self.DEFAULT_SYNC_PERIOD) if replication_sync_period: replication_sync_period = int(replication_sync_period) if not self._is_replication_mode_correct(replication_mode, @@ -3290,12 +3285,12 @@ class HPE3PARCommon(object): if target['replication_mode'] == replication_mode_num: cpg = self._get_cpg_from_cpg_map(target['cpg_map'], local_cpg) - rcg_target = {'targetName': target['target_device_id'], + rcg_target = {'targetName': target['backend_id'], 'mode': replication_mode_num, 'snapCPG': cpg, 'userCPG': cpg} rcg_targets.append(rcg_target) - sync_target = {'targetName': target['target_device_id'], + sync_target = {'targetName': target['backend_id'], 'syncPeriod': replication_sync_period} sync_targets.append(sync_target) @@ -3320,7 +3315,7 @@ class HPE3PARCommon(object): for target in self._replication_targets: # Only add targets that match the volumes replication mode. if target['replication_mode'] == replication_mode_num: - rcg_target = {'targetName': target['target_device_id'], + rcg_target = {'targetName': target['backend_id'], 'secVolumeName': vol_name} rcg_targets.append(rcg_target) optional = {'volumeAutoCreation': True} @@ -3409,8 +3404,8 @@ class HPE3PARCommon(object): pass def _delete_replicated_failed_over_volume(self, volume): - old_location, new_location = volume['provider_location'].split(':') - rcg_name = self._get_3par_remote_rcg_name(volume['id'], old_location) + location = volume.get('provider_location') + rcg_name = self._get_3par_remote_rcg_name(volume['id'], location) targets = self.client.getRemoteCopyGroup(rcg_name)['targets'] # When failed over, we want to temporarily disable config mirroring # in order to be allowed to delete the volume and remote copy group diff --git a/cinder/volume/drivers/hpe/hpe_3par_fc.py b/cinder/volume/drivers/hpe/hpe_3par_fc.py index 8d7017f6aaa..a4e52d1a1e8 100644 --- a/cinder/volume/drivers/hpe/hpe_3par_fc.py +++ b/cinder/volume/drivers/hpe/hpe_3par_fc.py @@ -96,27 +96,29 @@ class HPE3PARFCDriver(driver.TransferVD, 3.0.3 - Adds v2 unmanaged replication support 3.0.4 - Adding manage/unmanage snapshot support 3.0.5 - Optimize array ID retrieval + 3.0.6 - Update replication to version 2.1 """ - VERSION = "3.0.5" + VERSION = "3.0.6" def __init__(self, *args, **kwargs): super(HPE3PARFCDriver, self).__init__(*args, **kwargs) + self._active_backend_id = kwargs.get('active_backend_id', None) self.configuration.append_config_values(hpecommon.hpe3par_opts) self.configuration.append_config_values(san.san_opts) self.lookup_service = fczm_utils.create_lookup_service() def _init_common(self): - return hpecommon.HPE3PARCommon(self.configuration) + return hpecommon.HPE3PARCommon(self.configuration, + self._active_backend_id) - def _login(self, volume=None, timeout=None): + def _login(self, timeout=None): common = self._init_common() # If replication is enabled and we cannot login, we do not want to # raise an exception so a failover can still be executed. try: - common.do_setup(None, volume=volume, timeout=timeout, - stats=self._stats) + common.do_setup(None, timeout=timeout, stats=self._stats) common.client_login() except Exception: if common._replication_enabled: @@ -170,21 +172,21 @@ class HPE3PARFCDriver(driver.TransferVD, pass def create_volume(self, volume): - common = self._login(volume) + common = self._login() try: return common.create_volume(volume) finally: self._logout(common) def create_cloned_volume(self, volume, src_vref): - common = self._login(volume) + common = self._login() try: return common.create_cloned_volume(volume, src_vref) finally: self._logout(common) def delete_volume(self, volume): - common = self._login(volume) + common = self._login() try: common.delete_volume(volume) finally: @@ -195,21 +197,21 @@ class HPE3PARFCDriver(driver.TransferVD, TODO: support using the size from the user. """ - common = self._login(volume) + common = self._login() try: return common.create_volume_from_snapshot(volume, snapshot) finally: self._logout(common) def create_snapshot(self, snapshot): - common = self._login(snapshot['volume']) + common = self._login() try: common.create_snapshot(snapshot) finally: self._logout(common) def delete_snapshot(self, snapshot): - common = self._login(snapshot['volume']) + common = self._login() try: common.delete_snapshot(snapshot) finally: @@ -255,7 +257,7 @@ class HPE3PARFCDriver(driver.TransferVD, * Create a VLUN for that HOST with the volume we want to export. """ - common = self._login(volume) + common = self._login() try: # we have to make sure we have a host host = self._create_host(common, volume, connector) @@ -298,7 +300,7 @@ class HPE3PARFCDriver(driver.TransferVD, @fczm_utils.RemoveFCZone def terminate_connection(self, volume, connector, **kwargs): """Driver entry point to unattach a volume from an instance.""" - common = self._login(volume) + common = self._login() try: hostname = common._safe_hostname(connector['host']) common.terminate_connection(volume, hostname, @@ -454,7 +456,7 @@ class HPE3PARFCDriver(driver.TransferVD, pass def extend_volume(self, volume, new_size): - common = self._login(volume) + common = self._login() try: common.extend_volume(volume, new_size) finally: @@ -509,7 +511,7 @@ class HPE3PARFCDriver(driver.TransferVD, self._logout(common) def manage_existing(self, volume, existing_ref): - common = self._login(volume) + common = self._login() try: return common.manage_existing(volume, existing_ref) finally: @@ -523,7 +525,7 @@ class HPE3PARFCDriver(driver.TransferVD, self._logout(common) def manage_existing_get_size(self, volume, existing_ref): - common = self._login(volume) + common = self._login() try: return common.manage_existing_get_size(volume, existing_ref) finally: @@ -538,7 +540,7 @@ class HPE3PARFCDriver(driver.TransferVD, self._logout(common) def unmanage(self, volume): - common = self._login(volume) + common = self._login() try: common.unmanage(volume) finally: @@ -553,14 +555,14 @@ class HPE3PARFCDriver(driver.TransferVD, def attach_volume(self, context, volume, instance_uuid, host_name, mountpoint): - common = self._login(volume) + common = self._login() try: common.attach_volume(volume, instance_uuid) finally: self._logout(common) def detach_volume(self, context, volume, attachment=None): - common = self._login(volume) + common = self._login() try: common.detach_volume(volume, attachment) finally: @@ -568,7 +570,7 @@ class HPE3PARFCDriver(driver.TransferVD, def retype(self, context, volume, new_type, diff, host): """Convert the volume to be of the new type.""" - common = self._login(volume) + common = self._login() try: return common.retype(volume, new_type, diff, host) finally: @@ -582,7 +584,7 @@ class HPE3PARFCDriver(driver.TransferVD, "to a host with storage_protocol=%s.", protocol) return False, None - common = self._login(volume) + common = self._login() try: return common.migrate_volume(volume, host) finally: @@ -591,7 +593,7 @@ class HPE3PARFCDriver(driver.TransferVD, def update_migrated_volume(self, context, volume, new_volume, original_volume_status): """Update the name of the migrated volume to it's new ID.""" - common = self._login(volume) + common = self._login() try: return common.update_migrated_volume(context, volume, new_volume, original_volume_status) @@ -599,7 +601,7 @@ class HPE3PARFCDriver(driver.TransferVD, self._logout(common) def get_pool(self, volume): - common = self._login(volume) + common = self._login() try: return common.get_cpg(volume) except hpeexceptions.HTTPNotFound: @@ -609,34 +611,14 @@ class HPE3PARFCDriver(driver.TransferVD, finally: self._logout(common) - def replication_enable(self, context, volume): - """Enable replication on a replication capable volume.""" - common = self._login(volume) - try: - return common.replication_enable(context, volume) - finally: - self._logout(common) - - def replication_disable(self, context, volume): - """Disable replication on the specified volume.""" - common = self._login(volume) - try: - return common.replication_disable(context, volume) - finally: - self._logout(common) - - def replication_failover(self, context, volume, secondary): + def failover_host(self, context, volumes, secondary_backend_id): """Force failover to a secondary replication target.""" - common = self._login(volume, timeout=30) + common = self._login(timeout=30) try: - return common.replication_failover(context, volume, secondary) - finally: - self._logout(common) - - def list_replication_targets(self, context, volume): - """Provides a means to obtain replication targets for a volume.""" - common = self._login(volume, timeout=30) - try: - return common.list_replication_targets(context, volume) + # Update the active_backend_id in the driver and return it. + active_backend_id, volume_updates = common.failover_host( + context, volumes, secondary_backend_id) + self._active_backend_id = active_backend_id + return active_backend_id, volume_updates finally: self._logout(common) diff --git a/cinder/volume/drivers/hpe/hpe_3par_iscsi.py b/cinder/volume/drivers/hpe/hpe_3par_iscsi.py index 562829cadea..4873ca62da9 100644 --- a/cinder/volume/drivers/hpe/hpe_3par_iscsi.py +++ b/cinder/volume/drivers/hpe/hpe_3par_iscsi.py @@ -108,26 +108,28 @@ class HPE3PARISCSIDriver(driver.TransferVD, 3.0.5 - Adds v2 unmanaged replication support 3.0.6 - Adding manage/unmanage snapshot support 3.0.7 - Optimize array ID retrieval + 3.0.8 - Update replication to version 2.1 """ - VERSION = "3.0.7" + VERSION = "3.0.8" def __init__(self, *args, **kwargs): super(HPE3PARISCSIDriver, self).__init__(*args, **kwargs) + self._active_backend_id = kwargs.get('active_backend_id', None) self.configuration.append_config_values(hpecommon.hpe3par_opts) self.configuration.append_config_values(san.san_opts) def _init_common(self): - return hpecommon.HPE3PARCommon(self.configuration) + return hpecommon.HPE3PARCommon(self.configuration, + self._active_backend_id) - def _login(self, volume=None, timeout=None): + def _login(self, timeout=None): common = self._init_common() # If replication is enabled and we cannot login, we do not want to # raise an exception so a failover can still be executed. try: - common.do_setup(None, volume=volume, timeout=timeout, - stats=self._stats) + common.do_setup(None, timeout=timeout, stats=self._stats) common.client_login() except Exception: if common._replication_enabled: @@ -248,7 +250,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, pass def create_volume(self, volume): - common = self._login(volume) + common = self._login() try: return common.create_volume(volume) finally: @@ -256,14 +258,14 @@ class HPE3PARISCSIDriver(driver.TransferVD, def create_cloned_volume(self, volume, src_vref): """Clone an existing volume.""" - common = self._login(volume) + common = self._login() try: return common.create_cloned_volume(volume, src_vref) finally: self._logout(common) def delete_volume(self, volume): - common = self._login(volume) + common = self._login() try: common.delete_volume(volume) finally: @@ -274,21 +276,21 @@ class HPE3PARISCSIDriver(driver.TransferVD, TODO: support using the size from the user. """ - common = self._login(volume) + common = self._login() try: return common.create_volume_from_snapshot(volume, snapshot) finally: self._logout(common) def create_snapshot(self, snapshot): - common = self._login(snapshot['volume']) + common = self._login() try: common.create_snapshot(snapshot) finally: self._logout(common) def delete_snapshot(self, snapshot): - common = self._login(snapshot['volume']) + common = self._login() try: common.delete_snapshot(snapshot) finally: @@ -320,7 +322,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, * Create a host on the 3par * create vlun on the 3par """ - common = self._login(volume) + common = self._login() try: # If the volume has been failed over, we need to reinitialize # iSCSI ports so they represent the new array. @@ -444,7 +446,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, def terminate_connection(self, volume, connector, **kwargs): """Driver entry point to unattach a volume from an instance.""" - common = self._login(volume) + common = self._login() try: hostname = common._safe_hostname(connector['host']) common.terminate_connection( @@ -651,7 +653,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, return model_update def create_export(self, context, volume, connector): - common = self._login(volume) + common = self._login() try: return self._do_export(common, volume) finally: @@ -662,7 +664,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, Also retrieves CHAP credentials, if present on the volume """ - common = self._login(volume) + common = self._login() try: vol_name = common._get_3par_vol_name(volume['id']) common.client.getVolume(vol_name) @@ -765,7 +767,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, return current_least_used_nsp def extend_volume(self, volume, new_size): - common = self._login(volume) + common = self._login() try: common.extend_volume(volume, new_size) finally: @@ -820,7 +822,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, self._logout(common) def manage_existing(self, volume, existing_ref): - common = self._login(volume) + common = self._login() try: return common.manage_existing(volume, existing_ref) finally: @@ -834,7 +836,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, self._logout(common) def manage_existing_get_size(self, volume, existing_ref): - common = self._login(volume) + common = self._login() try: return common.manage_existing_get_size(volume, existing_ref) finally: @@ -849,7 +851,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, self._logout(common) def unmanage(self, volume): - common = self._login(volume) + common = self._login() try: common.unmanage(volume) finally: @@ -864,14 +866,14 @@ class HPE3PARISCSIDriver(driver.TransferVD, def attach_volume(self, context, volume, instance_uuid, host_name, mountpoint): - common = self._login(volume) + common = self._login() try: common.attach_volume(volume, instance_uuid) finally: self._logout(common) def detach_volume(self, context, volume, attachment=None): - common = self._login(volume) + common = self._login() try: common.detach_volume(volume, attachment) finally: @@ -879,7 +881,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, def retype(self, context, volume, new_type, diff, host): """Convert the volume to be of the new type.""" - common = self._login(volume) + common = self._login() try: return common.retype(volume, new_type, diff, host) finally: @@ -893,7 +895,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, "to a host with storage_protocol=%s.", protocol) return False, None - common = self._login(volume) + common = self._login() try: return common.migrate_volume(volume, host) finally: @@ -902,7 +904,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, def update_migrated_volume(self, context, volume, new_volume, original_volume_status): """Update the name of the migrated volume to it's new ID.""" - common = self._login(volume) + common = self._login() try: return common.update_migrated_volume(context, volume, new_volume, original_volume_status) @@ -910,7 +912,7 @@ class HPE3PARISCSIDriver(driver.TransferVD, self._logout(common) def get_pool(self, volume): - common = self._login(volume) + common = self._login() try: return common.get_cpg(volume) except hpeexceptions.HTTPNotFound: @@ -920,34 +922,14 @@ class HPE3PARISCSIDriver(driver.TransferVD, finally: self._logout(common) - def replication_enable(self, context, volume): - """Enable replication on a replication capable volume.""" - common = self._login(volume) - try: - return common.replication_enable(context, volume) - finally: - self._logout(common) - - def replication_disable(self, context, volume): - """Disable replication on the specified volume.""" - common = self._login(volume) - try: - return common.replication_disable(context, volume) - finally: - self._logout(common) - - def replication_failover(self, context, volume, secondary): + def failover_host(self, context, volumes, secondary_backend_id): """Force failover to a secondary replication target.""" - common = self._login(volume, timeout=30) + common = self._login(timeout=30) try: - return common.replication_failover(context, volume, secondary) - finally: - self._logout(common) - - def list_replication_targets(self, context, volume): - """Provides a means to obtain replication targets for a volume.""" - common = self._login(volume, timeout=30) - try: - return common.list_replication_targets(context, volume) + # Update the active_backend_id in the driver and return it. + active_backend_id, volume_updates = common.failover_host( + context, volumes, secondary_backend_id) + self._active_backend_id = active_backend_id + return active_backend_id, volume_updates finally: self._logout(common) diff --git a/releasenotes/notes/replication-v2.1-3par-b3f780a109f9195c.yaml b/releasenotes/notes/replication-v2.1-3par-b3f780a109f9195c.yaml new file mode 100644 index 00000000000..16289c10ed8 --- /dev/null +++ b/releasenotes/notes/replication-v2.1-3par-b3f780a109f9195c.yaml @@ -0,0 +1,3 @@ +--- +features: + - Adds v2.1 replication support to the HPE 3PAR driver.