diff --git a/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py b/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py index 5f11e887d6c..2a3cea3acb3 100644 --- a/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py +++ b/cinder/tests/unit/volume/drivers/hpe/test_hpe3par.py @@ -1697,6 +1697,293 @@ class HPE3PARBaseDriver(object): ] mock_client.assert_has_calls(expected + self.standard_logout) + @mock.patch.object(volume_types, 'get_volume_type') + def test_retype_non_rep_type_to_rep_type(self, _mock_volume_types): + + 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_client.getStorageSystemInfo.return_value = { + 'id': self.REPLICATION_CLIENT_ID, + 'serialNumber': '1234567' + } + mock_client.modifyVolume.return_value = ("anyResponse", {'taskid': 1}) + mock_client.getTask.return_value = self.STATUS_DONE + + _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}} + + mock_client.getVolume.return_value = { + 'name': mock.ANY, + 'snapCPG': mock.ANY, + 'comment': "{'display_name': 'Foo Volume'}", + 'provisioningType': mock.ANY, + 'userCPG': 'OpenStackCPG', + 'snapCPG': 'OpenStackCPGSnap'} + + 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 + + retyped = self.driver.retype( + self.ctxt, + self.volume, + self.volume_type_replicated, + None, + self.RETYPE_HOST) + self.assertTrue(retyped) + backend_id = self.replication_targets[0]['backend_id'] + expected = [ + mock.call.createRemoteCopyGroup( + self.RCG_3PAR_NAME, + [{'userCPG': HPE3PAR_CPG_REMOTE, + 'targetName': backend_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': backend_id}], + optional={'volumeAutoCreation': True}), + mock.call.modifyRemoteCopyGroup( + self.RCG_3PAR_NAME, + {'targets': [{'syncPeriod': SYNC_PERIOD, + 'targetName': backend_id}]}), + mock.call.startRemoteCopy(self.RCG_3PAR_NAME)] + mock_client.assert_has_calls(expected + self.standard_logout) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_retype_rep_type_to_non_rep_type(self, _mock_volume_types): + + 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_client.getStorageSystemInfo.return_value = { + 'id': self.REPLICATION_CLIENT_ID, + 'serialNumber': '1234567' + } + mock_client.modifyVolume.return_value = ("anyResponse", {'taskid': 1}) + mock_client.getTask.return_value = self.STATUS_DONE + + volume_1 = {'name': self.VOLUME_NAME, + 'id': self.VOLUME_ID, + 'display_name': 'Foo Volume', + 'replication_status': 'disabled', + 'provider_location': self.CLIENT_ID, + 'size': 2, + 'host': self.FAKE_CINDER_HOST, + 'volume_type': 'replicated', + 'volume_type_id': 'gold'} + + volume_type = {'name': 'replicated', + 'deleted': False, + 'updated_at': None, + 'deleted_at': None, + 'extra_specs': {'replication_enabled': 'False'}, + 'id': 'silver'} + + def get_side_effect(*args): + data = {'value': None} + if args[1] == 'gold': + data['value'] = { + 'name': 'replicated', + 'id': 'gold', + 'extra_specs': { + 'replication_enabled': ' True', + 'replication:mode': 'periodic', + 'replication:sync_period': '900', + 'volume_type': self.volume_type_replicated}} + elif args[1] == 'silver': + data['value'] = {'name': 'silver', + 'deleted': False, + 'updated_at': None, + 'extra_specs': { + 'replication_enabled': 'False'}, + 'deleted_at': None, + 'id': 'silver'} + return data['value'] + + _mock_volume_types.side_effect = get_side_effect + + mock_client.getVolume.return_value = { + 'name': mock.ANY, + 'snapCPG': mock.ANY, + 'comment': "{'display_name': 'Foo Volume'}", + 'provisioningType': mock.ANY, + 'userCPG': 'OpenStackCPG', + 'snapCPG': 'OpenStackCPGSnap'} + + 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 + + retyped = self.driver.retype( + self.ctxt, volume_1, volume_type, None, self.RETYPE_HOST) + self.assertTrue(retyped) + + expected = [ + mock.call.stopRemoteCopy(self.RCG_3PAR_NAME), + mock.call.removeVolumeFromRemoteCopyGroup( + self.RCG_3PAR_NAME, + self.VOLUME_3PAR_NAME, + removeFromTarget=True), + mock.call.removeRemoteCopyGroup(self.RCG_3PAR_NAME)] + + mock_client.assert_has_calls( + self.get_id_login + + self.standard_logout + + self.standard_login + + expected + + self.standard_logout, any_order =True) + + @mock.patch.object(volume_types, 'get_volume_type') + def test_retype_rep_type_to_rep_type(self, _mock_volume_types): + + 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_client.getStorageSystemInfo.return_value = { + 'id': self.REPLICATION_CLIENT_ID, + 'serialNumber': '1234567' + } + mock_client.modifyVolume.return_value = ("anyResponse", {'taskid': 1}) + mock_client.getTask.return_value = self.STATUS_DONE + + volume_1 = {'name': self.VOLUME_NAME, + 'id': self.VOLUME_ID, + 'display_name': 'Foo Volume', + 'replication_status': 'disabled', + 'provider_location': self.CLIENT_ID, + 'size': 2, + 'host': self.FAKE_CINDER_HOST, + 'volume_type': 'replicated', + 'volume_type_id': 'gold'} + + volume_type = {'name': 'replicated', + 'deleted': False, + 'updated_at': None, + 'deleted_at': None, + 'extra_specs': {'replication_enabled': ' True'}, + 'id': 'silver'} + + def get_side_effect(*args): + data = {'value': None} + if args[1] == 'gold': + data['value'] = { + 'name': 'replicated', + 'id': 'gold', + 'extra_specs': { + 'replication_enabled': ' True', + 'replication:mode': 'periodic', + 'replication:sync_period': '900', + 'volume_type': self.volume_type_replicated}} + elif args[1] == 'silver': + data['value'] = { + 'name': 'silver', + 'deleted': False, + 'updated_at': None, + 'extra_specs': { + 'replication_enabled': ' True', + 'replication:mode': 'periodic', + 'replication:sync_period': '1500', + 'volume_type': self.volume_type_replicated}, + 'deleted_at': None, + 'id': 'silver'} + return data['value'] + + _mock_volume_types.side_effect = get_side_effect + + mock_client.getVolume.return_value = { + 'name': mock.ANY, + 'snapCPG': mock.ANY, + 'comment': "{'display_name': 'Foo Volume'}", + 'provisioningType': mock.ANY, + 'userCPG': 'OpenStackCPG', + 'snapCPG': 'OpenStackCPGSnap'} + + 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 + + backend_id = self.replication_targets[0]['backend_id'] + retyped = self.driver.retype( + self.ctxt, volume_1, volume_type, None, self.RETYPE_HOST) + self.assertTrue(retyped) + + expected = [ + mock.call.stopRemoteCopy(self.RCG_3PAR_NAME), + mock.call.removeVolumeFromRemoteCopyGroup( + self.RCG_3PAR_NAME, + self.VOLUME_3PAR_NAME, + removeFromTarget=True), + mock.call.removeRemoteCopyGroup(self.RCG_3PAR_NAME), + mock.call.createRemoteCopyGroup( + self.RCG_3PAR_NAME, + [{'userCPG': HPE3PAR_CPG_REMOTE, + 'targetName': backend_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': backend_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, any_order =True) + @mock.patch.object(volume_types, 'get_volume_type') def test_retype_qos_spec(self, _mock_volume_types): _mock_volume_types.return_value = self.RETYPE_VOLUME_TYPE_1 diff --git a/cinder/volume/drivers/hpe/hpe_3par_common.py b/cinder/volume/drivers/hpe/hpe_3par_common.py index 9c686e01040..2a4687d952c 100644 --- a/cinder/volume/drivers/hpe/hpe_3par_common.py +++ b/cinder/volume/drivers/hpe/hpe_3par_common.py @@ -256,10 +256,11 @@ class HPE3PARCommon(object): 3.0.31 - Enable HPE-3PAR Compression Feature. 3.0.32 - Add consistency group capability to generic volume group in HPE-3APR + 3.0.33 - Added replication feature in retype flow. bug #1680313 """ - VERSION = "3.0.32" + VERSION = "3.0.33" stats = {} @@ -2878,7 +2879,8 @@ class HPE3PARCommon(object): retype_flow.add( ModifyVolumeTask(action), ModifySpecsTask(action), - TuneVolumeTask(action)) + TuneVolumeTask(action), + ReplicateVolumeTask(action)) taskflow.engines.run( retype_flow, @@ -3435,7 +3437,8 @@ class HPE3PARCommon(object): return replication_targets - def _do_volume_replication_setup(self, volume): + def _do_volume_replication_setup(self, volume, retype=False, + dist_type_id=None): """This function will do or ensure the following: -Create volume on main array (already done in create_volume) @@ -3462,7 +3465,11 @@ class HPE3PARCommon(object): # Grab the extra_spec entries for replication and make sure they # are set correctly. volume_type = self._get_volume_type(volume["volume_type_id"]) - extra_specs = volume_type.get("extra_specs") + if retype and dist_type_id is not None: + dist_type = self._get_volume_type(dist_type_id) + extra_specs = dist_type.get("extra_specs") + else: + extra_specs = volume_type.get("extra_specs") replication_mode = extra_specs.get( self.EXTRA_SPEC_REP_MODE, self.DEFAULT_REP_MODE) replication_mode_num = self._get_remote_copy_mode_num( @@ -3570,7 +3577,8 @@ class HPE3PARCommon(object): LOG.error(msg) raise exception.VolumeBackendAPIException(data=msg) - def _do_volume_replication_destroy(self, volume, rcg_name=None): + def _do_volume_replication_destroy(self, volume, rcg_name=None, + retype=False): """This will completely remove all traces of a remote copy group. It should be used when deleting a replication enabled volume @@ -3606,7 +3614,8 @@ class HPE3PARCommon(object): # Delete volume on the main array. try: - self.client.deleteVolume(vol_name) + if not retype: + self.client.deleteVolume(vol_name) except Exception: pass @@ -3659,6 +3668,49 @@ class HPE3PARCommon(object): initial_delay=self.initial_delay).wait() +class ReplicateVolumeTask(flow_utils.CinderTask): + + """Task to replicate a volume. + + This is a task for adding/removing the replication feature to volume. + It is intended for use during retype(). This task has no revert. + # TODO(sumit): revert back to original volume extra-spec + """ + + def __init__(self, action, **kwargs): + super(ReplicateVolumeTask, self).__init__(addons=[action]) + + def execute(self, common, volume, new_type_id): + + new_replicated_type = False + + if new_type_id: + new_volume_type = common._get_volume_type(new_type_id) + + extra_specs = new_volume_type.get('extra_specs', None) + if extra_specs and 'replication_enabled' in extra_specs: + rep_val = extra_specs['replication_enabled'] + new_replicated_type = (rep_val == " True") + + if common._volume_of_replicated_type(volume) and new_replicated_type: + # Retype from replication enabled to replication enable. + common._do_volume_replication_destroy(volume, retype=True) + common._do_volume_replication_setup( + volume, + retype=True, + dist_type_id=new_type_id) + elif (not common._volume_of_replicated_type(volume) + and new_replicated_type): + # Retype from replication disabled to replication enable. + common._do_volume_replication_setup( + volume, + retype=True, + dist_type_id=new_type_id) + elif common._volume_of_replicated_type(volume): + # Retype from replication enabled to replication disable. + common._do_volume_replication_destroy(volume, retype=True) + + class ModifyVolumeTask(flow_utils.CinderTask): """Task to change a volume's snapCPG and comment.