From 2ebccef1824cf4cb81c02c4b1bc3f511c4dcdbd4 Mon Sep 17 00:00:00 2001 From: Alex O'Rourke Date: Wed, 3 Feb 2016 16:11:58 +0000 Subject: [PATCH] 3PAR: Update replication to v2.1 This patch updates replication to match the v2.1 spec. This makes it so an entire backend can be replicated, and upon failover, all replicated volumes will be failed over together. Both sync and periodic replication modes are supported. Each replication_device entry should have a replication_mode value set to sync|periodic. A volume type extra_spec value of replication:mode should also be set. If replication:mode is periodic, replication:sync_period should be set as well. Which replication_device entry(s) are used is determined by the value of replication:mode set for each volume type. NOTE: If no extra_specs are provided, periodic mode is defaulted with a replication period of 900 seconds. cinder.conf should have the replication config group: [3parfcrep] hpe3par_api_url = http://10.10.10.10:8008/api/v1 hpe3par_username = user hpe3par_password = pass hpe3par_debug = False san_ip = 10.10.10.10 san_login = user san_password = pass volume_backend_name = 3parfcrep hpe3par_cpg = REMOTE_COPY_CPG2 volume_driver = cinder.volume.drivers.hpe.hpe_3par_fc.HPE3PARFCDriver replication_device = backend_id:eos16, replication_mode:periodic, cpg_map:REMOTE_COPY_CPG2:REMOTE_COPY_DEST2, hpe3par_api_url:http://11.11.11.11:8008/api/v1, hpe3par_username:user, hpe3par_password:pass, san_ip:11.11.11.11, san_login:user, san_password:pass If we are working with iSCSI, the replication device needs to contain entries for the hpe3par_iscsi_ips as such: [3pariscsirep] hpe3par_api_url = https://10.10.10.10:8080/api/v1 hpe3par_username = user hpe3par_password = pass hpe3par_debug = False hpe3par_iscsi_ips = 10.50.50.50,10.50.50.51 san_ip = 10.10.10.10 san_login = user san_password = pass volume_backend_name = 3pariscsirep hpe3par_cpg = REMOTE_COPY_CPG2 iscsi_ip_address = 10.50.50.50 volume_driver = cinder.volume.drivers.hpe.hpe_3par_iscsi.HPE3PARISCSIDriver replication_device = backend_id:eos16, replication_mode:periodic, cpg_map:REMOTE_COPY_CPG2:REMOTE_COPY_DEST2, hpe3par_api_url:https://11.11.11.11:8080/api/v1, hpe3par_username:user, hpe3par_password:pass, san_ip:11.11.11.11, san_login:user, san_password:pass, hpe3par_iscsi_ips:11.51.51.100 Closes-Bug: #1542078 Change-Id: Ia161b257278958c6a158d1239a77fc443c2985f0 --- cinder/tests/unit/test_hpe3par.py | 675 +++--------------- cinder/volume/drivers/hpe/hpe_3par_common.py | 353 +++++---- cinder/volume/drivers/hpe/hpe_3par_fc.py | 82 +-- cinder/volume/drivers/hpe/hpe_3par_iscsi.py | 86 +-- ...eplication-v2.1-3par-b3f780a109f9195c.yaml | 3 + 5 files changed, 335 insertions(+), 864 deletions(-) create mode 100644 releasenotes/notes/replication-v2.1-3par-b3f780a109f9195c.yaml diff --git a/cinder/tests/unit/test_hpe3par.py b/cinder/tests/unit/test_hpe3par.py index 3eabe366f..747b17952 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}] - - 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}] + 'san_private_key': HPE3PAR_SAN_SSH_PRIVATE}] + + 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,97 +1047,17 @@ 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'] = 'sync' + 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': { - '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.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.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}], - 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.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 = ( @@ -1161,8 +1066,6 @@ class HPE3PARBaseDriver(object): _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', @@ -1177,70 +1080,45 @@ class HPE3PARBaseDriver(object): 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"}) + volume = self.volume_replicated.copy() + volume['replication_status'] = 'failed-over' + self.driver.delete_volume(volume) - target_device_id = self.replication_targets[0]['target_device_id'] + rcg_name = self.RCG_3PAR_NAME + ".r" + self.CLIENT_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, + 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, - [{'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)] + 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) - 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): + 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() - self.replication_devs_unmgd[0]['replication_mode'] = 'sync' - conf.replication_device = self.replication_devs_unmgd + self.replication_targets[0]['replication_mode'] = 'sync' + 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.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 = ( @@ -1249,8 +1127,6 @@ class HPE3PARBaseDriver(object): _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}} @@ -1274,9 +1150,8 @@ 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.getCPG(HPE3PAR_CPG), mock.call.createVolume( self.VOLUME_3PAR_NAME, HPE3PAR_CPG, @@ -1287,11 +1162,10 @@ class HPE3PARBaseDriver(object): '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, + 'targetName': backend_id, 'mode': SYNC_MODE, 'snapCPG': HPE3PAR_CPG_REMOTE}], {'localUserCPG': HPE3PAR_CPG, @@ -1300,7 +1174,7 @@ 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.startRemoteCopy(self.RCG_3PAR_NAME)] mock_client.assert_has_calls( @@ -4336,85 +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. + def test_failover_host(self, _mock_volume_types): + # periodic vs. sync is not relevant when conducting a failover. We + # will just use periodic. conf = self.setup_configuration() self.replication_targets[0]['replication_mode'] = 'periodic' conf.replication_device = self.replication_targets @@ -4441,135 +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_backend_id = ( + self.replication_targets[0]['backend_id']) + invalid_backend_id = 'INVALID' - 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 - + volumes = [self.volume_replicated] + # Test invalid secondary target. self.assertRaises( exception.VolumeBackendAPIException, - self.driver.replication_enable, + self.driver.failover_host, context.get_admin_context(), - self.volume_replicated) + volumes, + invalid_backend_id) - @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( + # Test no secondary target. + self.assertRaises( + exception.VolumeBackendAPIException, + self.driver.failover_host, 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 + volumes, + None) - return_model = self.driver.replication_disable( + # 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(), - self.volume_replicated) - + volumes, + valid_backend_id) expected = [ mock.call.stopRemoteCopy(self.RCG_3PAR_NAME)] mock_client.assert_has_calls( @@ -4578,50 +4277,19 @@ class HPE3PARBaseDriver(object): 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) + self.assertEqual(expected_model, return_model) @mock.patch.object(volume_types, 'get_volume_type') - def test_list_replication_targets(self, _mock_volume_types): + def test_replication_failback_ready(self, _mock_volume_types): # Managed vs. unmanaged and periodic vs. sync are not relevant when - # enabling/disabling replication and listing replication targets. + # failing back a volume. # 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}) @@ -4643,151 +4311,28 @@ class HPE3PARBaseDriver(object): mock_create_client.return_value = mock_client mock_replication_client.return_value = mock_replicated_client - return_model = self.driver.list_replication_targets( + # 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(), - 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) + [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_list_replication_targets_non_replicated_type(self, - _mock_volume_types): + def test_replication_failback_not_ready(self, _mock_volume_types): # Managed vs. unmanaged and periodic vs. sync are not relevant when - # enabling/disabling replication and listing replication targets. + # 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_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): - # 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 - 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 - valid_target_device_id = ( - self.replication_targets[0]['target_device_id']) - invalid_target_device_id = 'INVALID' - - # test invalid secondary target - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.replication_failover, - 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, - '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) - - @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 - 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_replicated_client = self.setup_driver(config=conf) @@ -4809,57 +4354,21 @@ class HPE3PARBaseDriver(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 - valid_target_device_id = ( - self.replication_targets[0]['target_device_id']) - invalid_target_device_id = 'INVALID' - - # test invalid secondary target - self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.replication_failover, - 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 an unsuccessful fail-back. + volume = self.volume_replicated.copy() + volume['replication_status'] = 'failed-over' - # test a unsuccessful failover - mock_replicated_client.recoverRemoteCopyGroupFromDisaster.\ - side_effect = ( - exception.VolumeBackendAPIException( - "Error: Failover was unsuccessful.")) self.assertRaises( - exception.VolumeBackendAPIException, - self.driver.replication_failover, + exception.VolumeDriverException, + self.driver.failover_host, 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 205f7bb81..27dbd59e4 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.") - LOG.error(msg) - raise exception.VolumeBackendAPIException(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 + # 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) - 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']) - - 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'] + 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 - except Exception as ex: - msg = _("There was a problem with the failover (%s) and it was " - "unsuccessful.") % six.text_type(ex) + 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) - finally: - self._destroy_replication_client(cl) + raise exception.VolumeDriverException(data=msg) - return model_update + # 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'}}) - def list_replication_targets(self, context, volume): - """Provides a means to obtain replication targets for a volume. + return volume_update_list - 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. - """ - if not self._volume_of_replicated_type(volume): - return [] + def _is_host_ready_for_failback(self, volumes): + """Checks to make sure the volume has been synchronized - 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. + This ensures that all the remote copy targets have been restored + to their natural direction, and all of the volumes have been + fully synchronized. + """ 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.")) - - 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) + # If there was a problem, we will return false so we can + # log an error in the parent function. + return False - 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 8d7017f6a..a4e52d1a1 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) - 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) + common = self._login(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 562829cad..4873ca62d 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) - 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) + common = self._login(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 000000000..16289c10e --- /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. -- 2.45.2